Alex_McAvoy

想要成为渔夫的猎手

哈希查找

【概述】

查找最理想情况是:不经过任何比较,直接得出待查记录的存储位置

这就需要在记录的存储位置与其关键码之间建立一个确定的对应关系 $H$,使得每个关键码 $key$ 与唯一的存储位置 $H(key)$ 相对应

在查找时,根据这个确定关系找到给定值 $k$ 和映射 $H(k)$ ,若查找集合中存在这个记录,则必定在 $H(k)$ 的位置上,这种查找技术即哈希查找(Hash Search),又称散列查找

采用散列技术将记录存储在一块连续的存储空间中,这块连续的存储空间称为哈希表(Hash Table),将关键码映射为哈希表中适当存储位置的函数称为哈希函数(Hash Function),所得的存储位置称为哈希地址(Hash Address)

具体来说,散列过程为:

  • 存储记录时:通过散列函数计算记录的散列地址,并按散列地址存储该记录
  • 查找记录时:通过散列函数计算记录的散列地址,并按散列地址访问该记录

综上,哈希查找所用的散列技术,既是一种存储方法,也是一种查找方法,但散列不是一种完整的存储技术,其只是通过记录的关键码定位该记录,难以完整地表达记录间的逻辑关系,因此,散列主要是面向哈希查找的存储结构

【限制及问题】

由于哈希查找的特性要求关键码与地址一一对应,那么由此也存在一定的问题

哈希查找通过哈希函数建立了从记录的关键码集合到散列表的地址集合的一个映射,哈希函数的定义域是查找集合中全部记录的关键码,若哈希表中有 $m$ 个地址单元,则哈希函数的值域为 $[0,m-1]$

最理想情况下,对任意给定的查找集合 $T$,若选定了某个理想的哈希函数 $H$ 及其相应的哈希表 $L$,则对 $T$ 中记录 $r_i$ 的关键码 $k_i$,$H(k_i)$ 就是记录 $r_i$ 在哈希表 $L$ 中的存储位置,这种情况下的散列称为完美散列

但在实际应用中,往往会出现两个不同的关键码 $k1≠k2$,有 $H(k1)=H(k2)$,即两个不同的记录需要存放在同一个存储位置中,这种现象称为冲突(collision),此时,$k1$ 与 $k2$ 相对于 $H$ 称为同义词(synonym)

若记录按照哈希函数计算出的地址加入哈希表时产生了冲突,那么就必须另找一个存储位置来存放,这就产生了如何处理冲突的问题,因此,采用哈希查找需要考虑的主要问题是:

  • 哈希函数的设计:如何设计一简单、均匀、存储利用率高的哈希函数
  • 冲突的处理:如何采用合适的处理冲突的方法来解决冲突

此外,由于哈希查找的特性,其不适用于范围查找,即不能查找最值、找到某一范围内的记录

【哈希函数】

要求

哈希查找一般用于处理关键码来自范围很大的记录,并将这些记录存储在一个有限的散列表中,因此哈希函数的设计是一个十分关键也是十分复杂的问题

一般来说,希望哈希函数能将记录以相同概率 “散列” 到哈希表的所有地址空间中,因此,设计哈希函数一般遵循以下基本原则:

  • 计算简单:哈希函数不有很大的计算量,否则会降低查找效率
  • 哈希地址分布均匀:函数值要尽量均匀散布在地址空间,从而保证存储空间的有效利用并减少冲突

以上两个方面在实际应用中是矛盾的,为了保证散列地址的均匀性好,哈希函数的计算就必然复杂;反之,如果哈希函数的计算比较简单,那么均匀性就较差

一般来说,哈希函数依赖于关键码的分布情况,而在实际应用中,事先并不知道关键码的分布情况,或者关键码高度汇集,因此在设计哈希函数时,要根据具体情况来选择一较为合理的方案

直接定址法

直接定址法的哈希函数是关键码的线性函数,即:

其优点是:单调、均匀、不会产生冲突,但需事先知道关键字的分布情况,适合查找表较小且连续的情况

例如:关键码集合为 $\{10,30,50,70,80,90\}$,选取的散列函数为 $H(key)=\frac{1}{10}key$

