因本人最近在恶补数据结构与算法,学识经验有限,如有不正之处望读者指正,不胜感激;也望借此平台留下学习笔记以温故而知新。这一篇博客主要是朋友问我哈希表学的怎样了,问了几个问题都没有答上来,借此梳理下,希望对您有所帮助。
哈希表(Hash table,也叫散列表),是根据关键码值(Key value)直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。
记录的存储位置=f(关键字),这里的对应关系f称为散列函数,又称为哈希(Hash函数),采用散列技术将记录存储在一块连续的存储空间中,这块连续存储空间称为散列表或哈希表(Hash table)。
而当使用哈希表进行查询的时候,就是再次使用哈希函数将key转换为对应的数组下标,并定位到该空间获取value,如此一来,就可以充分利用到数组的定位性能进行数据定位。哈希查找是一种以O(1)时间复杂为目标的查找方式,效率极高。Python中的内置的字典结构dictionary,其key值的查找就是采用了哈希查找的方式,因而查询操作能够达到O(1)的时间复杂度。
散列表中每个位置被称为 Slot,这些Slot从0开始编号,开始时散列表为空,所有Slot被初始化为None。下图为一个长度为11的空散列表。
将散列表中的元素和它所在的位置对应起来的映射被称为散列函数,给定一个元素,通过散列表能够获得其在散列表中的位置。假设我们有以下元素:54, 26, 93, 17, 77, 31。通过余数法(remainder method),即使用该元素除以散列表的长度所得余数作为哈希值(hash value),。
计算出每个元素对应的哈希值以后,我们便可以将元素插入到哈希表中。
长度为11的散列表中有6个Slot被占用了,则该散列表的载荷因子为 。同时,如果有不同元素的余数相同则会发生碰撞(Collision),碰撞问题很大程度会影响散列表的查找速度。
最直观的一种,上图使用的就是这种散列法,公式:
index = value % 16
学过汇编的都知道,求模数其实是通过一个除法运算得到的,所以叫“除法散列法”。
求index是非常频繁的操作,而乘法的运算要比除法来得省时(对现在的CPU来说,估计我们感觉不出来),所以我们考虑把除法换成乘法和一个位移操作。公式:
index = (value * value) >> 28 (右移,除以2^28。记法:左移变大,是乘。右移变小,是除。)
如果数值分配比较均匀的话这种方法能得到不错的结果,但我上面画的那个图的各个元素的值算出来的index都是0——非常失败。也许你还有个问题,value如果很大,value * value不会溢出吗?答案是会的,但我们这个乘法不关心溢出,因为我们根本不是为了获取相乘结果,而是为了获取index。
平方散列法的缺点是显而易见的,所以我们能不能找出一个理想的乘数,而不是拿value本身当作乘数呢?答案是肯定的。
1,对于16位整数而言,这个乘数是40503
2,对于32位整数而言,这个乘数是2654435769
3,对于64位整数而言,这个乘数是11400714819323198485
这几个“理想乘数”是如何得出来的呢?这跟一个法则有关,叫黄金分割法则,而描述黄金分割法则的最经典表达式无疑就是著名的斐波那契数列,即如此形式的序列:0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233,377, 610, 987, 1597, 2584, 4181, 6765, 10946,…。另外,斐波那契数列的值和太阳系八大行星的轨道半径的比例出奇吻合。
对我们常见的32位整数而言,公式:
index = (value * 2654435769) >> 28
当两个元素被分配到同一个位置的时候,便会发生碰撞。解决碰撞的一个简单的办法就是在原理的 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)
基于哈希表使用python来实现简单的“字典”结构:
class MyDictionary(object):
# 字典类的初始化
def __init__(self):
self.table_size = 13 # 哈希表的大小
self.key_list = [None]*self.table_size #用以存储key的列表
self.value_list = [None]*self.table_size #用以存储value的列表
# 散列函数,返回散列值
# key为需要计算的key
def hashfuction(self, key):
count_char = 0
key_string = str(key)
for key_char in key_string: # 计算key所有字符的ASCII值的和
count_char += ord(key_char) # ord()函数用于求ASCII值
length = len(str(count_char))
if length > 3 : # 当和的位数大于3时,使用平方取中法,保留中间3位
mid_int = 100*int((str(count_char)[length//2-1])) \
+ 10*int((str(count_char)[length//2])) \
+ 1*int((str(count_char)[length//2+1]))
else: # 当和的位数小于等于3时,全部保留
mid_int = count_char
return mid_int%self.table_size # 取余数作为散列值返回
# 重新散列函数,返回新的散列值
# hash_value为旧的散列值
def rehash(self, hash_value):
return (hash_value+3)%self.table_size #向前间隔为3的线性探测
# 存放键值对
def __setitem__(self, key, value):
hash_value = self.hashfuction(key) #计算哈希值
if None == self.key_list[hash_value]: #哈希值处为空位,则可以放置键值对
pass
elif key == self.key_list[hash_value]: #哈希值处不为空,旧键值对与新键值对的key值相同,则作为更新,可以放置键值对
pass
else: #哈希值处不为空,key值也不同,即发生了“冲突”,则利用重新散列函数继续探测,直到找到空位
hash_value = self.rehash(hash_value) # 重新散列
while (None != self.key_list[hash_value]) and (key != self.key_list[hash_value]): #依然不能插入键值对,重新散列
hash_value = self.rehash(hash_value) # 重新散列
#放置键值对
self.key_list[hash_value] = key
self.value_list[hash_value] = value
# 根据key取得value
def __getitem__(self, key):
hash_value = self.hashfuction(key) #计算哈希值
first_hash = hash_value #记录最初的哈希值,作为重新散列探测的停止条件
if None == self.key_list[hash_value]: #哈希值处为空位,则不存在该键值对
return None
elif key == self.key_list[hash_value]: #哈希值处不为空,key值与寻找中的key值相同,则返回相应的value值
return self.value_list[hash_value]
else: #哈希值处不为空,key值也不同,即发生了“冲突”,则利用重新散列函数继续探测,直到找到空位或相同的key值
hash_value = self.rehash(hash_value) # 重新散列
while (None != self.key_list[hash_value]) and (key != self.key_list[hash_value]): #依然没有找到,重新散列
hash_value = self.rehash(hash_value) # 重新散列
if hash_value == first_hash: #哈希值探测重回起点,判断为无法找到了
return None
#结束了while循环,意味着找到了空位或相同的key值
if None == self.key_list[hash_value]: #哈希值处为空位,则不存在该键值对
return None
else: #哈希值处不为空,key值与寻找中的key值相同,则返回相应的value值
return self.value_list[hash_value]
# 删除键值对
def __delitem__(self, key):
hash_value = self.hashfuction(key) #计算哈希值
first_hash = hash_value #记录最初的哈希值,作为重新散列探测的停止条件
if None == self.key_list[hash_value]: #哈希值处为空位,则不存在该键值对,无需删除
return
elif key == self.key_list[hash_value]: #哈希值处不为空,key值与寻找中的key值相同,则删除
self.key_list[hash_value] = None
self.value_list[hash_value] = None
return
else: #哈希值处不为空,key值也不同,即发生了“冲突”,则利用重新散列函数继续探测,直到找到空位或相同的key值
hash_value = self.rehash(hash_value) # 重新散列
while (None != self.key_list[hash_value]) and (key != self.key_list[hash_value]): #依然没有找到,重新散列
hash_value = self.rehash(hash_value) # 重新散列
if hash_value == first_hash: #哈希值探测重回起点,判断为无法找到了
return
#结束了while循环,意味着找到了空位或相同的key值
if None == self.key_list[hash_value]: #哈希值处为空位,则不存在该键值对
return
else: #哈希值处不为空,key值与寻找中的key值相同,则删除
self.key_list[hash_value] = None
self.value_list[hash_value] = None
return
# 返回字典的长度
def __len__(self):
count = 0
for key in self.key_list:
if key != None:
count += 1
return count
def main():
H = MyDictionary()
H["kcat"]="cat"
H["kdog"]="dog"
H["klion"]="lion"
H["ktiger"]="tiger"
H["kbird"]="bird"
H["kcow"]="cow"
H["kgoat"]="goat"
H["pig"]="pig"
H["chicken"]="chicken"
print("字典的长度为%d"%len(H))
print("键 %s 的值为为 %s"%("kcow",H["kcow"]))
print("字典的长度为%d"%len(H))
print("键 %s 的值为为 %s"%("kmonkey",H["kmonkey"]))
print("字典的长度为%d"%len(H))
del H["klion"]
print("字典的长度为%d"%len(H))
print(H.key_list)
print(H.value_list)
if __name__ == "__main__":
main()
运行结果如下:
字典的长度为9
键 kcow 的值为为 cow
字典的长度为9
键 kmonkey 的值为为 None
字典的长度为9
字典的长度为8
[None, 'kgoat', None, 'kcat', 'kbird', 'kdog', None, 'kcow', None, 'ktiger', 'chicken', 'pig', None]
[None, 'goat', None, 'cat', 'bird', 'dog', None, 'cow', None, 'tiger', 'chicken', 'pig', None]
博客:https://blog.csdn.net/u010891397/article/details/87891546
博客:https://www.jianshu.com/p/a4f8a6f9f541