菜鳥學Python之雜湊表
在學習Python的時候有時候會想,為什麼dict和set的查詢速度這麼快,感覺就像是事先知道要找的元素的位置一樣?在學完雜湊表之後,這個問題也就夠被很好的解釋了。
定義
雜湊表是一種根據 關鍵碼 (key)去尋找 值 (value)的資料對映結構,該結構通過把關鍵碼 對映的位置 (index)去尋找存放值的地方。 舉個例子,也就是像小時候我們經常查的字典一樣,比如我們要查詢一個字 “一”(value),我們先得到它的拼音“yi”(key),然後就可以在字典的查詢目錄看到這個字在哪一頁(index),最後就得到這個字的詳細資訊。
實現方法
我們知道,陣列的查詢速度之所以是O(1)是因為數組裡面的元素都有一個下標,所以參考陣列的下標,給每個元素一種[ 邏輯下標 ]。我們把得到的邏輯下標稱為“ 槽 ”。
邏輯下標的計算方法採用的是 取模運算 : h(key) = key % M
雜湊衝突
當給出的值取到的“邏輯下標”相同時,雜湊衝突便產生了。這裡引用一下別人的圖

解決辦法
遇到這種情況該如何解決呢?我們首先能夠想到的是既然衝突了,那能不能夠把這些衝突的放進一個連結串列裡面呢?或者重新找過其他地方呢?
- 讓陣列中 衝突的槽 變成一個鏈式結構,但是這個方法有個缺點,當連結串列太長的時候,就會造成時間退化,查詢時間會從O(1)退化為O(n),因此這種方法比較少用。
- 尋找下一個槽,這裡給出的是比較常用的二次探查(以二次方作為偏移量)
裝載因子(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 複製程式碼
雜湊表是很高效的資料結構,對於新手來講也比較難理解,我手寫了一遍程式碼,然後再用電腦敲了一遍才基本瞭解。新手寫的,請見諒