1、散列(hash)
(1)基本概念
要想将查找的次数降低到常数级别,先要对数据项所处的位置有更多的先验知识。
散列表又称哈希表,是一种数据集,其中数据项的存储方式尤其有利于将来快速的查找定位。
散列表中每个存储位置成为槽,可以用来保存数据项,每个槽有唯一的名称,在插入数据项之前每个槽的值都是None,表示空槽。
实现从数据项到存储槽名称转换的称为散列函数。
列子:
数据项:54,26,93,17,77,31
求余数:将数据项除以散列表大小,得到余数作为槽号。
槽的大小是11,除以11的余数作为槽号。h(item)=item%11
槽被数据项占据的比例称为散列表的负载因子(6/11)
要查找某个项是否在表中只需要使用同一个散列函数,对查找项进行计算,看看相应的槽号是否是该数据项即可,复杂度为O(1)
2、完美散列函数
(1)如果一个散列函数能把数据项映射到不同的槽中称该散列函数为完美散列函数。
退而求其次用较好的散列函数:
冲突最少,计算难度低,充分分散数据项(节约空间)
完美散列函数的应用:一致性校验
压缩性:任意长度的数据,得到的指纹长度是固定的
易计算性且从指纹无法推出原数据
抗修改性:对原始数据的微小变动都会引起指纹的大改变。
抗冲突性:有相同指纹的数据十分困难的。
(2)python的散列函数库hashlib
包括md5,sha1,sha224,sha256,sha384,sha512这6种散列函数
eg
import hashlib
hashlib.md5("hello world").hexdigest()#返回16进制摘要
#用update方法对任意长的数据进行计算
m=hashlib.md5()
m.update("hello world")
m.hexdigest()
(3)应用
又来判断文件是否一致,例如网盘中上传之前先计算服务器中是否有该文件,如果有可以不用上传
用散列值加密保存密码
防文件篡改
3、区块链技术
(1)去中心化,每个节点都保存着全部数据信息。
有区块(block)组成,区块分为头(head)和体(body)
区块头记录了一些元数据和链接到前一个区块的信息(生成时间和前一个数据的散列值)
区块体记录了实际数据。
(2)防篡改性
由于散列值具有抗修改性,任何对某个区块数据的改动必然引起散列值的变化,为了不导致这个区块脱离链条,就需要修改所有后续区块,但是由于工作量证明机制,这种大规模的修改是不可能实现的,除非掌握了全网51%的算力。
(3)工作量证明
由于区块链是大规模的分布式数据库,同步较慢,新区块的添加速度需要得到控制,大家不惜付出海量的计算去算出一个区块链的有效散列值,率先算出来的人才有资格把区块挂到区块链中。
问题是有效散列问什么那么难计算: 因为很难计算所以控制了新区块链的生成速度,便于在整个分布式网络中进行同步
每个区块设置了一个难度系数Difficulty,用常数targetmax除以它,得到一个target,难度系数越高target越小。
矿工的工作是找到一个数值Nonce,把它跟整个区块数据一起计算散列,这个散列值必须小于target才是有效散列值。
由于散列值无法推回原值,这个Nonce的寻找只能靠暴力穷举,计算量+运气是唯一的方法。
(难度系数越大,target越小,因此Nonce越难找)
由于摩尔定律的存在,计算力将持续递增,为了维持每10分钟生成一个区块,难度系数将会持续递增,另外为了保持货币数量不会无限增加,每4年奖励的比特币减半。
4、散列函数的设计
(1)折叠法
将数据项按照位数分为若干段,再将几段数字相加,最后对散列表大小求余,得到散列值
eg:电话号码62767255
可以按两位分为四段(62,76,72,55)
相加为265
散列表包含11个槽,那么就是265%11=1
所以h(62767255)=1
又是折叠法还会包含一个隔数反转的步骤,提供了一个微调的手段。
eg:
可以按两位分为四段(62,76,72,55)
反转为62,67,72,55
相加为256
散列表包含11个槽,那么就是265%11=3
所以h(62767255)=3
(2)平方取中
首先将数据项做平方运算,然后取平方数的中间两位,再对散列表的大小求余
eg:44
44*44=1936
取中间数93
93%11=5
h(44)=5
(3)非数项的操作
把字符串中的每个字符看作ASCII码即可
如cat,ord(‘c’)=99,ord(‘a’)=96,ord(t’)=116
再将这些数相加,对散列表大小求余
def hash(astring,tablesize):
sum=0
for pos in range(len(astring)):
sum+=ord(astring[pos])
return sum%tablesize
然而上述方法对所有的变位词都返回相同的散列值,为防止这一点可以将字符串所在位置作为权重因子,乘以ord值。
5、冲突解决方案
需要一个系统化的方法在散列表中保存第二个冲突的数据项
(1)开放定址法
线性探测:最简单的方法就是从冲突槽开始往后扫描直到碰到一个空槽
如果散列表末尾未找到则从首部接着扫描。
在查找的时候也遵循同样的方法,如果再散列位置没有找到的话(还要非空),就必须向后查找,直到找到查找项或碰到空槽。
缺点是有聚集的趋势,会连锁性的影响其他数据项的插入
(2)改变:从逐个探测改为跳跃式探测eg:+3
rehash(pos)=(pos+skip)%sizeoftable
这里要注意skip的取值不能够被散列表大小整除,否则会产生周期,无法遍历
(3)二次探测:不在固定skip的值,而是逐步增加skip的值,如1,3,5,7,9
(4)数据项链法
把槽从只能容纳单个数据项的槽扩展为容纳数据项集合,当然随着冲突散列的增加,对数据项的查找时间也会相应地增加。
6、映射抽象数据类型
ADT Map结构是键-值关联的无序集合
(1)相关操作
Map():创建一个空映射,返回空映射对象
put(key,val):将key-value对加入映射中,如果key已经存在则更新val值
get(key):给定key,返回其关联值,如果不存在则返回None
del:通过del map[key]删除key-val关联对
len():返回映射中key-val关联的数目
in:通过key in map的语句形式返回key是否存在于关联中,返回bool值。
(2)关键在于快速查找,用散列表来进行实现
slots用来保存键值,data用来保存val
class HashTable:
def __init__(self):
self.size=11#hash表的大小,要选素数
self.slots=[None]*self.size
self.data=[None]*self.size
def hashfunction(self,key):
return key%self.size
def rehash(self,oldhash):
return (oldhash+1)%self.size
def put(self,key,data):
hashvalue=self.hashfunction(key)#求散列值
if self.slots[hashvalue]==None:
self.slots[hashvalue]=key
self.data[hashvalue]=data
else:
if self.slots[hashvalue]==key:
self.data[hashvalue]=data#key存在时替换
else:
nextslot=self.rehash(hashvalue)
while self.slots[nextslot]!=None \
and self.slots[nextslot]!=key:
nextslot=self.rehash(nextslot)
if self.slots[nextslot]==None:
self.slots[hashvalue]=key
self.data[hashvalue]=data
else:
self.data[hashvalue]=data#替换
def get(self,key):
startslot=self.hashfunction(key)#求散列值
data=None
stop=False
found=False
position=starslot
while self.slots[position]!= None and not found and not stop:
if self.slots[position]==key:
found=True
data=self.data[position]
else:
position=self.rehash(position)
if position==startslot:#找了一圈都没找到
stop=True
return True
#通过特殊方法实现[]访问
def __getitem__(self,key):
return self.get(key)
def __setitem__(self,key,data):
self.put(key,data)