处理冲突的方法
任何设计出来的散列函数都不可能绝对地避免冲突。 为此,必须考虑在发生冲突时应该如何处理,即为产生冲突的关键字寻找下一个「空」的 Hash 地址。
方法分两大类:开放定址法(在同一张表里另找位置)和拉链法(在表外挂链表)。这一节是 7.5 里手算量最大的一节——7.5.4 的两个
机制
开放定址法
所谓开放定址法,是指表中可存放新表项的空闲地址既向它的同义词表项开放,又向它的非同义词表项开放。其数学递推公式为
用
取定某一增量序列后,对应的处理方法就是确定的。 通常有以下 4 种取法:
1. 线性探测法,也称线性探测再散列。
它的病是「堆积」:线性探测法可能使第
边界辨析:
堆积是「非同义词之间」的争夺造成的(2014 年直接考过)。 同义词本来就要抢同一个地址,那是冲突的定义; 堆积特指本来互不相干的关键字被挤到一起、互相延长探测路径。 所以「堆积由同义词冲突引起」是错的,正确答案是非同义词之间发生冲突。
2. 平方探测法,也称二次探测法。
平方探测法是一种处理冲突的较好方法,可以避免出现「堆积」问题,它的缺点是不能探测到散列表上的所有单元,但至少能探测到一半单元。
3. 双散列法
需要使用两个散列函数:当通过第一个散列函数
补充:双散列法一定能探测到所有位置吗?
不一定,关键看第二个散列函数给出的步长与表长是否互素。 对某个关键字,记
为什么? 走了
例如(补充示例),
常用设计:表长
截图中的写法还要补一个条件
截图给出
。即使 是素数,也只有在 时,才能保证步长与 互素。 若 、 ,就得到 ,每次加 再模 ,永远停在初始地址。所以不能只记“表长是素数”,还要检查步长模 不能为 。
4. 伪随机序列法:
边界辨析:
开放定址法不能随便物理删除元素。 教材的注意框原文: 「采用开放定址法时,不能随便物理删除表中已有元素,否则会截断其他同义词元素的查找路径, 删除元素时可以做一个删除标记,进行逻辑删除。 但这样做的副作用是:执行多次删除后,表面上看起来散列表很满,实际上有许多位置未利用。」 这是 2023 年的命题追踪,两句都要能答:为什么不能物理删(截断查找路径), 逻辑删除的副作用是什么(假满)。
拉链法(链接法,chaining)
对于不同的关键字可能会通过散列函数映射到同一地址,为了避免非同义词发生冲突,可以把所有的同义词存储在一个线性链表中,这个线性链表由其散列地址唯一标识。假设散列地址为
拉链法适用于经常进行插入和删除的情况。
以关键字序列
flowchart LR T0["0"] --> N0["∧"] T1["1"] --> A1["01"] --> A2["14"] --> A3["27"] --> A4["79 ∧"] T2["2"] --> N2["∧"] T3["3"] --> B1["55"] --> B2["68 ∧"] T4["4"] --> N4["∧"] T5["5"] --> N5["∧"] T6["6"] --> C1["19"] --> C2["84 ∧"] T7["7"] --> D1["20 ∧"] T8["8"] --> N8["∧"] T9["9"] --> N9["∧"] T10["10"] --> E1["10"] --> E2["23 ∧"] T11["11"] --> F1["11 ∧"] T12["12"] --> N12["∧"] classDef slot fill:#c8e6c9,stroke:#1b5e20,stroke-width:2px classDef node fill:#e3f2fd,stroke:#1565c0 classDef nil fill:#eeeeee,stroke:#9e9e9e class T0,T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,T12 slot class A1,A2,A3,A4,B1,B2,C1,C2,D1,E1,E2,F1 node class N0,N2,N4,N5,N8,N9,N12 nil
注意表长只到 12:拉链法的散列表长度就等于散列函数的值域大小,不需要留空位,这与开放定址法不同。
边界辨析:
链内的次序取决于插入策略,题目必须交代。 上图 1 号槽里是
01→14→27→79, 而插入顺序是14, 01, 27, 79——既不是插入顺序也不是纯头插。 教材 7.5.5 的选择题里出现过「若限定在链首插入」这样的限定语,说明这是需要被指明的条件。 链内次序直接改变:按图中次序算出的 , 换成头插法则各链倒序,数值会变。做题时先看题目怎么规定,没规定就按图/按顺序插入并写明假设。
手算模板
开放定址法建表(以线性探测为例):
- 圈出
(散列函数的模)和 (表长)。探测公式里用的是 。 - 逐个关键字:
,比较次数从 1 开始计。 - 该位置非空且不等 → 比较次数
,地址换成 。 - 直到落到空位,写入。
- 把每个关键字的比较次数记成一行——这就是 7.5.4 的分子。
平方探测的顺序不要写错:
双散列法:
边界
| 说法 | 判断 | 说明 |
|---|---|---|
| 「堆积由同义词冲突引起」 | ❌ | 由非同义词之间的冲突引起(2014) |
| 「线性探测到表尾就失败」 | ❌ | 回绕到表首地址 0,因为公式里有 |
| 「线性探测时同义词一定相邻」 | ❌ | 中间可能夹着非同义词 |
| 「平方探测能探测到所有单元」 | ❌ | 不能,但至少能探测到一半单元 |
| 「平方探测的表长可以任意」 | ❌ | |
| 「平方探测的增量是 | ❌ | 是 |
| 「双散列法只用一个散列函数」 | ❌ | 两个,第二个算的是增量 |
| 「双散列法一定能探测到所有单元」 | ❌ | 对该关键字,步长与表长互素才行;不同探测位置数为 |
| 「双散列的表长是素数,就一定能遍历全表」 | ❌ | 还要保证步长模 |
| 「开放定址法可以直接删除元素」 | ❌ | 会截断同义词的查找路径,须做逻辑删除 |
| 「逻辑删除没有副作用」 | ❌ | 多次删除后表面很满、实际有许多位置未利用(2023) |
| 「拉链法会引起聚集现象」 | ❌ | 拉链法不会堆积,同义词各走各的链 |
| 「拉链法的表长要大于关键字个数」 | ❌ | 表长 |
| 「拉链法适合频繁插入删除」 | ✅ | 教材原话,与开放定址法的「不能物理删除」正相反 |
口径差异:
std::unordered_map/ JavaHashMap用的都是拉链法(Java 8 起链表过长会转红黑树), 所以有工程或算竞背景的人对「开放定址」往往很陌生, 而 408 的手算题几乎全考开放定址法(尤其线性探测)——因为它能出成填表题。 平方探测的素数、双散列的 从 0 起算,这些细节在工程里完全用不到,考试却直接问。
对照速查
| 方法 | 增量 | 会不会堆积 | 特殊约束 |
|---|---|---|---|
| 线性探测 | 会 | 探测到表尾回绕到 0 | |
| 平方探测 | 不会 | ||
| 双散列 | 不会 | 需两个散列函数, | |
| 伪随机序列 | 伪随机数序列 | 不会 | — |
| 开放定址法 | 拉链法 | |
|---|---|---|
| 冲突元素放哪 | 表内另找空位 | 表外同义词链 |
| 表长要求 | ||
| 堆积 | 线性探测会 | 不会 |
| 删除 | 只能逻辑删除 | 可直接从链上摘除 |
| 适用 | 表相对静态 | 经常插入和删除 |
考点
- 开放定址法的递推公式及四种增量序列。
- 堆积由非同义词冲突引起(2014)。
- 平方探测的三个约束:增量正负交替、
为 型素数、只能探测一半单元。 - 双散列法的
从 0 起算;能否遍历全表看步长与表长是否互素。 - 开放定址法不能物理删除 + 逻辑删除的副作用(2023)。
- 拉链法不会堆积、适合频繁增删。
- 手算填表——本节的主要工作量。
链接
- 🏠 返回总览:数据结构第 7 章:查找总览
- ⬅️ 上一节:7.5.1 散列表的基本概念
- ➡️ 下一节:7.5.4 散列查找及性能分析
- 🔗
与 的区分:7.5.2 散列函数的构造方法 - 🔗 同义词链就是一条单链表:速查:顺序表与链表对照
- 📖 名词库:第 7 章名词库