GESP7级C++考试语法知识(四、哈希表(3、哈希冲突)


第三课:《撞车事故现场------哈希冲突》


一、国王的邮箱系统出事故了!

1、上一课里,我们认识了哈希函数。

(1)智慧大臣发明了:

🏆 魔法编号机

规则:

复制代码
hash = x % 10;

(2)例如:

复制代码
18 → 8号邮箱
25 → 5号邮箱
42 → 2号邮箱

(3)大家都顺利找到了自己的邮箱。

国王高兴极了:

"这真是世界上最伟大的发明!"


2、可是第二天早上。

邮局管理员慌慌张张跑进皇宫。

大喊:

"不好了!不好了!"

"邮箱撞车了!"


二、什么叫撞车?

1、管理员拿出记录本。


居民:

复制代码
18

计算:

复制代码
18 % 10

得到:

复制代码
8

进入:

复制代码
8号邮箱

2、又来了一个居民:

复制代码
28

计算:

复制代码
28 % 10

得到:

复制代码
8

竟然也是:

复制代码
8号邮箱

3、第三位居民:

复制代码
38

计算:

复制代码
38 % 10

结果还是:

复制代码
8号邮箱

4、现在情况变成:

复制代码
8号邮箱

├── 18
├── 28
└── 38

5、国王傻眼了:

"怎么回事?"

"为什么三个居民抢同一个邮箱?"


三、哈希冲突出现了

这种现象有个专业名字:

哈希冲突(Hash Collision)


1、定义非常简单:

复制代码
两个不同的数据

经过哈希函数计算

得到同一个位置

就叫:

复制代码
哈希冲突

2、例如:

复制代码
18 → 8

28 → 8

38 → 8

虽然:

复制代码
18 ≠ 28 ≠ 38

但是:

复制代码
hash(18) = hash(28) = hash(38)

3、这就是:

💥 哈希冲突


四、为什么一定会冲突?

1、有的同学会问:

"汉克老师,能不能设计一个永远不冲突的哈希函数?"

答案:

❌ 基本不可能。


2、我们来看看。


邮箱数量:

复制代码
10个

居民数量:

复制代码
100个

3、请问:

复制代码
100个人
放进
10个邮箱

会发生什么?


肯定有邮箱里不止一个人。

例如:

复制代码
邮箱1
住10个人

邮箱2
住8个人

邮箱3
住12个人

这就像:

复制代码
100只小兔子

塞进10个笼子

总会有笼子里挤着好几只兔子。


4、这就是数学里的:

鸽巢原理

(又叫抽屉原理)


5、简单说:

复制代码
东西比位置多

冲突必然发生

五、哈希表管理员怎么办?

国王问:

"邮箱撞车了怎么办?"

智慧大臣笑了:

"没关系。"

"我们早就准备好了办法。"


于是。

哈希王国发明了两种解决方案。


第一种方案:拉链法


六、拉链法登场

(1)假设:

复制代码
18 → 8

28 → 8

38 → 8

都进入:

复制代码
8号邮箱

(2)那怎么办?

大臣说:

"不要赶走他们。"

"让他们排队。"


于是:

复制代码
8号邮箱

18
 ↓
28
 ↓
38

变成:

复制代码
8号邮箱
|
|--18
|
|--28
|
|--38

(3)就像:

🏠 一个宿舍

里面住了:

复制代码
18
28
38

三个人。


(4)这种方法叫:

拉链法(Chain)


(5)为什么叫拉链法?

因为像衣服拉链一样:

复制代码
一个接一个

连起来。


七、查找过程

1、现在找:

复制代码
28

2、先算:

复制代码
28 % 10

得到:

复制代码
8号邮箱

3、进入8号邮箱:

复制代码
18
28
38

4、依次检查:

复制代码
18
不是

28
找到

成功!


八、拉链法的优点

1、优点:

✅ 简单

✅ 好理解

✅ 实际应用广泛


2、C++很多哈希表实现都用了类似思想。


第二种方案:开放寻址法


九、停车场故事

1、智慧大臣又发明另一种办法。


(1)假设:

复制代码
18

进入:

复制代码
8号邮箱

(2)现在:

复制代码
28

也想进入:

复制代码
8号邮箱

(3)结果发现:

复制代码
8号邮箱满了

怎么办?


2、大臣说:

"向后找空位!"


(1)例如:

复制代码
邮箱编号

0
1
2
3
4
5
6
7
8 ←18
9 ←空

(2)于是:

复制代码
28

放到:

复制代码
9号邮箱

(3)再来:

复制代码
38

计算:

复制代码
8号邮箱

发现:

复制代码
8满了
9满了

继续找。


找到:

复制代码
0号邮箱

于是:

复制代码
38

放进去。


(4)结果:

复制代码
0 ←38

8 ←18

9 ←28

3、这种方法叫:

开放寻址法

(Open Addressing)


意思就是:

复制代码
原来的位置满了

继续找空位置

十、生活中的理解

1、拉链法像什么?


电影院。

第8排坐满了。

于是:

复制代码
18
28
38

都坐在同一排。


2、开放寻址法像什么?


停车场。

车位8满了。

那就:

复制代码
停9
停0
停1
......

一直找空车位。


十一、为什么冲突越少越好?

1、假设:

复制代码
8号邮箱

18
28
38
48
58
68
78
88
98

2、现在找:

复制代码
98

必须看:

复制代码
18
28
38
48
58
68
78
88
98

才能找到。


3、原本:

复制代码
O(1)

的查找。

慢慢变成:

复制代码
O(n)

了。


4、所以优秀哈希函数的目标是:

复制代码
让数据尽量均匀分散

不要全挤在一起。


十二、现实中的哈希表

1、实际C++中的:

复制代码
unordered_map

内部已经帮我们做好了:

✅ 哈希函数

✅ 冲突处理

✅ 自动扩容


2、例如:

复制代码
unordered_map<int,int> mp;

mp[18] = 1;
mp[28] = 2;
mp[38] = 3;

即使发生冲突。

程序员也不用管。

系统自动处理。


十三、小试牛刀

规则:

复制代码
hash = x % 10;

计算下面数字会进入哪个邮箱?


第一题

复制代码
15

计算:

复制代码
15 % 10 = 5

答案:

复制代码
5号邮箱

第二题

复制代码
25

计算:

复制代码
25 % 10 = 5

答案:

复制代码
5号邮箱

发生了什么?


答案:

💥 哈希冲突


第三题

复制代码
35

计算:

复制代码
35 % 10 = 5

答案:

复制代码
5号邮箱

结果:

复制代码
5号邮箱

15
25
35

又发生冲突了。


本课总结

1、今天我们认识了哈希表最大的敌人:

💥 哈希冲突(Hash Collision)


2、什么是冲突?

复制代码
不同数据

得到相同邮箱

例如:

复制代码
18 → 8

28 → 8

38 → 8

3、为什么会冲突?

复制代码
数据太多

邮箱太少

根据抽屉原理:

复制代码
冲突不可避免

4、解决办法:

方法1:拉链法

复制代码
8号邮箱

18
 ↓
28
 ↓
38

方法2:开放寻址法

复制代码
8号邮箱满了

去9号

9号满了

去0号

5、魔法口诀

复制代码
哈希表,很神奇,
快速查找效率高。

不同数据同位置,
这就叫做哈希冲突。

拉链法,排队站;
开放寻址找空房。

冲突越少越优秀,
查找速度才够强!

下一课,我们将正式进入 C++ 的真实哈希表世界:

《藏宝图仓库------认识 unordered_map》

届时同学们将第一次使用真正的:

复制代码
unordered_map<string,int>

实现姓名→分数、学号→学生等超级实用的哈希表应用!🚀