除留余数法

除留余数法是最简单、最常用的哈希函数构造方法,且其不需要事先知道关键码的分布

其基本思想是:选择某个适当的正整数 $p$,以关键码除以 $p$ 的余数作为散列地址,即:

其中,$m$ 为哈希表表长

可以看出,这种方法的关键在于选取合适的 $p$,一般情况下,若哈希表表长为 $m$,通常选取小于或等于表长 $m$ 的最小素数

数字分析法

数字分析法根据关键码在各个位上的分布情况,选取分布比较均匀的若干位组成哈希地址,其适合事先知道关键码的分布,且关键码中有若干位分布较均匀的情况

例如:关键码为 $8$ 位十进制数,哈希地址为 $2$ 位十进制数

平方取中法

由于在一个数平方后,其中间的几位数分布较为均匀

因此,平方取中法就是对关键码平方后,按哈希表的大小,取中间若干位作为哈希地址,其常用于不知道关键码的分布且关键码的位数不是很大的情况。

例如:哈希地址为 $2$ 位,关键码为 $1234$,那么其哈希地址为:

折叠法

折叠法是将关键字从左到右分割成位数相等的若干部分,最后一个部分位数不够可以短一些,然后将这几部分叠加求和,并按哈希表表长,取后几位作为哈希地址,通常采用以下两种叠加方法:

  • 移位叠加:将各部分的最后一位对齐相加
  • 间界叠加:从一端向另一端沿各部分分界来回折叠后,最后一位对齐相加

折叠法适用于关键码位数较多,且关键码每一位分布都不均匀的情况,其无需指定关键码的分布

例如:哈希地址为 $3$ 位,关键码为 $25346358705$,那么其哈希地址为:

【冲突处理方法】

由于关键码的复杂性与随机性,很难有理想的哈希函数存在,如果某记录按哈希函数计算出的哈希地址在加入哈希表时发生冲突,就必须另外再寻找一个地址存放,因此需要有合适的处理冲突的方法

开放定址法

开放定址法,就是由关键码得到的哈希地址一旦产生冲突,就去寻找下一个哈希地址,只要哈希表足够大,空的哈希地址总能找到,并将记录存入,用开放定址法处理冲突得到的哈希表叫做闭哈希表

从冲突的下一个位置起,依次寻找空的哈希地址,即:对于键值 $key$,设 $H(key)=d$,闭哈希表长度为 $m$,则发生冲突时,下一哈希地址为:

用开放定址法处理冲突的方法十分简单,但在处理冲突的过程中,会出现非同义词间对同一个哈希地址争夺的现象,这种现象称为堆积

根据 $d_i$ 的不同,有以下几种方法:

1)线性探测法

当发生冲突时,下一哈希地址为

例如:关键码集合为 $\{47, 7, 29, 11, 16, 92, 22, 8, 3\}$,哈希表表长为 $11$,哈希函数为 $H(key)=key%11$,用线性探测法处理冲突,则有:

2)平方探测法

当发生冲突时,下一哈希地址为:

其中 $d_i=1^2,-1^2,2^2,-2^2,..,q^2,-q^2)$,且满足 $q\leq \sqrt m$

例如:关键码集合为 $\{47, 7, 29, 11, 16, 92, 22, 8, 3\}$,散列表表长为 $11$,散列函数为 $H(key)=key \:\: mod \:\: 11$,用二次探测法处理冲突,则有

3)再探测法

当发生冲突时,下一哈希地址为 $H_i=Hash_2(key)$

该方法使用两个哈希函数,当通过第一个哈希函数 $Hash_1(key)$ 得到的地址冲突时,再利用第二个哈希函数 $Hash_2(key)$ 计算该关键字的地址增量,即:

其中,$m$ 为表长,$i$ 是冲突的次数,初始为 $0$,初始探测位置 $H_0=H(key)\:\:mod\:\:m$

值得注意的是,该方法最多经过 $m-1$ 次探测就会遍历表中所有位置,回到 $H_0$ 的位置

4)随机探测法

当发生冲突时,下一哈希地址为:

