开放定址散列法和再散列


目录

  1. 开放定址法
  2. 再散列
  3. 代码实现

1 开放定址散列法

前面利用分离链接法解决了散列表插入冲突的问题,而除了分离链接法外,还可以使用开放定址法来解决散列表的冲突问题。

开放定址法在遇见冲突情形时,将会尝试选择另外的单元,直到找到空的单元为止,一般来说,单元h0(X), h1(X), h2(x)为相继尝试的单元,则hi(X)=(Hash(X)+F(i)) mod TableSize,其中F(i)即为冲突解决的探测方法,

开放定址法中的探测方法的三种基本方式为,

  1. 线性探测法:探测步进为线性增长,最基本的方式为F(i)=i
  2. 平方探测法:探测步进为平方增长,最基本的方式为F(i)= i2
  3. 双散列:探测方法的步进由一个新的散列函数决定,最基本的方式为F(i)= i*Hash2(i),通常选择Hash2(i)=R-(X mod R),其中R为小于TableSize的素数。

2 再散列

对于使用平方探测的开放定址散列法,当元素填得太满的时候,操作运行的时间将消耗过长,而且插入操作有可能失败,此时则可以进行一次再散列来解决这一问题。

再散列会创建一个新的散列表,新的散列表大小为大于原散列表大小2倍的第一个素数,随后将原散列表的值重新散列至新的散列表中。

这一操作的开销十分大,但并不经常发生,且在发生前必然已经进行了多次插入,因此这一操作的实际情况并没有那么糟糕。

散列的时机通常有几种,

  1. 装填因子达到一半的时候进行再散列
  2. 插入失败时进行再散列
  3. 达到某一装填因子时进行散列

3 代码实现

完整代码

 from functools import partial as pro
from math import ceil, sqrt
from hash_table import HashTable, kmt_hashing class RehashError(Exception):
pass class OpenAddressingHashing(HashTable):
def __init__(self, size, hs, pb, fn=None, rf=0.5):
self._array = [None for i in range(size)]
self._get_hashing = hs
self._hashing = hs(size) if not fn else fn
self._probing = pb
self._rehashing_factor = rf def _sniffing(self, item, num, hash_code=None):
# Avoid redundant hashing calculation, if hashing calculation is heavy, this would count much.
if not hash_code:
hash_code = self._hashing(item)
return (hash_code + self._probing(num, item, self.size)) % self.size def _get_rehashing_size(self):
size = self.size * 2 + 1
while not is_prime(size):
size += 1
return size def rehashing(self, size=None, fn=None):
if not size:
size = self._get_rehashing_size()
if size <= (self.size * self.load_factor):
raise RehashError('Rehash size is too small!')
array = self._array
self._array = [None for i in range(size)]
self._hashing = self._get_hashing(size) if not fn else fn
self.insert(filter(lambda x: x is not None, array)) def find(self, item):
hash_code = ori_hash_code = self._hashing(item)
collision_count = 1
value = self._array[hash_code] # Build up partial function to shorten time consuming when heavy sniffing encountered.
collision_handler = pro(self._sniffing, hash_code=ori_hash_code) while value is not None and value != item:
hash_code = collision_handler(item, collision_count)
value = self._array[hash_code]
collision_count += 1
return value, hash_code def _insert(self, item):
if item is None:
return
value, hash_code = self.find(item)
if value is None:
self._array[hash_code] = item
if self.load_factor > self._rehashing_factor:
self.rehashing() def is_prime(num): # O(sqrt(n)) algorithm
if num < 2:
raise Exception('Invalid number.')
if num == 2:
return True
for i in range(2, ceil(sqrt(num))+1):
if num % i == 0:
return False
return True def linear_probing(x, *args):
return x def square_probing(x, *args):
return x**2 def double_hashing(x, item, size, *args):
r = size - 1
while not is_prime(r):
r -= 1
return x * (r - (item % r)) def test(h):
print('\nShow hash table:')
h.show() print('\nInsert values:')
h.insert(range(9))
h.show() print('\nInsert value (existed):')
h.insert(1)
h.show() print('\nInsert value (collided):')
h.insert(24, 47)
h.show() print('\nFind value:')
print(h.find(7))
print('\nFind value (not existed):')
print(h.find(77)) print('\nLoad factor is:', h.load_factor) if __name__ == '__main__':
test(OpenAddressingHashing(11, kmt_hashing, linear_probing))
print(30*'-')
test(OpenAddressingHashing(11, kmt_hashing, square_probing))
print(30*'-')
test(OpenAddressingHashing(11, kmt_hashing, double_hashing))

