哈希表 學習筆記
參考翻譯自:《復雜性思考》 及對應的online版本:http://greenteapress.com/complexity/html/thinkcomplexity004.html
使用哈希表可以進行非常快速的查找操作,查找時間為常數,同時不需要元素排列有序
python的內建數據類型:字典,就是用哈希表實現的
為了解釋哈希表的工作原理,我們來嘗試在不使用字典的情況下實現哈希表結構。
我們需要定義一個包含 鍵->值 映射 的數據結構,同時實現以下兩種操作:
add(k, v):
Add a new item that maps from key k to value v.
With a Python dictionary,d, this operation is written d[k] = v.
get(target):
Look up and return the value that corresponds to key target.
With a Python dictionary, d, this operation is written d[target] or d.get(target).
一種簡單是實現方法是建立一個線性表,使用元組來實現 key-value 的映射關系
1 class LinearMap(object): 2 """ 線性表結構 """ 3 def __init__(self): 4 self.items = [] 5 6 def add(self, k, v): # 往表中添加元素 7 self.items.append((k,v)) 8 9 def get(self, k): # 線性方式查找元素 10 for key, val in self.items: 11 if key==k: # 鍵存在,返回值,否則拋出異常 12 return val 13 raise KeyError
我們可以在使用add添加元素時讓items列表保持有序,而在使用get時采取二分查找方式,時間復雜度為O(log n)。 然而往列表中插入一個新元素實際上是一個線性操作,所以這種方法並非最好的方法。同時,我們仍然沒有達到常數查找時間的要求。
我們可以做以下改進,將總查詢表分割為若干段較小的列表,比如100個子段。通過hash函數求出某個鍵的哈希值,再通過計算,得到往哪個子段中添加或查找。相對於從頭開始搜索列表,時間會極大的縮短。盡管get操作的增長依然是線性,但BetterMap類使得我們離哈希表更近一步:
1 class BetterMap(object): 2 """ 利用LinearMap對象作為子表,建立更快的查詢表 """ 3 def __init__(self,n=100): 4 self.maps = [] # 總表格 5 for i in range(n): # 根據n的大小建立n個空的子表 6 self.maps.append(LinearMap()) 7 8 def find_map(self,k): # 通過hash函數計算索引值 9 index = hash(k) % len(self.maps) 10 return self.maps[index] # 返回索引子表的引用 11 12 # 尋找合適的子表(linearMap對象),進行添加和查找 13 def add(self, k, v): 14 m = self.find_map(k) 15 m.add(k,v) 16 17 def get(self, k): 18 m = self.find_map(k) 19 return m.get(k)
測試一下:
1 if __name__=="__main__": 2 table = BetterMap() 3 pricedata = [("Hohner257",257), 4 ("SW1664",280), 5 ("SCX64",1090), 6 ("SCX48",830), 7 ("Super64",2238), 8 ("CX12",1130), 9 ("Hohner270",620), 10 ("F64C",9720), 11 ("S48",1988)] 12 13 for item, price in pricedata: 14 table.add(k=item, v=price) 15 16 print table.get("CX12") 17 # >>> 1130 18 print table.get("QIMEI1248") 19 # >>> raise KeyError
由於每個鍵的hash值必然不同,所以對hash值取余的值基本也是不同的。
當n=100時, BetterMap的查找速度大約是LinearMap的100倍。
明顯,BetterMap的查找速度受到參數n的限制,同時其中每個LinearMap的長度不固定,使得子段中的元素依然是線性查找。如果,我們能夠限制每個子段的最大長度,這樣在單個子段中查找的時間負責度就有一個固定上限,則LinearMap.get方法的時間復雜度就成為了一個常數。由此,我們僅僅需要跟蹤元素的數量,每當某個LinearMap中的元素數量超過閾值時, 對整個hashtable進行重排,同時增加更多的LinearMap,這樣子就可以保證查找操作為一個常數啦。
以下是hashtable的實現:
1 class HashMap(object): 2 def __init__(self): 3 # 初始化總表為,容量為2的表格(含兩個子表) 4 self.maps = BetterMap(2) 5 self.num = 0 # 表中數據個數 6 7 def get(self,k): 8 return self.maps.get(k) 9 10 def add(self, k, v): 11 # 若當前元素數量達到臨界值(子表總數)時,進行重排操作 12 # 對總表進行擴張,增加子表的個數為當前元素個數的兩倍! 13 if self.num == len(self.maps.maps): 14 self.resize() 15 16 # 往重排過后的 self.map 添加新的元素 17 self.maps.add(k, v) 18 self.num += 1 19 20 def resize(self): 21 """ 重排操作,添加新表, 注意重排需要線性的時間 """ 22 # 先建立一個新的表,子表數 = 2 * 元素個數 23 new_maps = BetterMap(self.num * 2) 24 25 for m in self.maps.maps: # 檢索每個舊的子表 26 for k,v in m.items: # 將子表的元素復制到新子表 27 new_maps.add(k, v) 28 29 self.maps = new_maps # 令當前的表為新表
重點關注 add 部分,該函數檢查元素個數與BetterMap的大小,如果相等,則“平均每個LinearMap中的元素個數為1”,然后調用resize方法。
resize創建一個新表,大小為原來的兩倍,然后對舊表中的元素“rehashes 再哈希”一 遍,放到新表中。
resize過程是線性的,聽起來好像很不怎么好,因為我們要求的hashtable具有常數時間。但是,要知道我們並不需要經常進行重排操作,所以add操作在絕大部分時間中都是常數的,偶然出現線性。由於對n個元素進行add操作的總時間與n成比例,所以每次add的平均時間就是一個常數!
假設我們要添加32個元素,過程如下:
1. 由於初始長度為2,前兩次add不需要重排,第1,2次 總時間為 2
2. 第3次add,重排為4,耗時2,第3次時間為 3
3. 第4次add,耗時1 到目前為止,總時間為 6
4. 第5次add,重排為 8,耗時4,第5次時間為5
5. 第6~8次 共耗時3 到目前為止,總時間為 6+5+3 = 14
6. 第9次add,重排16, 耗時8,第9次時間為9
7. 第10~16次,共耗時7, 到目前為止,總時間為 14+9+7 = 30
在32次add后,總時間為62的單位時間,由以上過程可以發現一個規律,在n個元素add之后,當n為2的冪,則當前總單位時間為 2n-2,所以平均add時間絕對小於2單位時間。
當n為2的冪時,為最合適的數量,當n變大之后,平均時間為稍微上升,但重要的是,我們達到了O(1)。
