《算法导论3rd第十一章》散列表

前言

散列表(hash table)实现字典操作的一种有效数据结构。对于大部分的查找问题,使用散列表能达到O(1)的效率

直接寻址表

假设有限关键字集合U = { 0, 1, …, m - 1 },实际的关键字集合K(属于U的子集)。

  1. 用一个数组T[0…m - 1],其中每个位置对应U中的一个关键字k。
  2. 把关键字k映射到槽T(k)上的过程称为散列。

在这里插入图片描述
散表表仅支持INSERT、SEARCH、DELETE操作。

DIRECT-ADDRESS-SEARCH(T, k)
    return T[k]
DIRECT-ADDRESS-INSERT(T, x)
    T[x, key]=x
DIRECT-ADDRESS-DELETE(T, x)
    T[x, key]=null

练习

在这里插入图片描述

1-1

查找指定元素所用时间为O(1),如果找表中最大值,那么最坏情况下需要循环遍历散列表中所有有效值以确定最大值。运行时间为O(m).

1-2

如果插入元素x,那么把位向量的第x位设置为1,否则为0. 如果删除元素x,那么把位向量的第x位设置为0

BITMAP-SEARCH(V, k)
    if V[k] != 0
        return k
    else return NIL

BITMAP-INSERT(V, x)
    V[x] = 1

BITMAP-DELETE(V, x)
    V[x] = 0
1-3

数组+链表(实现可重)

1-4

(略)

散列表

直接寻址技术的缺点是非常明显的:如果全域U很大,则在一台标准的计算机可用内存容量中,要存储大小为|U|的一张表T也许不太实际,甚至是不可能的。还有,实际存储的关键字集合K相对U来说可能很小,使得分配给T的大部分空间都被浪费掉。散列表需要的存储空间比直接寻址表要少得多。

  • 在散列表方式下,该元素存放在槽h(k)中,即利用散列函数(hash function)h,由关键字k计算出槽的位置。
  • 两个关键字映射到同一个槽中称为冲突。
    在这里插入图片描述
通过链接法解决冲突

把散列到同一槽中的所有元素都放在一个链表中。槽j中有一个指针,它指向存储所有散列到j的元素的链表的表头,如果不存在这样的元素,则槽j中为null。

CHAIN-HASH-INSERT(T, x)
    insert x at the head of list T[h(x, key)]
CHAIN-HASH-SEARCH(T, k)
    search for an element with key k in list T[h(k)]
CHAIN-HASH-DELETE(T, x)
    delete x from the list T[h(x, key)]

在这里插入图片描述

练习

2-1假设用一个散列函数h将n个不同的关键字散列到一个长度为m的数组T中。假设采用的是简单均匀散列,那么期望的冲突数是多少?更准确地,集合{{k,l}:k≠l,且h(k)=h(l)}基的期望值是多少?

根据生日悖论的分析方式得

  1. 期望 E [ X k l ] = 1 / m E[X_{kl}]=1/m E[Xkl]=1/m
  2. 总期望 E [ ∑ k = 1 n − 1 ∑ l = k + 1 n X k l ] = ∑ k = 1 n − 1 ∑ l = k + 1 n 1 / m = n ( n − 1 ) / 2 m E[∑_{k=1}^{n-1}∑_{l=k+1}^nX_{kl}]=∑_{k=1}^{n-1}∑_{l=k+1}^n1/m=n(n-1)/2m E[k=1n1l=k+1nXkl]=k=1n1l=k+1n1/m=n(n1)/2m
2-2 对于一个用链接法解决冲突的散列表,说明将关键字5,28,19,15,20,33,12,17,10,插入到该表中的过程。设该表中有9个槽位,并设其散列函数为h(k)=k mod 9.

在这里插入图片描述

2-3 Marley教授做了这样一个假设,即如果将链模式改动一下,使得每个链表都能保持已排好序的顺序,散列的性能就可以有较大的提高。 Marley教授的改动对成功查找,不成功查找,插入和删除操作的运行时间有何影响?