分段解释

首先导入几个需要的模块,以及散列表类和散列函数(具体实现参考文末相关阅读),并定义一个再散列异常

 from functools import partial as pro
from math import ceil, sqrt
from hash_table import HashTable, kmt_hashing class RehashError(Exception):
pass

定义一个开放定址散列表类,接收参数包括,散列表初始大小size,散列函数的生成函数hs,探测函数pb,指定散列函数fn,再散列因子rf。当指定了散列函数时,使用指定的散列函数,否则使用传入的生成函数,根据散列表大小获得一个默认的散列函数。

 class OpenAddressingHashing(HashTable):
def __init__(self, size, hs, pb, fn=None, rf=0.5):
self._array = [None for i in range(size)]
self._get_hashing = hs
self._hashing = hs(size) if not fn else fn
self._probing = pb
self._rehashing_factor = rf

定义_sniffing方法,嗅探方法用于计算下一个嗅探位置。

Note: 此处为了避免冗余计算,开放一个参数供散列值传入,当进行同一个插入的不同嗅探时,其原始散列值是不变的,因此这里可以配合后面的偏函数,在多次嗅探中固定这一参数,从而避免多次散列函数的计算。这对于复杂的散列函数来说可以减少嗅探计算时间。

     def _sniffing(self, item, num, hash_code=None):
# Avoid redundant hashing calculation, if hashing calculation is heavy, this would count much.
if not hash_code:
hash_code = self._hashing(item)
return (hash_code + self._probing(num, item, self.size)) % self.size

定义_get_rehashing_size方法,用于计算需要再散列时新散列表的大小,通常为大于当前表大小2倍的第一个素数。

     def _get_rehashing_size(self):
size = self.size * 2 + 1
while not is_prime(size):
size += 1
return size

定义rehashing方法,用于进行再散列操作,

  1. 若没有指定再散列大小,则使用默认方式计算,
  2. 当传入的再散列大小小于已有元素数量时,引发再散列异常,
  3. 保存原始散列表信息,并新建一个散列表,更新散列函数,
  4. 利用新的散列函数,遍历原始散列表并插入新的散列表中。
     def rehashing(self, size=None, fn=None):
if not size:
size = self._get_rehashing_size()
if size <= (self.size * self.load_factor):
raise RehashError('Rehash size is too small!')
array = self._array
self._array = [None for i in range(size)]
self._hashing = self._get_hashing(size) if not fn else fn
self.insert(filter(lambda x: x is not None, array))

定义find方法,用于查找散列表内的指定元素,

Note: 这里使用偏函数处理嗅探函数,减少散列计算,利用嗅探函数循环嗅探新的位置,直到找到目标元素或None,此时返回元素值或None和对应的散列值。

     def find(self, item):
hash_code = ori_hash_code = self._hashing(item)
collision_count = 1
value = self._array[hash_code] # Build up partial function to shorten time consuming when heavy sniffing encountered.
collision_handler = pro(self._sniffing, hash_code=ori_hash_code) while value is not None and value != item:
hash_code = collision_handler(item, collision_count)
value = self._array[hash_code]
collision_count += 1
return value, hash_code

定义_insert方法,唯一的区别在于,当装载因子大于再散列因子时,需要进行一次再散列操作。

     def _insert(self, item):
if item is None:
return
value, hash_code = self.find(item)
if value is None:
self._array[hash_code] = item
if self.load_factor > self._rehashing_factor:
self.rehashing()

定义一个素数判断函数,用于计算一个值是否为素数,时间复杂度为O(sqrt(n))。

 def is_prime(num):  # O(sqrt(n)) algorithm
if num < 2:
raise Exception('Invalid number.')
if num == 2:
return True
for i in range(2, ceil(sqrt(num))+1):
if num % i == 0:
return False
return True

接着定义三个探测函数,分别为线性探测、平方探测和双散列。

 def linear_probing(x, *args):
return x def square_probing(x, *args):
return x**2 def double_hashing(x, item, size, *args):
r = size - 1
while not is_prime(r):
r -= 1
return x * (r - (item % r))

最后定义一个测试函数,并对三种探测函数分别进行测试。

 def test(h):
