哈希表(哈希查找)
前面对于顺序表进行查找时,在判断当前数据是否是要查找的数据时,需要去通过“=”来进行判断,直到有了相等的关键字才返回地址。在这种查找方式中,“比较”是必不可免的,那么是否有一种方法可以避免比较,即通过关键字的某种形式来存储该数据的地址,这样在查找时,将待查询的关键字通过一定计算就可以得到目的数据的地址。
哈希表概念
哈希表,也被称为散列表,就是应用了上述的存储方法,即在查找某个数据时,直接通过关键字计算数据存放的地址,然后直接访问。它在关键字与存储位置之间建立了一个映射。
根据上述定义,其实也可以理解为关键字与存储地址存在一个函数关系,这个函数就被称为哈希函数。
哈希函数在记录的关键字与记录的存储地址之间建立一种对应关系,可以写成以下形式:
a
d
d
r
(
d
a
t
a
i
)
=
H
(
k
e
y
i
)
addr\left( data_i \right) =H\left( key_i \right)
addr(datai)=H(keyi)
其中,
H
(
.
)
H(.)
H(.)是哈希函数,
k
e
y
i
key_i
keyi是元素
d
a
t
a
i
data_i
datai的关键字,
a
d
d
r
(
d
a
t
a
i
)
addr(data_i)
addr(datai)是元素
d
a
t
a
i
data_i
datai的存储地址。
在有了上述工具之后,就可以使用哈希表来辅助进行查找。哈希查找也被称为散列查找,是利用哈希函数继续宁查找的过程。过程也十分简单,首先利用哈希函数及记录的关键字计算出记录的存储地址,然后直接到指定地址进行查找。整个过程不需要经过比较,一次存取就能得到所查元素。
查找过程虽然简单,但是还是会存在问题,那就是不同的记录,其关键字通过哈希函数的计算,可能会得到相同的地址。这个问题是十分常见的,把不同的记录映射到同一个散列地址上,这种现象被称为冲突。在哈希表中同样也有解决冲突的办法,具体要结合对应的哈希函数来进行解决。
哈希函数构造方法
根据设定的哈希函数 H(key) 和所选中的处理冲突的方法将一组关键字映象到一个有限的、地址连续的地址集 (区间) 上并以关键字在地址集中的“象”作为相应记录在表中的存储位置如此构造所得的查找表称之为“哈希表”。
在一个哈希表中,如何选择一个好的哈希函数是最为重要的一件事情。
哈希函数实现的一般是从一个大的集合(部分元素,空间位置上一般不连续)到一个小的集合(空间连续)的映射。
一个好的哈希函数,对于记录中的任何关键字,将其映射到地址集合中任何一个地址的概率应该是相等的。即关键字经过哈希函数得到一个“随机的地址”。
所以对于一个哈希函数来说,有以下几个要求:
- 哈希函数应是简单的,能在较短的时间内计算出结果。
- 哈希函数的定义域尽可能包括需要存储的全部关键字,如果散列表允许有 m m m 个地址时,其值域必须在 0 到 m − 1 m-1 m−1之间。
- 散列函数计算出来的地址应能均匀分布在整个地址空间中。
接下来就对一些常用的哈希函数进行学习。
直接定址法
直接定址法中,哈希函数取关键字的线性函数,即
H
(
k
e
y
)
=
a
⋅
k
e
y
+
b
H\left( key \right) =a\cdot key+b
H(key)=a⋅key+b
其中
a
a
a和
b
b
b都是常数。
直接定址法思路比较简单,直接使用一个单调的一次函数来作为哈希函数。这里给出一个例子,如下所示。
这样的散列表的有点在于比较简单,均匀,也不会产生冲突,但是问题也很明显,就是需要事先知道关键字的分布情况,适合查找表较小且连续的情况,由于这样的限制,在现实应用中,此方法虽然简单,但是并不常用。
数字分析法
数字分析法主要是提取出所有关键字中,可以用于区分每个关键字的部分当作哈希函数。
假设关键字集合中每个关键字都是由 s s s位数组组成 ( u 1 , u 2 , . . . , u s ) (u_1,u_2,...,u_s) (u1,u2,...,us),数字分析法就是分析关键字集中的全体,从中提取出分布均匀的若干位或它们的组合作为地址。
接下来通过一个例子对数字分析法进行进一步理解。
如上所示,对于该关键字,只需要采用4567列中任意两列和另两位的叠加作为哈希地址即可,即可唯一识别一个记录,且分布较为均匀。
数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字分布较均匀,则可以考虑使用这个办法。
数字分析法完全依赖于关键码集合,如果换一个关键码集合,选择哪几位作为哈希地址就需要重新决定。
平方取中法
平方取中法,顾名思义,就是将关键字的平方值的中间几位取出作为存储地址。
这里之所以要用关键字的平方主要是为了扩大差别,同时平方值的中间各位又能收到整个关键字中各位的影响。
接下来通过一个例子对平方取中法进行理解。
平方取中法是较为常用的构造哈希函数的方法,适合于关键字中每一位都有某些数字重复出现且频度很高的情况,中间所取的位数,由哈希表长决定。
折叠法
折叠法就是将关键字分割成位数相同的若干部分(最后部分的倍数可以不同),然后取它们的叠加和(舍去进位)为哈希地址。其实叠加简单分为两种:
- 移位叠加:将分割后的几部分低位对齐相加。
- 间界叠加:从一端沿分割界来回折送,然后对齐相加。
接下来通过一个例子对于折叠法进行理解。
折叠法适合于关键字的数字位数特别多,而且每一位上数字分布大致均匀的情况。
除留余数法
除留余数法是最常用的一种构造哈希函数的方法。除留余数法的做法是取关键字被某个不大于哈希表长
m
m
m的数
p
p
p除后所得余数为哈希地址。
H
(
k
e
y
)
=
k
e
y
M
O
D
p
H\left( key \right) =key\,\,MOD\,\,p
H(key)=keyMODp
其中
m
m
m为哈希表长,
p
p
p为不大于
m
m
m的素数或是不含20以下质因子的合数,且
p
⩽
m
p \leqslant m
p⩽m。
接下来通过一个例子来对该方法进行理解。
从上述例子中也可以看出,除留余数法并不能确保每个关键字的哈希函数值都是不同的,这也就会增加“冲突”的可能。
处理冲突
处理冲突的实际含义是:为产生冲突的地址寻找下一个哈希地址。在构建哈希表的时候,如果有两个关键字的哈希函数值相同,也就代表这两个关键字应该放的位置相同,但是这样是明显不可取的,因为一个位置只能放一个数据。那么在后面一个数据想要放在已经放数据的位置上时,只能重新找一个空闲的位置进行放置了。而这个重新找空闲位置的过程,就是处理冲突的过程。
开放定址法
所谓的开放定址法,就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。
开放定址法在遇到重复的地址时,为产生冲突的地址
H
(
k
e
y
)
H(key)
H(key)求得一个地址序列:
H
0
,
H
1
,
.
.
.
,
H
s
,
1
⩽
s
⩽
m
H_0,H_1,...,H_s,1\leqslant s \leqslant m
H0,H1,...,Hs,1⩽s⩽m,其中每地址的计算公式如下所示。
H
i
=
(
H
(
k
e
y
)
+
d
i
)
M
O
D
m
,
i
=
1
,
.
.
.
,
s
H_i=\left( H\left( key \right) +d_i \right) \,\,MOD\,\,m, i=1,...,s
Hi=(H(key)+di)MODm,i=1,...,s
其中
H
(
k
e
y
)
H(key)
H(key)为哈希函数,
m
m
m为哈希表长。根据其中
d
i
d_i
di的取法可以简单讲开放定址法分为两种,即线性探测和二次探测。
线性探测
当 d i = 1 , 2 , 3 , . . . , m − 1 d_i=1,2,3,...,m-1 di=1,2,3,...,m−1时,称这种开放定址法为线性探测再散列。
接下来通过一个例子来对线性探测进行理解。
接下来简单分析一下上述过程:
- 19 m o d 11 = 8 19\ mod \ 11=8 19 mod 11=8,该位置为空,直接放置即可。
- 1 m o d 11 = 1 1\ mod\ 11=1 1 mod 11=1,该位置为空,直接放置即可。
- 23 m o d 11 = 1 23\ mod\ 11=1 23 mod 11=1,该位置已经放置了元素, ( 1 + 1 ) m o d 11 = 2 (1+1)mod\ 11=2 (1+1)mod 11=2,2处没有放置元素,直接放入即可。
- 14 m o d 11 = 3 14\ mod\ 11=3 14 mod 11=3,该位置为空,直接放置即可。
- 55 m o d 11 = 0 55\ mod \ 11=0 55 mod 11=0,该位置为空,直接放置即可。
- 68 m o d 11 = 2 68\ mod\ 11=2 68 mod 11=2,该位置放置了元素23,需要重新计算, ( 2 + 1 ) m o d 11 = 3 (2+1)mod\ 11=3 (2+1)mod 11=3,该位置放置了元素14,还需要重新计算, ( 2 + 2 ) m o d 11 = 4 (2+2)mod\ 11=4 (2+2)mod 11=4,该位置为空,故放置到位置4。
- 11 m o d 11 = 0 11\ mod\ 11=0 11 mod 11=0,该位置已经放置了元素55,故需要继续计算,这里省略过程,需要从1一直加到5才有空闲位置,故放在位置5。
- 82 m o d 1 = 5 82\ mod\ 1=5 82 mod 1=5,位置5放置了元素11,故需要重新计算, ( 5 + 1 ) m o d 11 = 6 (5+1)mod\ 11=6 (5+1)mod 11=6,位置6为空,故放置在位置6即可。
- 36 m o d 11 = 3 36\ mod\ 11=3 36 mod 11=3,位置3已经放置为元素14,故需要重新计算,这里省略过程,需要从1一直加到4才能有空闲位置,故放在位置7。
假设每个关键字的查找概率相同,则上述查找的
A
S
L
ASL
ASL为:
A
S
L
=
1
+
1
+
2
+
1
+
1
+
3
+
6
+
2
+
5
9
=
22
9
ASL=\frac{1+1+2+1+1+3+6+2+5}{9}=\frac{22}{9}
ASL=91+1+2+1+1+3+6+2+5=922
在实现线性冲突的时候,主要按照上述的流程进行实现,每取一个 d i d_i di都需要取判断一下当前是否找到了一个空闲位置,同时还需要考虑哈希表是否已经满的情况。
// 线性探测初始化哈希表
void init_hash_table_linear(int data[], int n, int hash_table[], int m, int p)
{
/// <summary>
/// 建立哈希表(使用线性探测处理冲突)
/// </summary>
/// <param name="data">关键字集合</param>
/// <param name="n">关键字个数</param>
/// <param name="hash_table">哈希表</param>
/// <param name="m">哈希表长度</param>
/// <param name="p">除留余数法的参数</param>
// 将哈希表的值全部初始化为-1,代表每个位置都没有放数据
for (int i = 0; i < m; i++)
hash_table[i] = -1;
// 将每个<哈希地址,关键字>添加到哈希表中
for (int i = 0; i < n; i++)
{
// 计算该关键字的哈希地址
int addr = Hash_Func(data[i], p); // 注意这里是对哈希函数参数p取模
// 判断当前哈希地址是否为空,若发生冲突则线性探索
if (hash_table[addr] != -1)
{
int flag = 0; // 判断是否哈希表已满
for (int i = 1; i < m; i++)
{
int next = Hash_Func(addr + i, m); // 下一个地址
if (hash_table[next] == -1)
{
flag = 1; // 未满,有位置可以添加
addr = next; // 地址赋值
break; // 立即退出,找到第一个即可
}
}
// 对表满情况做处理
if (flag == 0)
{
cout << "FULL" << endl;
return;
}
}
// 找到空闲位置后才放置关键字
hash_table[addr] = data[i];
}
}
如果在处理冲突的时候使用的是线性探测,那么在对应的查找的时候如果第一次根据哈希函数获得地址,但是地址中的关键字内容与查找的关键字不同,这时就需要使用对应的处理冲突方法来进行下一个地址的判断。同时也需要考虑查找失败的问题,查找失败有一个最好的判断方法就是如果找到了一个空地址(该地址中未装入关键字),那么说明查找失败。即哈希查找的整个流程如下所示。
故使用线性探测对应的查找代码如下所示。
// 线性探索查找
void Search_linear(int hash_table[], int m, int key, int p)
{
/// <summary>
/// 线性探索查找
/// </summary>
/// <param name="hash_table">哈希表</param>
/// <param name="m">哈希表表长</param>
/// <param name="key">待查询关键字</param>
/// <param name="p">哈希函数参数</param>
// 初始哈希地址
int index = Hash_Func(key, p);
int count = 1; // 统计比较次数
if (hash_table[index] == -1) // 没找到
{
cout << "NOT FOUND!" << endl;
cout << "比较次数:" << count << endl;
return;
}
if (hash_table[index] != key) // 需要线性探索
{
int flag = 0; // 判断是否查找到
for (int i = 1; i < m; i++)
{
int next = Hash_Func(index + i, m); // 下一个地址
count++;
if (hash_table[next] == -1) // 判断当前位置有无元素
{
break;
}
if (hash_table[next] == key)
{
flag = 1; // 找到了
index = next;
break; // 立即退出
}
}
if (flag) // 找到了
{
cout << "FOUND IT!!!" << endl;
cout << "比较次数:" << count << endl;
cout << "位置:" << index << endl;
}
else // 没找到
{
cout << "NOT FOUND!" << endl;
cout << "比较次数:" << count << endl;
}
}
else // 不需要线性探索
{
cout << "FOUND IT!!!" << endl;
cout << "比较次数:" << count << endl;
cout << "位置:" << index << endl;
}
}
二次探测
当 d i = 1 2 , − 1 2 , 2 2 , − 2 2 , . . . d_i=1^2,-1^2,2^2,-2^2,... di=12,−12,22,−22,...时,称这种开放定址法为二次探测再散列。
接下来通过一个例子对二次探测再散列进行理解。
接下来简单分析上述过程:
- 19 m o d 11 = 8 19\ mod \ 11=8 19 mod 11=8,该位置为空,直接放置即可。
- 1 m o d 11 = 1 1\ mod\ 11=1 1 mod 11=1,该位置为空,直接放置即可。
- 23 m o d 11 = 1 23\ mod\ 11=1 23 mod 11=1,该位置已经放置了元素, ( 1 + 1 ) m o d 11 = 2 (1+1)mod\ 11=2 (1+1)mod 11=2,2处没有放置元素,直接放入即可。
- 14 m o d 11 = 3 14\ mod\ 11=3 14 mod 11=3,该位置为空,直接放置即可。
- 55 m o d 11 = 0 55\ mod \ 11=0 55 mod 11=0,该位置为空,直接放置即可。
- 68 m o d 11 = 2 68\ mod\ 11=2 68 mod 11=2,该位置放置了元素23,需要重新计算, ( 2 + 1 ) m o d 11 = 3 (2+1)mod\ 11=3 (2+1)mod 11=3,该位置放置了元素14,还需要重新计算, ( 2 − 1 ) m o d 11 = 1 (2-1)mod\ 11=1 (2−1)mod 11=1,该位置放置了元素1,还需要重新计算, ( 2 + 4 ) m o d 11 = 6 (2+4)mod\ 11=6 (2+4)mod 11=6,该位置为空,故放置在位置6上。
- 11 m o d 11 = 0 11\ mod\ 11=0 11 mod 11=0,该位置已经放置了元素55,故需要继续计算, ( 0 + 1 ) m o d 11 = 1 (0+1)mod\ 11=1 (0+1)mod 11=1,位置1已经放置为元素1,故需要重新计算; ( 0 − 1 ) m o d 11 = 10 (0-1)mod\ 11=10 (0−1)mod 11=10,位置10为空,故放在位置10。
- 82 m o d 11 = 5 82\ mod\ 11=5 82 mod 11=5,该位置为空,直接放置即可。
- 36 m o d 11 = 3 36\ mod\ 11=3 36 mod 11=3,该位置已经放置了元素14,故需要重新计算, ( 3 + 1 ) m o d 11 = 4 (3+1)mod\ 11=4 (3+1)mod 11=4,位置4为空,故放置在位置4即可。
假设每个关键字的查找概率相同,则上述查找的
A
S
L
ASL
ASL为:
A
S
L
=
1
+
1
+
2
+
1
+
1
+
4
+
3
+
1
+
2
9
=
16
9
ASL=\frac{1+1+2+1+1+4+3+1+2}{9}=\frac{16}{9}
ASL=91+1+2+1+1+4+3+1+2=916
实现二次探测的时候也很简单,重点在于如何构造出 d i = 1 2 , − 1 2 , 2 2 , − 2 2 , . . . d_i=1^2,-1^2,2^2,-2^2,... di=12,−12,22,−22,...这样一个序列,我得想法是每次循环中分别对正数和负数都进行一次判断,这样对于参数的变换较为简单,只需要一个底数每次循环时变换即可。
// 二次探测初始化哈希表
void init_hash_table_square(int data[], int n, int hash_table[], int m, int p)
{
/// <summary>
/// 建立哈希表(使用二次探测处理冲突)
/// </summary>
/// <param name="data">关键字集合</param>
/// <param name="n">关键字个数</param>
/// <param name="hash_table">哈希表</param>
/// <param name="m">哈希表长度</param>
/// <param name="p">除留余数法的参数</param>
// 将哈希表的值全部初始化为-1,代表每个位置都没有放数据
for (int i = 0; i < m; i++)
hash_table[i] = -1;
// 将每个<哈希地址,关键字>添加到哈希表中
for (int i = 0; i < n; i++)
{
// 计算该关键字的哈希地址
int addr = Hash_Func(data[i], p); // 注意这里是对哈希函数参数p取模
// 判断当前哈希地址是否为空,若发生冲突则二次探索
if (hash_table[addr] != -1)
{
int flag = 0; // 判断是否哈希表已满
for (int i = 0; i < m; i++)
{
if (hash_table[i] == -1) // 查找空位
{
flag = 1;
break;
}
}
// 对表满情况做处理
if (flag == 0)
{
cout << "FULL" << endl;
return;
}
int next = addr; // 新的地址
int i = 1; // 底数
int k = 1; // 指数
while (hash_table[next] != -1)
{
// 正数
next = (int)pow(-1, k + 1) * (int)pow(i, 2) + addr;
next = Hash_Func(next, m);
if (hash_table[next] == -1) // 如果加正数就可以成功,则直接退出
break;
// 加负数
next = (int)pow(-1, k) * (int)pow(i, 2) + addr;
next = Hash_Func(next, m);
// 下一轮,底数++
i++;
}
addr = next; // 地址赋值
}
// 找到空闲位置后才放置关键字
hash_table[addr] = data[i];
}
}
结合上述实现方法,二次探测对应的查找代码如下所示。
// 二次探索查找
void Search_square(int hash_table[], int m, int key, int p)
{
/// <summary>
/// 二次探索查找
/// </summary>
/// <param name="hash_table">哈希表</param>
/// <param name="m">哈希表表长</param>
/// <param name="key">待查询关键字</param>
/// <param name="p">哈希函数参数</param>
// 初始哈希地址
int index = Hash_Func(key, p);
int count = 1; // 统计比较次数
if (hash_table[index] == -1) // 没找到
{
cout << "NOT FOUND!" << endl;
cout << "比较次数:" << count << endl;
return;
}
if (hash_table[index] != key) // 需要线性探索
{
int flag = 0; // 判断是否查找到
int next = index; // 新地址
int i = 1; // 底数
int k = 1; // 指数
while (true)
{
count++;
// 正数
next = (int)pow(-1, k + 1) * (int)pow(i, 2) + index;
next = Hash_Func(next, m);
if (hash_table[next] == -1) // 没找到
{
break;
}
if (hash_table[next] == key) // 如果加正数就可以找到,则直接退出
{
flag = 1;
break;
}
count++;
// 加负数
next = (int)pow(-1, k) * (int)pow(i, 2) + index;
next = Hash_Func(next, m);
if (hash_table[next] == -1) // 没找到
{
break;
}
if (hash_table[next] == key) // 如果加正数就可以找到,则直接退出
{
flag = 1;
break;
}
// 下一轮,底数++
i++;
}
index = next;
if (flag) // 找到了
{
cout << "FOUND IT!!!" << endl;
cout << "比较次数:" << count << endl;
cout << "位置:" << index << endl;
}
else // 没找到
{
cout << "NOT FOUND!" << endl;
cout << "比较次数:" << count << endl;
}
}
else // 不需要线性探索
{
cout << "FOUND IT!!!" << endl;
cout << "比较次数:" << count << endl;
cout << "位置:" << index << endl;
}
}
这里需要补充一下,因为涉及到负数的求模运算,最终我们需要的肯定是一个正数,故这里对求模运算进行一定的改进,如下所示。
// 哈希函数
int Hash_Func(int key, int p)
{
/// <summary>
/// 哈希函数,计算对应的地址
/// </summary>
/// <param name="key">关键字</param>
/// <param name="p">除留余数法中除数</param>
/// <returns>计算的地址</returns>
return (key % p + p) % p;
}
再哈希法
除了开放定址法之外,还可以使用再哈希法来处理冲突。再哈希法就是构造若干个哈希函数,当发生冲突时,计算下一个哈希地址,直到冲突不再发生,即:
H
i
=
R
H
i
(
k
e
y
)
,
i
=
1
,
2
,
.
.
.
,
k
H_i=RH_i\left( key \right) , i=1,2,...,k
Hi=RHi(key),i=1,2,...,k
其中
R
H
i
RH_i
RHi代表不同的哈希函数。
使用再哈希法的特点在于,不易产生聚集,但是需要增加计算时间。
链地址法
上述思路均为遇到相同的地址就换一个新的地址,那么是否可以讲所有的数据存放在一起呢。这里可以参考链表的做法,将所有哈希地址相同的记录都链接再同一个链表中,这种方法被称为链地址法。
不过既然涉及到链表的插入,那么必然就会分为头插和尾插,这里分别举一个例子。
这里主要对表后插入的那一组例子计算一次
A
S
L
ASL
ASL,如下所示。
A
S
L
=
1
+
2
+
1
+
2
+
1
+
1
+
2
+
1
+
1
9
=
12
9
ASL=\frac{1+2+1+2+1+1+2+1+1}{9}=\frac{12}{9}
ASL=91+2+1+2+1+1+2+1+1=912
链地址法的实现基本可以参考前面链表的实现方法,首先根据哈希函数的参数开辟对应的头结点数组,然后根据关键字数组将所有的关键字添加到对应的链表上面,插入方法这里选择的是尾插法。查找的时候也十分简单,先获取哈希地址,然后将那一根链表拿出,从头遍历即可。这里同时也实现了哈希增补的功能,即如果没有查找到对应的关键字,则将对应的关键字添加到对应的链表中。
// 结点
typedef struct Node
{
int data; // 数据域
Node* next; // 下一个结点
// 构造函数
Node()
{
data = -1;
next = NULL;
}
}Node;
// 链表
typedef struct List
{
Node** head; // 头结点数组
Node** tail; // 尾结点指针
int len; // 头结点数组长度
// 构造函数
List()
{
head = NULL;
len = 0;
tail = NULL;
}
// 初始化
void init_List(int p, int num[], int n)
{
/// <summary>
/// 初始化链表
/// </summary>
/// <param name="p">哈希函数参数</param>
/// <param name="num">关键字数组</param>
/// <param name="n">关键字个数</param>
len = p;
head = new Node * [p]; // 头指针数组
tail = new Node * [p]; // 尾指针数组
// 初始化头结点和尾指针
for (int i = 0; i < len; i++)
{
head[i] = new Node(); // 头结点
head[i]->data = i;
head[i]->next = NULL;
tail[i] = head[i];
}
// 逐个数据添加
for (int i = 0; i < n; i++)
{
int addr = Hash_Func(num[i], p); // 获取哈希地址
Node* t = new Node(); // 创建新节点
t->data = num[i];
t->next = NULL;
// 尾插法
tail[addr]->next = t;
tail[addr] = t;
}
}
// 查找
void search(int key, int p)
{
int count = 1; // 查找次数
int addr = Hash_Func(key, p); // 哈希地址
Node* t = head[addr]->next; // 找到对应的链表
// 不断遍历
while (t)
{
count++;
if (t->data == key) // 找到则退出
break;
t = t->next;
}
if (t)
{
cout << "FOUNT IT!!!" << endl;
cout << addr << endl;
cout << "次数:" << count << endl << endl;
}
else
{
Node* s = new Node();
s->data = key;
s->next = NULL;
tail[addr]->next = s;
tail[addr] = s;
cout << "NOT FOUNT!!" << endl << endl;
}
}
// 输出所有链表
void display_all()
{
for (int i = 0; i < len; i++)
{
Node* t = head[i]->next;
cout << i << ":";
while (t)
{
cout << t->data << "->";
t = t->next;
}
cout << endl;
}
}
}List;
性能分析
决定哈希表查找 A S L ASL ASL的因素有:
- 选用的哈希函数
- 选用的处理冲突的方法
- 哈希表 装填因子
其中哈希表的装填因子是哈希表中填入的记录数与哈希表的长度的比值,即:
α
=
哈希表中填入的记录数
哈希表长度
\alpha =\frac{\text{哈希表中填入的记录数}}{\text{哈希表长度}}
α=哈希表长度哈希表中填入的记录数
装填因子
α
\alpha
α标志哈希表的装满程度,直观来看,装填因子
α
\alpha
α越小,发生冲突的可能性越小,装填因子
α
\alpha
α越大,发生冲突的可能性就越大。
线性探测再散列的哈希查找成功时:
A
S
L
≈
1
2
(
1
+
1
1
−
α
)
ASL\approx \frac{1}{2}\left( 1+\frac{1}{1-\alpha} \right)
ASL≈21(1+1−α1)
二次探测再散列的哈希表查找成功时:
A
S
L
≈
−
1
α
ln
(
1
−
α
)
ASL\approx -\frac{1}{\alpha}\ln ^{\left( 1-\alpha \right)}
ASL≈−α1ln(1−α)
链地址法处理冲突的哈希表查找成功时:
A
S
L
≈
(
1
+
α
2
)
ASL\approx \left( 1+\frac{\alpha}{2} \right)
ASL≈(1+2α)