查找与删除操作时间减半,插入操作时间增加了。

2-4 说明在散列表内部,如何通过将所有未占用的槽位链接成一个自由链表,来分配和释放元素所占的存储空间。假定一个槽位可以存储一个标志,一个元素加上一个或两个指针。所有的字典和自由链表操作均应具有O(1)的期望运行时间。该自由链表需要是双向链表吗?或者,是不是单链表就足够了呢?

需要。单链表,在分配空间后,无法做到O(1).即自由表的删除操作

2-5试说明如果这些关键字均源于全域U,且|U|>mn,则U中还有一个大小为n的子集,其由散列到同一槽位中的所有关键字构成,使得链接法散列的查找时间最坏情况下位θ(n).

对于全域|U|>mn映射到m个槽中,若想U>mn,那么至少有一个槽位中的关键字大于n,其查找最坏时间为O(n)

2-6

(略)

散列函数

好的散列函数的设计可以减少冲突,有这样的三种设计方法。

1、除法散列法:hash(key) = key % m

其中,m是散列表的大小,该函数的一个指导原则是将m选取为接近散列集合大小的质数。

2、乘法散列法:hash(key) = floor( m * ( key * A ) % 1)

其中,floor()表示下取整,m无任何特殊要求, A ∈ ( 0 , 1 ) A\in (0,1) A(0,1),Don.Knuth认为A=(√5-1)/2(黄金分割点)最好。

A%1 表示取 A的小数部份

3、全域散列法:hasha,b(key)= ( (a * key + b) % p ) % m

其中, a ∈ 0 , 1 , . . . , m − 1 , b ∈ 0 , 1 , . . . , m − 1 a \in {0,1,...,m-1}, b \in {0,1,...,m-1} a0,1,...,m1b0,1,...,m1,a, b的值都为运行时动态确定,同除法散列一样,m应为质数,p为一个较大的素数。由于a,b的值在运行时随机确定,所以可以形成一个m * (m-1)的散列函数簇。基于随机的思想,全域散列法不管在什么情况下,其平均性能是最好的。

对于每一个关键字,随机选择散列函数,使之独立于要存储的关键字。这样就需要提供一组散列函数供选择,这一组散列函数,能将全域U内的所有关键字映射到槽{0,1,…,m-1}中,故称为全域散列。

练习

3-1

(略)

3-2假设将一个长度为r的字符串散列到m个槽中,并将其视为一个以128为基数的数,要求应用除法散列法。我们可以很容易地把数m表示为一个32位的机器字,但对长度为r的字符串,由于它被当做以128为基数的数来处理,就要占用若干个机器字。假设应用散列法计算一个字符串的散列值,那么如何才能在除了该串本身占用的空间外,只利用常数个机器字。
    sum = 0
    for i = 1 to r
        sum = (sum * 128 + s[i]) % m
3-3考虑除法散列法的另一种,其中h(k)=k mod m,m=2p-1,k为按基数2p表示字符串。试证明,如果串x可由串y通过其自身的字符置换排列导出,则x和y具有相同的散列值。给出一个应用的例子,其中这一特性在散列函数中是不希望出现的。

散列值和字符排序无关

  1. 设字符串 w = w 1 w 2 w=w_1w_2 w=w1w2

  2. h ( w ) = h ( w 1 ) 2 p + h ( w 2 ) m o d 2 p − 1 = h ( w 1 ) + h ( w 1 ) m o d 2 p − 1 h(w)= h(w_1)2^p + h(w_2) mod 2^p - 1 = h(w_1) + h(w_1) mod 2^p - 1 h(w)=h(w1)2p+h(w2)mod2p1=h(w1)+h(w1)mod2p1

3-4 考虑一个大小为m=1000的散列表和一个对应的散列函数h(k)=m(kAmod1),其中A=(√5-1)/2,试计算关键字61,62,63,64和65被映射到位置。

在这里插入图片描述

3-5

(略)

3-6

(略)

开放寻址法

