目录
本节介绍一种查找算法——哈希查找算法;该算法除了查找表自身之外,还涉及到哈希表、哈希函数、处理冲突的方法等内容。最后介绍哈希查找的python实现及其性能分析;
1. 基本概念
哈希函数:
一个把查找表中的关键字映射成该关键字对应的地址的函数;
冲突:
由同一个哈希函数,把不同的关键字映射到同一地址,这种情况称“冲突”;
同义词:
发生“冲突”的两个关键字;
哈希表:
哈希表就是一种以键-值(key-value)存储数据的结构,建立了关键字key和存储地址value之间的一种直接映射关系;
2. 构造哈希函数
2.1 直接定位法
该方法直接利用某个线性函数对关键字映射,值为映射地址,哈希函数为:
\[ H(key) = a \times key + b \]
优缺点:
- 计算简单,并且不会产生冲突;
- 适合关键字分布均匀的情况;
- 如果关键字分布不均匀,则会浪费大量空间;
2.2 除留余数法
采用下面的哈希函数,对关键字进行映射:
\[ H(key) = key \% p \]
其中,设查找表表长为\(m\),\(p\)是一个不大于但最接近或者等于m的质数;
优缺点:
- 简单,常用;
- p的选择影响效果,因此\(p\)是一个不大于但最接近或者等于m的质数;
# -*-coding:utf-8-*-
# @Time: 2019-04-16
# @ Author: chen, lsqin
class HashFunction:
"""构造哈希函数
直接定址法
除留余数法
"""
# ------------- method 1: 直接定址法 ---------------
def linear_function(self, key, a=1, b=1):
"""直接定位法
Argument:
key:
需要映射的关键字
a:
斜率
b:
偏置
Return:
value:
哈希值
"""
return a * key + b
# ------------- method 2: 除留余数法 ---------------
def _prime(self, value):
"""判断是否为质数"""
for i in range(2, value // 2 + 1):
if value % i == 0:
return False
return True
def _max_prime(self, value):
"""不大于(小于或等于)给定值的最大质数"""
for i in range(value, 2, -1):
if self._prime(i):
return i
def remainder_function(self, key):
"""除留余数
Argument:
key:
需要映射的关键字
Return:
value:
哈希值
"""
max_prime = self._max_prime(key) # 小于查找表长度的最大质数
return key % max_prime
if __name__ == '__main__':
hf = HashFunction()
hf.remainder_function(3)
2.3 数字分析法
适用于已知的关键字集合;如果更换了关键字,就需要重新构造新的散列函数;
2.4 平方取中法
取关键字的平方值的中间几位作为哈希值;
2.5 折叠法
将关键字分割成位数相同的几部分,然后取这几部分的叠加和作为哈希值;
3. 处理冲突的方法
3.1 开放定址法
开放地址法,指的是存放新表项的空闲地址既向它的同义项开放,又向它的非同义项开放,递推公式为:
\[ H_i = (H(key) +d_i) \%m \qquad i= 0,1,... \]
上述递推公式中可以看出,构造哈希函数的方法是除留余数法;其中,\(H(key)\)表示哈希值;在这种情况下,不同关键词可能映射到同一地址,也就是出现“冲突”情况,因此使用\(d_i\)来解决冲突,第\(i\)次解决冲突后的哈希值为\(H_i\);根据\(d_i\)取值方法的不同可以分成以下三种方法:线性探测法、平方探测法、再哈希法、伪随机序列法;
线性探测法:
当\(d_i = 1, 2, \cdots, m-1\)时,称为线性探测法;
当发生冲突时,即不同关键词映射到同一地址时,顺序查看存储地址的下一个单元,直到找到空闲单元;其中,\(m\)是表长,\(d_i = 1, 2, \cdots, m-1\)说明最多能探测\(m-1\)次,当探测到表尾地址\(m-1\)时,下一个探测地址为表头地址0;
线性探测法可能将第\(i\)个关键字的同义词存入第\(i+1\)个地址,而原本属于第\(i+1\)个地址的关键字可能存储\(i+2\),这样下去,就可能造成大量元素聚集在相邻的地址中;
平方探测法:
当\(d_i = 1^2, -1^2, 2^2, -2^2,\cdots, k^2, -k^2(k \leq m/2)\),其中\(m\)是表长,同时必须可以表示成\(4k+3\)的质数,也称为二次探测法;
平方探测法可以避免出现堆积问题,缺点是不能探测到哈希表上的所有单元,但至少能探测到一半单元;
再哈希法:
当\(d_i=H_2(key)\),称为再哈希法;即需要两个哈希函数,当使用第一个哈希函数\(H(key)\)发生冲突时,则再利用第二个哈希函数\(H_2(key)\)计算该关键字的地址;
伪随机序列法:
当\(d_i = 伪随机数序列\),称为伪随机序列法;
注意:使用开放地址法解决冲突,不能随便删除哈希表中的元素,因为,若删除元素将会截断其他具有相同哈希地址的关键字的查找地址;当删除元素时,只才采用逻辑上的删除,即给该元素做一个删除标记;当哈希表中存储多次删除后,哈希表其实还是满,实际上有很多元素已经逻辑删除。因此需要定期维护哈希表,将逻辑删除的元素进行物理删除;
3.2 拉链法
未避免上述开放地址法带来的缺点,即不能随意删除哈希表中的元素;这里有一种称为拉链法的解决冲突的方法,即把所有同义词存储在一个线性链表中,这个线性链表由其哈希地址唯一标识。
例如:关键字序列:\(\{19, 14, 23, 01, 68, 20, 84, 27, 55, 11, 10, 79\}\),哈希函数\(H(key) = key \% 13\),采用拉链法处理冲突,建立的表如下图:
4. 哈希查找
哈希查找的过程与构造哈希表的过程基本一致:对于一个给定的关键字key,根据哈希函数可以计算出哈希地址;
步骤如下:
Step 1:初始化\(Addr=Hash(key)\);
Step 2:检测查找表中地址为Addr的位置上是否有记录,若没有揭露,返回查找失败;若有记录,在与key相比较,若相等,返回查找成功,否则执行步骤Step 3;
Step 3:用给定的处理冲突方法计算下一个散列地址,并把Addr置为该地址,转入步骤Step 2;
下面使用python实现哈希查找,使用除留余数构造哈希函数、线性探测法解决冲突;
# -*- coding:utf-8 -*-
# @Time: 2019-04-17
# @ Author: chen
class HashSearch:
def __init__(self, length=0):
self.length = length # 需要构造的哈希表长度
self.table = [None for i in range(length)] # 初始化哈希表
self.li = None # 关键字序列
self.first_hash_value = None # 关键字哈希值
# ------------- hash function 1: 直接定址法 ---------------
def _linear_func(self, key, a, b):
"""直接定位法
Argument:
key:
需要映射的关键字
a, b: int
斜率、偏置
Return:
value:
哈希值
"""
self.first_hash_value = [a * item + b for item in key]
# ------------- hash function 2: 除留余数法 ---------------
def _prime(self, value):
"""判断是否为质数"""
for i in range(2, value // 2 + 1):
if value % i == 0:
return False
return True
def _max_prime(self, value):
"""不大于(小于或等于)给定值的最大质数"""
for i in range(value, 2, -1):
if self._prime(i):
return i
def _remainder_function(self, key, max_prime=None):
"""除留余数
Argument:
key:
需要映射的关键字
Return:
value:
哈希值
"""
if max_prime is None:
max_prime = self._max_prime(len(key)) # 小于查找表长度的最大质数
self.first_hash_value = [item % max_prime for item in key]
# ------------- 构造哈希表 1: 开放地址法—线性探测法 ---------------
def generate_hash_table_linear_probing(self, li, max_prime=None, a=1, b=1, hash_func='remainder_func'):
"""利用线性探测法解决冲突
Argument:
li: list
关键字序列
hash_func: str
选择使用的哈希函数;提供两种方式:
remainder_func: 表示除留余数法,默认;
linear_func: 表示线性定址法;
max_prime: int
当使用"remainder_func"时使用,指定最大质数;
a, b: int
当使用"linear_func"时使用,指定斜率、偏置;
Return:
table: list
构造的哈希表
"""
# ------ Step 1: 选择哈希函数 ------
self.li = li
if hash_func == 'remainder_func':
self._remainder_function(self.li, max_prime)
elif hash_func == 'linear_func':
self._linear_func(self.li, a, b)
else:
raise LookupError('select a correct hash function.')
# ----- Step 2: 迭代构造哈希表 -----
for first_hash, value in zip(self.first_hash_value, self.li):
# ----- Step 3: 迭代解决冲突 -----
for probing_times in range(1, self.length):
if self.table[first_hash] is None:
self.table[first_hash] = value
break
# ----- Step 4: 线性探测法处理冲突 -----
first_hash = (first_hash + 1) % self.length
return self.table
def hash_serach_linear_probing(self, key, hash_table, max_prime=None, a=1, b=1, hash_func='remainder_func'):
"""在哈希表中查找指定元素
Argument:
key: int
待查找的关键字
hash_table: list
查找表,上一步骤中构造的哈希表
hash_func: str
选择使用的哈希函数;提供两种方式:
remainder_func: 表示除留余数法,默认;
linear_func: 表示线性定址法;
max_prime: int
当使用"remainder_func"时使用,指定最大质数;
a, b: int
当使用"linear_func"时使用,指定斜率、偏置;
Return:
查找成功,返回待查找元素在查找表中的索引位置;否则,返回-1
"""
# ------ Step 1: 选择哈希函数 ------
if hash_func == 'remainder_func':
first_hash = key & max_prime
elif hash_func == 'linear_func':
first_hash = a * key + b
else:
raise LookupError('select a correct hash function.')
# ----- Step 2: 迭代解决冲突 -----
for probing_times in range(1, self.length):
if hash_table[first_hash] is None:
return -1
elif hash_table[first_hash] == key:
return first_hash
else:
# ----- Step 3: 线性探测法处理冲突 -----
first_hash = (first_hash + 1) % self.length
if __name__ == '__main__':
LIST = [19, 14, 23, 1, 68, 20, 84, 27, 55, 11, 10, 79] # 关键字序列
# ============
# 当使用"除留余数法"构造哈希函数时,max_prime应取不大于关键字序列长度的最大质数;
# max_prime也可以不指定,代码里自己计算其最大质数;
# 当使用"线性定址法"构造哈希函数时,注意哈希表的大小选择
# ============
max_prime = 13
length = 16 # 构造哈希表的长度
HS = HashSearch(length) # 初始化
# 构造的哈希表
hash_table = HS.generate_hash_table_linear_probing(li=LIST, max_prime=max_prime, hash_func='remainder_func')
print(hash_table)
# 查找指定元素
result = HS.hash_serach_linear_probing(1, hash_table, max_prime, hash_func='remainder_func')
print(result)