菜鳥學Python之雜湊表

小明同志發表於2019-04-09

雜湊表

在學習Python的時候有時候會想,為什麼dict和set的查詢速度這麼快,感覺就像是事先知道要找的元素的位置一樣?在學完雜湊表之後,這個問題也就夠被很好的解釋了。

定義

雜湊表是一種根據關鍵碼(key)去尋找(value)的資料對映結構,該結構通過把關鍵碼對映的位置(index)去尋找存放值的地方。 舉個例子,也就是像小時候我們經常查的字典一樣,比如我們要查詢一個字 “一”(value),我們先得到它的拼音“yi”(key),然後就可以在字典的查詢目錄看到這個字在哪一頁(index),最後就得到這個字的詳細資訊。

實現方法

我們知道,陣列的查詢速度之所以是O(1)是因為陣列裡面的元素都有一個下標,所以參考陣列的下標,給每個元素一種[邏輯下標]。我們把得到的邏輯下標稱為“”。

邏輯下標的計算方法採用的是取模運算: h(key) = key % M

雜湊衝突

當給出的值取到的“邏輯下標”相同時,雜湊衝突便產生了。這裡引用一下別人的圖

菜鳥學Python之雜湊表

解決辦法

遇到這種情況該如何解決呢?我們首先能夠想到的是既然衝突了,那能不能夠把這些衝突的放進一個連結串列裡面呢?或者重新找過其他地方呢?

  1. 讓陣列中 衝突的槽 變成一個鏈式結構,但是這個方法有個缺點,當連結串列太長的時候,就會造成時間退化,查詢時間會從O(1)退化為O(n),因此這種方法比較少用。
  2. 尋找下一個槽,這裡給出的是比較常用的二次探查(以二次方作為偏移量)
    菜鳥學Python之雜湊表

裝載因子(load factor)

load factor = 元素個數 / 雜湊表大小, 當裝載因子超過0.8時,就要開闢新的空間並重新進行雜湊了。

重雜湊(Rehashing)

重新開闢空間並雜湊的操作過程就叫做重雜湊

程式碼示例

下面給出實現雜湊表的程式碼

# 雜湊表是用陣列完成的
class Array(object):
    def __init__(self, size=32, init=None):
        self._size = size
        self._items = [init] * 32    # 得到一個空的陣列
        
    # 返回value
    def __getiter__(self, index):    
        return self._items[index]
    
    # 重置value
    def __setitem__(self, index, value):
        self._items[index] = value

    def __len__(self):
        return self._size

    def clear(self, value=None):
        for i in range(len(self._items)):
            self._items[i] = value

    def __iter__(self):
        for item in self._items:
            yield item


# 定義槽,傳入key和value
class Slot(object):
    def __init__(self, key, value):
        self.key = key
        self.value = value


# 定義雜湊表
class HashTable(object):
    # 首先定義兩個全域性變數
    UNUSED = None
    EMPTY = Slot(None, None)

    def __init__(self):
        self._table = Array(8, init=HashTable.UNUSED)
        self.length = 0  # 已使用槽的數量
    
    def __load_factor(self):
        """定義裝載因子"""
        return self.length / float(len(self._table))
    
    def __len__(self):
        return self.length
    
    def __hash__(self):
        """定義雜湊函式"""
        return abs(hash(key)) % len(self._table)
    
    
    def _find_key(self, key):
        """根據給出的key,獲得index"""
        index = self.__hash__(key)
        _len = len(self._table)
        while self._table[index] is not HashTable.UNUSED:   # 這個槽已經被使用過且為空,則重新查詢
            if self._table[index] is HashTable.EMPTY:
                index = (index * 5 + 1) % _len
                continue
            elif self._table[index].key == key:
                return index
            else:
                index = (index * 5 + 1) % _len
        return None

    def _slot_can_insert(self, index):
        """判斷找到的槽是否可用"""
        return (self._table[index] is HashTable.UNUSED or self._table[index] is HashTable.EMPTY)

    def _find_slot_for_insert(self, key):
        """查詢可以用的槽"""
        index = self.__hash__(key)
        _len = len(self._table)
        while not self._slot_can_insert(index):
            index = (index * 5 + 1) % _len
        return index

    def __contains__(self, key):
        index = self._find_key(key)
        return index is not None

    def add(self, value, key):
        """往雜湊表裡新增資料"""
        if key in self:
            index = self._find_key(key)
            self._table[index] = value
            return False
        else:
            index = self._find_slot_for_insert(key)
            self._table[index] = Slot(key, value)
            self.length += 1
            if self.__load_factor() > 0.8:
                self.rehash()
            return True

    def _rehash(self):
        """重雜湊"""
        old_table = self._table
        newsize = len(self._table) * 2
        self._table = Array(newsize, HashTable.EMPTY)
        self.length = 0

        for slot in old_table:
            if slot is not HashTable.UNUSED or slot is not HashTable.EMPTY:
                index = self._find_slot_for_insert(slot.key)
                self._table[index] = slot
                self.length += 1

    def get(self, key, default=None):
        """取值"""
        index = self._find_key(key)
        if index is None:
            return default
        else:
            return self._table[index].value

    def remove(self, key):
        index = self._find_key(key)
        if index is None:
            return KeyError()
        value = self._table[index]
        self.length -= 1
        self._table[index] = HashTable.EMPTY
        return value

    def __iter__(self):
        for slot in self._table:
            if slot not in (HashTable.UNUSED, HashTable.EMPTY):
                yield slot
複製程式碼

雜湊表是很高效的資料結構,對於新手來講也比較難理解,我手寫了一遍程式碼,然後再用電腦敲了一遍才基本瞭解。新手寫的,請見諒

相關文章