C++ 学习杂记06:std::unordered_map

概述

std::unordered_map是C++标准模板库(STL)中的一个关联容器,实现基于哈希表的键值对映射。自C++11起成为标准库的一部分,位于 <unordered_map>头文件中。

核心特性

数据结构

  • 基于哈希表:使用散列函数将键映射到存储桶(bucket)

  • 无序存储:元素不以特定顺序存储,遍历顺序不确定

  • 唯一键:每个键在容器中只能出现一次

  • 平均常数时间复杂度:插入、删除、查找操作平均O(1),最坏情况O(n)

模板声明

复制代码
复制代码
复制代码
template<
    class Key,
    class T,
    class Hash = std::hash<Key>,
    class KeyEqual = std::equal_to<Key>,
    class Allocator = std::allocator<std::pair<const Key, T>>
> class unordered_map;

基本操作

1. 构造与初始化

复制代码
复制代码
复制代码
// 默认构造
std::unordered_map<std::string, int> map1;

// 初始化列表构造
std::unordered_map<std::string, int> map2 = {
    {"apple", 1},
    {"banana", 2},
    {"orange", 3}
};

// 范围构造
std::vector<std::pair<std::string, int>> vec = {{"a", 1}, {"b", 2}};
std::unordered_map<std::string, int> map3(vec.begin(), vec.end());

// 拷贝构造
std::unordered_map<std::string, int> map4(map2);

2. 元素访问

复制代码
复制代码
复制代码
// operator[] - 访问或插入(键不存在时插入默认值)
int value = map["apple"];  // 如果不存在,会插入{"apple", 0}
map["apple"] = 10;         // 修改或插入

// at() - 边界检查访问
try {
    int val = map.at("apple");  // 存在则返回
} catch (const std::out_of_range& e) {
    // 键不存在时抛出异常
}

// find() - 安全查找
auto it = map.find("apple");
if (it != map.end()) {
    // 找到元素,it->first是键,it->second是值
}

3. 修改操作

复制代码
复制代码
复制代码
// 插入元素
auto result = map.insert({"apple", 1});  // 返回pair<iterator, bool>
map.emplace("banana", 2);                // 原地构造,更高效

// 插入或赋值 (C++17)
map.insert_or_assign("apple", 3);  // 存在则赋值,不存在则插入

// 删除元素
map.erase("apple");                 // 按键删除
auto it = map.find("banana");
if (it != map.end()) {
    map.erase(it);                  // 按迭代器删除
}
map.erase(map.begin(), map.end());  // 范围删除

// 清空
map.clear();

4. 容量查询

复制代码
复制代码
复制代码
bool empty = map.empty();     // 是否为空
size_t size = map.size();     // 元素数量
size_t max = map.max_size();  // 理论最大容量

哈希表特定操作

1. 桶接口

复制代码
复制代码
复制代码
// 桶数量相关
size_t bucket_count = map.bucket_count();  // 桶总数
size_t bucket_idx = map.bucket("apple");   // 键所在的桶索引

// 桶信息
size_t bucket_size = map.bucket_size(0);   // 指定桶中的元素数

// 负载因子
float load_factor = map.load_factor();     // 平均每个桶的元素数
float max_lf = map.max_load_factor();      // 最大负载因子
map.max_load_factor(0.75f);                // 设置最大负载因子

2. 哈希策略

复制代码
复制代码
复制代码
// 调整桶数量
map.rehash(100);           // 确保至少有100个桶
map.reserve(1000);         // 确保能容纳1000个元素而不超过最大负载因子

迭代器

类型

复制代码
复制代码
复制代码
// 迭代器类型
std::unordered_map<std::string, int>::iterator it;
std::unordered_map<std::string, int>::const_iterator cit;
std::unordered_map<std::string, int>::local_iterator lit;  // 桶内迭代器

遍历方式

复制代码
复制代码
复制代码
// 1. 基于范围的for循环 (C++11)
for (const auto& [key, value] : map) {  // C++17结构化绑定
    // 使用key和value
}

// 2. 迭代器遍历
for (auto it = map.begin(); it != map.end(); ++it) {
    const auto& key = it->first;
    const auto& value = it->second;
}

// 3. 遍历所有桶
for (size_t i = 0; i < map.bucket_count(); ++i) {
    for (auto lit = map.begin(i); lit != map.end(i); ++lit) {
        // 遍历第i个桶
    }
}

自定义类型作为键

1. 定义哈希函数

复制代码
复制代码
复制代码
struct Person {
    std::string name;
    int age;
    
    bool operator==(const Person& other) const {
        return name == other.name && age == other.age;
    }
};