其中 $d_i$ 是一个随机数列

链地址法

链地址法又称拉链法,其基本思想是:将所有哈希地址相同的记录存储在一个单链表(同义词子表)中,在哈希表中存储的是所有同义词子表的头指针

用链地址法处理冲突构造的哈希表叫做开哈希表,当 $n$ 个记录存储在长度为 $m$ 的开散列表中,其同义词子表的平均长度为 $\frac{n}{m}$

例如:关键码集合 $\{47, 7, 29, 11, 16, 92, 22, 8, 3\}$,散列函数为 $H(key)=key\:\:mod\:\:11$,用拉链法处理冲突,则有:

公共溢出区

建立公共溢出区是在哈希表的基础上加了一个溢出表,其用于存储发生冲突的记录,使得哈希表包含两部分:基本表、溢出表,通常溢出表与基本表大小相同

在查找时,对给定值通过散列函数计算散列地址,先与基本表的相应单元进行比较,若相等,则查找成功;否则,再到溢出表中进行顺序查找。

例如:关键码集合 $\{47, 7, 29, 11, 16, 92, 22, 8, 3\}$,散列函数为 $H(key)=key\:\: mod \:\:11$,用公共溢出区法处理冲突,则有:

【性能分析】

基本因素

哈希查找中,处理冲突的方法不同,得到的哈希表不同,哈希表的查找性能也不同,由于冲突的存在,产生冲突后的查找仍然是给定值与关键码进行比较的过程,因此,对哈希查找效率的量度采用平均查找长度

在查找过程中,关键码的比较次数取决于产生冲突的概率,产生的冲突越多,查找效率就越低,而影响冲突产生的因素有:

  • 散列函数是否均匀:直接影响冲突产生的概率,一般情况下,认为哈希函数是尽量均匀的,因此可以不考虑其对平均查找长度的影响
  • 处理冲突的方法:就处理冲突的方法来看,他们的平均查找长度不同,容易看出,由于开放定址法处理冲突可能会产生堆积,从而会增加平均查找长度,而链地址法处理冲突不会产生堆积

装填因子

记表中已填入的记录数为 $n$,表的长度为 $m$,则装填因子:

装填因子 $α$ 标志着哈希表的装满程度,由于表长 $m$ 是定值,$α$ 与填入表中的记录个数 $n$ 成正比,因此,填入表中的记录越多,$α$ 就越大,产生的冲突可能性就越大

实际上,哈希表的平均查找长度只是装填因子 $α$ 的函数,只是不同处理冲突的方法具有不同的函数,常见的几种不同处理冲突方法的平均查找长度 $ASL$ 如下:

查找成功时 查找失败时
线性探测法 $\frac{1}{2}(1+\frac{1}{(1-α)^2})$ $\frac{1}{2}(1+\frac{1}{1-α^2})$
平方探测法 $\frac{1}{α}ln(1+α)$ $\frac{1}{1-α}$
链地址法 $1+\frac{α}{2}$ $α+e^α$

在等概率情况下,查找成功的平均查找长度公式为:

其中 $n$ 为表中置入元素个数,$C_i$ 为置入每个元素时所需的比较次数

查找失败的平均查找长度公式为:

其中 $r$ 为哈希函数取值个数,$C_i$ 函数取值为 $i$ 时确定查找不成功时的比较次数

【实现】

下面给出链地址法的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
const int SIZE = 1000000;
const int M = 999997;
struct HashTable {
struct Node {
int next;
int value;
int key;
} data[SIZE];
int head[M], size;
int f(int key) { //哈希函数
return key % M;
}
int get(int key) {
int p = head[f(key)];
while (p) {
if (data[p].key == key)
return data[p].value;
p = data[p].next;
}
return -1;
}
int modify(int key, int value) {
int p = head[f(key)];
while (p) {
if (data[p].key == key)
return data[p].value = value;
p = data[p].next;
}
}
int add(int key, int value) {
if (get(key) != -1)
return -1;
data[++size] = (Node){head[f(key)], value, key};
head[f(key)] = size;
return value;
}
};
感谢您对我的支持,让我继续努力分享有用的技术与知识点!