文章目录
正好以前上算法课的时候有做过hash的笔记,直接把笔记搬运过来了,可能和具体实现有点不一样,但道理是一样的。当时上课的书是算法导论
所有代码实现:Tian-hy/c_ds
1. 直接寻址表(Direct-address Tables)
直接寻址表,记为 T [ 0... m − 1 ] T[0...m-1] T[0...m−1], 其中每个位置称为槽(Slot),对应全域U的一个关键字(Key),key指向satellite data,若没有key为k的satellite data,则 T [ k ] = N I L T[k]=NIL T[k]=NIL。
O ( 1 ) O(1) O(1)
DIRECT-ADDRESS-SEARCH(T, k)
return T[k]
O ( 1 ) O(1) O(1)
DIRECT-ADDRESS-INSERT(T, x)
T[x.key] = x
O ( 1 ) O(1) O(1)
DIRECT-ADDRESS-DELETE(T, x)
T[x.key] = NIL
优点 | 将对象直接存放在表的槽中,从而节约空间 |
只需要知道下标就可以找到元素,不必存储关键字。然而不存储关键字,就必须用某种方法确定槽是否为空。 | |
缺点 | 易造成表的空间不够或空间被浪费 |
2. 散列表
- 在直接寻址下,具有关键字k的元素被放到槽k中。在散列的方式下,该元素被放到 h ( k ) h(k) h(k)中;即利用散列函数(hash function)h,计算出关键字k的槽的位置。
h : U → { 0 , 1 , . . . , m − 1 } h:U \rightarrow \{0, 1, ..., m-1\} h:U→{ 0,1,...,m−1}
- 读作:关键字k的元素被散列到槽 h ( k ) h(k) h(k)上,或 h ( k ) h(k) h(k)是关键字k的散列值
- 尽管尽可能的使h随机,但是难免出现冲突。
优点:
- 散列表的存储需求只需要 θ ( ∣ K ∣ ) \theta (|K|) θ(∣K∣),且查找一个元素任然只需要 O ( 1 ) O(1) O(1)
缺点:
- O ( 1 ) O(1) O(1)是平均情况时间,对直接寻址表来说是最坏情况时间
- 会出现冲突(collision)
解决冲突1:链接法
1. 链接发的介绍
- 在链接法中,把散列到同一槽中的所有元素都放在一个链表中。槽j中有一个指针,指向散列到j的元素的链表的表头;弱不存在这样的元素,则指向NIL
Insert: O ( 1 ) O(1) O(1),插入相较于其他操作要稍微快一点,因为在此假设插入的元素没有出现在表中;若要检查x是否在表中需要付出额外代价,需要执行一个search来查找。
CHAINED-HASH-INSERT(T,x)
insert x at the head of list T[h(x.key)]
Search: O ( n ) O(n) O(n),查找方法的最坏情况运行时间与表的长度成正比。
O ( 1 ) O(1) O(1),期望时间。( O ( 1 + α ) O(1+\alpha) O(1+α),而 α \alpha α为 O ( 1 ) O(1) O(1))
CHAINED-HASH-SEARCH(T,k)
search for an element with key k in list T[h(k)]
Delete:
双向链表: O ( 1 ) O(1) O(1),因为我们输入的是x
,而x包含x.key
,所以无需搜索就可以直接找到x的位置,叫x.prev
的元素指向x.next
的元素,即可完成删除操作。
单向链表: O ( n ) O(n) O(n),我们可以直接找到x.key
的位置,但是因为是单向链表所以无法直接找到x.prev
,也就是说需要遍历链表找到x.prev
,再将前一个元素的key指向后一个元素的key,完成删除操作,渐进运行时间与search相同。
CHAINED-HASH-DELETE(T,x)
delete x from the list T[h(x.key)]
2. 链接法散列的分析(查找一个给定关键字的时间)
-
最坏情况下查找的时间为 θ ( n ) \theta(n) θ(n)
-
散列的平均性能依赖于所选取的散列函数h
-
Assume:
-
放n个元素、具有m个槽位的散列表T,定义装载因子(load factor) α = n / m \alpha =n/m α=n/m。
-
简单均匀散列(simple uniform hashing)
对于 j = 0 , 1 , . . . , m − 1 j=0, 1, ..., m-1 j=0,1,...,m−1,列表 T [ j ] T[j] T[j]的长度用 n j n_j nj来表示,于是有
n = n 0 + n 1 + . . . + n m − 1 n=n_0+n_1+...+n_{m-1} n=n0+n1+...+nm−1
n j n_j nj的期望长度为 E [ n j ] = α = n / m E[n_j]=\alpha=n/m E[nj]=α=n/m。 -
定义:
查找不成功,表中没有一个元素的关键字为k
查找成功,成功找到关键字为k的元素
-
定理11.1: 在简单均匀散列的假设下,对于用链接法解决冲突的散列表,一次不成功search的avg time为 θ ( 1 + α ) \theta(1+\alpha) θ(1+α)。
定理11.2: 在简单均匀散列的假设下,对于用链接法解决冲突的散列表,一次成功查询的avg time为 θ ( 1 + α ) \theta(1+\alpha) θ(1+α)。
结论: 若散列表中槽数与表中的元素成正比,全部字典操作平均情况下都可以在 O ( 1 ) O(1) O(1)的时间内完成
3. 散列函数
介绍散列的三种具体方法:两种启发式方法(乘法与除法进行散列)与一种利用随机技术来提供可证明的良好性能(全域散列,universal hashing)
-
一种好的方法导出的散列值,在某种程度上应独立与数据可能存在的任何模式,甚至很接近的关键字要被散列到截然不同的散列值上。
-
将关键字转换为自然数
将字符串转换为ASCII码,随后以128为基数来表示。
举例: p t → ( p = 112 , t = 116 ) → p t = ( 112 ∗ 128 ) + 116 = 14452 pt\rightarrow (p=112, t=116) \rightarrow pt=(112*128)+116=14452 pt→(p=1