数据结构与算法系列文章目录
【数据结构与算法】data structures & algorithms 第一章:复杂度分析
【数据结构与算法】data structures & algorithms 第二章:基本概念
【数据结构与算法】data structures & algorithms 第三章:线性数据结构
【数据结构与算法】data structures & algorithms 第四章:树的数据结构
【数据结构与算法】data structures & algorithms 第五章:图的数据结构
【数据结构与算法】data structures & algorithms 第六章:各类常见的排序算法
【数据结构与算法】data structures & algorithms 第七章:散列表算法的初步运用
【数据结构与算法】data structures & algorithms 第八章:红黑树的理解与使用
目录
- 数据结构与算法系列文章目录
- 一、散列表简介
- 二、散列函数的构造方法
-
- 三、冲突处理方法
-
- 1、开放地址法
-
- 1.1、线性探测法(Linear Probing)
- 1.2、平方探测法(Quadratic Probing)---二次探测
- 1.3、双散列探测法(Double Hashing)
- 1.4、再散列(Rehashing)
- 2、链地址法
-
- 2.1、分离链接法(Separate Chaining)
- 四、散列表的性能分析
-
- 1、线性探测法的查找性能
- 2、平方探测法和双散列探测法的查找性能
- 3、分离链接法的查找性能
- 4、总结
-
- 三、冲突处理方法
-
- 1、开放地址法
-
- 1.1、线性探测法(Linear Probing)
- 1.2、平方探测法(Quadratic Probing)---二次探测
- 1.3、双散列探测法(Double Hashing)
- 1.4、再散列(Rehashing)
- 2、链地址法
-
- 2.1、分离链接法(Separate Chaining)
- 四、散列表的性能分析
-
- 1、线性探测法的查找性能
- 2、平方探测法和双散列探测法的查找性能
- 3、分离链接法的查找性能
- 4、总结
-
一、散列表简介
-
已知的几种查找方法:
- 顺序查找, O ( N ) O(N) O(N);
- 二分查找(静态查找), O ( log N ) O(\log N) O(logN);
- 二叉搜索树, O ( h ) O(h) O(h) h为二叉查找树的高度
- 平衡二叉树, O ( log N ) O(\log N) O(logN);
-
如何快速搜索到需要的关键词,如果关键词不方便比较怎么办
- 查找的本质:已知对象找位置
- 有序安排对象:全序、半序;
- 直接”算出“对象位置:散列
- 散列查找法的两项基本工作:
- 计算位置:构造散列函数确定关键词存储位置;
- 解决冲突:应用某种策略解决多个关键词位置相同的问题;
- 时间复杂度几乎是常量: O ( 1 ) O(1) O(1),即查找时间与问题规模无关;
-
散列表(哈希表)
- 类型名称:符号表 ( S y m b o l T a b l e ) (SymbolTable) (SymbolTable)
- 数据对象集:符号表是”名字 ( N a m e ) (Name) (Name)-属性 ( A t t r i b u t e ) (Attribute) (Attribute)“对的集合
- 操作集: T a b l e ∈ S y m b o l T a b l e , N a m e ∈ N a m e T y p e , A t t r ∈ A t t r i b u t e T y p e Table \in SymbolTable,\ Name \in NameType,\ Attr \in AttributeType Table∈SymbolTable, Name∈NameType, Attr∈AttributeType
- S y m b o l T a b l e I n i t i a l i z e T a b l e ( i n t T a b l e S i z e ) SymbolTable\ Initialize\ Table(int\ TableSize) SymbolTable Initialize Table(int TableSize):
创建一个长度为 T a b l e S i z e TableSize TableSize的符号表;
- B o o l e a n I s I n ( S y m b o l T a b l e T a b l e , N a m e T y p e N a m e ) Boolean\ IsIn(SymbolTable\ Table,\ NameType\ Name) Boolean IsIn(SymbolTable Table, NameType Name):
查找特定的名字 N a m e Name Name是否在符号表 T a b l e Table Table中;
- A t t r i b u t e T y p e F i n d ( S y m b o l T a b l e T a b l e , N a m e T y p e N a m e ) AttributeType\ Find(SymbolTable\ Table,\ NameType\ Name) AttributeType Find(SymbolTable Table, NameType Name):
获取 T a b l e Table Table中指定名字 N a m e Name Name对应的属性;
- S y m b o l T a b l e M o d e f y ( S y m b o l T a b l e T a b l e , N a m e T y p e N a m e , A t t r i b u t e T y p e A t t r ) SymbolTable\ Modefy(SymbolTable\ Table,\ NameType\ Name,\ AttributeType\ Attr) SymbolTable Modefy(SymbolTable Table, NameType Name, AttributeType Attr):
将 T a b l e Table Table中指定名字 N a m e Name Name的属性修改为 A t t r Attr Attr;
- S y m b o l T a b l e I n s e r t ( S y m b o l T a b l e T a b l e , N a m e T y p e N a m e , A t t r i b u t e T y p e A t t r ) SymbolTable\ Insert(SymbolTable\ Table,\ NameType\ Name,\ AttributeType\ Attr) SymbolTable Insert(SymbolTable Table, NameType Name, AttributeType Attr):
向 T a b l e Table Table中插入一个新名字 N a m e Name Name及其属性 A t t r Attr Attr;
- S y m b o l T a b l e D e l e t e ( S y m b o l T a b l e T a b l e , N a m e T y p e N a m e ) SymbolTable\ Delete(SymbolTable\ Table,\ NameType\ Name) SymbolTable Delete(SymbolTable Table, NameType Name):
从 T a b l e Table Table中删除一个名字 N a m e Name Name及其属性;
-
散列的基本思想:
- 以关键字key为自变量,通过一个确定的函数h (散列函数),计算处对应的函数值h(key),作为数据对象的存储地址;
- 可能不同的关键字会映射到同一个散列地址上,即 h ( k e y i ) = h ( k e y j ) , 当 k e y i ≠ k e y j h(key_i) = h(key_j),\ 当key_i \neq key_j h(keyi)=h(keyj), 当keyi=keyj,称为冲突(Collison);–需要某种冲突解决策略
-
装填因子(Loading Factor):设散列表空间大小为m,填入表中元素个数是n,则称 α = n / m \alpha\ =\ n / m α = n/m为散列表的装填因子;
-
如果没有冲突溢出:
T 查 询 = T 插 入 = T 删 除 = O ( 1 ) T_{查询}\ =\ T_{插入}\ =\ T_{删除}\ =\ O(1) T查询 = T插入 = T删除 = O(1);
二、散列函数的构造方法
一、散列表简介
-
已知的几种查找方法:
- 顺序查找, O ( N ) O(N) O(N);
- 二分查找(静态查找), O ( log N ) O(\log N) O(logN);
- 二叉搜索树, O ( h ) O(h) O(h) h为二叉查找树的高度
- 平衡二叉树, O ( log N ) O(\log N) O(logN);
-
如何快速搜索到需要的关键词,如果关键词不方便比较怎么办
- 查找的本质:已知对象找位置
- 有序安排对象:全序、半序;
- 直接”算出“对象位置:散列
- 散列查找法的两项基本工作:
- 计算位置:构造散列函数确定关键词存储位置;
- 解决冲突:应用某种策略解决多个关键词位置相同的问题;
- 时间复杂度几乎是常量: O ( 1 ) O(1) O(1),即查找时间与问题规模无关;
-
散列表(哈希表)
- 类型名称:符号表 ( S y m b o l T a b l e ) (SymbolTable) (SymbolTable)
- 数据对象集:符号表是”名字 ( N a m e ) (Name) (Name)-属性 ( A t t r i b u t e ) (Attribute) (Attribute)“对的集合
- 操作集: T a b l e ∈ S y m b o l T a b l e , N a m e ∈ N a m e T y p e , A t t r ∈ A t t r i b u t e T y p e Table \in SymbolTable,\ Name \in NameType,\ Attr \in AttributeType Table∈SymbolTable, Name∈NameType, Attr∈AttributeType
- S y m b o l T a b l e I n i t i a l i z e T a b l e ( i n t T a b l e S i z e ) SymbolTable\ Initialize\ Table(int\ TableSize) SymbolTable Initialize Table(int TableSize):
创建一个长度为 T a b l e S i z e TableSize TableSize的符号表;
- B o o l e a n I s I n ( S y m b o l T a b l e T a b l e , N a m e T y p e N a m e ) Boolean\ IsIn(SymbolTable\ Table,\ NameType\ Name) Boolean IsIn(SymbolTable Table, NameType Name):
查找特定的名字 N a m e Name Name是否在符号表 T a b l e Table Table中;
- A t t r i b u t e T y p e F i n d ( S y m b o l T a b l e T a b l e , N a m e T y p e N a m e ) AttributeType\ Find(SymbolTable\ Table,\ NameType\ Name) AttributeType Find(SymbolTable Table, NameType Name):
获取 T a b l e Table Table中指定名字 N a m e Name Name对应的属性;
- S y m b o l T a b l e M o d e f y ( S y m b o l T a b l e T a b l e , N a m e T y p e N a m e , A t t r i b u t e T y p e A t t r ) SymbolTable\ Modefy(SymbolTable\ Table,\ NameType\ Name,\ AttributeType\ Attr) SymbolTable Modefy(SymbolTable Table, NameType Name, AttributeType Attr):
将 T a b l e Table Table中指定名字 N a m e Name Name的属性修改为 A t t r Attr Attr;
- S y m b o l T a b l e I n s e r t ( S y m b o l T a b l e T a b l e , N a m e T y p e N a m e , A t t r i b u t e T y p e A t t r ) SymbolTable\ Insert(SymbolTable\ Table,\ NameType\ Name,\ AttributeType\ Attr) SymbolTable Insert(SymbolTable Table, NameType Name, AttributeType Attr):
向 T a b l e Table Table中插入一个新名字 N a m e Name Name及其属性 A t t r Attr Attr;
- S y m b o l T a b l e D e l e t e ( S y m b o l T a b l e T a b l e , N a m e T y p e N a m e ) SymbolTable\ Delete(SymbolTable\ Table,\ NameType\ Name) SymbolTable Delete(SymbolTable Table, NameType Name):
从 T a b l e Table Table中删除一个名字 N a m e Name Name及其属性;
-
散列的基本思想:
- 以关键字key为自变量,通过一个确定的函数h**(散列函数),计算处对应的函数值h(key)**,作为数据对象的存储地址;
- 可能不同的关键字会映射到同一个散列地址上,即 h ( k e y i ) = h ( k e y j ) , 当 k e y i ≠ k e y j h(key_i) = h(key_j),\ 当key_i \neq key_j h(keyi)=h(keyj), 当keyi=keyj,称为冲突(Collison);–需要某种冲突解决策略
-
装填因子(Loading Factor):设散列表空间大小为m,填入表中元素个数是n,则称 α = n / m \alpha\ =\ n / m α = n/m为散列表的装填因子;
-
如果没有冲突溢出:
T 查 询 = T 插 入 = T 删 除 = O ( 1 ) T_{查询}\ =\ T_{插入}\ =\ T_{删除}\ =\ O(1) T查询 = T插入 = T删除 = O(1);
二、散列函数的构造方法
Index Hash(const char *Key, int TableSize) {
unsigned int h = 0;
while (*Key != '\0')
h = (h << 5) + *Key++;
return h % TableSize;
}
三、冲突处理方法
- 常用处理冲突的思路:
- 换个位置:开放地址法;
- 同一位置的冲突对象组织在一起:链地址法;
1、开放地址法
- 一旦产生了冲突(该地址已有其它元素),就按某种规则去寻找另一空地址;
- 若发生了第i次冲突,试探地下一个地址将增加di,基本公式是:
h i ( k e y ) = ( h ( k e y ) + d i ) m o d T a b l e S i z ( 1 ≤ i < T a b l e S z i e ) h_i(key) = (h(key) + d_i)\ mod\ TableSiz\ \ (1 \leq i < TableSzie) hi(key)=(h(key)+di) mod TableSiz (1≤i<TableSzie);
- **di**决定了不同的解决冲突方案:线性探测 d i = i d_i = i di=i、平方探测 d i = ± i 2 d_i = \pm i^2 di=±i2、双散列 d i = i ∗ h 2 ( k e y ) d_i = i * h_2(key) di=i∗h2(key);
1.1、线性探测法(Linear Probing)
-
线性探测法:以**增量序列 1,2,…,(TableSize - 1)**循环试探下一个存储地址;
-
例子设关键词序列为 { 47 , 7 , 29 , 11 , 9 , 84 , 54 , 20 , 30 } \{47, 7, 29, 11, 9, 84, 54, 20, 30\} {47,7,29,11,9,84,54,20,30},
- 散列表表长TableSize = 13(装填因子 α = 9 / 13 ≈ 0.69 \alpha = 9 / 13 \approx 0.69 α=9/13≈0.69);
- 散列函数为: h ( k e y ) = k e y m o d 11 h(key) = key\ mod\ 11 h(key)=key mod 11;
用线性探测法处理冲突,列出依次插入后的散列表,并估算查找性能;
-
线性探测法处理冲突,容易出现聚集现象;
-
散列表查找性能分析2
- 成功平均查找长度 (ASLs)
- 不成功平均查找长度 (ASLu)
1.2、平方探测法(Quadratic Probing)—二次探测
- 平方探测法:以增量序列 1 2 , − 1 2 , 2 2 , − 2 2 , ⋯ , q 2 , − q 2 1^2,-1^2, 2^2, -2^2, \cdots, q^2, -q^2 12,−12,22,−22,⋯,q2,−q2 且 q ≤ ⌊ T a b l e S i z e / 2 ⌋ q \leq \lfloor TableSize / 2 \rfloor q≤⌊TableSize/2⌋循环试探下一个存储地址;
- 如果散列表长度TableSize 是某个 4k + 3(k是正整数)形式的素数时,平方探测法就可以探查到整个散列表空间;
- 不然容易在几个地址上来回探测,如下图:
struct HashTbl{
int TableSize;
Cell *TheCells;
};
using HashTable = *HashTbl;
HashTable initializeTable (int TableSize) {
if (TableSize < minTableSize) {
Error ("散列表太小");
return NULL;
}
HashTable H = new HashTable;
if (H == NULL) {
FatalError ("空间溢出");
}
H->TableSize = NextPrime(TableSize);
H->TheCells = new Cell[H->TableSize];
if (H->TheCells == NULL) {
FatalError ("空间溢出");
}
for (int i = 0; i < H->TableSize; ++i) {
H->TheCells[i].Info = Empty;
}
return H;
}
Position Find (elementType key, HashTable H) {
Position currentPos, newPos;
int CNum = 0;
newPos = currentPos = Hash (key, H->TableSize);
while (H->TheCells[newPos].Info != Empty && H->TheCells[newPos].Element != key) {
if (++CNum % 2) {
newPos = currentPos + (CNum + 1) / 2 * (CNum + 1) / 2;
while (newPos >= H->TableSize) {
newPos -= H->TableSize;
}
}
else {
newPos = currentPos - CNum / 2 * CNum / 2;
while (newPos < 0) {
newPos += H->TableSize;
}
}
}
return newPos;
}
void Insert (elementType key, HashTable H) {
Position Pos = Find (key, H);
if (H->TheCells[Pos].Info != Legitimate) {
H->TheCells[Pos].Info = Legitimate;
H->TheCells[Pos].Element = key;
}
}
- 在开放地址法的散列表中,删除操作要很小心;
通常只能懒惰删除,即需要增加一个删除标志(Deleted),而并不是真正删除它;
以便查找时不会断链,其空间可以在下次插入时重用;
1.3、双散列探测法(Double Hashing)
- 双散列探测法:di为i * h2(key),h2(key)是另一个散列函数
探测序列成:h2*(key), 2h2(key), 3h2(key), …
- 对任意的key, h 2 ( k e y ) ≠ 0 h_2(key) \neq 0 h2(key)=0;
- 探测序列还应该保证所有的散列存储单元都应该能够被探测到;选择以下形式有良好的效果:
h 2 ( k e y ) = p − ( k e y m o d p ) h_2(key) = p - (key\ mod\ p) h2(key)=p−(key mod p),其中: p < T a b l e S i z e , p 、 T a b l e S i z e 都 是 素 数 p < TableSize,\ p、TableSize都是素数 p<TableSize, p、TableSize都是素数;
1.4、再散列(Rehashing)
- 当散列表元素太多(即装填因子 α \alpha α太大)时,查找效率会下降;
- 实用最大装填因子一般取 0.5 ≤ α ≤ 0.85 0.5 \leq \alpha \leq 0.85 0.5≤α≤0.85;
- 当装填因子过大时,解决的方法是加倍扩大散列表,这个过程叫做再散列;
2、链地址法
2.1、分离链接法(Separate Chaining)
struct ListNode {
elementType Element;
ListNode *Next;
};
using Position = *ListNode;,
using List = *ListNode;
struct HashTbl {
int TableSize;
List TheLists;
};
using HashTable = *HashTbl;
Position Find (elementType key, HashTable H) {
int Pos = Hash(key, H->TableSize);
Position P = H->TheLists[Pos].Next;
while (P != NULL && strcmp(P->Element, key)) {
P = P->Next;
}
return P;
}
四、散列表的性能分析
- 平均查找长度(ASL)用来度量散列表查找效率:成功、不成功;
- 关键词的比较次数,取决于产生冲突的多少,影响产生冲突多少有以下三个因素:
- 散列函数是否均匀;
- 处理冲突的方法;
- 散列表的装填因子 α \alpha α;
1、线性探测法的查找性能
- 线性探测法的期望探测次数 满足下列公式:
p = { 1 2 [ 1 + 1 ( 1 − α ) 2 ] ( 对 插 入 和 不 成 功 查 找 而 言 ) 1 2 ( 1 + 1 1 − α ) ( 对 成 功 查 找 而 言 ) p = \begin{cases} \frac{1}{2} [1 + \frac1{{(1 - \alpha)}^2}] &(对插入和不成功查找而言) \\ \frac12(1 + \frac1{1 - \alpha}) &(对成功查找而言) \end{cases} p={21[1+(1−α)21]21(1+1−α1)(对插入和不成功查找而言)(对成功查找而言)
- 当 α = 0.5 \alpha = 0.5 α=0.5时,
- 插入操作和不成功查找的期望 A S L u = 0.5 ∗ ( 1 + 1 / ( 1 − 0.5 ) 2 ) = 2.5 ASLu\ = \ 0.5 * (1 + 1 / {(1 - 0.5)}^2) = 2.5 ASLu = 0.5∗(1+1/(1−0.5)2)=2.5 次;
- 成功查找的期望 A S L s = 0.5 ∗ ( 1 + 1 / ( 1 − 0.5 ) ) = 1.5 ASLs\ =\ 0.5* (1 + 1/(1 - 0.5)) = 1.5 ASLs = 0.5∗(1+1/(1−0.5))=1.5 次;
2、平方探测法和双散列探测法的查找性能
- 平方探测法和双散列探测法的探测次数 满足下列公式:
p = { 1 1 − α ( 对 插 入 和 不 成 功 查 找 而 言 ) − 1 α ln ( 1 − α ) ( 对 成 功 查 找 而 言 ) p = \begin{cases} \frac1{1 - \alpha} &(对插入和不成功查找而言) \\ - \frac1{\alpha} \ln(1 - \alpha) &(对成功查找而言) \end{cases} p={1−α1−α1ln(1−α)(对插入和不成功查找而言)(对成功查找而言)
- 当 α = 0.5 \alpha = 0.5 α=0.5 时,
- 插入操作和不成功查找的期望 A S L u = 1 / ( 1 − 0.5 ) = 2 ASLu\ =\ 1/ (1 - 0.5) = 2 ASLu = 1/(1−0.5)=2 次;
- 成功查找的期望 A S L s = − 1 / 0.5 ∗ ln ( 1 − 0.5 ) ≈ 1.39 ASLs\ =\ -1 / 0.5 * \ln(1 - 0.5) \approx 1.39 ASLs = −1/0.5∗ln(1−0.5)≈1.39 次;
3、分离链接法的查找性能
- 所有地址链表的平均长度定义成装填因子 α \alpha α, α \alpha α有可能超过1;
- 其期望探测次数p为:
p = { α + e − α ( 对 插 入 和 不 成 功 查 找 而 言 ) 1 + α 2 ( 对 成 功 查 找 而 言 ) p = \begin{cases} \alpha + e^{-\alpha} &(对插入和不成功查找而言) \\ 1 + \frac{\alpha}2 &(对成功查找而言) \end{cases} p={α+e−α1+2α(对插入和不成功查找而言)(对成功查找而言)
- 当 α = 1 \alpha = 1 α=1 时,
- 插入操作和不成功查找的期望 A S L u = 1 + e − 1 = 1.37 ASLu\ =\ 1 + e^{-1} = 1.37 ASLu = 1+e−1=1.37 次;
- 成功查找的期望 A S L s = 1 + 1 / 2 = 1.5 ASLs\ =\ 1 + 1/2 = 1.5 ASLs = 1+1/2=1.5 次;
4、总结
- 选择合适的 h(key),散列法的查找效率期望是常数O(1),它几乎与关键字的空间的大小n无关;也适合于关键字直接比较计算量大的问题;
- 它是以较小的 α \alpha α为前提;因此,散列方法是一个以空间换时间;
- 散列方法的存储对关键字是随机的,不便于顺序查找关键字,也不适合于范围查找,或最大值最小值查找;
4.1、开放地址法
- 散列表是一个数组,存储效率高,随机查找;
- 散列表有”聚集”现象;
4.2、分离链法
- 散列表是顺序存储和链式存储的结合,链表部分的存储效率和查找效率都比较低;
- 关键词删除不需要“懒惰删除”法,从而没有存储“垃圾”;
- 太小的 α \alpha α可能导致空间浪费,大的 α \alpha α又将付出更多的时间代价;
- 不均匀的链表长度导致时间效率的严重下降;
Index Hash(const char *Key, int TableSize) {
unsigned int h = 0;
while (*Key != '\0')
h = (h << 5) + *Key++;
return h % TableSize;
}
三、冲突处理方法
- 常用处理冲突的思路:
- 换个位置:开放地址法;
- 同一位置的冲突对象组织在一起:链地址法;
1、开放地址法
- 一旦产生了冲突(该地址已有其它元素),就按某种规则去寻找另一空地址;
- 若发生了第i次冲突,试探地下一个地址将增加di,基本公式是:
h i ( k e y ) = ( h ( k e y ) + d i ) m o d T a b l e S i z ( 1 ≤ i < T a b l e S z i e ) h_i(key) = (h(key) + d_i)\ mod\ TableSiz\ \ (1 \leq i < TableSzie) hi(key)=(h(key)+di) mod TableSiz (1≤i<TableSzie);
- **di**决定了不同的解决冲突方案:线性探测 d i = i d_i = i di=i、平方探测 d i = ± i 2 d_i = \pm i^2 di=±i2、双散列 d i = i ∗ h 2 ( k e y ) d_i = i * h_2(key) di=i∗h2(key);
1.1、线性探测法(Linear Probing)
-
线性探测法:以**增量序列 1,2,…,(TableSize - 1)**循环试探下一个存储地址;
-
例子设关键词序列为 { 47 , 7 , 29 , 11 , 9 , 84 , 54 , 20 , 30 } \{47, 7, 29, 11, 9, 84, 54, 20, 30\} {47,7,29,11,9,84,54,20,30},
- 散列表表长TableSize = 13(装填因子 α = 9 / 13 ≈ 0.69 \alpha = 9 / 13 \approx 0.69 α=9/13≈0.69);
- 散列函数为: h ( k e y ) = k e y m o d 11 h(key) = key\ mod\ 11 h(key)=key mod 11;
用线性探测法处理冲突,列出依次插入后的散列表,并估算查找性能;
-
线性探测法处理冲突,容易出现聚集现象;
-
散列表查找性能分析2
- 成功平均查找长度 (ASLs)
- 不成功平均查找长度 (ASLu)
1.2、平方探测法(Quadratic Probing)—二次探测
- 平方探测法:以增量序列 1 2 , − 1 2 , 2 2 , − 2 2 , ⋯ , q 2 , − q 2 1^2,-1^2, 2^2, -2^2, \cdots, q^2, -q^2 12,−12,22,−22,⋯,q2,−q2 且 q ≤ ⌊ T a b l e S i z e / 2 ⌋ q \leq \lfloor TableSize / 2 \rfloor q≤⌊TableSize/2⌋循环试探下一个存储地址;
- 如果散列表长度TableSize 是某个 4k + 3(k是正整数)形式的素数时,平方探测法就可以探查到整个散列表空间;
- 不然容易在几个地址上来回探测,如下图:
struct HashTbl{
int TableSize;
Cell *TheCells;
};
using HashTable = *HashTbl;
HashTable initializeTable (int TableSize) {
if (TableSize < minTableSize) {
Error ("散列表太小");
return NULL;
}
HashTable H = new HashTable;
if (H == NULL) {
FatalError ("空间溢出");
}
H->TableSize = NextPrime(TableSize);
H->TheCells = new Cell[H->TableSize];
if (H->TheCells == NULL) {
FatalError ("空间溢出");
}
for (int i = 0; i < H->TableSize; ++i) {
H->TheCells[i].Info = Empty;
}
return H;
}
Position Find (elementType key, HashTable H) {
Position currentPos, newPos;
int CNum = 0;
newPos = currentPos = Hash (key, H->TableSize);
while (H->TheCells[newPos].Info != Empty && H->TheCells[newPos].Element != key) {
if (++CNum % 2) {
newPos = currentPos + (CNum + 1) / 2 * (CNum + 1) / 2;
while (newPos >= H->TableSize) {
newPos -= H->TableSize;
}
}
else {
newPos = currentPos - CNum / 2 * CNum / 2;
while (newPos < 0) {
newPos += H->TableSize;
}
}
}
return newPos;
}
void Insert (elementType key, HashTable H) {
Position Pos = Find (key, H);
if (H->TheCells[Pos].Info != Legitimate) {
H->TheCells[Pos].Info = Legitimate;
H->TheCells[Pos].Element = key;
}
}
- 在开放地址法的散列表中,删除操作要很小心;
通常只能懒惰删除,即需要增加一个删除标志(Deleted),而并不是真正删除它;
以便查找时不会断链,其空间可以在下次插入时重用;
1.3、双散列探测法(Double Hashing)
- 双散列探测法:di为i * h2(key),h2(key)是另一个散列函数
探测序列成:h2*(key), 2h2(key), 3h2(key), …
- 对任意的key, h 2 ( k e y ) ≠ 0 h_2(key) \neq 0 h2(key)=0;
- 探测序列还应该保证所有的散列存储单元都应该能够被探测到;选择以下形式有良好的效果:
h 2 ( k e y ) = p − ( k e y m o d p ) h_2(key) = p - (key\ mod\ p) h2(key)=p−(key mod p),其中: p < T a b l e S i z e , p 、 T a b l e S i z e 都 是 素 数 p < TableSize,\ p、TableSize都是素数 p<TableSize, p、TableSize都是素数;
1.4、再散列(Rehashing)
- 当散列表元素太多(即装填因子 α \alpha α太大)时,查找效率会下降;
- 实用最大装填因子一般取 0.5 ≤ α ≤ 0.85 0.5 \leq \alpha \leq 0.85 0.5≤α≤0.85;
- 当装填因子过大时,解决的方法是加倍扩大散列表,这个过程叫做再散列;
2、链地址法
2.1、分离链接法(Separate Chaining)
struct ListNode {
elementType Element;
ListNode *Next;
};
using Position = *ListNode;,
using List = *ListNode;
struct HashTbl {
int TableSize;
List TheLists;
};
using HashTable = *HashTbl;
Position Find (elementType key, HashTable H) {
int Pos = Hash(key, H->TableSize);
Position P = H->TheLists[Pos].Next;
while (P != NULL && strcmp(P->Element, key)) {
P = P->Next;
}
return P;
}
四、散列表的性能分析
- 平均查找长度(ASL)用来度量散列表查找效率:成功、不成功;
- 关键词的比较次数,取决于产生冲突的多少,影响产生冲突多少有以下三个因素:
- 散列函数是否均匀;
- 处理冲突的方法;
- 散列表的装填因子 α \alpha α;
1、线性探测法的查找性能
- 线性探测法的期望探测次数 满足下列公式:
p = { 1 2 [ 1 + 1 ( 1 − α ) 2 ] ( 对 插 入 和 不 成 功 查 找 而 言 ) 1 2 ( 1 + 1 1 − α ) ( 对 成 功 查 找 而 言 ) p = \begin{cases} \frac{1}{2} [1 + \frac1{{(1 - \alpha)}^2}] &(对插入和不成功查找而言) \\ \frac12(1 + \frac1{1 - \alpha}) &(对成功查找而言) \end{cases} p={21[1+(1−α)21]21(1+1−α1)(对插入和不成功查找而言)(对成功查找而言)
- 当 α = 0.5 \alpha = 0.5 α=0.5时,
- 插入操作和不成功查找的期望 A S L u = 0.5 ∗ ( 1 + 1 / ( 1 − 0.5 ) 2 ) = 2.5 ASLu\ = \ 0.5 * (1 + 1 / {(1 - 0.5)}^2) = 2.5 ASLu = 0.5∗(1+1/(1−0.5)2)=2.5 次;
- 成功查找的期望 A S L s = 0.5 ∗ ( 1 + 1 / ( 1 − 0.5 ) ) = 1.5 ASLs\ =\ 0.5* (1 + 1/(1 - 0.5)) = 1.5 ASLs = 0.5∗(1+1/(1−0.5))=1.5 次;
2、平方探测法和双散列探测法的查找性能
- 平方探测法和双散列探测法的探测次数 满足下列公式:
p = { 1 1 − α ( 对 插 入 和 不 成 功 查 找 而 言 ) − 1 α ln ( 1 − α ) ( 对 成 功 查 找 而 言 ) p = \begin{cases} \frac1{1 - \alpha} &(对插入和不成功查找而言) \\ - \frac1{\alpha} \ln(1 - \alpha) &(对成功查找而言) \end{cases} p={1−α1−α1ln(1−α)(对插入和不成功查找而言)(对成功查找而言)
- 当 α = 0.5 \alpha = 0.5 α=0.5 时,
- 插入操作和不成功查找的期望 A S L u = 1 / ( 1 − 0.5 ) = 2 ASLu\ =\ 1/ (1 - 0.5) = 2 ASLu = 1/(1−0.5)=2 次;
- 成功查找的期望 A S L s = − 1 / 0.5 ∗ ln ( 1 − 0.5 ) ≈ 1.39 ASLs\ =\ -1 / 0.5 * \ln(1 - 0.5) \approx 1.39 ASLs = −1/0.5∗ln(1−0.5)≈1.39 次;
3、分离链接法的查找性能
- 所有地址链表的平均长度定义成装填因子 α \alpha α, α \alpha α有可能超过1;
- 其期望探测次数p为:
p = { α + e − α ( 对 插 入 和 不 成 功 查 找 而 言 ) 1 + α 2 ( 对 成 功 查 找 而 言 ) p = \begin{cases} \alpha + e^{-\alpha} &(对插入和不成功查找而言) \\ 1 + \frac{\alpha}2 &(对成功查找而言) \end{cases} p={α+e−α1+2α(对插入和不成功查找而言)(对成功查找而言)
- 当 α = 1 \alpha = 1 α=1 时,
- 插入操作和不成功查找的期望 A S L u = 1 + e − 1 = 1.37 ASLu\ =\ 1 + e^{-1} = 1.37 ASLu = 1+e−1=1.37 次;
- 成功查找的期望 A S L s = 1 + 1 / 2 = 1.5 ASLs\ =\ 1 + 1/2 = 1.5 ASLs = 1+1/2=1.5 次;
4、总结
- 选择合适的 h(key),散列法的查找效率期望是常数O(1),它几乎与关键字的空间的大小n无关;也适合于关键字直接比较计算量大的问题;
- 它是以较小的 α \alpha α为前提;因此,散列方法是一个以空间换时间;
- 散列方法的存储对关键字是随机的,不便于顺序查找关键字,也不适合于范围查找,或最大值最小值查找;
4.1、开放地址法
- 散列表是一个数组,存储效率高,随机查找;
- 散列表有”聚集”现象;
4.2、分离链法
- 散列表是顺序存储和链式存储的结合,链表部分的存储效率和查找效率都比较低;
- 关键词删除不需要“懒惰删除”法,从而没有存储“垃圾”;
- 太小的 α \alpha α可能导致空间浪费,大的 α \alpha α又将付出更多的时间代价;
- 不均匀的链表长度导致时间效率的严重下降;