hash基础
hash: 一般翻译做“散列”,也有直接音译为“哈希”的
-
Hash算法(哈希算法、散列算法)
- 就是把任意长度的输入,通过散列算法,变换成固定长度的输出,该输出就是散列值。
- 也可以理解为空间映射函数,是从一个非常大的取值空间映射到一个非常小的取值空间,是一种压缩映射,即散列值的空间通常远小于输入的空间。
(ps:y = x^2 ) - 不同的输入可能会散列成相同的输出,而不可能从散列值来唯一的确定输入值- -> 输入与输出存在多对一的关系,只能单向加密。
- 它是一种单向密码体制,即它是一个从明文到密文的不可逆的映射,只有加密过程,没有解密过程。
-
主要思想
根据结点的关键码值来确定其存储地址:以关键码值为自变量,通过一定的函数关系h(K)(称为散列函数),计算出对应的函数值来,把这个值解释为结点的存储地址,将结点存入到此存储单元(哈希表)中。
检索时,用同样的方法计算地址,然后到相应的单元里去取要找的结点(值和地址一对一关系,set不可存放可变类型)。
这就实现了:通过散列方法可以对结点进行快速检索。
数据集合U={d1,d2…di},每个数据di值为datai,根据data选择相应的关键值key进行了哈希函数运算得出的[0,m]的整数H(key),放入对应的哈希表A[key]位置
-
dict与set
在python中,dict与set实现原理是一样的,都是将实际的值放到list中。
唯一不同的是hash函数操作的对象,对于dict,hash函数操作的对象是key:
而对set是直接操作它的元素,假设操作内容为x,其作为因变量,放入hash函数,通过运算后取list的余数,转化为list的一个下标,此下标位置对于set而言用来放其本身 ;
而对于dict则是创建了两个list,一个list该下标放次key,另一个list中该下标放对应的value
常用的构造散列函数的方法
通常可利用除留余数、移位、循环哈希、平方取中等方法
-
除留余数法
取关键字被某个不大于散列表表长m的数p除后所得的余数为散列地址。即 H(key) = key MOD p, p<=m。
不仅可以对关键字直接取模,也可在折叠、平方取中等运算之后取模。对p的选择很重要,一般取素数或m,若p选的不好,容易产生同义词。
KEYS = (12, 6554, 12345, 34234, 234234, 6456456, 34534, 67645, 2343432, 23423, 1343324) hash_table_len = len(KEYS) + 2 hash_table = {} # 哈希函数 def hash_func(k): return k % hash_table_len def hash_insert(k): k_tno = hash_func(k) if hash_table[k_tno] == None: hash_table[k_tno] = k else: for i in range(k_tno+1, hash_table_len): if hash_table[i] == None: break hash_table[i] = k return k_tno def hash_search(k): j = hash_func(k) if k == hash_table[j]: return j else: for i in range(j+1, hash_table_len): if k == hash_table[i]: break return i if __name__ == '__main__': for i in range(hash_table_len): hash_table[i] = None for k in KEYS: hash_insert(k) print("构造哈希表如下:") print(hash_table) print("查询:") for k in KEYS: print("{0} 所在哈希表的位置是:{1}".format(k, hash_search(k)))
-
直接寻址法
取关键字或关键字的某个线性函数值为散列地址。即H(key)=key或H(key) = a*key + b,其中a和b为常数(这种散列函数叫做自身函数)
def hash_func(k): return a * key + b
-
数字分析法
数字分析法就是找出数字的规律,尽可能利用这些数据来构造冲突几率较低的散列地址。
(分析一组数据,比如一组员工的出生年月日,这时我们发现出生年月日的前几位数字大体相同,这样的话,出现冲突的几率就会很大,但是我们发现年月日的后几位表示月份和具体日期的数字差别很大,如果用后面的数字来构成散列地址,则冲突的几率会明显降低。) -
平均取中法
取关键字平方后的中间几位作为散列地址,这种方法的原理是通过取平方扩大差别,平方值的中间几位和这个数的每一位都相关,则对不同的关键字得到的哈希函数值不易产生冲突,由此产生的哈希地址也较为均匀。
如44平方后得1936,取中93再取list的余 。KEYS = (12, 6554, 12345, 34234, 234234, 6456456, 34534, 67645, 2343432, 23423, 1343324) # 哈希函数 def hash_func(k): k2 = str(k**2) k2_len = len(k2)//2 return int(k2[k2_len-1:k2_len+2]) if __name__ == '__main__': se = set() for k in KEYS: print(k,'对应的hash值为:',hash_func(k)) se.add(hash_func(k)) print(len(se),se)
-
折叠法
将关键字分割成位数相同的几部分,最后一部分位数可以不同,然后取这几部分的叠加和(去除进位)作为散列地址。
如项目是436-555-4601,以2为分组,分成了(43,65,55,46,01),全部加起来:43+65+55+46+01=210.
假设list有11个元素,则210%11=1,所以将436-555-4601放在下标为1的地方。 -
随机数法
H(key) = random(key),选择一随机函数,取关键字的随机值作为散列地址,通常用于关键字长度不同的场合。
hash碰撞及其解决方法
哈希函数是以数据的关键值作为自变量x,输出的y就是数据对于的散列值
会存在两个数据通过哈希函数产生相同输出的情况,即两个元素通过散列函数H得到的地址相同:
即 H(keyi)= H(keyj),这就叫hash碰撞(或hash冲突)
-
冲突主要取决于
- 散列函数,一个好的散列函数的值应尽可能平均分布。
- 处理冲突方法。
- 负载因子的大小。太大不一定就好,而且浪费空间严重,负载因子和散列函数是联动的。
-
解决冲突的办法:
- 线性探查法:冲突后,线性向前试探,找到最近的一个空位置。缺点是会出现堆积现象。存取时,可能不是同义词的词也位于探查序列,影响效率。
- 向后探测:假设一个元素算出来下标为5,另一个元素算出来下标也为5,从开头开始探测第0第1位是否为空,当看到为空的就放入,不过这样相邻探测的不好之处在于容易发生聚集,所以最好是跳跃着进行探测,定义一个skip的值,比如3,用方程rehash(pos) = (pos + skip)%sizeoftable,即使查看0,3,6这样跳跃着来
- 双散列函数法:在位置d冲突后,再次使用另一个散列函数产生一个与散列表桶容量m互质的数c,依次试探(d+n*c)%m,使探查序列跳跃式分布。
- 链式存储:将发生冲突的元素放到同一位置,然后通过“指针”来串联起来。
Python自带的hash–常用hash算法介绍
常见 Hash 算法有 MD5 和 SHA 系列,目前 MD5 和 SHA1 已经被破解,一般推荐至少使用 SHA2-256 算法。
-
内置函数hash()
在python中有内置的哈希函数hash(),返回一个对象(数字、字符串,不能直接用于 list、set、dictionary)的哈希值。
print(type(13742),'hash值为:',hash(13742)) # <class 'int'> hash值为: 13742 print(type(13742.0),'hash值为:',hash(13742.0)) # <class 'float'> hash值为: 13742
相同的数值,不同类型,哈希值是一样的
print(type('13742'),'hash值为:',hash('13742')) # <class 'str'> hash值为: 8985398967764240863 print(type('13742'),'hash值为:',hash('13742')) # <class 'str'> hash值为: 8985398967764240863
相同字符串在同一次运行时的哈希值是相同的,但是不同次运行的哈希值不同
因为Python的字符串hash算法有一个启动时随机生成secret prefix/suffix的机制,存在随机化现象:对同一个字符串输入,不同解释器进程得到的hash结果可能不同。 -
hashlib模块:实现了对同一字符串data,两次运行结果相同
hashlib提供了常见的摘要算法,如MD5,SHA1等等;而且对字符串当需要做可重现可跨进程保持一致性的hash,需要用到hashlib模块。
使用hashilib模块一般分为4步:
- 在python中引用hashlib模块。
- 创建一个hash对象,使用hash算法命名的构造函数,或者通用构造函数。
- 使用hash对象调用update(arg)方法填充对象:将字节对象arg填充到hashlib对象中,arg通常为要加密的字符串
- 调用digest()或者hexdigest()方法来获取摘要(加密结果):
hash对象.digest()返回加密结果 ,它是一个字节对象,长度为md5.digest_size
hash对象.hexdigest() 返回加密结果,它是一个字符串对象,长度为md5.digest_size*2,只包含16进制数字
-
MD4算法
基于 32位操作数的位操作来实现的
-
MD5算法
对输入仍以512位分组,其输出是4个32位字的级联,与 MD4 相同。MD5比MD4来得复杂,并且速度较之要慢一点,但更安全,在抗分析和抗差分方面表现更好。
-
SHA-1
对长度小于264的输入,产生长度为160bit(16进制:长度40)的散列值,因此抗穷举(brute-force)性更好。SHA-1 设计时基于和MD4相同原理,并且模仿了该算法。
-
SHA-256
输入是最大长度小于264位的消息,输出是256bit(16进制:长度64)的消息摘要
-
SHA-384
最大计算明文长度为2^128bit,属于分组算法,分组长度为1024bit,产生的信息摘要长度为6*64bit(长度6*64/4=96)
-
SHA-512
整个格式化的信息基本上有三个部分:原始消息、填充位、原始消息的大小。它们的组合大小应该是1024位的整数倍。这是因为格式化的消息将被处理为每个1024位的块,因此每个块应该有1024位可以使用。
输出是8*64bit(长度8*64/4=128)的消息摘要
常用算法详解参考:http://www.sohu.com/a/232586831_100078137
import hashlib data = 'hello world' #1.MD5 md5 = hashlib.md5() md5.update(data.encode('utf-8')) print('md5 拼接前:{0},长度为:{1}'.format(md5.hexdigest(),len(md5.hexdigest()))) md5.update(data.encode('utf-8')) print('mad5 拼接后:{0},长度为:{1}'.format(md5.hexdigest(),len(md5.hexdigest()))) #2.SHA-1 sha1 = hashlib.sha1() sha1.update(data.encode('utf-8')) print('SHA-1:{0},长度为:{1}'.format(sha1.hexdigest(),len(sha1.hexdigest()))) #3.SHA-256 sha256 = hashlib.sha256() sha256.update(data.encode('utf-8')) print('SHA-256:{0},长度为:{1}'.format(sha256.hexdigest(),len(sha256.hexdigest()))) #4.SHA-384 sha384 = hashlib.sha384() sha384.update(data.encode('utf-8')) print('SHA-384:{0},长度为:{1}'.format(sha384.hexdigest(),len(sha384.hexdigest()))) #5.SHA-512 sha512 = hashlib.sha512() sha512.update(data.encode('utf-8')) print('SHA-512:{0},长度为:{1}'.format(sha512.hexdigest(),len(sha512.hexdigest())))
需要注意:update会进行字符串拼接,即拼接前后运行结果不同,需要重新md5 = hashlib.md5()
sha512.update(data.encode('utf-8')) print('SHA-512:{0},长度为:{1}'.format(sha512.hexdigest(),len(sha512.hexdigest())))
需要注意:update会进行字符串拼接,即拼接前后运行结果不同,需要重新md5 = hashlib.md5()