// 自定义哈希函数
struct PersonHash {
    size_t operator()(const Person& p) const {
        return std::hash<std::string>()(p.name) ^ 
               (std::hash<int>()(p.age) << 1);
    }
};

// 使用自定义哈希
std::unordered_map<Person, std::string, PersonHash> person_map;

2. 使用标准库组合哈希 (C++14)

复制代码
复制代码
复制代码
#include <functional>

struct PersonHash {
    size_t operator()(const Person& p) const {
        return std::hash<std::string>{}(p.name) ^ 
               (std::hash<int>{}(p.age) << 1);
    }
};

性能特性

时间复杂度

操作 平均情况 最坏情况
插入 O(1) O(n)
删除 O(1) O(n)
查找 O(1) O(n)
遍历 O(n) O(n)

空间复杂度

  • 需要额外的空间存储桶数组和链表指针

  • 通常比std::map占用更多内存

std::map对比

特性 std::unordered_map std::map
底层结构 哈希表 红黑树
顺序 无序 按键排序
操作复杂度 平均O(1) O(log n)
内存使用 通常更高 通常更低
迭代器稳定性 插入可能使所有迭代器失效 除删除元素外迭代器稳定
自定义键要求 需要哈希函数 需要比较函数

最佳实践

1. 预分配空间

复制代码
复制代码
复制代码
std::unordered_map<int, std::string> map;
map.reserve(1000);  // 预分配,避免多次rehash

2. 选择合适的哈希函数

  • 对于整数类型,标准哈希通常足够

  • 对于字符串,考虑使用更好的分布

  • 避免哈希冲突过多

3. 查找优化

复制代码
复制代码
复制代码
// 避免重复查找
auto it = map.find(key);
if (it != map.end()) {
    // 使用it->second
} else {
    // 键不存在
}

// 而不是:
if (map.count(key) > 0) {  // 额外查找
    int value = map[key];   // 再次查找
}

4. 遍历时修改

复制代码
复制代码
复制代码
// 安全删除遍历中的元素
for (auto it = map.begin(); it != map.end(); ) {
    if (condition) {
        it = map.erase(it);  // erase返回下一个有效迭代器
    } else {
        ++it;
    }
}

线程安全性

  • 多个线程可以同时读取容器

  • 如果有任何线程修改容器,所有线程访问都需要同步

  • 使用std::shared_mutex(C++17)或互斥锁保护并发访问

特殊成员函数 (C++17)

1. extract()- 节点句柄

复制代码
复制代码
复制代码
// 提取节点而不重新分配内存
auto nh = map.extract("apple");
if (!nh.empty()) {
    nh.key() = "new_apple";  // 修改键
    map.insert(std::move(nh));  // 重新插入
}

2. merge()- 合并容器

复制代码
复制代码
复制代码
std::unordered_map<std::string, int> map1, map2;
// ... 填充数据
map1.merge(map2);  // 尝试合并所有元素

常见问题与解决方案

1. 哈希冲突过多

  • 调整负载因子

  • 使用更好的哈希函数

  • 增加桶数量

2. 自定义键的哈希质量差

  • 确保哈希函数分布均匀

  • 考虑使用现成的哈希组合库

3. 迭代器失效

  • 插入操作可能使所有迭代器失效

  • 删除操作只使指向被删元素的迭代器失效

总结

std::unordered_map提供了高效的键值对存储,适合需要快速查找而不关心顺序的场景。正确使用时,其性能通常优于基于树的std::map。使用时需注意内存使用、哈希函数质量和线程安全性。

相关推荐
t***5441 小时前
如何在 Dev-C++ 中设置 MinGW 和 Clang 的路径
java·前端·c++
cpp_25012 小时前
P2722 [USACO3.1] 总分 Score Inflation
数据结构·c++·算法·动态规划·题解·洛谷·背包dp
t***5442 小时前
如何在 Dev-C++ 中配置 Clang 编译器集
开发语言·c++
王老师青少年编程2 小时前
csp信奥赛C++高频考点专项训练之贪心算法 --【删数问题】:删数问题
c++·算法·贪心·csp·信奥赛
qq_254617772 小时前
attribute((constructor)) 在C/C++中的应用
开发语言·c++
云深麋鹿2 小时前
C++ | 多态
开发语言·c++
故事还在继续吗3 小时前
C++17关键特性
开发语言·c++·算法
Rabitebla3 小时前
【数据结构】消失的数字+ 轮转数组:踩坑详解
c语言·数据结构·c++·算法·leetcode
Queenie_Charlie3 小时前
关于二叉树(2)
数据结构·c++·二叉树·简单树结构