哈希小结
定义
也叫做散列,把任意长度的输入通过哈希算法(散列算法)变换成固定长度的输出,输出称为哈希值(散列值)。这种转换实际上是一种压缩映射,因为散列值的空间通常来说要远小于输入空间,这样不同的输入可能会散列成相同的输出,从而造成哈希冲突,但同时也不能保证根据散列值来推断输入。
应用
- 快速检索:需要在一个集合中快速检索是否存在某个数时,可以将哈希值作为索引,并在相应的地址中存储输入值,但需要注意冲突问题。
可以认为哈希的平均复杂度为 O ( 1 ) O(1) O(1)。
- 安全加密:将明文做哈希处理后得到的散列值,作为加密后的密码或校验值等,目前常用的哈希函数有MD5、SHA等。由于一个哈希值可以对应无数个明文,因此该过程是不可逆的。
这里有个问题,如果数据库存储的是MD5加密后的密码,那么理论上并不需要知道明文密码是什么,只需要找到一串字符可以得到相同的散列值就可以登录。
哈希函数
常用的哈希函加密数通常需要满足以下5点:
- 确定性,即对于相同的输入应该有确定的散列值。
- 快速的,即能够快速计算得到散列值。
- 不可逆的,即除非尝试所有可能的消息,否则无法从散列值生成消息。
- 雪崩性,即对输入很小的改变能引起散列值巨大的变化,使新散列值看起来和旧的毫不相干。
- 抗冲突性,即找到具有两个相同散列值的两个不同消息是不可行的。
前两条哈希函数都满足,关键是后三条,尤其是3和5的平衡。因为要满足3的话,输入空间应该要远大于散列空间(如对100取模,那么散列值为1可能的输入就有1,101,201…),这样的话容易产生冲突,即容易找到两个散列值相同的不同消息。因此,结合5的话可以知道散列空间不能太小。
详细可以参见关于密码学中的hash:https://zhuanlan.zhihu.com/p/44544072
常用的哈希函数有如下几种(用于哈希存储,假设需要存储的值为 k e y key key,哈希值是存储地址的索引):
- 直接寻址法:直接用 k e y key key值或者 k e y key key的线性函数,即 H ( k e y ) = k e y H(key)=key H(key)=key或 H ( k e y ) = a ∗ k e y + b H(key)=a*key+b H(key)=a∗key+b
- 除留余数法:取 k e y key key被某个不大于散列表长度 m m m的数 p p p除得到的余数作为哈希值,即 H ( k e y ) = k e y M O D p , p < < m H(key)=key\ MOD\ p,\ p<<m H(key)=key MOD p, p<<m。其中 p p p的取值非常重要,一般取素数(理由见下面),若没有取好,很容易产生冲突。
- 随机数法:将 k e y key key值作为随机函数的 s e e d seed seed值,随机函数的结果作为散列值。
- 平方取中法:将 k e y key key平方后取中间几位作为散列值。
- 数字分析法:取 k e y key key中比较随机的几位作为散列值,如身份证中前几位很多人都相同,那么可以取最后几位作为散列值。
冲突解决
由于一般情况下哈希函数的输入空间远远大于散列空间,设计良好的哈希函数虽然能够尽力避免冲突,但产生冲突还是难以避免,针对冲突的解决方案主要有如下几种:
- 开放地址法:若某个地址产生冲突,就去寻找散列空间中的下一个地址,直到不产生冲突。
例:假设关键字集合为 { 12 , 67 , 56 , 16 , 25 , 37 , 22 , 29 , 15 , 47 , 48 , 42 } \{12,67,56,16,25,37,22,29,15,47,48,42\} {12,67,56,16,25,37,22,29,15,47,48,42},采用除留余数法 p = 12 p=12 p=12,则产生的散列表如下:
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
k e y key key | 12 | 25 | 37 | 15 | 16 | 29 | 67 | 56 | 22 | 47 |
可以看到 k e y = 37 key=37 key=37时产生了冲突。
- 再散列法:采用两种或以上的散列函数,若某一种冲突则换一种重新散列。
- 链地址法:对于产生冲突的 k e y key key在一条链表中串联起来,相应的检索也在对应散列值的链表中完成。
为什么对关键字取模一般用质数
如果输入是独立随机均匀的,那么
p
p
p取任意值都是一样的。但现实情况中,往往不满足均匀这一条件,关键字之间往往是有联系的。
看如下这种情况:
k
e
y
=
{
6
,
12
,
28
,
24
,
30
,
36...
}
key=\{6,12,28,24,30,36...\}
key={6,12,28,24,30,36...},若取
p
=
10
p=10
p=10,那么毫无疑问$hash(key)\in{0,2,4,6,8}
,
没
有
映
射
到
,没有映射到
,没有映射到{1,3,5,7,9}
,
这
样
就
加
大
了
冲
突
的
可
能
性
。
分
析
:
由
于
,这样就加大了冲突的可能性。 分析:由于
,这样就加大了冲突的可能性。分析:由于hash(key)=key%p
,
假
设
,假设
,假设key/p=a
,
则
,则
,则key=ap+hash(key)
,
得
,得
,得hash(key)=key-ap
。
将
式
子
变
形
可
得
。将式子变形可得
。将式子变形可得hash(key)=gcd(key,p)(\frac{key}{gcd(key,p)}-\frac{ap}{gcd(key,p)})
。
因
此
,
。因此,
。因此,hash(key)
的
值
是
的值是
的值是key
和
和
和p
的
最
大
公
约
数
的
倍
数
。
由
于
上
述
中
的
的最大公约数的倍数。由于上述中的
的最大公约数的倍数。由于上述中的key
都
是
6
的
倍
数
,
因
此
都是6的倍数,因此
都是6的倍数,因此hash(key)
都
是
都是
都是gcd(6,10)=2
的
倍
数
。
若
的倍数。若
的倍数。若p$选为素数则不会出现这种情况。
参见知乎:
- https://www.zhihu.com/question/20806796
- http://www.vvbin.com/?p=376
好的素数取值可以参见:https://planetmath.org/goodhashtableprimes
简单代码实现
哈希函数使用除留余数法,解决碰撞冲突采用链地址法。
class MyHashMap {
private:
vector<list<pair<int, int>>> data;
static const int base = 769;
static int hash(int key) {
return key % base;
}
public:
/** Initialize your data structure here. */
MyHashMap(): data(base) {}
/** value will always be non-negative. */
void put(int key, int value) {
int h = hash(key);
for (auto it = data[h].begin(); it != data[h].end(); it++) {
if ((*it).first == key) {
(*it).second = value;
return;
}
}
data[h].push_back(make_pair(key, value));
}
/** Returns the value to which the specified key is mapped, or -1 if this map contains no mapping for the key */
int get(int key) {
int h = hash(key);
for (auto it = data[h].begin(); it != data[h].end(); it++) {
if ((*it).first == key) {
return (*it).second;
}
}
return -1;
}
/** Removes the mapping of the specified value key if this map contains a mapping for the key */
void remove(int key) {
int h = hash(key);
for (auto it = data[h].begin(); it != data[h].end(); it++) {
if ((*it).first == key) {
data[h].erase(it);
return;
}
}
}
};