邓俊辉《数据结构》学习笔记-第九章 词典(自用)
Hashing(哈希/散列:赖以高效组织数据并实现相关算法的重要思想)
1.原理
首先要明确为什么要使用散列。
以电话簿为例,可能的电话R=108=100M,实有的电话N=25000=25K。
如果用数组来实现电话的查询,虽然时间效率是常数,但是空间效率=N/R=0.025%,太低了不可取,所以我们需要散列。
散列原理
使用定址可以将关键码映射至散列空间,但是会存在冲突,即不同的关键码映射到同一个词条
2.评价标准与设计原则
3.设计散列表he散列函数(尽可能得降低冲突的概率,越随机越好)
(1)除余法 hash(key)=key%MM取素数时,数据对散列表的覆盖最充分,分布最均匀
(2)MAD法(multiply-add-divide)
(3)数字分析(select digits)
(4)平方取中(mid-square)
(5)
(6)
(7)(伪)随机数法(慎用!!!因为随机数发生器的不确定性,创建的散列表可移植性差)
(8)多项式法(针对字符串)
(9)近似多项式法(非常适用于英文字符串)
static size_t hasnCode(char s[]){
int h=0;
for(size_t n=strlen(s),i=0;i<n;i++){
h=((h<<5)|h>>27);//数位变换,32位二进制转换,前5个和后27个互换位置
h+=(int)s[i];//累计
}
return size_t;
}
4.制定可行的预案,排解冲突
4.1 基本方法(封闭地址/开放地址)
(1)多槽位(multiple slots)
但是每个桶配备多少个槽位是难以预测的
(2)独立链(linklist-chaining/separate chaining)——封闭地址(发生冲突时申请额外的空间)
(3)公共溢出区(over-flow area)
但是一旦发生冲突,处理冲突词条的时间将正比于溢出区的规模
(4)开放地址/闭散列(散列表所占的空间在物理上始终是地址连续的一块,所有冲突在连续的空间中排解)
那么该如何组织查找链呢?
1.线性试探——插入
template <typename K,typename V> int Hashtable<K,V>::probe4Hit(const K& k){
int r=hashCode(k) %M;//从首个桶起沿查找链,跳过所有冲突和被懒惰删除的桶
while(hr[r]&&(k!=hr[r]->key)||!ht[r]&&laziRemoved(r))
r=(r+1)%M;//线性试探(注意并列判断的次序,命中可能性更大者前置)
return r;//调用者根据ht[r]是否为空,即可判断查找是否成功
}
2.惰性删除
template <typename K,typename V> int Hashtable<K,V>::probe4Free(const K& k){
int r=hashCode(k) %M;//从首个桶起
while(ht[r])
r=(r+1)%M;
return r;
template <typename K,typename V> int Hashtable<K,V>::probe4Free(const K& k){
int r=hashCode(k) %M;//从首个桶起
while(ht[r])
r=(r+1)%M;//沿查找链找到的第一个空桶
return r; //调用者根据ht[r]是否为空,即可判断查找是否成功
}
}
4.2 改进
(1)平方试探
由于开放地址的物理结构更为紧凑,所以性能更好,对于大规模数据更有优势,但是对于线性试探会存在大量不该发生的冲突,因为它的试探位置间距太近。所以改进方式为拉大间距
优点 缓解数据聚集
缺点
1.破坏数据访问的局部性,若涉及外存,I/O激增(但是通常情况下,缓存页面都在若干KB左右,这里以1KB为例,如果一个桶单元记录引用,那么只需要4字节,每一个缓存页面就可以容纳1000/4约256(162)个桶单元,相应的每一次额外的I/O兑换,则要连续发生十六次冲突)
2.存在空桶会不被发现的情况
(装填因子为什么必须小于0.5呢?因为上图中的第二段可以看出,装填因子=6/11>0.5,导致空桶没被找出)
(2)双向平方试探
此时,我们已经可以保证,在起始于任何一个位置的平方探测我们的[M/2]上者必然会彼此互异,但是下者却得不到这样的保证,所以要进一步改进,这样可以进一步提高装填因子。
但是并不是所有素数都可以遍及,如下图所示(4k+3结论来源于双平方定理:任一素数p可表示为一对整数的平方和,当且仅当p%4=1)
(3)重散列