数据结构基础--散列表
数据结构基础——散列表(17)
散列表的定义
散列表(哈希表,Hash Table):是一种数据结构。特点是:可以根据数据元素的关键字计算出它在散列表中的存储地址
散列函数(哈希函数):Addr=H(key)\text{Addr=H(key)}Addr=H(key)建立了 “关键字”→“存储地址” 的映射关系。
理想情况下,在散列表中查找一个元素的时间复杂度为O(1)
冲突、同义词:
冲突(碰撞):在散列表中插入一个数据元素时,需要根据关键字的值确定其存储地址,若该地址已经存储了其他元素,则称这种情况为 “冲突(碰撞)”
同义词:若不同的关键字通过散列函数映射到同一个存储地址,则称它们为 “同义词”
Q : 如何减少“冲突”?
A : 构造更合适的散列函数,让各个关键字尽可肯地映射到不同的存储位置,从而减少“冲突”
Q : 若”冲突“无可避免,如何处理冲突
A : 用拉链法或者开放定址法
-
拉链法(又称链接法、链地址法):把所有”同义词“存储在一个链表中
-
开放定址法:如果发生”冲突“,就给新元素找另一个空闲位置
散列函数的构造
散列函数的构造常用方法:
除留余数法(最常用)
直接定址法
数字分析法
平方取中法
散列函数(哈希函数):Addr=H(key)\text{Addr=H(key)}Addr=H(key)建立了 “关键字”→“存储地址” 的映射关系。
设计散列函数需要注意的问题
- 定义域必须涵盖所有可能出现的关键字。
- 值域不能超出散列表的地址范围。
- 尽可能减少冲突。散列函数计算出来的地址应尽可能均匀分布在整个地址空间。
- 散列函数应尽量简单,能够快速计算出任意一个关键字对应的散列地址。
除留余数法
质数的定义:
质数(又称素数)是指在大于 1 的自然数中,除了 1 和它自身外,不能被其他自然数整除的数。
除留余数法 —— H(key)=key%p\text{H(key)} = \text{key} \% \text{p}H(key)=key%p
散列表表长为 m,取一个不大于 m 但最接近或等于 m 的质数 p
适用场景:较为通用,只要关键字是整数即可
例:散列表表长 (m = 13)。13 是质数,因此可令散列函数 H(key)=key%13\text{H(key)} = \text{key} \% 13H(key)=key%13
PS:注意一定取一个不大于 m 但最接近或等于 m 的质数 p
原因:对质数取余,可以分布更均匀,从而减少冲突
直接定址法
直接定址法 —— H(key)=key\text{H(key)} = \text{key}H(key)=key 或 H(key)=a*key + b\text{H(key)} = \text{a*key + b}H(key)=a*key + b 其中,a 和 b 是常数。这种方法计算最简单,且不会产生冲突。若关键字分布不连续,空位较多,则会造成存储空间的浪费。
适用场景:关键字分布基本连续
数字分析法
数字分析法 —— 选取数码分布较为均匀的若干位作为散列地址
设关键字是 r 进制数(如十进制数),而 r 个数码在各位上出现的频率不一定相同,可能在某些位上分布均匀一些,每种数码出现的机会均等;而在某些位上分布不均匀,只有某几种数码经常出现,此时可选取数码分布较为均匀的若干位作为散列地址。
适用场景:关键字集合已知,且关键字的某几个数码位分布均匀
平方取中法
平方取中法 —— 取关键字的平方值的中间几位作为散列地址。
具体取多少位要视实际情况而定。这种方法得到的散列地址与关键字的每位都有关系,因此使得散列地址分布比较均匀。
适用场景:关键字的每位取值都不够均匀。
拉链法
拉链法(又称链接法、链地址法):把所有 “同义词” 存储在一个链表中
散列表的插入操作
如何在散列表(拉链法解决冲突)中插入一个新元素?
S 1:结合散列函数计算新元素的散列地址
S 2:将新元素插入散列地址对应的链表(可用头插法,也可用尾插法)
散列表的查找操作
S 1:根据散列函数计算目标元素的散列地址
S 2:顺序查找散列地址对应的链表,直到查找成功或查找失败
在分析查找长度时,通常只统计 “关键字的对比次数”,而链表 “空指针的对比次数” 不计入查找长度
散列表的删除操作
S 1:根据散列函数计算目标元素的散列地址
S 2:顺序查找散列地址对应的链表,若查找成功,将目标元素从链表中删除
开放定址法
开放定址法的常用方法:
线性探测法
平方探测法
双散列法
伪随机序列法
开放定址法:如果发生”冲突“,就给新元素找另一个空闲位置
Q : 用什么规则确定“另一个空闲位置”
A :需确定一个“探测的顺序”,从初始化散列表地址出发,去寻找下一个空闲位置。
egegeg :d0=0d_0 = 0d0=0,d1=1d_1 = 1d1=1,d2=−1d_2 = -1d2=−1,d3=2d_3 = 2d3=2,d4=−2d_4 = -2d4=−2
注意:did_idi表示第 i 次发生冲突时,下一个探测地址与初始化散列表地址的相对偏移量
注意 : 0 < = i <= m - 1
Hi=(H(key)+di)%mH_i = (H(\text{key}) + d_i) \% mHi=(H(key)+di)%m
HiH_iHi为发生第 i 次冲突时的散列地址
did_idi为偏移量
mmm为散列表表长
H(key)H(\text{key})H(key)为初始化散列地址
查找操作原理类似,根据探测序列依次对比各存储单元内的关键字
若探测到目标关键字,则查找成功。
若探测到空单元,则查找失败。
散列表的删除操作
步骤:
S 1:先根据散列函数算出散列地址,并对比关键字是否匹配。若匹配,则 “查找成功”
S 2:若关键字不匹配,则根据 “探测序列” 对比下一个地址的关键字,直到 “查找成功” 或 “查找失败”
S 3:若 “查找成功”,则删除找到的元素
- 注:采用 “开放定址法” 时,删除元素不能简单地将被删元素的空间置为空,否则将截断在它之后的探测路径,可以做一个 “已删除” 标记,进行逻辑删除。
- 注:无论线性探测法、平方探测法、双散列法、伪随机序列法原理都一样。删除元素时,只能逻辑删除
采用 “开放定址法” 删除时,会u带来查找效率低下,散列表看起来很慢,但是实际很空,我们可以采取不定期整理散列表内的数据这个方法来解决问题
散列查找的性能分析
(以线性探测法为例)
**成功查找的平均查找长度(ASL)**ASL成功
ASL成功=1n∑i=1nCi
\text{ASL}_{\text{成功}} = \frac{1}{n} \sum_{i = 1}^{n} C_i
ASL成功=n1i=1∑nCi
其中,n 为散列表中已存在的元素个数,(C_i) 为成功查找第 i 个元素所需的比较次数
**失败查找的平均查找长度(ASL)**ASL失败
ASL失败=1r∑i=1rCi
\text{ASL}_{\text{失败}} = \frac{1}{r} \sum_{i = 1}^{r} C_i
ASL失败=r1i=1∑rCi
其中,r 为散列函数取值的个数,CiC_iCi 为散列函数取值为 i 时查找失败的比较次数
特别注意:散列函数取值的个数、散列表长度可能不相同
装填因子
散列表的装填因子:α=表中记录数n散列表长度m\alpha = \frac{\text{表中记录数}n}{\text{散列表长度}m}α=散列表长度m表中记录数n,反映了一个散列表“满”的程度
装填因子越大,越容易发生冲突。从而导致插入、查找操作效率降低,ASL 增大
聚集(堆积)现象
聚集(堆积)现象:在处理冲突的过程中,几个初始散列地址不同的元素争夺同一个后继散列地址的现象称作 “聚集”(或称作 “堆积”)
线性探测法在发生冲突时,总是往后探测相邻的后一个单元,很容易造成同义词、非同义词的 “聚集(堆积)现象”,从而影响查找效率,导致 ASL 提升
结论:采用线性探测法处理冲突更容易导致 “聚集(堆积)现象”,用其他方法处理冲突可以将元素 “打散”,从而减少 “聚集(堆积)现象”