【C++】unordered_map、unordered_set 的使用
文章目录
- 1. `unordered_set` 和 `unoredered_map` 的概念
- 1.1 基本概念
- 1.2 与 map、set 的对比
- 2. `unordered_set` 和 `unoredered_map` 的常见使用
- 2.1 构造函数
- 2.1.1 `unordered_set` 的构造函数
- 2.1.2 `unordered_map` 的构造函数
- 2.2 常用接口
- 2.2.1 插入操作
- 2.2.1.1 insert 函数
- 2.2.1.2 emplace 函数
- 2.2.2 查找
- 2.2.2.1 find 函数
- 2.2.2.2 count 函数
- 2.2.3 operator[]
- 2.2.4 删除操作
- 2.2.4.1 erase 删除元素
- 2.2.4.2 clear 函数
- 2.3 C++ 手册中的 `unordered_map`、`unordered_set`
- 2.3.1 自定义哈希函数
- 2.3.2 自定义比较函数
1. unordered_set
和 unoredered_map
的概念
unordered_map
和 unordered_set
是 C++ 标准库中基于哈希表实现的容器,适用于高效查找、插入和删除操作,不保证元素顺序。
1.1 基本概念
unordered_map
:- 存储键值对(key-value),键唯一,值可重复。
- 通过哈希函数将键映射到桶(bucket),支持快速访问值(平均 O(1) 时间复杂度)。
- 适用场景:统计频率、缓存键值数据等(如统计单词出现次数)。
unordered_set
:- 仅存储唯一键的集合,不允许重复元素。
- 同样基于哈希表,用于快速检查元素是否存在(如过滤重复元素)。
1.2 与 map、set 的对比
特性 | unordered_map/unordered_set | map/set |
---|---|---|
底层实现 | 哈希表 | 红黑树 |
元素顺序 | 无序 | 按键排序 |
查找时间复杂度 | 平均O(1),最坏O(n) | O(logN) |
插入/删除时间复杂度 | 平均O(1),最坏O(n) | O(logN) |
内存开销 | 较高(需要维护桶结构) | 较低 |
是否需要哈希函数 | 是(需要为键定义哈希函数) | 否(需要重载比较运算符) |
2. unordered_set
和 unoredered_map
的常见使用
2.1 构造函数
2.1.1 unordered_set
的构造函数
构造函数 | 接口说明 |
---|---|
unordered_set() | 构造一个空的 unordered_set |
unordered_set(InputIterator first, InputIterator last) | 使用 [first, last) 区间的元素初始化容器。 |
unordered_set(const unordered_set& us) | 拷贝构造,生成与 us 相同的容器。 |
unordered_set(std::initializer_list<value_type> il) | 使用初始化列表构造容器。 |
使用示例:
#include <iostream>
using namespace std;
#include <unordered_set>
#include <vector>int main()
{unordered_set<int> s1; // 构造空的容器vector<int> v = { 7,3,6,2,8,9 };unordered_set<int> s2(v.begin(), v.end()); // 迭代器区间构造unordered_set<int> s3 = s2; // 拷贝构造,也可写成 s3(s2)unordered_set<int> s4 = { 2,6,9,4,7,1 }; // 初始化列表构造return 0;
}
2.1.2 unordered_map
的构造函数
构造函数 | 接口说明 |
---|---|
unordered_map() | 构造一个空的 unordered_map |
unordered_map(InputIterator first, InputIterator last) | 使用 [first, last) 区间的元素初始化容器。 |
unordered_map(const unordered_map& um) | 拷贝构造,生成与 us 相同的容器。 |
unordered_map(std::initializer_list<value_type> il) | 使用初始化列表构造容器。 |
使用示例:
#include <iostream>
using namespace std;
#include <unordered_map>
#include <vector>int main()
{unordered_map<string, string> m1; // 构造空的容器vector<pair<string, string>> v = { {"left", "左"}, {"right", "右"},{"hello", "你好"}, {"zkp", "ZKP"} };unordered_map<string, string> m2(v.begin(), v.end()); // 迭代器区间构造unordered_map<string, string> m3 = m2; // 拷贝构造,也可写成 m3(m2)unordered_map<string, string> m4 = { {"left", "左"}, {"right", "右"},{"hello", "你好"}, {"zkp", "ZKP"} }; // 初始化列表构造return 0;
}
2.2 常用接口
2.2.1 插入操作
2.2.1.1 insert 函数
返回值:
pair<iterator, bool>
时间复杂度:O(1),哈希冲突过多可能会导致退化为 O(N)
适用场景:
- 插入单个元素或范围元素
- 避免覆盖已存在的键(
unordered_map
)或元素(unordered_set
)
unordered_map
// unordered_map
int main()
{unordered_map<string, int> m;m.insert({ "left", 1 }); // 插入元素m.insert({ "right", 2 }); // 正常插入m.insert({ "left", 3 }); // 再插入和前面相同键的元素会导致插入失败auto it = m.begin();while (it != m.end()){cout << it->first << ": " << it->second << endl;++it;}return 0;
}
unordered_set
// unordered_set
int main()
{unordered_set<int> s;s.insert(1); // 插入元素s.insert(2); // 正常插入pair<unordered_set<int>::iterator, bool> flag = s.insert(1); // 再插入和前面相同键的元素会导致插入失败// auto result = s.insert(1); 也可以这么写if (flag.second)cout << "insert sucess" << endl;elsecout << "insert fail" << endl;return 0;
}
2.2.1.2 emplace 函数
emplace
是 unordered_map
和 unordered_set
等容器提供的高效插入方法,它允许直接在容器内部构造元素,避免临时对象的创建和拷贝/移动开销。
用法、返回值什么的其实和 insert
基本上一样,用来提高插入效率的。
但是,实际上的用法还是有一点点区别,主要体现在初始化列表上。
例如,当你尝试 emplace({"left, 1})
时:
{"left", 1}
是一个 初始化列表(std::initializer_list
),它会被编译器视为一个单一参数。emplace
的模板参数推导需要将参数分解为 键和值的独立构造参数,而初始化列表无法被分解为多个独立参数。
对比 insert
为什么 insert
可以使用 insert({"left", 1})
,而 emplace 不能呢?
下面来说说我自己的理解:
insert 支持 insert({“left”, 1}),是因为它的参数是 一个已经构造好的元素
这句话的意思是,insert 它支持是因为它已经构造好了一个元素,可以直接使用参数进行初始化;
而emplace
的参数仅仅只是参数,需要使用参数进行构造并初始化,所以emplace
无法推导列表中的参数到底是什么类型
也就是说,emplace
是直接在容器里面进行构造,然后再通过我们传递的参数对构造出来的元素进行初始化
unordered_map
int main()
{unordered_map<string, int> m;//m.emplace({ "left", 1 }); // 这样是不行的m.emplace("left", 1); // 插入元素m.emplace("right", 2 ); // 正常插入m.emplace("left", 3 ); // 再插入和前面相同键的元素会导致插入失败auto it = m.begin();while (it != m.end()){cout << it->first << ": " << it->second << endl;++it;}return 0;
}
unordered_set
int main()
{unordered_set<int> s;s.emplace(1); // 插入元素s.emplace(2); // 正常插入pair<unordered_set<int>::iterator, bool> flag = s.emplace(1); // 再插入和前面相同键的元素会导致插入失败// auto result = s.emplace(1); 也可以这么写if (flag.second)cout << "insert sucess" << endl;elsecout << "insert fail" << endl;return 0;
}
2.2.2 查找
2.2.2.1 find 函数
iterator find(const Key& key); // 非 const 版本
const_iterator find(const Key& key) const; // const 版本
参数
- key:要查找的键(Key 类型)。
返回值
- 如果找到键,返回指向该键值对的 迭代器(iterator 或 const_iterator)。
- 如果未找到键,返回 end() 迭代器。
哈希表的查找接口比较简单,直接看代码。
unordered_map
int main()
{unordered_map<string, int> m;m.emplace("left", 1); m.emplace("right", 2 ); m.emplace("zkp", 3 ); auto result = m.find("zkp");if (result != m.end())cout << "Found: " << result->second << endl;elsecout << "Not Found" << endl;return 0;
}
unordered_set
int main()
{unordered_set<int> s;s.emplace(1); s.emplace(2); auto result = s.find(3);if (result != s.end())cout << "Found: " << *result << endl;elsecout << "Not Found" << endl;return 0;
}
2.2.2.2 count 函数
count 函数看名字就知道是统计某一个键值出现的次数,但是 unordered_map
、unordered_set
都是不允许重复键值的容器,所以它的值只会是 0 或 1。我们平常在unordered_map
、unordered_set
中使用的查找更多是 find
,count
函数一般是在允许重复键值的容器中使用,如:unordered_multimap
、unordered_multiset
// unordered_multiset
int main()
{unordered_multiset<int> ms;ms.insert(1);ms.insert(1);ms.insert(1);cout << ms.count(1) << endl;return 0;
}
2.2.3 operator[]
这个是 unordered_map
的特性,unordered_set
仅存储唯一元素,只有键,没有值的概念,因此不需要 operator[]
。
我们之前在讲 map
容器的时候说过,map
容器允许直接使用 operator[]
进行插入、查找、修改的行为,这个 unordered_map
容器一样允许插入、查找、修改。
操作 | 行为 | 适用场景 |
---|---|---|
插入 | 键不存在时自动插入,值初始化 | 快速插入键值对 |
修改 | 键存在时直接修改值 | 更新已有的键 |
查找 | 键存在时返回值,不存在插入时有副作用 | 需谨慎!查找还是建议用find |
因为只要键值不存在,你通过operator[]查找 Key 的时候,是会插入的,所以还是建议使用 find
int main()
{unordered_map<string, int> m;m["zkp"]++; // 键不存在,插入cout << m["zkp"] << endl; // 查找m["zkp"]++; // 键存在,修改值cout << m["zkp"] << endl;m["zkp"] = 3; // 键存在,修改值cout << m["zkp"] << endl;if (!m["ZKP"]) // 因为 "ZKP" 这个键并不存在,这里会直接插入 "ZKP",并将它的值初始化为 0cout << m["ZKP"] << endl;return 0;
}
2.2.4 删除操作
2.2.4.1 erase 删除元素
erase 常用下面几种
函数名 | 功能 | 返回值 |
---|---|---|
iterator erase(iterator pos) | 删除迭代器 pos 指向的元素 | 返回下一个有效迭代器 |
size_type erase(const Key& key) | 删除键为 Key 的元素 | 返回删除的元素个数 |
unordered_map
int main()
{unordered_map<string, int> m;m["zkp"] = 1;m["Zkp"] = 2;m["ZKp"] = 3;m["ZKP"] = 4;m.erase("zkp"); // 删除键为 "zkp" 的元素auto pos = m.find("Zkp");m.erase(pos); // 删除迭代器为 pos 的元素for (auto& it : m)cout << it.first << ": " << it.second << endl;return 0;
}
unordered_set
int main()
{unordered_set<int> s = { 1,2,3,4,5,6 };s.erase(2);auto pos = s.find(5);s.erase(pos);for (auto x : s)cout << x << " ";cout << endl;return 0;
}
2.2.4.2 clear 函数
和其他容器的 clear
函数一样,都是清空容器,而非销毁容器。
2.3 C++ 手册中的 unordered_map
、unordered_set
我们可以看到,它们的模板参数除了 K、T,还多了 Hash
(允许你自定义哈希函数),Pred
(允许你自定义比较函数),Alloc
(允许你自定义申请资源的方式)。
这里我们来讲讲自定义哈希函数和自定义比较函数。
2.3.1 自定义哈希函数
其实就是通过仿函数重定义操作,直接看代码吧。
#include <iostream>
#include <unordered_set>
#include <functional>struct Point {int x, y;bool operator==(const Point& other) const {return x == other.x && y == other.y;}
};// 自定义哈希函数
namespace std {template <>struct hash<Point> {size_t operator()(const Point& p) const {// 结合 x 和 y 的哈希值生成唯一的哈希码return hash<int>()(p.x) ^ (hash<int>()(p.y) << 1);}};
}int main() {std::unordered_set<Point> pointSet;pointSet.emplace(Point{ 1, 2 });pointSet.emplace(Point{ 3, 4 });if (pointSet.find(Point{ 1, 2 }) != pointSet.end()) {std::cout << "Point (1, 2) exists in the set." << std::endl;}
}
2.3.2 自定义比较函数
#include <iostream>
#include <unordered_map>
using namespace std;// 用户定义类型
struct User {string name;int age;// 构造函数初始化成员变量User(string _name, int _age) : name(_name), age(_age) {}bool operator==(const User& other) const {return name == other.name && age == other.age;}
};// 散列函数
namespace std {template <>struct hash<User> {size_t operator()(const User& u) const {return hash<string>()(u.name) ^ hash<int>()(u.age);}};
}int main() {unordered_map<User, int, hash<User>> userMap;// 插入一些数据userMap[{User("John", 30)}] = 1;userMap[{User("Jane", 25)}] = 2;// 查找某个用户是否存在auto searchKey = User("John", 30);if (userMap.find(searchKey) != userMap.end()) {cout << "Found John!" << endl;}return 0;
}