Python:說說字典和雜湊表,雜湊衝突的解決原理

丹楓無跡發表於2018-10-09

Python 用雜湊表來實現 dict。

雜湊表其實是一個稀疏陣列(總是有空白元素的陣列稱為稀疏陣列)。在一般書中,雜湊表裡的單元通常叫做表元(bucket)。在 dict 的雜湊表當中,每個鍵值對都佔用一個表元,每個表元都有兩個部分,一個是對鍵的引用,一個是對值的引用。因為每個表元的大小一致,所以可以通過偏移量來讀取某個表元。

 

Python會設法保證大概還有三分之一的表元是空的,當快要達到這個閥值的時候,會進行擴容,將原雜湊表複製到一個更大的雜湊表裡。

如果要把一個物件放入到雜湊表裡,就先要計算這個元素鍵的雜湊值。這就要求鍵(key)必須是可雜湊的。

 

一個可雜湊的物件必須滿足以下條件:

  1. 支援 hash() 函式,並且通過 __hash__() 方法所得到的雜湊值是不變的。
  2. 支援通過 __eq__() 方法來檢測相等性。
  3. 若 a == b 為真,則 hash(a) == hash(b) 也為真。

下面主要來說明一下雜湊表的演算法:

為了獲取鍵 search_key 所對應的值 search_value,python 會首先呼叫 hash(search_key) 計算 search_key 的雜湊值,把這個值最低的幾位數字當作偏移量,在雜湊表裡查詢表元(具體取幾位,得看當前雜湊表的大小)。若找到的表元是空的,則丟擲 KeyError 異常;若不為空,則表元裡會有一對 found_key:found_value,檢驗 search_key 和 found_key 是否相等,若相等,則返回 found_value。若不相等,這種情況稱為雜湊衝突。

為了解決雜湊衝突,演算法會在雜湊值中另外再取幾位,然後用特殊的方法處理一下,把得到的新數值作為偏移量在雜湊表中查詢表元,若找到的表元是空的,則同樣丟擲 KeyError 異常;若非空,則比較鍵是否一致,一致則返回對應的值;若又發現雜湊衝突,則重複以上步驟。

新增新元素跟上面的過程幾乎一樣,只不過在發現空表元的時候會放入這個新元素,不為空則為雜湊重複,繼續查詢。

 

當往 dict 裡新增新元素並且發生了雜湊衝突的時候,新元素可能會被安排存放到另一個位置。於是就會發生下面的情況:dict([key1, value1], [key2, value2]) 和 dict([key2, value2], [key1, value1]) 兩個字典,在進行比較的時候是相等的,但如果 key1 和 key2 雜湊衝突,則這兩個鍵在字典裡的順序是不一樣的。

無論何時,往 dict 裡新增新的鍵,python 解析器都可能做出為字典擴容的決定。擴容導致的結果就是要新建一個更大的雜湊表,並把字典裡已有的元素新增到新的雜湊表裡。這個過程中可能發生新的雜湊衝突,導致新雜湊表中鍵的次序變化。如果在迭代一個字典的同時往裡面新增新的鍵,會發生什麼?不湊巧擴容了,不湊巧鍵的次序變了,然後就 orz 了。

 

由於雜湊表必須是稀疏的,這導致它在空間上的消耗必然要大很多,這是典型的空間換時間。

相關文章