在文章【数据结构Python描述】使用列表手动实现一个字典类中,对UnsortedListMap
的5个主要方法进行效率分析后可知,使用列表这种数据结构来实现的映射效率较低,本文将使用哈希表这种数据结构实现的映射,通过这种方式实现的映射,其效率要远高于前者。
一、哈希表简介
通过列表保存键值对实现的映射之所以效率较低,原因在于最坏情况下,__getitem__
、__setitem__
、__delitem__
、__iter__
方法均需要遍历整个列表。
然而,由于列表天然具有通过索引以 O ( 1 ) O(1) O(1) 时间复杂度访问其中元素的特点,因此可以考虑是否可以将键值对中的键通过某种转换关系转换成列表的索引值,然后将键值对保存在使用索引标识的列表单元处。
实际上,上面描述的这种特殊列表就是哈希表,而转换关系就可以认为是哈希函数。
1. 哈希函数简介
因此,哈希函数
h
h
h 的目标是为了将每一个键值对中的键
k
k
k 转换为
[
0
,
N
−
1
]
[0, N-1]
[0,N−1] 范围内的一个整数
h
(
k
)
h(k)
h(k)即哈希值,其中
N
N
N 是哈希表
A
A
A 这种特殊列表的容量,而键值对 (k, v)
就保存在
A
[
h
(
k
)
]
A[h(k)]
A[h(k)] 单元处。
在希望获取某一个键值对时,只需先对 k k k 执行相同的哈希函数获得哈希值,然后通过哈希值获取哈希表对应单元处的键值对即可。
2. 哈希碰撞简介
需要注意的是,针对不同的键,如果通过哈希函数得到了相同的哈希值,这时就发生了所谓的哈希碰撞,此时需要对哈希表做额外的处理,这类额外处理一般都会降低哈希表的效率,具体的处理方案包括分离链接法、开放寻址法以及线性查找法等,后面将对其做详细介绍。
3. 哈希函数深究
实际上,通常将哈希函数分为两个组成部分,即哈希码和压缩函数:
- 哈希码负责将一个键 k k k 转换为一个整数,该整数甚至可以为负;
- 压缩函数负责将上述整数进一步转换为 [ 0 , N − 1 ] [0, N-1] [0,N−1] 范围内的一个整数。
将哈希函数分为哈希码和压缩函数两个部分的好处在于哈希码的计算可以独立于哈希表的容量(和普通列表一样,哈希表的容量需要根据其中元素的数量动态调整)。
这样一来,我们可以对作为键的任意不可变对象先通过一种统一的方式计算出其哈希码,然后由压缩函数使用哈希码并根据哈希表的容量来计算该键对应的哈希值。
哈希码
哈希函数的第一部分会先使用作为键的任意不可变对象 k k k 计算出一个整数,该整数被称为哈希码。
Python 有一个专门用于计算哈希码的内建函数 hash(x)
,该函数对任意对象 x
可返回一个用作哈希码的整数,而如本文所一直强调的,在 Python 中只有不可变类型才是可哈希的。
这一限制可确保一个对象的哈希码在其生命周期内保持不变,则该对象的哈希值在其生命周期内保持不变。
上述限制可避免这样的可能异常情况:将键值对插入哈希表时,键的哈希值对应表中某一个单元;在获取一个键值对时,键的哈希值可能对应表中另外一个单元。
在 Python 所有的内建数据类型中,因 int
、 float
、 str
、 tuple
、以及 frozenset
的对象均为不可变的,因此这些类型的对象都是可哈希的;而 list
的对象可变,对于 list
的实例对象 x
,执行 hash(x)
将抛出 TypeError
异常。
压缩函数
一般而言,键 k k k 的哈希码一般不能立即作为哈希表的索引来用,因为哈希码可能为负,甚至可能超过哈希表的容量。
因此,我们需要一种方式能够将哈希码映射成 [ 0 , N − 1 ] [0, N-1] [0,N−1] 范围内的一个整数,该方式就是压缩函数。评价一个压缩函数好坏的标准是:对于给定的一组各不相同的哈希码,压缩函数可以将哈希碰撞的可能性降到最低。
下面是一些典型的压缩函数:
取模运算法
一种简单的压缩函数为取模运算法,即取:
i m o d N i \ mod \ N i mod N
其中 i i i 为哈希码, N N N 为哈希表的容量,在 N N N 为质数时发生哈希碰撞的可能性将比较小。
当 N N N 不是质数时,发生哈希碰撞的可能性将大大增加,例如:当一组键的哈希码为 { 200 , 205 , 210 , 215 , 220 , ⋅ ⋅ ⋅ , 600 } \{200, 205, 210, 215, 220, \cdot\cdot\cdot, 600\} {200,205,210,215,220,⋅⋅⋅,600} ,且哈希表的容量为 100 100 100 时,则每一个哈希码都将和另外 3 3 3 个哈希码发生碰撞。
如果压缩函数选择得当,那么两个不同的哈希码发生碰撞的概率为 1 / N {\left. 1\middle/ N\right.} 1/N。
MAD方法
上述取模算法有一个缺点是,当哈希码为 p N + q pN+q pN+q 的形式,则发生哈希碰撞的概率依然很大,而所谓的 MAD(Multiply-Add-and-Divide)方法可以很好的改善这个问题,即:
[ ( a i + b ) m o d p ] m o d N [(ai+b) \ mod \ p] \ mod \ N [(ai+b) mod p] mod N
其中 N N N 依然为哈希表的容量, p p p 是比 N N N 更大的质数,而 a a a 和 b b b 都是 [ 0 , p − 1 ] [0, p-1] [0,p−1] 范围内的整数且 a > 0 a\gt0 a>0 。
4. 处理哈希碰撞
由上述讨论可知,使用哈希表实现用于保存键值对的映射,其主要思想在于:给定一个哈希表
A
A
A (时刻牢记哈希表本质是一种特殊的列表),以及一个哈希函数
h
h
h ,实现将键值对 (k, v)
保存在哈希表的
A
[
h
(
k
)
]
A[h(k)]
A[h(k)] 单元。
然而,问题在于:当对于两个不同的键
k
1
k_1
k1 和
k
2
k_2
k2 有
h
(
k
1
)
=
h
(
k
2
)
h(k_1)=h(k_2)
h(k1)=h(k2) ,即无可避免地发生了哈希碰撞,此时不能简单的将键值对 (k, v)
插入哈希表。同样,在进行查找、删除等操作时也需要对这种情况进行考虑。
因此,下面就将介绍几种处理哈希碰撞的方案:
分离链接法
处理哈希碰撞的一种简单而高效的方式是分离链接法(Separate Chaining),即将所有满足
h
(
k
)
=
j
h(k)=j
h(k)=j 的键值对 (k, v)
保存一个二级容器(如:使用列表保存键值对实现的映射对象)中,而哈希表的单元
A
[
j
]
A[j]
A[j] 引用该二级容器。
如下图所示,一个容量为 13 13 13 的哈希表保存了 10 10 10 个键值对(图中省略了所有键值对中的值),且哈希函数为 h ( k ) = k m o d 13 h(k)=k \ mod \ 13 h(k)=k mod 13 :
对于上述处理哈希碰撞的方案,对某一个单元 A [ j ] A[j] A[j] 所引用的二级容器进行查找时,在最坏的情况下,时间复杂度和二级容器的大小呈正比。
假设现在有一个能尽量降低哈希碰撞发生概率的哈希函数,待存储的键值对数目为 n n n ,哈希表容量为 N N N ,则期望每一个哈希表单元处引用的二级容器的容量为 n / N {\left. n\middle/ N\right.} n/N (即希望哈希表每一个单元保存的值的数量尽可能平均)。
基于以上设定,则映射 __getitem__
、 __setitem__
以及 __delitem__
这三个核心方法的最坏时间复杂度为
O
(
⌈
n
/
N
⌉
)
O(\lceil {\left. n\middle/ N\right.} \rceil)
O(⌈n/N⌉) 。一般将
λ
=
n
/
N
\lambda={\left. n\middle/ N\right.}
λ=n/N 记为哈希表的负载系数。显然
λ
\lambda
λ 最好应当小于
1
1
1 ,否则必然会发生哈希碰撞,此时映射几个核心方法的期望时间复杂度为
O
(
1
)
O(1)
O(1) 。
线性查找法
上述解决哈希碰撞的分离链接法虽然实现起来较为方便,但是其也有一个缺陷:需要一个辅助的二级容器来保存发生哈希碰撞的键值对。这对内存敏感型的设备如可穿戴设备不利。下面将要介绍的线性查找法就可以确保不使用额外的二级容器来避免哈希冲突:
在使用线性查找法时,当尝试将键值对 (k, v)
向哈希表
A
[
j
]
A[j]
A[j] (其中
j
=
h
(
k
)
j=h(k)
j=h(k) )单元插入时发现后者已被占用,则重新尝试
A
[
(
j
+
1
)
m
o
d
N
]
A[(j+1) \ mod \ N]
A[(j+1) mod N] 单元。如果
A
[
(
j
+
1
)
m
o
d
N
]
A[(j+1) \ mod \ N]
A[(j+1) mod N] 依然已被占用则尝试
A
[
(
j
+
2
)
m
o
d
N
]
A[(j+2) \ mod \ N]
A[(j+2) mod N] ,以此类推,直到在哈希表中找到空的单元。
为解释上述过程,简单起见,这里假定哈希表容量为 11 11 11 ,键均为整数,哈希函数为 h ( k ) = k m o d 11 h(k) = k \ mod \ 11 h(k)=k mod 11 (由于键为整数,即该哈希函数只需实现压缩函数的功能),则下图(省略键值对中的值)表示了在如图状态下插入键为 15 15 15 的键值对所要尝试的操作:
上述策略使得映射的三个核心方法 __getitem__
、 __setitem__
以及 __delitem__
在实现的第一步就需要进行特别的考虑。
具体地,当实现 __delitem__
时,我们不能简单地将找到的键值对进行删除,否则,假设插入键为
15
15
15 的键值对后删除了键为
37
37
37 的键值对,则后续对于键为
15
15
15 的键值对的搜索就将失败,因为线性查找策略将先从索引为
4
4
4 的单元处开始,然后到索引为
5
5
5 处,然后再发现索引为
6
6
6 的单元为空。因此:
- 对于
__delitem__
:在删除某单元处的键值对后,应该让其引用一个用于标记当前位置是否空闲的对象(一般为object
的实例); - 对于
__getitem__
:在查找键值对时,当遇到object
的实例时就跳过,直到找到期望的键值对或空的单元,抑或回到查找开始的单元; - 对于
__setitem__
:在查找空闲位置(包括空单元和被标记为空闲单元的两种情况)时,需要记录第一个空闲位置。
5. 负载系数和再哈希
负载系数
负载系数 λ = n / N \lambda={\left. n\middle/ N\right.} λ=n/N 会极大地影响哈希表的操作效率,因为:
对于使用分离链接法处理哈希碰撞的哈希表,当 λ \lambda λ 非常接近 1 1 1 时,发生哈希碰撞的几率就会大大增加,这会降低后续操作的效率,因此实验表明这种哈希表的的负载系数应满足 λ < 0.9 \lambda\lt0.9 λ<0.9 。
对于使用线性查找法处理哈希碰撞的哈希表,实验证明当 λ \lambda λ从 0.5 0.5 0.5 开始接近 1 1 1 时,键值对将在哈希表一系列连续单元处发生簇集,此时也会降低后续操作的效率,因此实验表明这种哈希表的负载系数应满足 λ < 0.5 \lambda\lt0.5 λ<0.5 。
再哈希
为了保持哈希表的负载系数在合理范围内,当插入键值对后导致负载系数超过某范围,则通常的做法都是先扩充哈希表的容量以降低负载系数,然后重新将所有键值对插入扩充后的哈希表中,这个过程就叫做再哈希。
实际上,由于哈希函数被分为两个部分,而第一部分计算哈希码的过程和哈希表的容量无关,因此再哈希时只需要通过压缩函数计算处最终的哈希值(代表键值对应该保存的哈希表单元的索引)即可。
二、哈希表实现映射
根据使用的哈希函数在产生哈希碰撞时是采用分离链接法还是线性查找法处理,下面给出两种基于哈希表实现的两个映射类。
1. 基于哈希表实现映射的基类HashMapBase
虽然处理哈希碰撞的两种策略各异,但是通过二者实现的映射依然具有很多共通之处,为此这里先将映射基类 MapBase 进行扩充得到 HashMapBase
,具体地:
- 映射中保存键值对的哈希表用列表来表示,即映射中有
self._table
实例属性,且所有单元初始化为None
; - 映射中的键值对数量使用实例属性
self._n
来表示; - 如果哈希表的负载系数超过 0.5 0.5 0.5 ,则倍增哈希表容量且通过再哈希将所有键值对转到新的哈希表中;
- 定义了一个新的实用方法
_hash_function()
,该方法基于 Python 内建的函数hash()
先计算出一个哈希码,然后实用本文介绍的 MAD 方法作为压缩函数。
扩充后的基类 HashMapBase
中留待具体子类实现的是如何表示哈希表的每一个单元。对于分离链接法,一个单元是一个二级容器(如:列表、链表等);对于线性查找法,单元的含义不一定是哈希表一个索引代表的位置,有可能是几个索引代表的位置。
因此,这里的 HashMapBase
类假定下列方法为抽象方法,留待具体子类实现:
_bucket_getitem(j, k)
:根据键 k k k 的哈希值 j j j 搜索第 j j j 个单元处键为 k k k 的键值对,返回找到的键值对,否则抛出KeyError
异常;_bucket_setitem(j, k, v)
:根据键 k k k 的哈希值 j j j 修改第 j j j 个单元处键为 k k k 的键值对,如果键 k k k 已存在则修改已有的值,否则插入新的键值对然后为self._n
加一;_bucket_delitem(j, k)
:根据键 k k k 的哈希值 j j j 删除第 j j j 个单元处键为 k k k 的键值对并将self._n
减一,如不存在该键值对则抛出KeyError
异常。
__init__
def __init__(self, cap=11, p=109345121):
"""创建一个空的映射"""
self._table = cap * [None]
self._n = 0
self._prime = p # MAD压缩函数中大于哈希表容量的大质数
self._scale = 1 + randrange(p-1) # MAD压缩函数中的缩放系数a
self._shift = randrange(p) # MAD压缩函数中的负载系数b
_hash_function
根据下列公式计算哈希值:
[ ( a i + b ) m o d p ] m o d N [(ai+b) \ mod \ p] \ mod \ N [(ai+b) mod p] mod N
def _hash_function(self, k):
"""哈希函数"""
return (self._scale * hash(k) + self._shift) % self._prime % len(self._table)
__len__
def __len__(self):
return self._n
__getitem__
def __getitem__(self, k):
j = self._hash_function(k)
return self._bucket_getitem(j, k) # 可能抛出KeyError异常
__setitem__
def __setitem__(self, k, v):
j = self._hash_function(k)
self._bucket_setitem(j, k, v)
if self._n > len(self._table) // 2: # 确保负载系数小于0.5
self._resize(2 * len(self._table) - 1) # 通常2 * n - 1为质数
def _resize(self, cap):
"""将哈希表容量调整为cap"""
old = list(self.items()) # 通过迭代获得已有的所有键值对
self._table = cap * [None]
self._n = 0
for k, v in old:
self[k] = v # 将键值对重新插入新的哈希表中
需要注意的是, items()
方法来源于继承链上的 Mapping
类,在 Mapping
类中该方法的返回值是 ItemsView(self)
( self
是一个映射对象)。
而 ItemsView
的初始化方法直接继承自 MappingView
,因此映射对象用以初始化 ItemsView
的实例,因为其初始化方法具体为:
def __init__(self, mapping):
self._mapping = mapping
因此, ItemsView
有一个实例属性 _mapping
为映射实例对象,而 ItemsView
的实例又是一个迭代器对象,因为其中的 __iter__
方法使用 yield (key, self._mapping[key])
。
__delitem__
def __delitem__(self, k):
j = self._hash_function(k)
self._bucket_delitem(j, k) # 可能抛出KeyError异常
self._n -= 1
2. 基于分离链接法实现的映射ChainHashMap
下面先给出基于分离链接法处理哈希碰撞实现的映射类 ChainHashMap
,在其下面实现的前三个方法均使用索引 j
来访问哈希表的某单元处,且检查该单元处是否为空(即引用 None
),只有在调用 _bucket_setitem
且当该单元处为空时才需要使之引用一个二级容器(这里使用的是基于普通列表实现的映射 UnsortedListMap
的实例)。
_bucket_getitem(j, k)
def _bucket_getitem(self, j, k):
bucket = self._table[j]
if bucket is None:
raise KeyError('Key Error: ' + repr(k))
return bucket[k]
_bucket_setitem(j, k, v)
需要注意的是,当键 k k k 对应的键值对第一次插入映射中时,需要将映射中键值对的数量加一。
def _bucket_setitem(self, j, k, v):
if self._table[j] is None:
self._table[j] = UnsortedListMap() # 使得哈希表该单元处引用一个二级容器
oldsize = len(self._table[j])
self._table[j][k] = v
if len(self._table[j]) > oldsize: # k为新的键
self._n += 1
_bucket_delitem(j, k)
需要注意的是,在该方法中并没有执行 self._n -= 1
的操作,该操作由方法 __delitem__
进行处理。
def _bucket_delitem(self, j, k):
bucket = self._table[j]
if bucket is None:
raise KeyError('Key Error: ' + repr(k))
del bucket[k]
__iter__()
需要注意的是,映射对象的每一个非空单元处都引用了一个二级容器,这里使用的二级容器是UnsortedListMap
,其是一个迭代器。
def __iter__(self):
for bucket in self._table:
if bucket is not None:
for key in bucket:
yield key
3. 基于线性查找法实现的映射ProbeHashMap
_is_available(j)
def _is_available(self, j):
"""当哈希表索引为j的单元处为空或键值对被删除,则返回True"""
return self._table[j] is None or self._table[j] is ProbeHashMap._AVAIL
_find_slot(j, k)
def _find_slot(self, j, k):
"""
查找索引为j的哈希表单元处是否有键k
该方法的返回值为一个元组,且返回的情况如下:
- 当在索引为j的哈希表单元处找到键k,则返回(True, fisrt_avail);
- 当未在哈希表任何单元处找到键k,则返回(False, j)。
"""
first_avail = None
while True:
if self._is_available(j):
if first_avail is None:
first_avail = j
if self._table[j] is None:
return False, first_avail
elif k == self._table[j].key:
return True, j
j = (j + 1) % len(self._table)
对于上述方法,有些读者可能会有这样的疑问,即是否在某些情况下 while
循环会一直无法得到返回,具体情况为: self._table
所有单元均被标记为 ProbeHashMap._AVAIL
。
_bucket_getitem(j, k)
def _bucket_getitem(self, j, k):
found, s = self._find_slot(j, k)
if not found:
raise KeyError('Key Error: ' + repr(k))
return self._table[s].value
_bucket_setitem(j, k, v)
def _bucket_setitem(self, j, k, v):
found, s = self._find_slot(j, k)
if not found:
self._table[s] = self._Item(k, v)
self._n += 1
else:
self._table[s].value = v
_bucket_delitem(j, k)
def _bucket_delitem(self, j, k):
found, s = self._find_slot(j, k)
if not found:
raise KeyError('Key Error: ' + repr(k))
self._table[s] = ProbeHashMap._AVAIL
__iter__()
def __iter__(self):
for j in range(len(self._table)):
if not self._is_available(j):
yield self._table[j].key
三、哈希表实现映射的效率
操作 | 基于普通列表的效率 | 基于哈希表的期望效率 | 基于哈希表的最坏效率 |
---|---|---|---|
__getitem__ | O ( n ) O(n) O(n) | O ( 1 ) O(1) O(1) | O ( n ) O(n) O(n) |
__setitem__ | O ( n ) O(n) O(n) | O ( 1 ) O(1) O(1) | O ( n ) O(n) O(n) |
__delitem__ | O ( n ) O(n) O(n) | O ( 1 ) O(1) O(1) | O ( n ) O(n) O(n) |
__len__ | O ( 1 ) O(1) O(1) | O ( 1 ) O(1) O(1) | O ( 1 ) O(1) O(1) |
__iter__ | O ( n ) O(n) O(n) | O ( n ) O(n) O(n) | O ( n ) O(n) O(n) |
四、哈希表实现映射的测试代码
from abc import ABC, abstractmethod
from collections.abc import MutableMapping
from random import randrange
class MapBase(MutableMapping, ABC):
"""提供用于保存键值对记录类的自定义映射基类"""
class _Item:
__slots__ = 'key', 'value'
def __init__(self, key, value):
self.key = key
self.value = value
def __eq__(self, other):
return self.key == other.key # 使用'=='语法基于键比较两个键值对是否相等
def __ne__(self, other):
return not (self == other) # 使用'!='语法基于键比较两个键值对是否不等
def __lt__(self, other):
return self.key < other.key # 使用'<'语法基于键比较两个键值对
class UnsortedListMap(MapBase):
def __init__(self):
"""创建一个空的映射对象"""
self._table = [] # 映射中的键值对记录保存在列表中
def __getitem__(self, key):
"""返回与键key关联的值value,当键key不存在则抛出KeyError异常"""
for item in self._table:
if key == item.key:
return item.value
raise KeyError('key error: ', repr(key))
def __setitem__(self, key, value):
"""将key-value添加至映射对象中,当存在键值key时将其值替换为value"""
for item in self._table: # 遍历查询映射中是否存在键key
if key == item.key:
item.value = value
return
self._table.append(self._Item(key, value)) # 映射中不存在键key
def __delitem__(self, key):
"""删除键key代表的键值对,当键key不存在则抛出KeyError异常"""
for j in range(len(self._table)): # 遍历查询映射中是否存在键key
if key == self._table[j].key:
self._table.pop(j)
return
raise KeyError('key error: ', repr(key)) # 映射中不存在键key
def __len__(self):
"""返回映射中键值对数量"""
return len(self._table)
def __iter__(self):
"""生成一个映射中所有键的迭代"""
for item in self._table:
yield item.key
def __str__(self):
"""返回映射对象的字符串表示形式"""
return str(dict(self.items()))
class HashMapBase(MapBase):
"""使用哈希表实现映射的基类"""
def __init__(self, cap=11, p=109345121):
"""创建一个空的映射"""
self._table = cap * [None]
self._n = 0
self._prime = p # MAD压缩函数中大于哈希表容量的大质数
self._scale = 1 + randrange(p-1) # MAD压缩函数中的缩放系数a
self._shift = randrange(p) # MAD压缩函数中的便宜系数b
def _hash_function(self, k):
"""哈希函数"""
return (self._scale * hash(k) + self._shift) % self._prime % len(self._table)
@abstractmethod
def _bucket_getitem(self, j, k):
pass
@abstractmethod
def _bucket_setitem(self, j, k, v):
pass
@abstractmethod
def _bucket_delitem(self, j, k):
pass
def __len__(self):
return self._n
def __getitem__(self, k):
j = self._hash_function(k)
return self._bucket_getitem(j, k) # 可能抛出KeyError异常
def __setitem__(self, k, v):
j = self._hash_function(k)
self._bucket_setitem(j, k, v)
if self._n > len(self._table) // 2: # 确保负载系数小于0.5
self._resize(2 * len(self._table) - 1) # 通常2 * n - 1为质数
def __delitem__(self, k):
j = self._hash_function(k)
self._bucket_delitem(j, k) # 可能抛出KeyError异常
self._n -= 1
def _resize(self, cap):
"""将哈希表容量调整为cap"""
old = list(self.items()) # 通过迭代获得已有的所有键值对
self._table = cap * [None]
self._n = 0
for k, v in old:
self[k] = v # 将键值对重新插入新的哈希表中
class ChainHashMap(HashMapBase):
"""使用分离链接法处理哈希碰撞实现的哈希映射"""
def _bucket_getitem(self, j, k):
bucket = self._table[j]
if bucket is None:
raise KeyError('Key Error: ' + repr(k))
return bucket[k]
def _bucket_setitem(self, j, k, v):
if self._table[j] is None:
self._table[j] = UnsortedListMap() # 使得哈希表该单元处引用一个二级容器
oldsize = len(self._table[j])
self._table[j][k] = v
if len(self._table[j]) > oldsize: # k为新的键
self._n += 1
def _bucket_delitem(self, j, k):
bucket = self._table[j]
if bucket is None:
raise KeyError('Key Error: ' + repr(k))
del bucket[k]
def __iter__(self):
for bucket in self._table:
if bucket is not None:
for key in bucket:
yield key
class ProbeHashMap(HashMapBase):
"""使用线性查找法处理哈希碰撞实现的哈希映射"""
_AVAIL = object() # 哨兵标识,用于标识被键值对被删除的哈希表单元
def _is_available(self, j):
"""当哈希表索引为j的单元处为空或键值对被删除,则返回True"""
return self._table[j] is None or self._table[j] is ProbeHashMap._AVAIL
def _find_slot(self, j, k):
"""查找索引为j的哈希表单元处是否有键k
该方法的返回值为一个元组,且返回的情况如下:
- 当在索引为j的哈希表单元处找到键k,则返回(True, fisrt_avail);
- 当未在哈希表任何单元处找到键k,则返回(False, j)。
"""
first_avail = None
while True:
if self._is_available(j):
if first_avail is None:
first_avail = j
if self._table[j] is None:
return False, first_avail
elif k == self._table[j].key:
return True, j
j = (j + 1) % len(self._table)
def _bucket_getitem(self, j, k):
found, s = self._find_slot(j, k)
if not found:
raise KeyError('Key Error: ' + repr(k))
return self._table[s].value
def _bucket_setitem(self, j, k, v):
found, s = self._find_slot(j, k)
if not found:
self._table[s] = self._Item(k, v)
self._n += 1
else:
self._table[s].value = v
def _bucket_delitem(self, j, k):
found, s = self._find_slot(j, k)
if not found:
raise KeyError('Key Error: ' + repr(k))
self._table[s] = ProbeHashMap._AVAIL
def __iter__(self):
for j in range(len(self._table)):
if not self._is_available(j):
yield self._table[j].key