Py学习  »  Python

【数据结构Python描述】仿照Python解释器使用哈希表手动实现一个字典类

TakingCoding4Granted • 4 年前 • 364 次点击  

在文章 【数据结构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 m o d 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\} { 2 0 0 , 2 0 5 , 2 1 0 , 2 1 5 , 2 2 0 , , 6 0 0 } ,且哈希表的容量为100时,则每一个哈希码都将和另外3个哈希码发生碰撞。

如果压缩函数选择得当,那么两个不同的哈希码发生碰撞的概率为 1 / N {\left. 1\middle/ N\right.} 1 / N

MAD方法

上述取模算法有一个缺点是,当哈希码为 p N + q pN+q p N + q 的形式,则发生哈希碰撞的概率依然很大,而所谓的MAD(Multiply-Add-and-Divide)方法可以很好的改善这个问题,即:

[ ( a i + b )   m o d   p ]   m o d   N [(ai+b) \ mod \ p] \ mod \ N [ ( a i + b ) m o d p ] m o d 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 k 1 k 2 k_2 k 2 h ( k 1 ) = h ( k 2 ) h(k_1)=h(k_2) h ( k 1 ) = h ( k 2 ) ,即发生了哈希碰撞,此时不能简单的将键值对 (k, v) 插入哈希表。同样,在进行查找、删除等操作时也需要对这种情况进行考虑。

因此,下面就将介绍几种处理哈希碰撞的方案:

分离链接法

处理哈希碰撞的一种简单而高效的方式是 分离链接法 (Separate Chaining),即将所有满足 h ( k ) = j h(k)=j h ( k ) = j 的键值对 (k, v) 保存一个二级容器(如: 使用列表保存键值对实现的映射对象 )中,而哈希表的单元 A [ j ] A[j] A [ j ] 引用该二级容器。

如下图所示,一个容量为13的哈希表保存了10个键值对(图中省略了所有键值对中的值),且哈希函数为 h ( k ) = k   m o d   13 h(k)=k \ mod \ 13 h ( k ) = k m o d 1 3

在这里插入图片描述

对于上述处理哈希碰撞的方案,对某一个单元 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,否则必然会发生哈希碰撞,此时映射几个核心方法的期望时间复杂度为 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 ) m o d N ] 单元。如果 A [ ( j + 1 )   m o d   N ] A[(j+1) \ mod \ N] A [ ( j + 1 ) m o d N ] 依然已被占用则尝试 A [ ( j + 2 )   m o d   N ] A[(j+2) \ mod \ N] A [ ( j + 2 ) m o d N ] ,以此类推,直到在哈希表中找到空的单元。

为解释上述过程,假定哈希表容量为11,键均为整数,哈希函数为 h ( k ) = k   m o d   11 h(k) = k \ mod \ 11 h ( k ) = k m o d 1 1 ,则下图(省略键值对中的值)表示了在如图状态下插入键为15的键值对所要尝试的操作:

在这里插入图片描述

上述策略使得映射的三个核心方法 __getitem__ __setitem__ 以及 __delitem__ 在实现的第一步就需要进行特别的考虑。

具体地,当实现 __delitem__ 时,我们不能简单地将找到的键值对进行删除,例如:如插入键为15的键值对后,如果删除了键为37的键值对,则后续对于键为15的键值对的搜索就将失败,因为线性查找策略将先从索引为4的单元处开始,然后到索引为5处,然后再发现索引为6的单元为空。因此:

  • 对于 __delitem__ :在删除某单元处的键值对后,将其引用一个用于标记的对象(一般为 object 的实例);
  • 对于 __getitem__ :在查找键值对时,当遇到 object 的实例时就跳过,直到找到期望的键值对或空的单元,抑或回到查找开始的单元;
  • 对于 __setitem__

5. 负载系数和再哈希

负载系数

负载系数 λ = n / N \lambda={\left. n\middle/ N\right.} λ = n / N 会极大地影响哈希表的操作效率,因为:

