数据结构 散列表—— 冲突解决方法
冲突解决方法
尽管通过合理设计散列函数能大幅减少冲突,但冲突仍无法完全避免。冲突解决方法的核心是:当多个关键字通过散列函数映射到同一散列地址时,为后续关键字找到新的空闲存储位置,同时保证后续查找时能准确找到这些关键字。常用的冲突解决方法可分为“开放定址法”和“链地址法”两大类,此外还有再散列法、公共溢出区法等辅助方式。
1. 开放定址法
开放定址法的核心思路是:当关键字keykeykey的初始散列地址H0=H(key)H_0 = H(key)H0=H(key)发生冲突时,在散列表内部按照一定规则依次探测其他地址,直到找到空闲地址或确认表满。探测地址的序列可表示为:
Hi=(H(key)+di)%m(i=1,2,...,m−1)H_i = (H(key) + d_i) \% m \quad (i=1,2,...,m-1)Hi=(H(key)+di)%m(i=1,2,...,m−1)
其中mmm是散列表容量,did_idi是探测增量(不同探测规则对应不同的did_idi),且所有探测地址均在0∼m−10 \sim m-10∼m−1范围内(避免越界)。
(1)线性探测法
线性探测法的探测增量为线性递增序列,即di=id_i = idi=i(i=1,2,...,m−1i=1,2,...,m-1i=1,2,...,m−1),探测序列为:
H1=(H0+1)%m,H2=(H0+2)%m,...H_1 = (H_0 + 1) \% m, \quad H_2 = (H_0 + 2) \% m, \quad ...H1=(H0+1)%m,H2=(H0+2)%m,...
例如,散列表容量m=10m=10m=10,散列函数H(key)=key%10H(key)=key\%10H(key)=key%10:
- 关键字key1=25key_1=25key1=25,初始地址H0=5H_0=5H0=5(空闲),存入地址5;
- 关键字key2=35key_2=35key2=35,初始地址H0=5H_0=5H0=5(冲突),探测H1=6H_1=6H1=6(空闲),存入地址6;
- 关键字key3=45key_3=45key3=45,初始地址H0=5H_0=5H0=5(冲突),探测H1=6H_1=6H1=6(冲突),探测H2=7H_2=7H2=7(空闲),存入地址7。
这种方法的优点是计算简单,缺点是容易产生“聚集现象”——多个冲突的关键字会连续占据一段地址(如25、35、45占据5、6、7),后续关键字即使初始地址不冲突,也可能被这段连续地址阻挡,增加冲突概率。
(2)二次探测法
二次探测法通过非线性增量改善聚集问题,探测增量为二次函数序列,常用di=±i2d_i = \pm i^2di=±i2(i=1,2,...,ki=1,2,...,ki=1,2,...,k,k≤m/2k \leq m/2k≤m/2),探测序列为:
H1=(H0+12)%m,H2=(H0−12)%m,H3=(H0+22)%m,H4=(H0−22)%m,...H_1 = (H_0 + 1^2) \% m, \quad H_2 = (H_0 - 1^2) \% m, \quad H_3 = (H_0 + 2^2) \% m, \quad H_4 = (H_0 - 2^2) \% m, \quad ...H1=(H0+12)%m,H2=(H0−12)%m,H3=(H0+22)%m,H4=(H0−22)%m,...
仍以m=10m=10m=10、H0=5H_0=5H0=5为例:
- 冲突时先探测5+1=65+1=65+1=6,再探测5−1=45-1=45−1=4,接着探测5+4=95+4=95+4=9,再探测5−4=15-4=15−4=1,避免了线性探测的连续聚集。
二次探测法能有效减少聚集,但需注意:当mmm为形如4k+34k+34k+3的质数时,才能保证探测到散列表的所有地址(避免遗漏空闲位置)。
2. 链地址法
链地址法(又称拉链法)的核心思路是:将散列表的每个地址作为“链表头指针”,同一散列地址的关键字(称为“同义词”)存储在该地址对应的链表中(称为“同义词链表”)。当发生冲突时,无需在散列表内探测其他地址,只需将新关键字插入对应链表的头部或尾部即可。
例如,散列表容量m=10m=10m=10,散列函数H(key)=key%10H(key)=key\%10H(key)=key%10,存储关键字25、35、45、15的过程如下:
- 关键字25:H0=5H_0=5H0=5,地址5的链表为空,创建链表节点存入25;
- 关键字35:H0=5H_0=5H0=5,地址5的链表非空,将35插入链表尾部(或头部);
- 关键字45、15:同理,均插入地址5的链表,最终该链表包含25、35、45、15四个节点。
链地址法的优点很明显:
- 无聚集现象:同义词仅在同一链表内存储,不影响其他地址;
- 删除方便:只需删除链表中的对应节点,无需调整其他关键字位置;
- 空间利用率高:散列表仅存储链表头指针,链表长度可随关键字数量动态变化(无需预设过大的散列表容量)。
缺点是需额外存储链表指针,且查找时需遍历链表(但链表长度通常较短,效率仍较高)。
3. 其他辅助冲突解决方法
(1)再散列法
再散列法(又称双散列法)是当初始地址冲突时,使用第二个散列函数计算探测增量di=H2(key)d_i = H_2(key)di=H2(key)(H2(key)H_2(key)H2(key)与H(key)H(key)H(key)不同),探测序列为Hi=(H0+i×H2(key))%mH_i = (H_0 + i \times H_2(key)) \% mHi=(H0+i×H2(key))%m。这种方法能进一步减少聚集,但需设计两个散列函数,计算稍复杂。
(2)公共溢出区法
公共溢出区法将散列表分为“基本表”和“溢出表”两部分:所有关键字先尝试存入基本表(按初始散列地址),若发生冲突,则统一存入溢出表;查找时先查基本表,若未找到则查溢出表。这种方法适合冲突较少的场景,溢出表可简化为顺序存储结构。
综上,冲突解决方法的选择需结合实际场景:开放定址法适合散列表容量固定、关键字数量稳定的场景(如嵌入式系统的小型缓存);链地址法适合关键字数量动态变化、追求低聚集的场景(如数据库索引、哈希表容器)。理解不同方法的探测规则和特点,是设计高效散列表的关键。
