雜湊表
在學習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
複製程式碼
雜湊表是很高效的資料結構,對於新手來講也比較難理解,我手寫了一遍程式碼,然後再用電腦敲了一遍才基本瞭解。新手寫的,請見諒