算法设计与分析——散列表/哈希表(Hash Table)(五):开放寻址法

分类目录:《算法设计与分析》总目录
相关文章:
·散列表/哈希表(Hash Table)(一):基础知识
·散列表/哈希表(Hash Table)(二):直接寻址表
·散列表/哈希表(Hash Table)(三):散列表原理
·散列表/哈希表(Hash Table)(四):散列函数
·散列表/哈希表(Hash Table)(五):开放寻址法
·散列表/哈希表(Hash Table)(六):完全散列


在开放寻址法中,所有的元素都存放在散列表里。也就是说,每个表项或包含动态集合的一个元素,或包含 N o n e None None。当查找某个元素时,要系统地检查所有的表项,直到找到所需的元素,或者最终查明该元素不在表中。不像链接法,这里既没有链表,也没有元素存放在散列表外。因此在开放寻址法中,散列表可能会被填满,以至于不能插入任何新的元素。该方法导致的一个结果便是装载因子 a a a绝对不会超过1。

开放寻址法的核心思想是,如果出现了散列冲突,我们就重新探测一个空闲位置,将其插入。比如,我们可以使用线性探测法。当我们往散列表中插入数据时,如果某个数据经过散列函数散列之后,存储位置已经被占用了,我们就从当前位置开始,依次往后查找,看是否有空闲位置,如果遍历到尾部都没有找到空闲的位置,那么我们就再从表头开始找,直到找到为止。

当然,也可以将用作链接的链表存放在散列表未用的槽中,但开放寻址法的好处就在于它不用指针,而是计算出要存取的槽序列。于是,不用存储指针而节省的空间,使得可以用同样的空间来提供更多的槽,潜在地减少了冲突,提高了检索速度。

为了使用开放寻址法插入一个元素,需要连续地检查散列表,或称为探查,直到找到一个空槽来放置待插入的关键字为止。检查的顺序不一定是 0 , 1 , ⋯   , m − 1 0, 1, \cdots, m-1 0,1,,m1,而是要依赖于待插入的关键字。为了确定要探查哪些槽,我们将散列函数加以扩充,使之包含探查号以作为其第二个输入参数。这样,散列函数就变为:
h : U × { 0 , 1 , ⋯   , m − 1 } → { 0 , 1 , ⋯   , m − 1 } h:U×\{0, 1, \cdots, m-1\}\rightarrow \{0, 1, \cdots, m-1\} h:U×{0,1,,m1}{0,1,,m1}
对每一个关键字 k k k,使用开放寻址法的探查序列是 < 0 , 1 , ⋯   , m − 1 > <0, 1, \cdots, m-1> <0,1,,m1>的一个排列,使得当散列表逐渐填满时,每一个表位最终都可以被考虑为用来插入新关键字的槽。

查找关键字 k k k的算法的探查序列与将 k k k插入时的算法一样。因此,查找过程中碰到一个空槽时,查找算法就(非成功地)停止,因为如果 k k k在表中,它就应该在此处,而不会在探查序列随后的位置上(之所以这样说,是假定了关键字不会从散列表中删除)。

从开放寻址法的散列表中删除操作元素比较困难。当我们从槽 i i i中删除关键字时,不能仅将 N o n e None None置于其中来标识它为空。如果这样做,就会有问题:在插入关键字 k k k时,发现槽 i i i被占用了,则就被插人到后面的位置上;此时将槽 i i i中的关键字删除后,就无法检索到关键字 k k k了。

有一个解决办法,就是在槽 i i i中置一个特定的值 D E L E T E D DELETED DELETED替代 N o n e None None来标记该槽。这样就将这样的一个槽当做空槽,使得在此仍然可以插入新的关键字。

线性探查

