开放定址散列法和再散列


目录

  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. hdu 3648 Median Filter (树状数组)

    Median Filter Time Limit: 4000/2000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)Tot ...

  2. [CF107D]Crime Management

    题目大意:有一种长度为$n(n\leqslant 10^{18})$的字符串,给定$m(m\leqslant10^3)$种限制,即字符$c$出现的次数为$cnt$,若一个字符有多种限制,则满足任意一个 ...

  3. BZOJ 3270 博物馆 && CodeForces 113D. Museum 期望概率dp 高斯消元

    大前提,把两个点的组合看成一种状态 x 两种思路 O(n^7) f[x]表示在某一个点的前提下,这个状态经过那个点的概率,用相邻的点转移状态,高斯一波就好了 O(n^6) 想象成臭气弹,这个和那个的区 ...

  4. Win7命令mklink的使用

    C盘空间越来越小,在Win7里还标红了,心里看得不舒服,得想一些方法腾出一些空间.看了AppData,Chrome占了1G多的空间. 当时安装Chrome浏览器时因为不能指定安装目录,所以Chrome ...

  5. Codeforces Round #525 (Div. 2) F. Ehab and a weird weight formula

    F. Ehab and a weird weight formula 题目链接:https://codeforces.com/contest/1088/problem/F 题意: 给出一颗点有权值的树 ...

  6. POJ2492:A Bug's Life(种类并查集)

    A Bug's Life Time Limit: 10000MS   Memory Limit: 65536K Total Submissions: 45757   Accepted: 14757 题 ...

  7. 百练3383:Cell Phone Network

    传送门:http://bailian.openjudge.cn/practice/3383/ [题解] 题目就是最小支配集. 学习了最小支配集的解法: 树形dp(有空可以推一推) 贪心:DFS遍历后逆 ...

  8. NYOJ 284 坦克大战 (广搜)

    题目链接 描述 Many of us had played the game "Battle city" in our childhood, and some people (li ...

  9. bzoj 2324 ZJOI 营救皮卡丘 费用流

    题的大概意思就是给定一个无向图,边有权值,现在你有k个人在0点,要求走到n点,且满足 1:人们可以分头行动,可以停在某一点不走了 2:当你走到x时,前x-1个点必须全部走过(不同的人走过也行,即分两路 ...

  10. Django-models class Meta:元类

    Django模型之Meta选项详解 Model 元数据就是 "不是一个字段的任何数据" -- 比如排序选项, admin 选项等等.   Django模型类的Meta是一个内部类, ...