主頁 > 後端開發 > 【資料結構Python描述】仿照Python解釋器使用哈希表手動實作一個字典類

【資料結構Python描述】仿照Python解釋器使用哈希表手動實作一個字典類

2020-11-07 12:11:11 後端開發

文章目錄

  • 一、哈希表簡介
    • 1. 哈希函式簡介
    • 2. 哈希碰撞簡介
    • 3. 哈希函式深究
      • 哈希碼
      • 壓縮函式
        • 取模運演算法
        • MAD方法
    • 4. 處理哈希碰撞
      • 分離鏈接法
      • 線性查找法
    • 5. 負載系數和再哈希
      • 負載系數
      • 再哈希
  • 二、哈希表實作映射
    • 1. 基于哈希表實作映射的基類`HashMapBase`
      • `__init__`
      • `_hash_function`
      • `__len__`
      • `__getitem__`
      • `__setitem__`
      • `__delitem__`
    • 2. 基于分離鏈接法實作的映射`ChainHashMap`
      • `_bucket_getitem(j, k)`
      • `_bucket_setitem(j, k, v)`
      • `_bucket_delitem(j, k)`
      • `__iter__()`
    • 3. 基于線性查找法實作的映射`ProbeHashMap`
      • `_is_available(j)`
      • `_find_slot(j, k)`
      • `_bucket_getitem(j, k)`
      • `_bucket_setitem(j, k, v)`
      • `_bucket_delitem(j, k)`
      • `__iter__()`
  • 三、哈希表實作映射的效率
  • 四、哈希表實作映射的測驗代碼

在文章【資料結構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所有的內建資料型別中,因intfloatstrtuple、以及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時,則每一個哈希碼都將和另外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的哈希表保存了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,否則必然會發生哈希碰撞,此時映射幾個核心方法的期望時間復雜度為 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,鍵均為整數,哈希函式為 h ( k ) = k m o d 11 h(k) = k \ mod \ 11 h(k)=k mod 11,則下圖(省略鍵值對中的值)表示了在如圖狀態下插入鍵為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

_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對應的鍵值對第一次插入映射中時,需要將映射中鍵值對的數量加一,

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)

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)

_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

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/205426.html

標籤:java

上一篇:“1024”征文活動結果新鮮出爐!快來看看是否榜上有名?~~

下一篇:極客日報1106:小紅書回應 “涉黃內容” 已封禁;iOS 14.2 推送,你更新了嗎?

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more