对于使用 分离链接法 处理哈希碰撞的哈希表,当 λ \lambda λ 非常接近1时,发生哈希碰撞的几率就会大大增加,这会降低后续操作的效率,因此实验表明这种哈希表的的负载系数应满足 λ < 0.9 \lambda\lt0.9 λ < 0 . 9

对于使用 线性查找法 处理哈希碰撞的哈希表,实验证明当 λ \lambda λ 从0.5开始接近1时,键值对将在哈希表一系列连续单元处发生 簇集 ,此时也会降低后续操作的效率,因此实验表明这种哈希表的负载系数应满足 λ < 0.5 \lambda\lt0.5 λ < 0 . 5

再哈希

为了保持哈希表的负载系数在合理范围内,当插入键值对后导致负载系数超过某范围,则通常的做法都是先扩充哈希表的容量以降低负载系数,然后重新将所有键值对插入扩充后的哈希表中,这个过程就叫做 再哈希

实际上,由于哈希函数被分为两个部分,而第一部分计算哈希码的过程和哈希表的容量无关,因此再哈希时只需要通过压缩函数计算处最终的哈希值(代表键值对应该保存的哈希表单元的索引)即可。

二、哈希表实现映射

根据使用的哈希函数在产生哈希碰撞时是采用分离链接法还是线性查找法处理,下面给出两种基于哈希表实现的两个映射类。

1. 基于哈希表实现映射的基类 HashMapBase

虽然处理哈希碰撞的两种策略各异,但是通过二者实现的映射依然具有很多共通之处,为此这里先将 映射基类MapBase 进行扩充得到 HashMapBase ,具体地:

  • 映射中的键值对的哈希表用列表来表示,即映射中有 self._table 实例属性,且所有单元初始化为 None
  • 映射中的键值对数量使用实例属性 self._n 来表示;
  • 如果哈希表的负载系数超过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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

_hash_function

根据下列公式计算哈希值:

[ ( a i + b )   m o d   p ]   m o d   N [(ai+b) \ mod \ p] \ mod \ N [ ( a i + b ) m o d p ] m o d N

def _hash_function(self, k):
    """哈希函数"""
    return (self._scale * hash(k) + self._shift) % self._prime % len(self._table)
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

__len__

def __len__(self):
    return self._n
  • 1
  • 2
  • 1
  • 2

__getitem__

def __getitem__(self, k):
    j = self._hash_function(k)
    return self._bucket_getitem(j, k)  # 可能抛出KeyError异常


    

  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

__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  # 将键值对重新插入新的哈希表中
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

需要注意的是, items() 方法来源于继承链上的 Mapping 类,在 Mapping 类中该方法的返回值是 ItemsView(self) self 是一个映射对象)。

ItemsView 的初始化方法直接继承自 MappingView ,因此映射对象用以初始化 ItemsView 的实例,因为其初始化方法具体为:

def __init__(self, mapping):
	self._mapping = mapping
  • 1
  • 2
  • 1
  • 2

因此, 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
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

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]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

_bucket_setitem(j, k, v)

需要注意的是,当键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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

_bucket_delitem(j, k)

def _bucket_delitem(self, j, k):
    bucket = self._table[j]
    if bucket is None:
        raise KeyError('Key Error: ' + repr(k))
    del bucket[k]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

__iter__()

需要注意的是,映射对象的每一个非空单元处都引用了一个二级容器,这里使用的二级容器是 UnsortedListMap ,其是一个迭代器。

def __iter__(self):
    for bucket in self._table:
        if bucket is not None:
            for key in bucket:
                yield key
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

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
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

_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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

_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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

_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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

_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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

__iter__()

def __iter__(self):
    for j in range(len(self._table)):
        if not self._is_available(j):
            yield self._table[j].key
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

三、哈希表实现映射的效率

操作 基于普通列表的效率 基于哈希表的期望效率 基于哈希表的最坏效率
__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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/74988
 
364 次点击