给定一个普通的散列函数 h ′ : U → { 0 , 1 , ⋯   , m − 1 } h':U\rightarrow \{0, 1, \cdots, m-1\} h:U{0,1,,m1},称之为辅助,线性探查方法采用的散列函数为:
h ( k , i ) = ( h ′ ( k ) + i ) m o d    m h(k ,i)=(h'(k)+i)\mod m\quad h(k,i)=(h(k)+i)modm
给定一个关键字 k k k,首先探查槽 T [ h ′ ( k ) ] T[h'(k)] T[h(k)],即由辅助散列函数所给出的槽位。再探查槽T T [ h ′ ( k ) + 1 ] T[h'(k)+1] T[h(k)+1],依此类推,直至槽 T [ m − 1 ] T[m-1] T[m1]。然后,又绕到槽 T [ 0 ] , T [ 1 ] , ⋯ T[0], T[1], \cdots T[0],T[1],直到最后探查到槽 T [ h ′ ( k ) − 1 ] T[h'(k)-1] T[h(k)1]。在线性探查方法中,初始探查位置决定了整个序列,故只有 m m m种不同的探查序列。

线性探査方法比较容易实现,但它存在着一个问题,称为一次群集。随着连续被占用的槽不断增加,平均査找时间也随之不断增加。群集现象很容易出现,这是因为当个空槽前有 i i i个满的槽时,该空槽为下一个将被占用的概率是 i + 1 m \frac{i+1}{m} mi+1。连续被占用的槽就会变得越来越长,因而平均查找时间也会越来越大。

二次探查

二次探查采用如下形式的散列函数:
h ( k , i ) = ( h ′ ( k ) + c 1 i + c 2 i 2 ) m o d    m h(k ,i)=(h'(k)+c_1i+c_2i^2)\mod m\quad h(k,i)=(h(k)+c1i+c2i2)modm
其中 h ′ h' h是一个辅助散列函数, c 1 c_1 c1 c 2 c_2 c2为正的辅助常数。初始的探查位置为 T [ h ′ ( k ) ] T[h'(k)] T[h(k)],后续的探查位置要加上一个偏移量,该偏移量以二次的方式依赖于探查序号 i i i。这种探查方法的效果要比线性探查好得多,但是,为了能够充分利用散列表, c 1 c_1 c1 c 2 c_2 c2 m m m的值要受到限制。此外,如果两个关键字的初始探查位置相同,那么它们的探查序列也是相同的,这是因为 h ( k 1 , 0 ) = h ( k 2 , 0 ) h(k_1,0)=h(k_2,0) h(k1,0)=h(k2,0)蕴涵着 h ( k 1 , i ) = h ( k 2 , i ) h(k_1,i)=h(k_2,i) h(k1,i)=h(k2,i)。这一性质可导致一种轻度的群集,称为二次群集。像在线性探查中一样,初始探查位置决定了整个序列,这样也仅有 m m m个不同的探查序列被用到。

双重散列

双重散列是用于开放寻址法的最好方法之一,因为它所产生的排列具有随机选择排列的许多特性。双重散列采用如下形式的散列函数:
h ( k , i ) = ( h 1 ( k ) + i h 2 ( k ) ) m o d    m h(k ,i)=(h_1(k)+ih_2(k))\mod m\quad h(k,i)=(h1(k)+ih2(k))modm

其中 h 1 h_1 h1 h 2 h_2 h2均为辅助散列函数。初始探查位置为 T [ h 1 ( k ) ] T[h_1(k)] T[h1(k)],后续的探查位置是前一个位置加上偏移量 h 2 ( k ) h_2(k) h2(k) m m m。因此,不像线性探查或二次探查,这里的探查序列以两种不同方式依赖于关键字 k k k,因为初始探查位置、偏移量或者二者都可能发生变化。下图给出了一个使用双重散列法进行插入的例子。
双重探查

为了能查找整个散列表,值 h 2 ( k ) h_2(k) h2(k)必须要与表的大小 m m m互质。当 m m m为素数或者2的幂时,双重散列法中用到了 Θ ( m 2 ) \Theta(m^2) Θ(m2)种探查序列,而线性探查或二次探查中用了 Θ ( m ) \Theta(m) Θ(m)种,故前者是后两种方法的种改进。因为每一对可能的 ( h 1 ( k ) , h 2 ( k ) (h_1(k), h_2(k) (h1(k),h2(k)都会产生一个不同的探查序列。因此,对于 m m m的每一种可能取值,双重散列的性能看起来就非常接近“理想的”均匀散列的性能。

尽管除素数和2的幂以外的 m m m值在理论上也能用于双重散列中,但是在实际中,要高效地产生 h 2 ( k ) h_2(k) h2(k)确保使其与 m m m互质,将变得更加困难。部分原因是这些数的相对密度中可能较小。

开放寻址散列的分析

像在链接法中的分析一样,开放寻址法的分析也是以散列表的装载因子 a = n m a=\frac{n}{m} a=mn来表达的当然,使用开放寻址法,每个槽中至多只有一个元素,因而 n ≤ m n≤m nm,也就意味着 a ≤ 1 a≤1 a1

假设采用的是均匀散列。在这种理想的方法中,用于插入或查找每一个关键字 k k k的探查序列等可能地为 < 0 , 1 , ⋯   , m − 1 > <0, 1, \cdots, m-1> <0,1,,m1>的任意一种排列。当然,每一个给定的关键字有其相应的唯一固定的探查序列。我们这里想说的是,考虑到关键字空间上的概率分布及散列函数施于这些关键字上的操作,每一种探查序列都是等可能的。

现在就来分析在均匀散列的假设下,用开放寻址法来进行散列时探查的期望次数。则:

  1. 给定一个装载因子为 a = n m < 1 a=\frac{n}{m}<1 a=mn<1的开放寻址散列表,并假设是均匀散列的,则对于一次不成功的查找,其期望的探查次数至多为 1 1 − a \frac{1}{1-a} 1a1
  2. 假设采用的是均匀散列,平均情况下,向一个装载因子为 a a a的开放寻址散列表中插入一个元素至多需要做 1 1 − a \frac{1}{1-a} 1a1次探查。
  3. 对于一个装载因子为 a < 1 a<1 a<1的开放寻址散列表,一次成功查找中的探查期望数至多为 1 a ln ⁡ 1 1 − a \frac{1}{a}\ln\frac{1}{1-a} a1ln1a1
  • 6
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
哈希表是一种高效的数据结构,可以用来存储和查找键值对。其中,哈希函数将键映射到一个特定的桶中,每个桶中存储一组键值对。在哈希表中,如果两个键被映射到同一个桶中,就会发生碰撞。为了解决这个问题,可以使用链表。 链表是一种解决哈希表碰撞问题的方。具体来说,对于哈希表中的每个桶,可以使用一个链表来存储所有映射到该桶的键值对。如果发生碰撞,只需要将新的键值对添加到链表的末尾即可。 下面是一个使用链表实现哈希表的示例代码: ```python class Node: def __init__(self, key, value): self.key = key self.value = value self.next = None class HashTable: def __init__(self, capacity): self.capacity = capacity self.buckets = [None] * capacity def hash_function(self, key): return hash(key) % self.capacity def put(self, key, value): index = self.hash_function(key) node = self.buckets[index] while node: if node.key == key: node.value = value return node = node.next new_node = Node(key, value) new_node.next = self.buckets[index] self.buckets[index] = new_node def get(self, key): index = self.hash_function(key) node = self.buckets[index] while node: if node.key == key: return node.value node = node.next return None def remove(self, key): index = self.hash_function(key) node = self.buckets[index] prev = None while node: if node.key == key: if prev: prev.next = node.next else: self.buckets[index] = node.next return prev = node node = node.next ``` 在这个示例中,我们定义了一个Node类来表示哈希表中的每个节点,每个节点包含一个键、一个值和一个指向下一个节点的指针。我们还定义了一个HashTable类来实现哈希表,其中包含一个桶数组和一些基本的操作方,如put、get和remove。 在put方中,我们首先使用哈希函数计算出键的索引,然后遍历桶中的链表,查找该键是否已经存在于哈希表中。如果找到了该键,我们只需要更新其对应的值即可。否则,我们创建一个新的节点,并将其添加到链表的开头。 在get方中,我们同样使用哈希函数计算出键的索引,然后遍历桶中的链表,查找该键的值。如果找到了该键,我们返回其对应的值。否则,返回None。 在remove方中,我们首先使用哈希函数计算出键的索引,然后遍历桶中的链表,查找该键。如果找到了该键,我们将其从链表中删除即可。 总的来说,链表是一种简单且常用的哈希表解决碰撞问题的方

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

von Neumann

您的赞赏是我创作最大的动力~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值