散列
使查找算法的复杂度降到O(1)
散列表(哈希表)是一种数据集 ,其中数据项的存储方式尤其有利于将来快速的查找定位。散列表中的每一个存储位置,称为槽(slot),可以用来保存数据项,每个槽有一个唯一的名称。
实现从数据项到存储槽名称的转换的,称为散列函数
槽被数据项占据的比例称为散列表的 “ 负载因子“
被分配到同一个槽中,这种情况称为“冲突collision”
给定一组数据项,如果一个散列函数能把每个数据项映射到不同的槽中,那么这个散列函数就可以称为“完美散列函数”
好的散列函数需要具备特性冲突最少(近似完美)、计算难度低(额外开销小)、充分分散数据项(节约空间)
散列函数库hashlib:
Python自带MD5和SHA系列的散列函数库:hashlib
import hashlib
print(hashlib.md5("hello".encode("utf-8")).hexdigest())
print(hashlib.sha1("hello".encode("utf-8")).hexdigest())
用update方法来对任意长的数据分部分来计算,这样不管多大都不会有内存不足的问题
import hashlib
m = hashlib.md5()
m.update("hello".encode("utf-8"))
m.update("this is part #2".encode("utf-8"))
print(m.hexdigest())
完美散列函数用于数据一致性校验
- 数据文件一致性判断
- 加密形式保存密码
- 防文件篡改
- 彩票投注应用
区块链技术
区块链是一种分布式数据库
- 通过网络连接的节点
- 每个节点都保存着这个数据库的所有数据
- 任何地点存入的数据都会完成同步
区块链最本质特征是“去中心化”
- 不存在任何控制中心、协调中心节点
- 所有节点都是平等的,无法被控制
区块链由一个个区块(block)组成,区块分为头(head)和体(body)
区块头记录了一些元数据和链接到前一个区块的信息生成时间、前一个区块(head+body) 的散列值
区块体记录了实际数据
区块链不可修改性:
由于散列值具有抗修改性,任何对某个区块数据的改动必然引起散列值的变化
散列函数设计
1.折叠法
折叠法设计散列函数的基本步骤是:
将数据项按照位数分为若干段,再将几段数字相加,最后对散列表大小求余,得到散列值。
有时候折叠法还会包括一个隔数反转的步骤。虽然隔数反转从理论上看来毫无必要,但这个步骤确实为折叠法得到散列函数提供了一种微调手段,以便更好符合散列特性。
2.平方取中法
平方取中法,首先将数据项做平方运算,然后取平方数的中间两位,再对散列表的大小求余。
3.非数项
对非数字的数据项进行散列,把字符串中的每个字符看作ASCII码即可,再将这些整数累加,对散列表大小求余。
def hash(astring,tablesize):
sum = 0
for pos in range(len(astring)):
sum = sum + ord(astring[pos]) # ord取ascii码
return sum % tablesize
print(hash("ahfa",9))
这样的散列函数对所有的变位词都返回相同的散列值为了防止这一点,可以将字符串所在的位置作为权重因子,乘以ord。
设计散列函数坚持的一个基本出发点是,散列函数不能成为存储过程和查找过程的计算负担。
冲突解决方案
如果两个数据项被散列映射到同一个槽,
需要一个系统化的方法在散列表中保存第二个数据项,这个过程称为“解决冲突”
1.线性探测linear probing
从冲突的槽开始往后扫描,直到碰到一个空槽如果到散列表尾部还未找到,则从首部接着扫描。
用线性探测方法来解决散列冲突的话,则散列表的查找也遵循同样的规则。如果在散列位置没有找到查找项的话,就必须向后做顺序查找直到找到查找项,或者碰到空槽(查找失败)。
线 性 探 测 法 的 一 个 缺 点 是 有 聚 集 (clustering)的趋势,即如果同一个槽冲突的数据项较多的话,这些数据项就会在槽附近聚集起来,从而连锁式影响其它数据项的插入。
2.线性探测的改进
避免聚集的一种方法就是将线性探测扩展,从逐个探测改为跳跃式探测。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y4fmuYgQ-1613300333304)(D:\我的文件\桌面的文件\7.png)]
再散列rehashing(重新寻找空槽的过程)
- newhashvalue = rehash(oldhashvalue)
- 对于线性探测来说,rehash(pos)= (pos+ 1)%sizeoftable
- “+3 ” 的跳跃式探测则是:rehash(pos)=(pos+ 3)% sizeoftable
- 跳跃式探测的再散列通式是:rehash(pos)=(pos+skip)% sizeoftable
- 跳跃式探测中,需要注意的是skip的取值,不能被散列表大小整除,否则会产生周期,造成很多空槽永远无法探测到一个技巧是,把散列表的大小设为素数。
- 还 可 以 将 线 性 探 测变 为 “ 二 次 探 测quadratic probing”,不再固定skip的值,而是逐步增加skip值,如1、3、5、7、9。这样槽号就会是原散列值以平方数增加:h, h+1,h+4, h+9, h+16…
3.数据项链Chaining
将容纳单个数据项的槽扩展为容纳数据项集合(或者对数据项链表的引用)。
映射抽象数据类型(ADT Map)
ADT Map的结构是键-值关联的无序集合关键码具有唯一性
通过关键码可以唯一确定一个数据值。
用一个HashTable类来实现ADT Map。注意散列表的大小,虽然可以是任意数,但考虑到要让冲突解决算法能有效工作,应该选择为素数。
class HashTable:
def __init__(self):
self.size = 11
self.slots = [None] * self.size # slot列表用于保存key
self.data = [None] * self.size # data列表 用于保存数据项
def put(self, key, data):
hashvalue = self.hashfunction(key)
if self.slots[hashvalue] == None: # key不存在,未冲突
self.slots[hashvalue] = key
self.data[hashvalue] = data
else:
if self.slots[hashvalue] == key: # key已存在,替换val
self.data[hashvalue] = data # replace
else:
nextslot = self.rehash(hashvalue)
while self.slots[nextslot] != None and self.slots[nextslot] != key: # 散列冲突,再散列,直到找到空槽或者key
nextslot = self.rehash(nextslot)
if self.slots[nextslot] == None:
self.slots[nextslot] = key
self.data[nextslot] = data
else:
self.data[nextslot] = data # replace
def hashfunction(self, key):
return key % self.size
def rehash(self, oldhash):
return (oldhash + 1) % self.size
def get(self,key):
startsolt = self.hashfunction(key) # 标记散列值为查找起点
data = None
stop = False
found = False
position = startsolt
while self.slots[position] != None and not found and not stop: # 找key,直到空槽或回到起点
if self.slots[position] == key:
found = True
data = self.data[position]
else:
position = self.rehash(position) # 未找到key,再散列继续找
if position == startsolt:
stop = True # 回到起点,停
return data
def __getitem__(self, key):
return self.get(key)
def __setitem__(self, key, data):
self.put(key, data)
应用:
H = HashTable()
H[54] = "cat"
H[26] = "dog"
H[93] = "lion"
H[17] = "tiger"
print(H.slots)
print(H.data)
print(H[93])
H[26] = "duck"
print(H[26])
运行结果:
散列算法分析:
散列在最好的情况下,可以提供O(1)常数级时间复杂度的查找性能。
评估散列冲突的最重要信息就是负载因子λ,一般来说:
- 如果λ较小 ,散列冲突的几率就小,数据项通常会保
存在其所属的散列槽中 - 如果λ 较大 ,意味着散列表填充较满,冲突会越来越多,冲突解决也越复杂,也就需要更多的比较来找到空槽;如果采用数据链的话,意味着每条链上的数据项增多
如果采用线性探测的开放定址法来解决冲突(λ在0~1之间):
如果采用数据链来解决冲突(λ可大于1):