python中字典和集合内部用的就是哈希表(散清單)是以他們的查找速度非常快。
數組可以迅速通過下标去查找,O(1);但是删除數組元素就要向前依次移動其他元素,是以O(n);
循環雙端連結清單在知道一個節點的情況下迅速删除它,O(1),但是它的節點查找又成了 O(n),比較慢。
擴充:
給元素一個邏輯下标,這樣在存儲一個元素的時候,通過這個元素計算一個邏輯下标,這樣就可以迅速通過這個下标找到這個元素,這也就是哈希表的工作原理。
舉例:
有個長度為13個元素的數組,定義一個哈希函數
h(key) = key % M
使用取餘運算是的h(key)的結果不會超過數組長度的下标,再來分别插入以下元素:
765, 431, 96, 142, 579, 226, 903, 388
先來計算下它們應用哈希函數後的結果:
M = 13
h(765) = 765 % M = 11
h(431) = 431 % M = 2
h(96) = 96 % M = 5
h(142) = 142 % M = 12
h(579) = 579 % M = 7
h(226) = 226 % M = 5
h(903) = 903 % M = 6
h(388) = 388 % M = 11
可以看到 96 和 226,以及 765 和 388 産生了哈希沖突。
下圖模拟哈希表的插入過程(連結法):
當出現哈希沖突的時候,就把這個位置當成連結表,226就往這個連結表裡面插入,是以用連結法解決哈希沖突,缺點也很明顯,如果哈希沖突的比較多,導緻這個鍊過長,這時去查找元素的時候,沒法通過O(1)時間去定位,就發生了時間複雜度退化的情況。
另外一種方法就相對較好:開放尋址法
它的思想比較簡單,當一個槽被占用的時候,采用一種方式尋找下一個可用的槽,這裡的槽指的是數組中的一個位置,根據找下一個槽的方式不同分為幾種:
(1)線性探查:當一個槽被占用,找下一個可用的槽
h(k, i) = (h'(k) +i) % m, i=0,1,2,....,m-1
#k 是插入元素的key值
#i 是位置
#h'(k) + i 是對M取模
#m-1 是槽的長度
(2)2次探查:【python内置使用的就是二次探查】
當一個槽被占用時,以2次方作為偏移量
h(k,i)=(h′(k)+c1+c2i2)%m,i=0,1,...,m−1
選取c1,c2兩個常數,之前計算得到的一個哈希表的位置(h'(k))之後,加上c1和c2i2的和,對m取模,以2次方作為偏移量。
(3)雙重散列:重新計算哈希結果
h(k,i)=(h1(k)+ih2(k))%m
這種方法根據哈希函數進行二次哈希,用的很少
python實際上使用的是二次探查法:
h(k, i) = (home +i2) % m
意思是如果遇到了沖突,就在原始計算的位置上不斷加上i的平方。之後再對m使用取模。
下面的代碼模拟了計算邏輯下标的過程:
inserted_index_set = set()
M = 13
def h(key, M=13):
return key % M
to_insert = [765, 431, 96, 142, 579, 226, 903, 388]
for number in to_insert:
index = h(number)
first_index = index
i = 1
while index in inserted_index_set: #如果計算發現已經占用,繼續計算得到下一個可用槽的位置
print('\th({number}) = {number} % M = {index} collision'.format(number=number, index=index))
index = (first_index + i*i) % M #根據二次方探查的公式重新計算下一個需要插入的位置
i += 1
else:
print('h({number}) = {number} % M = {index}'.format(number=number, index=index))
inserted_index_set.add(index)
這段代碼輸出的結果如下:
h(765) = 765 % M = 11
h(431) = 431 % M = 2
h(96) = 96 % M = 5
h(142) = 142 % M = 12
h(579) = 579 % M = 7
h(226) = 226 % M = 5 collision
h(226) = 226 % M = 6
h(903) = 903 % M = 6 collision
h(903) = 903 % M = 7 collision
h(903) = 903 % M = 10
h(388) = 388 % M = 11 collision
h(388) = 388 % M = 12 collision
h(388) = 388 % M = 2 collision
h(388) = 388 % M = 7 collision
h(388) = 388 % M = 1
遇到沖突之後會重新計算,每個待插入元素最終的下标就是:
h(765) => 11
h(431) => 2
h(96) => 5
h(142) => 12
h(579) => 7
h(226) => 5(collision) => 6
h(903) => 6(collision) => 7(collision) => 10
h(338) => 11(collision) => 12(collision) => 2(collision) => 7(collision) => 1
詳細解釋如下:
h(226)的邏輯下标是5,沖突了,是以5+12=6,檢視6的位置為空,OK,找到位置了;
h(903)的邏輯下标是6,沖突了,是以6+12=7,7沖突,繼續,6+22=10,10的位置為空,找到位置了;
h(388)的邏輯下标是11,沖突了,是以11+12=12,也沖突,11+22=15,15%13=2,2沖突,繼續,11+32= 20,20%13=7,7沖突,繼續,11+42=27,27%13=1, 1的位置為空,找到位置了;