除了"链表法"解决冲突的方式外,还有“开放寻址法”。它的好处在于它不用指针,而是计算要存取的槽序列。于是,不用存储指针而节省的空间,使得可以用同样的空间来提供更多的槽,潜在的减少了冲突,提高了检索速度。开放寻址法解决冲突的方式主要有三种:

1、线性探查:h( key , i ) = ( hash ( key ) + i ) % m

其中,hash(key)为前面所说的任何一种散列函数,线性探查的思想是:当发生冲突的时候,以 +i 个槽的方式寻找空的槽,直到所有槽满了为止,故为线性探查,一般 i=1。这种方法的缺陷是容易陷入群集:即随着元素增加,连续被占用的槽也在增加,表面上看就形成元素的堆积,这样,后续元素的平均查找时间也在增加。

2、二次探查: h ( k e y , i ) = ( h a s h ( k e y ) + c 1 i + c 2 i 2 ) h ( key, i ) = ( hash( key ) + c_1i + c_2i^2 ) % m h(key,i)=(hash(key)+c1i+c2i2)

其中, c 1 、 c 2 c_1、c_2 c1c2为正的辅助常数,i = 0,1,…,m-1,和线性探查不同的是,当发生冲突的时候,后续的探查位置加上一个依赖于 i 的二次方的偏移量,这种探查方式比线性探查要好很多。其中,c1、c2被证明当皆为1/2时性能最好。缺陷是同样群在二次群集的情况,但相对线性探查要好很多。

3、双重探查: h ( k e y , i ) = ( h a s h 1 ( k e y ) + i ∗ h a s h 2 ( k e y ) ) h ( key, i ) = ( hash_1( key ) + i*hash_2( key ) ) % m h(key,i)=(hash1(key)+ihash2(key))

其中,i = 0,1,…,m-1, h a s h 1 ( k e y ) 、 h a s h 2 ( k e y ) hash_1( key ) 、hash_2( key ) hash1(key)hash2(key)均为辅助散列函数,双重试探法的首个探查位置为 h a s h 1 ( k e y ) hash_1( key ) hash1(key) 当产生碰撞之后,接下来的探查位置为 ( h a s h 1 ( k e y ) + h a s h 2 ( k e y ) ) ( hash_1( key ) + hash_2( key ) ) (hash1(key)+hash2(key)) % m,因此我们发现在双重试探法中,不仅初始探查位置依赖于关键字key,探查序列中的增量 h a s h 2 ( k e y ) hash_2(key) hash2(key)同样依赖于关键字key,因而整个散列表提供了 m 2 m^2 m2种不同的探查序列,较之于前两种开放寻址具备了更多的灵活性。

练习

4-1 考虑将关键字10,22,31,4,15,28,17,88,59用开放寻址法插入到一个长度为m=11的散列表中,主散列函数为h’(k)=k mod m.说明用线性探查,二次探查(c1=1,c2=3)以及双重散列h2(k)=1+(k mod (m-1))将这些关键字插入散列表的结果

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

4-2请写出HASH-DELETE的伪代码;修改HASH-INSERT,使之能处理特殊值DELETED
HASH-DELETE(T, k)
    i = 0
    repeat
        j = h(k, i)
        if T[j] == k
            T[j] = DELETE
            return j
        else i = i + 1
    until T[j] == NIL or i == m
    error "element not exist"

HASH-INSERT(T, k)
    i = 0
    repeat
        j = h(k, i)
        if T[j] == NIL or T[j] == DELETE
            T[j] = k
            return j
        else i = i + 1
    until i == m
    error "hash table overflow"

完全散列

如果数据是固定,在使用链接法的1级散列产生冲突后,在每个冲突的槽再次用较小的2级散列,经过随机多次的2级散列筛选后,从而达到2级散列表不发生冲突的目的。

因为数据是固定的,通过多次随机得二次散列,使之不冲突,从而达到完全的散列
在这里插入图片描述

主要参考

算法导论第十一章 散列表
Hash Tables
散列表、全域散列与完全散列
算法导论第十一(11)章散列(Hash)表

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值