unordered_set哈希集合——像超级快速的储物柜
较难7unordered_set哈希集合——像超级快速的储物柜
你有没有遇到过这种情况:老师让你检查班级里有没有重复的学号?或者游戏里需要记录你已经访问过的关卡,避免重复进入?这时候,你需要一种能够快速查找、并且自动去重的“神奇盒子”。在计算机里,这个盒子就是 哈希集合(Hash Set)。它像一台超级智能的储物柜:无论你要存放什么东西(数字、字符串、甚至自定义对象),它都能瞬间计算出应该放在哪个柜子,而且如果两样东西一模一样,它会告诉你“这个已经有了”。这种数据结构能让你用极快的速度(平均只需一步)完成插入、删除和查找。
今天我们就来认识两种常见的哈希集合:C++ 的 unordered_set 和 Python 的 set。它们都基于哈希表(hash table)实现,拥有唯一、无序、快速查找的特点。
从生活中的例子引入
想象一下,你有一个超级大的储物柜,每个柜子都有一个号码。但不同的是,这个储物柜有一个神奇的本领:无论你要存放什么东西,它都能瞬间计算出一个号码,然后直接把东西放进对应的柜子里。而且,如果两个一模一样的东西想要放进去,储物柜会告诉你“这个已经存在了,不能再放”。这就像我们去图书馆借书,每本书都有唯一的条码,如果扫到同样的条码,系统会说“这本书已经借出了”。这种只能存一份、查找超级快的数据结构,就是计算机中的“哈希集合”。
在C++中,unordered_set(无序集合)就是一种哈希集合。它就像那个神奇的储物柜,里面的每个元素都是独一无二的(不能重复),而且存储的顺序和插入的顺序无关(无序)。你可以很快地检查某个元素是否存在,插入一个新元素,或者删除一个元素——这些操作平均只需要常数时间(O(1))。
理解STL的unordered_set原理
unordered_set底层使用哈希表(hash table)。哈希表工作的核心是哈希函数:给定一个元素,哈希函数计算出一个整数(哈希值),这个整数决定了元素应该放在哈希表的哪个“桶”(bucket)里。因为哈希函数设计得好的话,不同的元素会被均匀分配到不同的桶,这样查找时只需要到对应的桶里找,不需要遍历整个表,所以非常快。
但是,不同的元素也可能被计算到同一个桶(这叫“哈希冲突”),不过别担心,C++的unordered_set内部会处理冲突(通常用链表法或开放地址法)。你只需要知道:平均情况下插入、删除、查找都是O(1)的时间复杂度,但最坏情况下(比如所有元素都冲突到同一个桶)会退化成O(n),不过实际使用中很少发生。
unordered_set最重要的特点是:元素不可重复。如果你想放一个已经存在的元素,插入操作会失败(不会覆盖原有元素)。另外,元素没有固定的顺序,所以你不能像使用vector那样用下标访问。
哈希函数和冲突解决——深入浅出
哈希函数就像一个“指纹生成器”。对于整数,它可能直接取模;对于字符串,它会根据每个字符计算出一个数字。好的哈希函数会让不同的元素尽量得到不同的桶号,减少冲突。当冲突发生时(两个不同的元素被分到了同一个桶),unordered_set会在该桶内用链表(或红黑树)把所有冲突的元素串起来。这意味着:如果很多元素都挤到同一个桶,查找就会变慢(需要遍历那个链表)。但因为哈希函数设计得够好,这种情况很少出现。
一个小比喻:每个桶好比一个乒乓球筐,哈希函数给每个球分配一个筐号。如果筐号分配得均匀,每个筐里只有一两个球,找球就很快;如果分配得很差,所有球都掉进同一个筐里,那就得在一堆球里翻来翻去。
常用成员函数一览
| 函数 | 说明 | 示例 |
|---|---|---|
insert(val) | 插入元素val,如果已存在则什么都不做。返回pair<iterator,bool>,bool表示是否成功插入 | mySet.insert(42); |
erase(val) | 删除值为val的元素,返回删除的元素个数(0或1) | mySet.erase(42); |
find(val) | 查找元素,返回迭代器,如果找不到则返回end() | auto it = mySet.find(42); |
count(val) | 返回值为val的元素个数(因为集合唯一,所以要么0要么1) | if (mySet.count(42)) ... |
size() | 返回集合中元素个数 | int n = mySet.size(); |
empty() | 判断集合是否为空 | if (mySet.empty()) ... |
clear() | 清空所有元素 | mySet.clear(); |
begin() / end() | 获取开始/结束迭代器,用于遍历 | for (auto& x : mySet) ... |
注意:unordered_set的迭代器是前向迭代器,可以遍历,但不能随机访问(不支持it + n操作)。
C++完整代码实现(带详细注释)
下面是一个完整的C++程序,演示unordered_set的基本用法,包括插入、查找、删除和遍历。
#include <iostream>
#include <unordered_set> // 使用unordered_set需要包含此头文件
using namespace std;
int main() {
// 创建一个存储整数的unordered_set
unordered_set<int> mySet;
// 插入一些元素
mySet.insert(10);
mySet.insert(20);
mySet.insert(30);
mySet.insert(20); // 重复插入20,因为20已存在,这次插入会失败
// 使用insert的返回值来检查是否真的插入了
auto result = mySet.insert(20);
if (result.second == false) {
cout << "20已经存在,第二次插入失败了" << endl;
}
// 打印当前集合的大小
cout << "集合中元素个数: " << mySet.size() << endl; // 输出3
// 查找元素:find返回迭代器,如果等于end()说明没找到
int key = 20;
auto it = mySet.find(key);
if (it != mySet.end()) {
cout << "找到了元素: " << *it << endl;
} else {
cout << "没有找到元素: " << key << endl;
}
// 另一种查找方法:count
if (mySet.count(30) > 0) {
cout << "30存在于集合中" << endl;
}
// 删除元素
mySet.erase(10); // 删除值为10的元素
cout << "删除后大小: " << mySet.size() << endl; // 输出2
// 遍历集合(注意顺序是不确定的)
cout << "当前集合中的元素:";
for (int x : mySet) {
cout << " " << x;
}
cout << endl;
// 清空集合
mySet.clear();
cout << "清空后是否为空: " << (mySet.empty() ? "是" : "否") << endl;
return 0;
}
运行结果示例(每次可能不一样,因为顺序随机):
20已经存在,第二次插入失败了
集合中元素个数: 3
找到了元素: 20
30存在于集合中
删除后大小: 2
当前集合中的元素: 30 20
清空后是否为空: 是
存储自定义类型
unordered_set还可以存储自定义类型(比如结构体),但需要提供哈希函数和相等比较。稍后会在第四个知识点中详细讲解。这里简单提一下:如果你存储的是指针,注意指针地址比较。
时间复杂度总结
- 插入操作:平均O(1),最坏O(n)
- 删除操作:平均O(1),最坏O(n)
- 查找操作:平均O(1),最坏O(n)
- 遍历操作:O(n)
Python等价功能的代码实现(带详细注释)
Python中的set(集合)就是哈希集合,和C++的unordered_set非常相似。下面演示同样的功能:
# Python的set就是哈希集合
my_set = set() # 创建一个空集合
# 插入元素
my_set.add(10)
my_set.add(20)
my_set.add(30)
result = my_set.add(20) # 重复添加20,不会报错,但集合仍然只有一个20
# 注意:add没有返回值,但我们可以通过len检查
print("集合中元素个数:", len(my_set)) # 输出3
# 查找元素:用in运算符
key = 20
if key in my_set:
print("找到了元素:", key)
else:
print("没有找到元素:", key)
# also可以用count? 实际上可以用in, 没有count方法, 但可以:
print("20是否存在:", key in my_set)
# 删除元素
my_set.discard(10) # discard不会报错即使不存在,remove会报错
# 或者 my_set.remove(10) # 如果10不存在会抛出KeyError
print("删除后大小:", len(my_set)) # 输出2
# 遍历集合(顺序不确定)
print("当前集合中的元素:", end=" ")
for x in my_set:
print(x, end=" ")
print()
# 清空集合
my_set.clear()
print("清空后是否为空:", len(my_set) == 0) # 输出True
Python的set简单易用,很多内建操作如in、len等,不需要像C++那样处理迭代器。另外,Python set也支持集合运算:并集|、交集&、差集-、对称差^。
Python集合运算的实用例子
想象你在整理两本课外书里的生词:
book1 = {"apple", "banana", "cherry", "date"}
book2 = {"banana", "date", "elderberry", "fig"}
# 并集:两本书里出现过的所有生词
all_words = book1 | book2 # 或 book1.union(book2)
print("所有生词:", all_words)
# 交集:两本书都出现的生词
common = book1 & book2 # 或 book1.intersection(book2)
print("共同生词:", common)
# 差集:只在第一本书里出现的生词
only_book1 = book1 - book2 # 或 book1.difference(book2)
print("只在第一本中:", only_book1)
# 对称差:两本书中不重复出现的生词
unique_per_book = book1 ^ book2 # 或 book1.symmetric_difference(book2)
print("各自独有的生词:", unique_per_book)
输出:
所有生词: {'cherry', 'elderberry', 'fig', 'date', 'banana', 'apple'}
共同生词: {'date', 'banana'}
只在第一本中: {'cherry', 'apple'}
各自独有的生词: {'elderberry', 'cherry', 'apple', 'fig'}
这些运算在去重、比较数据时非常有用。
新手容易犯的错误
-
误以为元素有序
unordered_set中的元素没有固定顺序,不能像数组一样用下标访问。如果你需要有序的集合,应该使用set(红黑树实现,插入删除O(log n))。 -
插入重复元素但以为会覆盖
在unordered_set中,重复插入同一个值不会覆盖已有元素,而是什么也不做。如果你需要允许重复,应该用unordered_multiset。 -
在遍历时修改元素
你不能通过迭代器直接修改集合中的元素(因为修改后可能改变它的哈希值,导致找不到)。如果要修改,必须先删除原元素再插入新元素。 -
忘记包含头文件
C++中使用unordered_set必须包含<unordered_set>头文件,否则编译会报错。 -
自定义类型未提供哈希函数
如果你打算在unordered_set中存储自定义的结构体,必须提供哈希函数和相等比较函数(重载operator==)。否则编译器会报错。例如:
struct Student {
int id;
string name;
// 需要提供相等比较
bool operator==(const Student& other) const {
return id == other.id;
}
};
// 自定义哈希函数
struct StudentHash {
size_t operator()(const Student& s) const {
return hash<int>()(s.id);
}
};
unordered_set<Student, StudentHash> roster;
-
误用
count来检查元素是否存在(C++)
count返回0或1,可以判断是否存在。但要注意:有些新手会写成if (mySet.count(key) == 1),这没错,但更推荐用if (mySet.find(key) != mySet.end()),因为count有时会遍历整个桶(在多集合中可能效率低),不过对于unordered_set差别不大。 -
Python中
remove与discard混用
remove如果元素不存在会抛出KeyError;discard则静默处理。如果你不确定元素是否存在,建议用discard,或者先用in检查。
完整示例:用哈希集合作班级点名去重
假设老师要统计全班同学的学号,防止重复登记。下面是一个C++示例:
#include <iostream>
#include <unordered_set>
#include <vector>
using namespace std;
int main() {
// 假设已登记的学号列表(可能有重复)
vector<int> registered = {101, 102, 103, 101, 104, 102, 105};
// 使用unordered_set去重
unordered_set<int> unique_ids;
for (int id : registered) {
// insert返回pair,second表示是否插入成功
auto result = unique_ids.insert(id);
if (!result.second) {
cout << "学号 " << id << " 已存在,跳过重复" << endl;
}
}
cout << "最终不重复的学号个数: " << unique_ids.size() << endl;
cout << "学号列表: ";
for (int id : unique_ids) {
cout << id << " ";
}
cout << endl;
// 快速查找某个学号是否存在
int target = 103;
if (unique_ids.find(target) != unique_ids.end()) {
cout << "学号 " << target << " 在集合中" << endl;
}
return 0;
}
输出:
学号 101 已存在,跳过重复
学号 102 已存在,跳过重复
最终不重复的学号个数: 5
学号列表: 105 104 103 102 101
学号 103 在集合中
总结要点和注意事项
unordered_set的核心是哈希表,提供平均常数时间的插入、删除、查找,但元素顺序不确定,且不能重复。- 使用场景:当你需要快速判断一个元素是否存在,并且不需要元素有顺序时,
unordered_set是理想选择。例如:记录已访问过的节点、去重等。 - C++与Python差异:
- C++的
insert返回一个pair,可以用来判断是否插入成功;Python的add直接修改集合,不返回状态。 - C++用
find或count查找;Python用in运算符更直观。 - C++需要包含头文件
<unordered_set>;Python的set是内置类型。
- C++的
- 性能注意:虽然平均很快,但哈希函数的选择会影响性能。如果哈希函数很差,会导致大量冲突,退化成链表。C++标准库的实现会自动处理,但自定义类型时需要自己提供好的哈希函数。
- 不可存放重复元素:如果同一个值(根据相等比较)多次插入,只有第一次有效。
- 迭代器稳定性:插入操作可能会导致哈希表rehash(重新分配桶),从而使已有迭代器失效。删除操作通常不会导致迭代器失效,除非被删除的正是迭代器指向的元素。
相关指引
掌握了unordered_set,你就拥有了一个“超级快速查找”的利器。接下来建议学习:
unordered_map(哈希映射):相当于给每个元素关联了一个“值”,就像给储物柜里的每个物品贴上了标签。它和unordered_set类似,但能存储键值对。set(有序集合):如果需要元素按顺序排列(比如从小到大),可以用set,但它的插入和查找速度是O(log n),比哈希集合慢一些。unordered_multiset/multiset:如果你需要允许重复元素的集合,可以用它们(前者哈希,后者有序)。
在Python中,除了set,还可以了解frozenset(不可变集合,可以作为字典的键)以及collections.Counter(用于计数,更像一个允许重复的集合)。
继续探索,你会发现这些容器就像万能工具箱,总有一款适合你的问题!
例题精讲
关于C++中的unordered_set容器,以下哪个说法是正确的?
在Python中,set类型的行为与C++的unordered_set类似,都支持元素的唯一性和快速查找,但不保持元素的顺序。
以下C++代码试图统计一个数组中不同整数的个数,请补全空缺部分。<br><code>int countDistinct(vector<int>& nums) {
___ st;
for (int x : nums) {
st.insert(x);
}
return st.size();
}</code>下列关于unordered_set和set的说法中,错误的是?
以下Python代码使用set(类似于unordered_set)去除列表中的重复元素,请补全空缺。<br><code>def remove_duplicates(lst):
return list(___)</code>