1. 哈希的引入🗻
哈希存储的基本思想是以关键字(key)为自变量,通过散列函数或哈希(Hash)函数,计算出函数值(哈希地址),作为数据元素的地址,并将数据元素存入相应地址的存储单元中。查找时再根据要查找的关键字使用同样的散列函数计算出哈希地址,然后直接到相应的存储单元中取出元素即可。
例:假设有一个集合:S={16,76,63,57,40},使用Hash法存储该集合。现选取哈希函数:h(K)=K%m. 即关键字值对m(正整数)取余作为哈希地址。本例中集合元素个数n等于5,为了能他们的使哈希值不重复,这里m可以取11.每个元素分别计算哈希地址得:
h(16) = 16%11 = 5
h(57) = 57%11 = 2
… …
h(40) = 40%11 = 7
产生哈希表如下图:
在查找时,同样对查找关键字进行一次哈希函数,所得值即为元素所有地址。本例中若要查找16,h(16)=5,在地址(下标)位置取出元素即可。
2. 哈希冲突🍂
由于散列表的大小M远远小于原有集合的大小N,所以冲突是不可避免的。
概率论中有一个很经典的问题 – 生日悖论,说的就是将每个人出生的日期映射到大小为365的散列表中,此时会发生严重的冲突!如果在场人数为N,那么装填因子为N/365. 冲突(至少有两个同学生日相同)的概率为:
冲突是不可避免的,只能尽可能的减少并且采取一定的策略解决冲突。综上,使用哈希表需要解决两个问题:
- (1) 构造合适的哈希函数,尽可能减少冲突。
- (2) 制定好应对冲突的方案。
2.1 哈希函数🔍
2.1.1 取余法
h
a
s
h
(
k
e
y
)
=
k
e
y
%
m
hash(key) = key \% m
hash(key)=key%m
m为素数时,数据对散列表的覆盖最充分,分布最均匀。
2.1.2 MAD法
-
取余法的缺陷:
-
不动点:无论m取值如何,都有hash(0) = 0
-
相邻的关键码散列地址必相邻
MAD = multiply and add.
h
a
s
h
(
k
e
y
)
=
(
a
∗
k
e
y
+
b
)
%
m
hash(key) = (a * key +b) \% m
hash(key)=(a∗key+b)%m
其实,伪随机数法就是使用了MAD法
2.2 冲突排解 🌵
2.2.1 开放散列
对哈希表的每个位置,都能够存多个entry,叫做开放散列。
例如哈希表的每个位置(桶),都分配多个槽位的方法:
在公共溢出区方法中,如果查找一个词条没有在bucket array中找到,需要遍历整个公共溢出区。
2.2.2 封闭散列
“旅客要在每个生人门口敲叩,才能敲到自己的家门”–泰戈尔
在查找时,沿着查找链查找下一个桶单元,如果命中则返回;如果遇到了一个空桶,说明散列表中没有此项。
2.2.2.1 排解冲突的方法
(1)线性试探
一旦冲突,就试探下一个桶单元,直到命中或到达空桶失败
在线性试探中,
- 只要有空桶,就一定会找到。
- 但是,已发生过(并已排解)的冲突会导致本不必发生的冲突clustering
(2)平方试探
(3)双向平方试探
(4)再散列
预先约定第二散列函数 h a s h 2 ( k e y ) hash_2(key) hash2(key),一旦发生冲突,以 h a s h 2 ( k e y ) hash_2(key) hash2(key)为偏移增量,重新确定地址。
[
h
a
s
h
(
k
e
y
)
+
a
∗
h
a
s
h
2
(
k
e
y
)
]
%
m
[hash(key) + a *hash_2(key)] \% m
[hash(key)+a∗hash2(key)]%m
(5)重散列
随着装填因子变大,冲突概率、排解难度都将激增。
此时,不如集体搬迁到一个更大的散列表。
2.2.2.2 插入和删除
插入:若词条不存在,则插入最后一个空桶。
查找链:可能因此彼此重叠
删除:不能简单的清除桶,因为这样与之重叠的查找链就会断裂,后面的词条虽然存在,但就是访问不到。
因此,可以采用懒惰删除:仅作删除标记。这样,
- 查找词条时:直接跳过,查找链在此延续;
- 插入词条时:在此插入
3. 桶排序 🎑
桶排序(Bucket sort)的原理是将数组分到有限数量的桶里。每个桶再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序),最后依次把各个桶中的记录列出来记得到有序序列。当要被排序的数组内的数值是均匀分配的时候,桶排序使用线性时间(Θ(n)), 突破了排序的下界 O ( n l o g n ) O(nlogn) O(nlogn), 这是因为数的取值范围有了限制!
看这篇回答里的动图,非常直观:
https://zhuanlan.zhihu.com/p/125737294
桶排序只能应用于待排序的列表元素都在某个较小范围的情况,比如生日:只有365个取值。
4. 基数排序 🎹
从右向左,对于每一个域都做一次桶排序,最后的结果就一定是排好序的了。
基数排序的正确性证明:
时间复杂度分析:
设t为域的个数,则时间复杂度为O(t*(2m+n)),当t为常数且m<<n时,时间复杂度为O(n)!