散列表学习记录
散列表
已知的查找方式:
查找方式 | 时间复杂度 | 缺陷 |
---|---|---|
顺序查找 | \(O(N)\) | 效率低 |
二分查找 | \(O(log_2N)\) | 只适合静态查找 |
二叉搜索树 | \(O(h)\) | 只适合静态查找 |
AVL树 | \(O(log_2N)\) | 涉及字符串比较 |
查找的本质:已知对象找位置
- 有序安排对象:全序(二分查找),半序(查找树)
- 直接算出位置(散列查找)
散列查找的基本工作
- 计算位置:构造散列函数
- 解决冲突:应用某种策略
时间复杂度\(O(1)\),即查找时间与问题规模无关,散列方法以空间换时间
如果没有冲突:\(T_{查询}=T_{插入}=T_{删除}=O(1)\)
抽象数据结构
类型名称:符号表(SymbolTable
)
数据对象集:符号表是"名字(Name)- 属性(Attribute)"对的集合
操作集:Table ∈SymbolTable , Name ∈ NameType , Attr ∈ AttributeType
SymbolTable InitializeTable( int TableSize ); //创建一个长度为TableSize的符号表
Boolean IsIn( SymbolTable Table, NameType Name ); //查找特定的名字Name是否在符号表Table中
AttributeType Find( SymbolTable Table, NameType Name ); //获取Table中指定名字Name对应的属性
SymbolTable Modefy( SymbolTable Table, NameType Name , AttributeType Attr); //将Table中指定名字Name的属性修改为Attr
SymbolTable Insert( SymbolTable Table, NameType Name , AttributeType Attr); //向Table中插入一个新名字Name及其属性Attr
SymbolTable Delete( SymbolTable Table, NameType Name , AttributeType Attr); //从Table中删除一个名字Name及其属性
装填因子
设散列表空间为\(m\),填入元素个数为\(n\),则\(α=n/m\),\(α\)为散列表的装填因子
散列查找的基本思想:
- 以关键字\(key\)为自变量,通过一个确定的散列函数\(h\),计算出对应的函数值\(h( key )\)作为数据对象的存储地址
- 当不同关键字映射到相同的散列地址上,即 \(h(key_i) = h(key_j),(key_i ≠key_j)\) 时,需要某种解决策略
散列函数的考虑因素
- 计算简单,以提高转换速度
- 关键词对应的地址空间分布应尽量均匀
数字关键词构造
-
直接定址法:
\(h(key)=a*key+b\)
-
除留余数法:
\(h(key) = key\text{ }mod\text{ }p\)
\(p\) 一般取\(TableSize\),且为了分布均匀, \(p\) 取素数
-
数字分析法:
分析对象关键字较为随机的部分取为关键字作为地址
eg:对于11位手机号,选取更为随机的后4位:\(h(key) = atoi(key + 7)\text{ }(char* key)\)
-
折叠法:
把关键字分割成位数相同的部分后叠加
eg:\(h(56793542) = 542 + 793 + 056 = 391 (千位去除)\)
-
平方取中法
eg:\(h(56793542) = 641 (3225506412905764取中间部分)\)
字符关键词构造
-
ASCII码加和法:
\(h(key)=(\text{ }\sum_{i=1}^{n}key[i]\text{ })\text{ }mod\text{ }TableSize\)
*冲突严重,ASCII编码范围0127,而对于例如10位数的字符串,变化非常多,但ASCII码加和范围仅为01270,计算结果窄,关键词变化宽
-
简单改进,前3个字符位移:
eg:\(h(key)=(key[0]*27^2+key[1]*27^1+key[2]*27^0)\text{ }mod\text{ }TableSize\)
-
好的散列函数,位移法:
涉及所有\(n\)个字符,且分布均匀:\(h(key)=(\text{ }\sum_{i=0}^{n-1}key[n-i-1]*32^i\text{ })\text{ }mod\text{ }TableSize\)
eg:h("abcde") = \(a*32^4+b*32^3+c*32^2+d*32^1+e*32^0\)
如何快速计算该式子,减少乘法次数?
h("abcde") = \((((a*32+b)*32+c)*32+......\)
且有:\(x*32=x<<5\)
Index Hash(const char *key,int TableSize) { unsigned int h = 0; while( *key != '\0') h = ( h << 5 ) + *key++; return h % TableSize; }
冲突处理
开放地址法(Open Addressing):换个位置
产生冲突时,按某种规则去寻找另一空地址
若发生了第\(i\)次冲突,试探得下一个地址将增加di,基本公式是:\(h(key)=(h(key)+d_i)\)
不同的解决冲突方案决定了\(d_i\)
-
线性探测(Linear Probing):\(d_i=i\)
以增量序列\(1,2...(TableSize-1)\)循环试探下一个存储地址
线性探测冲突时,易形成聚集现象
-
平方探测(Quadratic Probing):\(d_i=±i^2\)
以增量序列\(1^2,-1^2,2^2,-2^2,...,q^2,-q^2\)且\(q≤\lfloor TableSize/2 \rfloor\)循环试探下一个存储地址
有定理显示:如果散列表常地\(TableSize\)是某个\(4k+3\)(\(k\)是整数)形式得素数时,平方探测法就可以探查到整个散列表空间
-
双散列(Double Hashing):\(d_i=i*h_2(key)\),\(h_2(key)\)是另一个散列函数
探测序列:\(h_2(key),2h_2(key),3h_2(key),...\)
对任意的\(key\),有\(h_2(key)≠0\)
探测序列还应该保证所有的散列存储单元都应该能被探测到,选择以下形式被证明有良好效果:
-
再散列(Rehashing)
当散列表元素太多,即装填因子\(α\)太大时,查找效率会下降,可以用再散列加倍扩大散列表
散列表扩大时,原有元素需要重新计算放置到新表中
实用最大装填因子一般取\(0.5≤α≤0.85\)
分离链接法(Separate Chaining):将相应位置上冲突的所有关键词存储在同一个单链表中
散列表的性能分析
平均查找长度(ASL)用来度量散列表查找效率:成功、不成功
关键词的比较次数,取决于产生冲突的多少
影响产生冲突多少有一下三个因素:
- 散列函数是否均匀
- 处理冲突的方法
- 散列表的装填因子α,当装填因子α<0.5时,各种探测法的期望探测次数都不大,也比较接近