STL中的unordered_map和unordered_set:哈希表的快速通道

🚀 新一代关联容器:unordered家族

如果你已经熟悉了mapset,那么unordered_mapunordered_set就是它们的快速版。简单来说:

  • setunordered_set:有序变无序,速度更快
  • mapunordered_map:有序变无序,速度更快

底层实现从红黑树变成了哈希表,时间复杂度从O(logN)降到了平均O(1)!

🔑 核心差异一览表

特性 map/set(红黑树) unordered_map/set(哈希表)
底层结构 红黑树(平衡二叉搜索树) 哈希表(数组+链表/红黑树)
时间复杂度 O(logN) 平均O(1),最坏O(N)
有序性 按键值有序排列 无序(名字里就告诉你了!)
迭代器类型 双向迭代器(可++和--) 前向迭代器(只能++)
Key要求 支持<比较 1. 支持==比较 2. 能转为整型(用于哈希)

💻 基本使用:跟map/set几乎一样

好消息是,unordered_系列的使用接口跟map/set几乎一模一样

cpp 复制代码
#include <unordered_set>
#include <unordered_map>
#include <iostream>

int main() {
    // unordered_set使用
    std::unordered_set<int> us = {5, 2, 7, 2, 8, 5, 9};
    
    for (int num : us) {
        std::cout << num << " ";  
        // 输出可能是: 9 8 7 2 5(无序!)
    }
    
    // unordered_map使用
    std::unordered_map<std::string, int> scores;
    scores["小明"] = 90;
    scores["小红"] = 85;
    scores["小刚"] = 88;
    
    // 同样支持operator[]
    std::cout << "小明的分数: " << scores["小明"] << std::endl;
    
    return 0;
}

⚡ 性能对比:谁更快?

理论归理论,实际跑一跑:

cpp 复制代码
#include <unordered_set>
#include <set>
#include <vector>
#include <ctime>
#include <iostream>

int main() {
    const int N = 1000000;  // 100万数据
    std::vector<int> v;
    
    // 生成随机数据
    for (int i = 0; i < N; ++i) {
        v.push_back(rand() + i);
    }
    
    std::set<int> s;
    std::unordered_set<int> us;
    
    // 测试插入性能
    clock_t begin1 = clock();
    for (auto e : v) s.insert(e);
    clock_t end1 = clock();
    std::cout << "set插入耗时: " << end1 - begin1 << "ms\n";
    
    clock_t begin2 = clock();
    us.reserve(N);  // 预分配空间,提升性能
    for (auto e : v) us.insert(e);
    clock_t end2 = clock();
    std::cout << "unordered_set插入耗时: " << end2 - begin2 << "ms\n";
    
    // 测试查找性能
    // ...(类似代码)
    
    return 0;
}

典型结果

  • 插入:unordered_setset快2-10倍
  • 查找:unordered_setset快10-100倍
  • 内存:unordered_set通常占用更多(哈希表有数组开销)

🎯 什么时候用unordered系列?

优先使用unordered_系列:

  1. 只需要快速查找,不需要有序

    cpp 复制代码
    // 检查用户名是否存在(不关心顺序)
    std::unordered_set<std::string> users = {"Alice", "Bob", "Charlie"};
    if (users.find(input_name) != users.end()) {
        // 用户存在
    }
  2. 统计频率/计数

    cpp 复制代码
    // 统计单词频率(最快的方法!)
    std::unordered_map<std::string, int> wordCount;
    for (const auto& word : words) {
        wordCount[word]++;  // 平均O(1)!
    }
  3. 缓存系统

    cpp 复制代码
    // LRU缓存实现(简化版)
    std::unordered_map<int, std::string> cache;
    // 快速检查缓存命中

使用map/set系列:

  1. 需要有序遍历

    cpp 复制代码
    // 按分数从高到低显示
    std::map<int, std::string, std::greater<int>> scoreBoard;
  2. 需要范围查询

    cpp 复制代码
    // 查找分数在80-90之间的学生
    auto low = scores.lower_bound(80);
    auto high = scores.upper_bound(90);
  3. Key不支持哈希

    cpp 复制代码
    struct Point { int x, y; };
    // Point不能直接用作unordered_set的Key
    // 但可以作为set的Key(需要实现operator<)

🔧 自定义类型作为Key

如果你想用自定义类型作为unordered_容器的Key,需要做两件事:

cpp 复制代码
struct Person {
    std::string name;
    int age;
    
    // 1. 实现相等比较
    bool operator==(const Person& other) const {
        return name == other.name && age == other.age;
    }
};

// 2. 实现哈希函数
struct PersonHash {
    std::size_t operator()(const Person& p) const {
        // 组合name和age的哈希值
        return std::hash<std::string>{}(p.name) ^ 
               (std::hash<int>{}(p.age) << 1);
    }
};

int main() {
    std::unordered_set<Person, PersonHash> people;
    people.insert({"小明", 18});
    people.insert({"小红", 20});
    
    return 0;
}

