散列(哈希)
一. 概念
如果数据项之间是按照大小排好序的话,就可以利用二分查找来降低算法复杂度;现在我们进一步来构造一个新的数据结构,能使得查找算法的复杂度降为O(1),这种概念叫做"散列Hashing"
-
能够使得查找的次数降低到常数级别,我们对数据项所处的位置就必须有更多的先验知识
-
如果我们事先能知道要找的数据项应该出现在数据集中的什么位置,就可以直接到那个位置看看数据项是否存在即可
-
又数据项的值来确定其存放位置,如何能做到这一点呢?
- 散列表是一种数据集,其中数据项的存储方式尤其有利于将来快速的查找定位
- 散列表中的每一个存储位置,称为槽(slot),可以用来保存数据项,每个槽都有一个唯一的名称
例如:一个包含11个槽的散列表,槽的名称分别为0~10,在插入数据项之前,每个槽的值都是None,表示空槽,散列函数实现从数据项到存储槽名称的转换
散列函数接受数据项作为参数,返回整数值0~10,表示数据项存储的槽号(名称)
-
有一种常用的散列函数是"求余数", 将数据项除以散列表的大小,得到的余数作为槽号
"求余数"的这种方法会以不同形式出现在所有散列函数中,因为散列函数返回的槽号必须在散列表大小范围之内,所以一般会对散列表大小求余
-
数据项都保存在散列表后,当要查找某个数据项是否存在于表中,我们只需要使用同一个散列函数,对查找项进行计算,测试下返回的槽号所对应的槽中是否有数据项即可,这样就实现了O(1)事件复杂度的查找算法.
但是如果保存44,h(44) = 0, 它跟77被分配到同一个槽----0号槽中,这种情况被称为"冲突collision"
二. 完美散列函数
给定一组数据项,如果一个散列函数能把每个数据项映射到不同的槽中,那么这个散列函数就可以被称为"完美散列函数"
1. 对于固定的一组数据,总是能想办法设计出完美的散列函数
2. 但如果数据项经常性的变动很难有一个系统性的方法来设计对应的完美散列函数
方案:
-
获得完美散列函数的一种方法是扩大散列表的容量,大到所有可能出现的数据项都能够占据不同的槽
但这种方式对于可能数据项范围过大的情况并不实用,假如完美要保存手机号(11位数字),完美散列函数要求散列表具有百亿个槽,会浪费太多存储空间
-
好的散列函数要具备 冲突最少(近似完美), 计算难度低(额外开销小), 充分分散数据项(节约空间)
-
除了用于在散列表中安排数据项的存储位置,散列技术还用在信息处理的很多领域
-
由于完美散列函数能够对任何不同的数据生成不同的散列值,如果把散列值当做数据的"指纹"或者"摘要", 这种特性被广泛应用在数据的一致性校验上
由任意长度的数据生成长度固定的"指纹", 还要求具备唯一性,这在数学上是无法做到的,但是设计巧妙的"准完美"散列函数能在实用范围内做到这一点.
-
作为一致性校验的数据"指纹"函数需要具备如下的特性:
- 压缩性: 任意长度的数据,得到的"指纹"长度是固定的
- 易计算性,从元数据计算"指纹"很容易;(从指纹计算原数据是不可能的)
- 抗修改性:对原数据的微小变动,都会引起"指纹"的大改变
- 抗冲突性:已知原数据和"指纹", 要找到相同指纹的数据(伪造)是非常困难的
-
最著名的近似完美散列函数是MD5和SHA系列函数,MD5(Message digest)将任何长度的数据变换为固定长为127位(16字节)的"摘要"
三. 散列函数设计
(一) 折叠法
步骤:
-
将数据项按照位数分为若干段
-
再将几段数字相加
-
最后对散列表大小求余,得到散列值
例如对号码62767255,可以两位两位分为4段(62, 76,72,55) 相加,等于265,散列表包含11个槽,那么就是265%11=1.所以h(62767255)=1
有时候折叠法还会包括一个隔数反转的步骤,比如:
例如对号码62767255,可以两位两位分为4段(62, 76,72,55) 隔数反转为(62, 67,72,55)相加,等于256,散列表包含11个槽,那么就是256%11=3.所以h(62767255)=3
虽然隔数反转从理论上看来毫无必要,但这个步骤确实为折叠法得到散列函数提供了一种微调手段,以便更好符合散列特性
(二) 平方取中法
步骤:
-
将数据项做平方运算
-
取平方数的中间两位
-
对散列表的大小求余
例如,对44进行散列,首先44*44=1936,然后取中间的93,对散列表大小11求余,93%11=5
(三) 非数项
我们也可以对非数字的数据项进行散列,把字符串中的每个字符看作ASCII码即可,如cat,ord(‘c’)==99, ord(‘a’) = 96, ord(‘t’)=116, 加起来,然后对散列表大小求余
99+97+116=312
312%11=4
这样的散列函数对所有的"变位词"都返回相同的散列值,为了防止这一点,可以将字符串所在的位置作为权重因子,乘以ord值