哈希表这个专题,我在刷题和实际项目里反复绕回来好几次。每次遇到“判断是否存在”“统计出现次数”“快速去重”这类需求,第一反应就是它。但真正让我觉得“懂它了”,不是记住那些API怎么调用,而是想明白它底层在干什么、什么场景会退化、什么时候该换别的结构。这篇就把我自己的理解从头捋一遍,结合C++的工程实践和刷题中的高频考法,聊点实在的。
1. 设计动机与核心本质:为什么“直接寻址”能改变查找的效率
1.1 从数组下标到“任意键值”:哈希表究竟解决了什么问题
数组为什么查找快?因为它按下标定位,O(1)。但数组的下标有天然局限——必须是整数,而且范围不能太离谱。实际业务里,我们想找的往往是“某个字符串出现过没有”“某个ID对应的对象是谁”,这就不能直接用数组下标了。
哈希表做的事,本质上就是“把任意类型的键,通过一个函数映射成数组下标”。这个函数叫哈希函数,映射出来的下标叫哈希值,底层的数组叫桶数组。所以哈希表 = 哈希函数 + 数组存储,它继承了数组O(1)随机访问的优势,又打破了键必须是连续整数的限制。
我经常拿它跟查字典类比:如果字典按拼音首字母分了好几百个格子,你查“哈希”这个词,先算它的首字母是H,直接去H那一格翻,而不是从第一页往后找。哈希函数就是那个“算首字母”的规则,格子就是桶。
1.2 哈希表的复杂度真相:表面上O(1),实际是“均摊O(1)”
很多初学者背过结论——“哈希表查找O(1)”。这句话其实隐藏了一个前提:哈希函数要足够均匀,冲突要足够少。哈希表的复杂度是均摊O(1),也就是绝大多数操作直接命中,偶尔遇到冲突要在桶里多找几步。极端情况下,如果所有键都映射到同一个桶,哈希表就退化成链表/红黑树,复杂度变成O(n)或O(log n)。
所以哈希表的神奇不是魔法,而是概率与设计的平衡。一个合格的哈希表实现,要保证两件事:哈希函数分布均匀,冲突处理策略高效。这两点也正是我刷题和写代码时踩坑最多的地方。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. C++中哈希表的工程实现:unordered_map与unordered_set
2.1 C++标准库容器的选择:什么时候用map,什么时候用unordered_map
C++里最容易搞混的,就是std::map和std::unordered_map。很多初学者以为它们差不多,其实底层机制天差地别:
| 容器 | 底层结构 | 查找复杂度 | 元素顺序 | 适用场景 |
|---|---|---|---|---|
std::map |
红黑树 | O(log n) | 自动按key排序 | 需要有序遍历时 |
std::unordered_map |
哈希表 | 均摊O(1) | 无序 | 只查存,不关心顺序 |
刷算法题时,90%的场景用unordered_map就够了。但有个例外——如果题目要求输出结果有序,就得用map,或者事后把key取出来排序。
unordered_set和unordered_map的区别就更简单了:set只存键,不存值。判断“某个元素是否存在”“去重”,用unordered_set就够了,没必要上map。
2.2 自定义类型的哈希支持:让编译器认识你的结构体
标准库给整数、字符串、浮点数都内置了哈希函数,但自定义结构体就不行了。比如我写过这样的代码:
cpp复制struct Node {
int x, y;
bool operator==(const Node& other) const {
return x == other.x && y == other.y;
}
};
unordered_map<Node, int> mp; // 编译报错:没有hash<Node>的模板特化
报错原因是编译器不知道怎么把Node算成一个哈希值。解决办法是手动提供一个仿函数:
cpp复制struct NodeHash {
size_t operator()(const Node& n) const {
return hash<int>()(n.x) ^ (hash<int>()(n.y) << 1);
}
};
unordered_map<Node, int, NodeHash> mp;
这里hash<int>()(n.x)是对x取哈希,(hash<int>()(n.y) << 1)是把y的哈希左移一位再异或。目的就一个:把x和y的信息混合在一起,尽量避免不同组合算出相同结果。这种写法在实际工程里非常常见,面试里也偶尔会考到。
2.3 自定义哈希的工程写法:从普通函数到lambda
除了显式写一个仿函数结构体,C++还支持在自定义类型内部声明友元哈希函数。更简洁的方式是用lambda来构造unordered_map:
cpp复制auto hash_fn = [](const Node& n) {
return hash<int>()(n.x) ^ (hash<int>()(n.y) << 1);
};
unordered_map<Node, int, decltype(hash_fn)> mp(10, hash_fn);
这里有两个细节要注意。第一,unordered_map第二个模板参数是哈希函数类型,所以用了decltype(hash_fn);第二,构造函数的第一个参数是初始化桶的数量,建议显式传一个足够大的值,避免默认桶数太小反复扩容。虽然decltype写法看着繁琐,但省掉了外部结构体定义,代码上下结构更紧凑。
我曾经在一个项目里把坐标点(x, y)存进unordered_set做去重,一开始没写自定义哈希,编译直接报错。后来补上之后,地图路径去重的效率从原本每次遍历O(n)降到了O(1),这就体现了“工程上哈希表用得对不对”的差距。
3. 冲突处理策略:链地址法与开放地址法的甄别
3.1 哈希冲突的必然性:为什么不能“完全避免”
不管哈希函数设计得多好,当数据量超过桶的数量时,必然有不同键映射到同一个桶。这就是哈希冲突。拉链法和开放地址法是两种主流的解决思路,它们的设计哲学完全不同。
- 链地址法(拉链法):每个桶背后挂一个链表(或红黑树),冲突的元素挂在同一个桶的链表里。查找时先定位到桶,再在链表里顺序找。
- 开放地址法:冲突时不额外开空间,而是在数组本身继续探测下一个空位。具体探测方式有线性探测、二次探测、双重哈希。
C++标准库的unordered_map底层用的就是链地址法;而开地址法常见于一些自研的高性能哈希表,以及某些语言运行时(比如Python的dict早期版本)。
3.2 开放地址法详解:线性探测的实现与“堆积”问题
开放地址法的核心是:如果计算出的桶位已经被占了,就按规则找下一个空位。最简单的规则是线性探测:
cpp复制int hash1 = hash(key) % capacity;
int index = hash1;
while (table[index] != EMPTY && table[index].key != key) {
index = (index + 1) % capacity;
}
这里% capacity保证探测索引在数组范围内循环。线性探测的实现简单,但有个致命弱点——容易产生“堆积”:一旦某个区域连续被占,后续插入的键会越探越远,探测序列变长,性能下降。就好比你停在一个停车场,发现一个区域连续满位,只好一辆车一辆车往前挪找空位,挪的地方越多,后面的车也被迫挪得更远。
二次探测就是为了缓解这个问题,探测步长变成平方序列:
cpp复制int index = (hash1 + i * i) % capacity; // i从0开始
这样探测位置会跳着走,避免原地打转,但要注意capacity的选择——如果容量不是质数,二次探测可能无法遍历所有槽位。所以开放地址法的表长通常要求是质数,这也是一个隐藏门槛。
双重哈希则更高级,用第二个哈希函数计算探测步长,冲突概率更低,但实现和调试成本也更高。
3.3 删除操作的特殊处理:墓碑标记与懒删除
开放地址法里,删除节点不能直接置空。为什么?因为如果删掉一个探测路径上的关键节点,后面本来合法的节点可能就找不到了。
举个例子:A和B哈希到同一个位置,A先插入占据位置0,B线性探测到位置1。查找B时,先看位置0发现不是B,继续探测位置1找到B。假设B之后C也插到了位置2。现在删除位置0的A,把位置0置空。再查找B时,从位置0开始发现是空的,按线性探测的逻辑就会停住,认为B“不存在”——即使B就在位置1。
解决办法是引入“墓碑”标记:删除时把槽位标记为DELETED而不是EMPTY,探测到DELETED不停止,继续往下找;插入时遇到DELETED则优先复用它。下面是带墓碑标记的查找逻辑示例:
cpp复制bool find(int key) {
int index = hash(key) % capacity;
while (table[index] != EMPTY) {
if (table[index].state == OCCUPIED && table[index].key == key) {
return true;
}
index = (index + 1) % capacity;
}
return false;
}
这里判断条件是“当前槽位不是EMPTY”,所以不会被DELETED干扰。工程上还有个常见做法:当DELETED数量过多时,重建整张表清理墓碑。这就是为什么某些哈希表在大量删除后会偶尔卡顿——它在做压缩迁移。
3.4 负载因子与扩容时机:为什么0.75是个常见阈值
负载因子 = 已存储元素数 / 桶总数。负载因子越高,冲突越多,性能越差;负载因子越低,空间浪费越严重。C++标准库unordered_map的默认最大负载因子是1.0,Java的HashMap是0.75,Python的dict约0.66。不同语言阈值不同,但道理一样:要在“空间浪费”和“冲突概率”之间找平衡。
扩容的过程也叫rehash:新开一个更大的桶数组,然后把旧表所有元素重新计算哈希并插入新表。这个过程是O(n)的,所以哈希表偶尔会有一次明显的卡顿。为了避免用户感知到单次卡顿,业界还有渐进式rehash——每次操作搬一部分数据,但这对实现复杂度要求高,竞赛/面试一般不要求手写。
我在实际项目里遇到过一个问题:一开始给unordered_map没指定初始桶数量,数据量达到百万级别后频繁rehash,程序总卡顿。后来初始化时直接指定桶数量:
cpp复制unordered_map<string, int> mp;
mp.reserve(1000000);
这行代码提前分配好足够大的桶数组,避免后续resize。reserve是实现细节里的一个宝藏函数,建议用哈希表处理大批量数据时都调用一下。
4. 哈希表的应用扩展:计数、查找与去重的算法套路
4.1 两数之和的本质:把“查找补数”变成哈希表的一次查询
LeetCode 1. Two Sum可能是很多人做过的第一道哈希表题。题目要求找两个下标,使它们的和等于目标值。暴力法两重循环O(n²),而用哈希表可以把第二层循环省掉:
cpp复制vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> pos;
for (int i = 0; i < nums.size(); ++i) {
int complement = target - nums[i];
if (pos.count(complement)) {
return {pos[complement], i};
}
pos[nums[i]] = i;
}
return {};
}
核心思想是“边遍历边查表”:当遍历到nums[i]时,前面所有元素都已经存进哈希表,只要查一下target - nums[i]在不在里面。有两处细节值得注意:一是先查再插,避免自己和自己匹配;二是用count而不是find,语义更直接,C++里也存在写法差异,但结果一样。
4.2 变位词分组:哈希表的键本身就是一个“排序后的字符串”
题目:给一个字符串数组,把字母相同但顺序不同的词分到一组,比如"eat"、"tea"、"ate"是一组的。
最直观的做法:每个字符串排序后作为键,原字符串加入对应分组。
cpp复制vector<vector<string>> groupAnagrams(vector<string>& strs) {
unordered_map<string, vector<string>> mp;
for (string& s : strs) {
string key = s;
sort(key.begin(), key.end());
mp[key].push_back(s);
}
vector<vector<string>> ans;
for (auto& p : mp) ans.push_back(p.second);
return ans;
}
这个解法的复杂度是O(n·k·log k),k是字符串平均长度。排序是O(k·log k),HashMap操作是O(1)。面试官可能会追问:如果字符串非常长,排序开销太大怎么办?这时候可以用“字母出现次数”作为键,比如"a2b1c0..."形式的字符串,或者用26个质数映射到字符再相乘。后者其实非常巧妙:每个字母对应一个质数,字符串的键就是所有字母对应质数的乘积。质数的乘积结果唯一,天然避免排列干扰,查找也能O(k)完成。我第一次看到这个解法时觉得非常惊艳,哈希表的键的设计空间比想象中大得多。
4.3 最长连续序列:只存“连续段的起点”
LeetCode 128. Longest Consecutive Sequence,题目要求找出数组中连续整数(如1,2,3,4)的最长长度,要求时间复杂度O(n)。
最直接的想法是排序,但那是O(n·log n),不符合要求。用哈希表的思路:先把所有数字放进unordered_set,再遍历每个数,判断它是不是某个连续段的起点。判断起点的条件是“num - 1不在集合中”。如果是起点,就向后扩展:
cpp复制int longestConsecutive(vector<int>& nums) {
unordered_set<int> s(nums.begin(), nums.end());
int best = 0;
for (int num : s) {
if (s.count(num - 1) == 0) {
int cur = num;
int len = 1;
while (s.count(cur + 1)) {
++cur;
++len;
}
best = max(best, len);
}
}
return best;
}
这个代码的精髓在于:只有“连续段的起点”才进入while循环,非起点直接跳过,所以每个元素最多被访问两次,整体还是O(n)。我用这个思路做过扩展题——“最长连续等差子序列”,核心还是在哈希表里记录每个位置的延续长度,非常类似。
4.4 频率统计与去重:哈希表是很多问题的“工具链底座”
有一类题表面上看是滑动窗口、双指针、堆,但底层都需要哈希表来维护“窗口内元素的出现次数”。比如“无重复字符的最长子串”“找到字符串中所有字母异位词”,都需要维护一个字符→次数的映射,然后通过控制计数来移动窗口。
我总结过一个套路:只要题目里出现“某个东西是否出现过”“某个值出现了几次”“两个集合的交集/差集”,优先考虑哈希表。它不是银弹,但确实是“判断存在性”问题的最优解之一。
以下是几种常见变形和推荐用法:
| 场景 | 推荐容器 | 理由 |
|---|---|---|
| 判断元素是否存在 | unordered_set |
只存key,内存更省 |
| 统计元素出现次数 | unordered_map |
key→count映射 |
| 维护一个动态窗口的字符频率 | unordered_map<char,int> |
可随时增减计数 |
| 需要按顺序输出 | map |
自动排序,虽然慢一点 |
| 去重并保留插入顺序 | unordered_set + vector |
哈希表帮忙查重,vector记录顺序 |
后面我会细讲一个我自己踩过的坑:这种“去重并保留顺序”的组合方案,在数据量上来之后,时间瓶颈从哈希表转移到了vector的线性遍历。
5. 哈希表的高阶边界:迭代器失效、rehash代价与内存观测
5.1 rehash与迭代器失效:为什么“遍历中插入”会崩溃
很多新手在做“用哈希表边遍历边插入”时,会遇到莫名其妙的崩溃,根因就是rehash。unordered_map在插入元素导致负载因子超过阈值时,会新建一个更大的桶数组,并把旧数据全部迁移过去。迁移过程中,旧桶数组被释放,所有迭代器/引用/指针都会失效。
从C++11标准开始,unordered_map的插入操作会不会导致迭代器失效,取决于是否发生rehash:不发生rehash时,插入不使迭代器失效;发生rehash时,所有迭代器全部失效。
我写一个反面案例来说明:
cpp复制unordered_map<int, int> mp;
for (int i = 0; i < 100000; ++i) {
mp[i] = i;
}
for (auto it = mp.begin(); it != mp.end(); ++it) {
if (some_condition(it->first)) {
mp[it->first * 2] = it->second; // 危险操作,可能触发rehash
}
}
第二个循环中,边遍历边插入新元素,一旦插入导致rehash,it就失效了,++it直接未定义行为。规避方案有两个:一是先把要插入的元素存在临时数组,循环结束再批量插入;二是提前reserve足够大的空间,避免rehash。
这个坑我真实踩过:有一次在做图算法时需要动态添加新节点,我以为unordered_map是普通的哈希表不会变地址,结果线上偶发崩溃,排查了很久才发现是rehash导致的迭代器失效。从那之后,我对“遍历中修改容器”这件事就格外谨慎。
5.2 删除元素时的迭代器移动:怎么写才安全
另一个坑是删除。在遍历unordered_map时,如果直接erase(it),it会变成野指针。正确的写法是用返回值接住下一个迭代器:
cpp复制for (auto it = mp.begin(); it != mp.end(); ) {
if (it->second == 0) {
it = mp.erase(it);
} else {
++it;
}
}
C++11以后,erase(it)返回被删除元素的下一个迭代器,这样循环可以安全继续。很多老版本博客还在推荐“先记下前一个,再erase,再恢复”,那是C++11之前老标准的行为,现在不用了。写代码时,最好直接查一下当前标准对某个容器erase行为的定义。
5.3 内存占用与性能优化:哈希表不是一个“穷光蛋”
哈希表的空间开销是“桶数组 + 节点对象”。桶数组通常至少要达到元素数量/负载因子的规模,相当于有至少一倍的空间被浪费。如果你用unordered_map存1亿个整数,光桶数组就可能占用几GB。所以在真实项目中,如果内存吃紧,也会考虑用排序数组+二分查找替代哈希表,尤其当数据是静态不常变的时候。
再举一个我遇到过的实战问题:一个日志分析模块需要统计每个用户当天出现的次数,用unordered_map<string, int>存,用户数约2000万,内存直接爆了。后来我把用户ID从string改成uint64_t,内存立刻降了约60%。哈希表的内存优化,往往不是哈希表本身的结构问题,而是“键的类型设计问题”。
5.4 哈希碰撞攻击与性能崩溃:理论O(1)如何变成实际O(n)
如果哈希函数是可预测的,恶意输入可以让大量键映射到同一个桶,哈希表就会退化成链表,查找变成O(n),这是哈希碰撞攻击的基础。具体原理很简单:攻击者知道当前哈希表的代码和哈希函数,构造一批哈希值相同但内容不同的键,全部塞进同一个桶,导致增删查全部退化为链表遍历,服务器CPU被打满。
现代的unordered_map实现通常会加入随机种子,每次进程启动时哈希函数的行为不同,攻击者无法提前预测。这就是为什么C++里std::hash<string>不保证跨平台的确定性结果。竞赛平台更注重性能,因此有些题目会把哈希的随机化导致的不确定性也算进“非确定性算法”的范畴。
这个问题跟开放地址法也有关系:开放地址法更容易受到哈希碰撞攻击的影响,因为一个桶位被占后,冲突元素会占用附近空位,导致后续插入的探测路径更长,甚至出现“雪崩”。因此,在安全性要求高的场景,很多自研哈希表会选择双重哈希或随机化哈希函数,而不是只靠固定函数做线性/二次探测。
6. 哈希表实战中的选型对比与经验总结
6.1 哈希表、树表与排序数组:不同数据规模下的取舍
我不是哈希表的无脑吹,它虽然强,但并不是所有场景都最优。以下是我在项目里总结的选型思路,供参考:
| 维度 | unordered_map | map | 排序数组+二分 |
|---|---|---|---|
| 查找复杂度 | 均摊O(1) | O(log n) | O(log n) |
| 插入复杂度 | 均摊O(1) | O(log n) | 插入O(n)不可接受 |
| 有序遍历 | 不支持 | 支持 | 支持 |
| 区间查询 | 不支持 | 支持 | 支持 |
| 内存开销 | 较高(桶+节点) | 中等(树节点) | 最低 |
| 适用场景 | 频繁查找/插入,不需要有序 | 需要有序遍历、区间查询 | 静态数据,构建后不再变化 |
有一类“静态字典”的业务场景,数据构建后不更新,只做查询。这时最省内存的其实不是哈希表,而是把数据排好序后做二分查找,性能也能到O(log n),而且内存占用小、缓存友好。尤其是当数据规模在几万级别时,二分查找和哈希表的速度差距几乎可以忽略,但内存差异非常明显。我在做离线词表匹配时用排序数组替代unordered_map,内存从800MB降到200MB。
6.2 哈希表有关的常见错误:为什么代码对但结果错
刷题时哈希表的错误往往不是语法错误,而是逻辑问题。我总结过三类高频错误:
第一,遍历容器时修改容器(rehash导致迭代器失效),这个前面已经说过。第二,删除了还在使用的元素,比如用unordered_map做图遍历时,递归过程中清空了访问标记,导致绕过已访问节点。第三,把哈希表的“均摊”当成“严格”,在一个多次查询的循环中每个查询都重新插入删除,结果插入/删除的次数远大于查询次数,整体效率并不好。
还有个小细节是C++中operator[]和find的区别:mp[key]如果key不存在,会默认构造一个值插入进去,key原本不存在就变成了存在。如果你只想查不想插入,用count或find,否则会在不经意间扩大哈希表容量,甚至白白触发一次rehash。
6.3 哈希表的调试与性能剖析:怎么确认真的是哈希表慢
遇到“明明用了哈希表还很慢”的困惑时,不要急着换结构,先确认问题出在哪个环节。我在实际排查时一般按下面几步走:
- 看负载因子。如果已经接近默认阈值又没扩容,性能会下降,可以先
reserve。 - 看哈希函数的分布。把一批key的哈希值打印出来,观察是否集中在特定数值区间。
- 统计每个桶的节点数。如果个别桶节点数远多于平均,说明哈希函数对这类key不够均匀。
- 确认不存在rehash导致的间歇性卡顿。在插入大量数据时,观察耗时是否出现偶发尖峰。
- 对比排序数组+二分是否更快。数据量小且静态时,这完全有可能。
我自己踩过一个很典型的案例:一个模块用unordered_map<string, int>做词频统计,数据量100万,运行耗时偏高。我当时以为是rehash,结果是哈希函数对短字符串分布不均,导致部分桶特别长。后来自定义了一个针对短字符串的哈希函数,耗时直接降了一半。这说明“哈希表慢”时,不一定是因为哈希表这个结构不行,往往是“哈希函数不适合你的数据”。
6.4 开放地址法在工程中的适用场景:何时应该手写哈希表
既然标准库已经有unordered_map,什么场景还需要手写开放地址法的哈希表?我的经验是,在性能极致敏感或数据量巨大时,链地址法由于要维护链表节点(伴随缓存不友好和额外内存分配),往往比开放地址法慢。开放地址法因为所有数据都在连续数组中,缓存友好,内存占用低,查插速度非常快。
我手写过一次开放地址法哈希表,用于一个高性能路由表模块,键是uint64_t,值是自定义结构体。实现要点如下:
cpp复制class OpenAddressingHashMap {
private:
vector<pair<uint64_t, int>> table;
vector<char> state; // 0空, 1占用, 2删除
int capacity;
int size;
static constexpr double LOAD_FACTOR = 0.7;
int hash(uint64_t key) {
return (key ^ (key >> 33)) * 0xff51afd7ed558ccdULL % capacity;
}
void rehash() {
vector<pair<uint64_t, int>> old_table = move(table);
vector<char> old_state = move(state);
capacity *= 2;
table.assign(capacity, {0, 0});
state.assign(capacity, 0);
size = 0;
for (int i = 0; i < old_table.size(); ++i) {
if (old_state[i] == 1) insert(old_table[i].first, old_table[i].second);
}
}
public:
OpenAddressingHashMap(int cap = 1024) : capacity(cap), size(0) {
table.assign(capacity, {0, 0});
state.assign(capacity, 0);
}
void insert(uint64_t key, int val) {
if ((double)(size + 1) / capacity > LOAD_FACTOR) rehash();
int idx = hash(key);
while (state[idx] == 1) {
if (table[idx].first == key) {
table[idx].second = val;
return;
}
idx = (idx + 1) % capacity;
}
table[idx] = {key, val};
state[idx] = 1;
++size;
}
int get(uint64_t key) {
int idx = hash(key);
while (state[idx] != 0) {
if (state[idx] == 1 && table[idx].first == key) {
return table[idx].second;
}
idx = (idx + 1) % capacity;
}
return -1;
}
void erase(uint64_t key) {
int idx = hash(key);
while (state[idx] != 0) {
if (state[idx] == 1 && table[idx].first == key) {
state[idx] = 2; // 标记删除,不置0
--size;
return;
}
idx = (idx + 1) % capacity;
}
}
};
这个实现里我专门保留了state数组来标记删除状态,避免删除后查找路径断掉。处理rehash时先把旧数据搬到临时变量,再重新分配新数组,然后逐个插入。这样完成的哈希表,比标准库的unordered_map在连续查找场景下快很多,因为它的内存布局极其紧凑。
手写哈希表的过程会让人对“哈希函数选择”“负载因子调优”“删除策略”有更直观的理解。如果你准备面试,强烈建议手写一遍,面试官看到你能讲清楚开放地址法和链地址法的差异,通常都会眼前一亮。
6.5 哈希表的工程落地建议:从刷题到实际项目
刷题时哈希表的用法很单纯:查存、计数、去重。但落地到工程项目,要考虑的问题就多了。我把自己实际用下来的经验也总结一下:
第一,理解并习惯用reserve预分配空间,能显著降低rehash带来的性能抖动。第二,如果数据有明确范围,考虑用数组替代哈希表,比如字符频率统计用int[26],比unordered_map<char,int>更快更省空间。第三,对自定义类型一定要认真设计哈希函数,不要简单粗暴地返回固定值。第四,判断“是否存在”时用count/find,避免operator[]意外插入。第五,在团队代码里,如果多个地方都需要同一个自定义类型的哈希函数,把哈希函数放成公共组件,别在每个文件里各自实现,不然以后改逻辑会出现多处不一致。
哈希表是一个“用起来简单、用得好难”的数据结构。它的原理不复杂,但工程里的坑一点都不少。我在写这篇文章的过程中,也重新审视了自己以前写过的代码,发现很多地方其实可以用更合理的方式用哈希表。希望这篇能帮你在刷题和落地上都少踩几个坑。