⚠️ 注意事项

1. 迭代器失效

cpp 复制代码
std::unordered_set<int> s = {1, 2, 3, 4, 5};
auto it = s.find(3);
s.erase(2);  // 这里可能导致it失效(取决于实现)

// 安全做法:先保存值,再删除
int value = *it;
s.erase(it);  // 用迭代器删除是安全的

2. 内存使用

cpp 复制代码
std::unordered_set<int> us;
us.reserve(1000);  // 预分配空间,避免多次扩容

// 查看当前状态
std::cout << "桶数量: " << us.bucket_count() << std::endl;
std::cout << "负载因子: " << us.load_factor() << std::endl;

3. 最坏情况

虽然平均O(1),但最坏情况可能退化为O(N):

  • 所有元素哈希到同一个桶
  • 哈希函数设计不好

🔄 与multiset/multimap的关系

也有对应的unordered_multisetunordered_multimap,允许重复Key:

cpp 复制代码
std::unordered_multiset<int> ums = {1, 2, 2, 3, 3, 3};
std::cout << ums.count(2);  // 输出: 2

// unordered_multimap不支持operator[]
std::unordered_multimap<std::string, int> umm;
umm.insert({"小明", 90});
umm.insert({"小明", 95});  // 允许重复key

🎓 实际应用场景

场景1:两数之和(LeetCode 1)

cpp 复制代码
std::vector<int> twoSum(std::vector<int>& nums, int target) {
    std::unordered_map<int, int> numToIndex;  // 值->索引
    
    for (int i = 0; i < nums.size(); i++) {
        int complement = target - nums[i];
        if (numToIndex.find(complement) != numToIndex.end()) {
            return {numToIndex[complement], i};
        }
        numToIndex[nums[i]] = i;
    }
    return {};
}

场景2:查找重复元素

cpp 复制代码
bool hasDuplicate(std::vector<int>& nums) {
    std::unordered_set<int> seen;
    for (int num : nums) {
        if (seen.find(num) != seen.end()) {
            return true;
        }
        seen.insert(num);
    }
    return false;
}

场景3:分组异位词(LeetCode 49)

cpp 复制代码
std::vector<std::vector<std::string>> groupAnagrams(
    std::vector<std::string>& strs) {
    
    std::unordered_map<std::string, std::vector<std::string>> groups;
    
    for (const auto& str : strs) {
        std::string key = str;
        std::sort(key.begin(), key.end());  // 排序作为key
        groups[key].push_back(str);
    }
    
    std::vector<std::vector<std::string>> result;
    for (const auto& pair : groups) {
        result.push_back(pair.second);
    }
    return result;
}

💡 选择指南

快速决策流程:

复制代码
需要关联容器吗?
    ↓
需要有序吗?
    ↓ 是 → 用map/set(红黑树)
    ↓ 否
需要最快查找吗?
    ↓ 是 → 用unordered_map/unordered_set(哈希表)
    ↓ 否 → 都可以,根据具体情况选择

经验法则:

  1. 默认选择 :大多数情况下用unordered_系列,更快
  2. 需要顺序 :用map/set
  3. 内存敏感 :考虑map/set(哈希表有额外开销)
  4. 简单类型 :用unordered_系列
  5. 复杂类型 :如果能实现好的哈希函数,用unordered_;否则用map/set

🎉 总结

unordered_mapunordered_set是C++11引入的重要改进:

  • 优点:查找极快(平均O(1)),使用简单
  • 缺点:无序,内存占用可能更多

记住这个简单规则:

  • unordered_系列
  • 有序map/set系列

两者接口几乎一样,切换成本很低。在实际项目中,根据需求灵活选择,就能写出既高效又优雅的代码!

最后在这里祝大家在2026新的一年里平安喜乐,万事顺遂,勇往直前!

2026年,愿我们:

写更优雅的代码

解更复杂的问题

建更强大的系统

过更丰富的人生

Happy New Year, Happy Coding! 🎆

相关推荐
jllllyuz2 小时前
基于帧差法与ViBe算法的MATLAB前景提取
开发语言·算法·matlab
DsirNg2 小时前
CategoryTree 性能优化完整演进史
开发语言·前端
optimistic_chen2 小时前
【Redis 系列】常用数据结构---String类型
数据结构·数据库·redis·缓存·string
wen__xvn2 小时前
代码随想录算法训练营DAY1第一章 数组part01
数据结构·算法·leetcode
kevin_水滴石穿2 小时前
C#获取程序集和文件版本
开发语言·c#
we1less2 小时前
[audio] AudioTrack (五) 共享内存创建分析
android·java·开发语言
樊梓慕2 小时前
【嵌入式】buildroot构建ros2环境
c++·机器人
爱编码的傅同学2 小时前
【程序地址空间】页表的映射方式
c语言·数据结构·c++·算法
序属秋秋秋2 小时前
《Linux系统编程之进程控制》【进程替换】
linux·c语言·c++·操作系统·进程·系统编程·进程替换