print('\nShow hash table:')
h.show() print('\nInsert values:')
h.insert(range(9))
h.show() print('\nInsert value (existed):')
h.insert(1)
h.show() print('\nInsert value (collided):')
h.insert(24, 47)
h.show() print('\nFind value:')
print(h.find(7))
print('\nFind value (not existed):')
print(h.find(77)) print('\nLoad factor is:', h.load_factor) if __name__ == '__main__':
test(OpenAddressingHashing(11, kmt_hashing, linear_probing))
print(30*'-')
test(OpenAddressingHashing(11, kmt_hashing, square_probing))
print(30*'-')
test(OpenAddressingHashing(11, kmt_hashing, double_hashing))

三种探测函数测试项及对应结果如下,

初始建立散列表

     print('\nShow hash table:')
h.show()

三者结果均相同,

Show hash table:
[0] None
[1] None
[2] None
[3] None
[4] None
[5] None
[6] None
[7] None
[8] None
[9] None
[10] None

接着尝试插入超过装填因子一半的元素数量,此时散列表会自动进行再散列

Insert values:
[0] 0
[1] 1
[2] 2
[3] 3
[4] 4
[5] 5
[6] 6
[7] 7
[8] 8
[9] None
[10] None
[11] None
[12] None
[13] None
[14] None
[15] None
[16] None
[17] None
[18] None
[19] None
[20] None
[21] None
[22] None

接着插入已存在的元素

     print('\nInsert value (existed):')
h.insert(1)
h.show()

结果不变,

再插入两个会造成冲突的元素,三者结果分别如下,可以看到,不同的探测函数将元素插入到了散列表的不同位置。

Linear_probing | Square_probing | Double_hashing
[0] 0 | [0] 0 | [0] 0
[1] 1 | [1] 1 | [1] 1
[2] 2 | [2] 2 | [2] 2
[3] 3 | [3] 3 | [3] 3
[4] 4 | [4] 4 | [4] 4
[5] 5 | [5] 5 | [5] 5
[6] 6 | [6] 6 | [6] 6
[7] 7 | [7] 7 | [7] 7
[8] 8 | [8] 8 | [8] 8
[9] 24 | [9] None | [9] None
[10] 47 | [10] 24 | [10] None
[11] None | [11] None | [11] 47
[12] None | [12] None | [12] None
[13] None | [13] None | [13] None
[14] None | [14] None | [14] None
[15] None | [15] None | [15] 24
[16] None | [16] None | [16] None
[17] None | [17] 47 | [17] None
[18] None | [18] None | [18] None
[19] None | [19] None | [19] None
[20] None | [20] None | [20] None
[21] None | [21] None | [21] None
[22] None | [22] None | [22] None

最后,测试查找函数以及获取装填因子,由于探测函数不同,因此查找不存在结果时,最后处在的位置也不同。

--------------------------------------
Linear_probing
--------------------------------------
Find value:
(7, 7) Find value (not existed):
(None, 11) Load factor is: 0.4782608695652174 --------------------------------------
Square_probing
--------------------------------------
Find value:
(7, 7) Find value (not existed):
(None, 9) Load factor is: 0.4782608695652174 --------------------------------------
Double_hashing
--------------------------------------
Find value:
(7, 7) Find value (not existed):
(None, 21) Load factor is: 0.4782608695652174

相关阅读


1. 散列表

2. 分离链接法

