散列表(Hash table,也叫哈希表),通过哈希函数(Hash Function)来计算对应键值,再根据键值将所需查询的数据影射到表中的一个位置而实现数据访问的一种数据结构。类比下Python字典里通过 key值来查找 对应 value的过程。
散列表中每个位置被称为 Slot,这些Slot从0开始编号,开始时散列表为空,所有Slot被初始化为None。下图为一个长度为11的空散列表。
将散列表中的元素和它所在的位置对应起来的映射被称为散列函数,给定一个元素,通过散列表能够获得其在散列表中的位置。假设我们有以下元素:54, 26, 93, 17, 77, 31。通过余数法(remainder method),即使用该元素除以散列表的长度所得余数作为哈希值(hash value),。
Item | 54 | 26 | 93 | 17 | 77 | 31 |
Hash Value | 10 | 4 | 5 | 6 | 0 | 9 |
计算出每个元素对应的哈希值以后,我们便可以将元素插入到哈希表中。
长度为11的散列表中有6个Slot被占用了,则该散列表的载荷因子为 。同时,如果有不同元素的余数相同则会发生碰撞(Collision),碰撞问题很大程度会影响散列表的查找速度。
除了上面提到的余数函数外,还有其他几种散列函数:
Folding Method:分组求和再取余数
将元素分成相等长度的片段(最后一段可能长度不等),再对所有片段求和后取余数。比如,将436-555-4601等分为(43,65,55,46,01),,所以这个元素被插入到第一个Slot。有时候也会把其中一些元素倒转过来,如 。
Mid-square Method:将元素平方后,对中间的数字取余数
如 。
使用 ord()函数,将字符串表示成有序的数值序列,对这些数据求和再取余数。
为字符类元素创建哈希值,单词“cat”可以被认为是‘c’,‘a’,‘t’组成的序列。Python中ord()函数可以得到对应字符的ASCII码值。将所有字符的码值累加再取余数便得到字符串对应的哈希值。
>>> ord('c')
99
>>> ord('a')
97
>>> ord('t')
116
程序实现为:
def hash(astring, tablesize):
sum = 0
for pos in range(len(astring)):
sum = sum + ord(astring[pos])
return sum%tablesize
为了避免对回文字总是给出相同的 hash value,可以将每个字母所处的顺序作为权重,对字符的ASCII码值加权后再求和取余数。
当两个元素被分配到同一个位置的时候,便会发生碰撞。解决碰撞的一个简单的办法就是在原理的 slot以后逐个寻找,直到找到下一个空的 slot来存放元素,这种方法被称为开放寻址法(Open Addressing)。线性探测(linear probing)每一次只搜寻一个 Slot。
使用余数法作为哈希函数将这组数据(54,26,93,17,77,31,44,55,20)存入散列表时,当准备存入44时,44%11=0,此时0这个位置已经被77占据了。根据线性探测法,Slot 1为空,将44放了Slot 2。后面的55和20采用同学的方法。
线性探测法的一个缺点是当多个元素具有相同的 hash value时会造成元素的聚集。比如上面例子中准备放入20时,前面的 Slots因为被占满了,而不得不向后寻求空位。
为了解决聚集问题,可以对 hash value 进行 “+1” 或者“+3”的操作,这实际上是rehash的过程。,其中 或者 。更一般的写法是 。skip的取值应该使得每个Slot都被探测到,同时 散列表的长度取质数也是这个原因。
如果一开始的 have value 为 h的话,则后续的探测地址为 h+1,h+4,h+9,h+16。
利用链表将相同的元素连接在一个 Slot上。
Python中一种很重要的数据结构是字典,字典存储的其实是键值-数值的关系对,键值被用来查找数值,这种键值和数值的对应关系通常被称为 Map。下面实现将新的键值对插入到字典的功能,使用余数法构造散列函数,“+1”法进行 rehash。
class HashTable:
"""
self.slots列表用来存储键, self.data列表用来存储值.
当我们通过键查找值时,键在 self.slots中的index即为值
在 self.data中的index
"""
def __init__(self):
self.size = 11
self.slots = [None] * self.size
self.data = [None] * self.size
def put(self,key,data):
hashvalue = self.hashfunction(key,len(self.slots)) #计算 hashvalue
#如果 slots当前 hashvalue 位置上的值为None,则将新值插入
if self.slots[hashvalue] == None:
self.slots[hashvalue] = key
self.data[hashvalue] = data
else:
# 如果 slots 当前 hashvalue 位置上的值为key,则用新值替代旧值
if self.slots[hashvalue] == key:
self.data[hashvalue] = data
else: # 如果 slots 当前 hashvalue 位置上的值为其他值的话,则开始探测后面的位置
nextslot = self.rehash(hashvalue,len(self.slots)) # 重新 rehash,实际相当于探测 hashvalue后一个位置
# 如果后一个位置不为空,且不等于当前值即被其他值占用,则继续探测后一个
while self.slots[nextslot] != None and self.slots[nextslot] != key:
nextslot = self.rehash(nextslot,len(self.slots))
# 如果后一个值为空,则插入;为原来的值,则替换
if self.slots[nextslot] == None:
self.slots[nextslot]=key
self.data[nextslot]=data
else:
self.data[nextslot] = data #replace
"""余数法计算 hashvalue"""
def hashfunction(self,key,size):
return key%size
"""使用 +1 法来重新 rehash"""
def rehash(self,oldhash,size):
return (oldhash+1)%size
def get(self,key):
startslot = self.hashfunction(key,len(self.slots))
data = None
stop = False
found = False
position = startslot
while self.slots[position] != None and not found and not stop:
if self.slots[position] == key: #如果slots当前位置上的值等于 key,则找到了对应的 value
found = True
data = self.data[position]
else: # 否则的话,rehash后继续搜寻下一个可能的位置
position=self.rehash(position,len(self.slots))
if position == startslot: # 如果最后又回到了第一次搜寻的位置,则要找的 key不在 slots中
stop = True
return data
def __getitem__(self,key):
return self.get(key)
def __setitem__(self,key,data):
self.put(key,data)
最理想的情况下是,使用线性探测的开放寻址法,探索成功的算法复杂度为,失败的时间复杂度为。
本文的例子和算法介绍部分,很多内容参考了这本书的内容,里面关于算法的每一部的运行结果都有图示,大家可以看看。