Python与数据结构[4] -> 散列表[2] -> 开放定址法与再散列的 Python 实现的更多相关文章

  1. C# Dictionary源码剖析---哈希处理冲突的方法有:开放定址法、再哈希法、链地址法、建立一个公共溢出区等

    C# Dictionary源码剖析 参考:https://blog.csdn.net/exiaojiu/article/details/51252515 http://www.cnblogs.com/ ...

  2. Python与数据结构[4] -> 散列表[1] -> 分离链接法的 Python 实现

    分离链接法 / Separate Chain Hashing 前面完成了一个基本散列表的实现,但是还存在一个问题,当散列表插入元素冲突时,散列表将返回异常,这一问题的解决方式之一为使用链表进行元素的存 ...

  3. java 解决Hash(散列)冲突的四种方法--开放定址法(线性探测,二次探测,伪随机探测)、链地址法、再哈希、建立公共溢出区

    java 解决Hash(散列)冲突的四种方法--开放定址法(线性探测,二次探测,伪随机探测).链地址法.再哈希.建立公共溢出区 标签: hashmaphashmap冲突解决冲突的方法冲突 2016-0 ...

  4. 开放定址法——线性探测(Linear Probing)

    之前我们所采用的那种方法,也被称之为封闭定址法.每个桶单元里存的都是那些与这个桶地址比如K相冲突的词条.也就是说每个词条应该属于哪个桶所对应的列表,都是在事先已经注定的.经过一个确定的哈希函数,这些绿 ...

  5. 开放定址法——平方探测(Quadratic Probing)

    为了消除一次聚集,我们使用一种新的方法:平方探测法.顾名思义就是冲突函数F(i)是二次函数的探测方法.通常会选择f(i)=i2.和上次一样,把{89,18,49,58,69}插入到一个散列表中,这次用 ...

  6. Python与数据结构[0] -> 链表/LinkedList[0] -> 单链表与带表头单链表的 Python 实现

    单链表 / Linked List 目录 单链表 带表头单链表 链表是一种基本的线性数据结构,在C语言中,这种数据结构通过指针实现,由于存储空间不要求连续性,因此插入和删除操作将变得十分快速.下面将利 ...

  7. Python与数据结构[4] -> 散列表[0] -> 散列表与散列函数的 Python 实现

    散列表 / Hash Table 散列表与散列函数 散列表是一种将关键字映射到特定数组位置的一种数据结构,而将关键字映射到0至TableSize-1过程的函数,即为散列函数. Hash Table: ...

  8. Python数据结构——散列表

    散列表的实现常常叫做散列(hashing).散列仅支持INSERT,SEARCH和DELETE操作,都是在常数平均时间执行的.需要元素间任何排序信息的操作将不会得到有效的支持. 散列表是普通数组概念的 ...

  9. 【Python算法】哈希存储、哈希表、散列表原理

    哈希表的定义: 哈希存储的基本思想是以关键字Key为自变量,通过一定的函数关系(散列函数或哈希函数),计算出对应的函数值(哈希地址),以这个值作为数据元素的地址,并将数据元素存入到相应地址的存储单元中 ...

随机推荐

  1. Why is the ibdata1 file continuously growing in MySQL?

    We receive this question about the ibdata1 file in MySQL very often in Percona Support. The panic st ...

  2. codeforces 1065D

    题目链接:https://codeforces.com/problemset/problem/1065/D 题意:给你一个又1~n^2组成的n行n列的矩阵,你可以走日字型,直线,斜线,现在要求你从1走 ...

  3. 把java的class文件打成jar包的步骤

    现在我的文件夹的目录在: C:\Users\linsenq\Desktop\cglibjar 我要把位于这个目录下的所有文件夹以及这个文件夹下的.class文件打成jar包 第一步:用win+R 打开 ...

  4. mysql删除id最小的条目

    DELETE FROM 表1 WHERE Mid in (select Mid from (SELECT Min(Mid) Mid FROM 表1 c1) t1);

  5. [Python]简单的外星人入侵游戏

    alien_invasion.py: import sys import pygame from setting import Settings from ship import Ship impor ...

  6. MUI -- plus初始化原理及plus is not defined,mui is not defined 错误汇总

    不要在没有plus和mui的环境下调用相关API 普通浏览器里没有plus环境,只有HBuilder真机运行和打包后才能运行plus api. 在普通浏览器里运行时plus api时控制台必然会输出p ...

  7. C# 序列化原因 (转)

         1.什么是序列化 序列化是将对象状态转换为可保持或传输的格式的过程,在序列化过程中,对象的公共字段和私有字段以及类的名称(包括包含该类的程序集)都被转换为字节流,然后写入数据流.与序列化相对 ...

  8. [洛谷P1032] 字串变换

    洛谷题目链接:字串变换 题目描述 已知有两个字串 A, B 及一组字串变换的规则(至多6个规则): A1 -> B1 A2 -> B2 规则的含义为:在 A$中的子串 A1 可以变换为 B ...

  9. GDSOI2015的某道题目

    分析: 看到这个$3^i$就觉得很奇怪的样子...为什么一定要是$3^i$...而且不能重复使用... 不能重复使用就代表不会产生进位,那么一定是若干个$3^i$相加减的式子... 仔细观察,我们发现 ...

  10. 51nod 拉勾专业算法能力测评消灭兔子 优先队列+贪心

    题目传送门 这道题一开始想了很久...还想着写网络流 发现根本不可能.... 然后就想着线段树维护然后二分什么的 最后发现优先队列就可以了 代码还是很简洁的啦 233 就是把兔子按血量从大到小排序一下 ...