雜湊表⽤的是陣列⽀持按照下標隨機訪問資料的特性,所以雜湊表其實就是陣列的⼀種擴充套件,由陣列演化⽽來。可以說,如果沒有陣列,就沒有雜湊表。
雜湊表⽤的就是陣列⽀持按照下標隨機訪問的時候,時間複雜度是O(1)的特性。我們通過雜湊函式把元素的鍵值對映為下標,然後將資料儲存在陣列中對應下標的位置。當我們按照鍵值查詢元素時,我們⽤同樣的雜湊函式,將鍵值轉化陣列下標,從對應的陣列下標的位置取資料。
雜湊函式在雜湊表中起著⾮常關鍵的作⽤。
雜湊函式,顧名思義,它是⼀個函式。我們可以把它定義成hash(key),其中key表示元素的鍵值,hash(key)的值表示經過雜湊函式計算得到的雜湊值。
三點雜湊函式設計的基本要求:
1. 雜湊函式計算得到的雜湊值是⼀個⾮負整數;
2. 如果key1 = key2,那hash(key1) == hash(key2);
3. 如果key1 ≠ key2,那hash(key1) ≠ hash(key2)。
其中,第⼀點理解起來應該沒有任何問題。因為陣列下標是從0開始的,所以雜湊函式⽣成的雜湊值也要是⾮負整數。第⼆點也很好理解。相同的key,經過雜湊函式得到的雜湊值也應該是相同的。
第三點理解起來可能會有問題。這個要求看起來合情合理,但是在真實的情況下,要想找到⼀個不同的key對應的雜湊值都不⼀樣的雜湊函式,⼏乎是不可能的。即便像業界著名的MD5、SHA、CRC等雜湊演算法,也⽆法完全避免這種雜湊衝突。⽽且,因為陣列的儲存空間有限,也會加⼤雜湊衝突的概率。
雜湊衝突
再好的雜湊函式也⽆法避免雜湊衝突。那究竟該如何解決雜湊衝突問題呢?我們常⽤的雜湊衝突解決⽅法有兩類,開放定址法(open addressing)和連結串列法(chaining)。
開放定址法
開放定址法的核⼼思想是,如果出現了雜湊衝突,我們就重新探測⼀個空閒位置,將其插⼊。那如何重新探測新的位置呢?我先講⼀個⽐較簡單的探測⽅法,線性探測(Linear Probing)。
當我們往雜湊表中插⼊資料時,如果某個資料經過雜湊函式雜湊之後,儲存位置已經被佔⽤了,我們就從當前位置開始,依次往後查詢,看是否有空閒位置,直到找到為⽌。
從圖中可以看出,雜湊表的⼤⼩為10,在元素x插⼊雜湊表之前,已經6個元素插⼊到雜湊表中。x經過Hash演算法之後,被雜湊到位置下標為7的位置,但是這個位置已經有資料了,所以就產⽣了衝突。於是我們就順序地往後⼀個⼀個找,看有沒有空閒的位置,遍歷到尾部都沒有找到空閒的位置,於是我們再從表頭開始找,直到找到空閒位置2,於是將其插⼊到這個位置。
在雜湊表中查詢元素的過程有點⼉類似插⼊過程。我們通過雜湊函式求出要查詢元素的鍵值對應的雜湊值,然後⽐較陣列中下標為雜湊值的元素和要查詢的元素。如果相等,則說明就是我們要找的元素;否則就順序往後依次查詢。如果遍歷到陣列中的空閒位置,還沒有找到,就說明要查詢的元素並沒有在雜湊表中。
雜湊表跟陣列⼀樣,不僅⽀持插⼊、查詢操作,還⽀持刪除操作。對於使⽤線性探測法解決衝突的雜湊表,刪除操作稍微有些特別。我們不能單純地把要刪除的元素設定為空。這是為什麼呢?
在查詢的時候,⼀旦我們通過線性探測⽅法,找到⼀個空閒位置,我們就可以認定雜湊表中不存在這個資料。但是,如果這個空閒位置是我們後來刪除的,就會導致原來的查詢演算法失效。本來存在的資料,會被認定為不存在。這個問題如何解決呢?
我們可以將刪除的元素,特殊標記為deleted。當線性探測查詢的時候,遇到標記為deleted的空間,並不是停下來,⽽是繼續往下探測。
你可能已經發現了,線性探測法其實存在很⼤問題。當雜湊表中插⼊的資料越來越多時,雜湊衝突發⽣的可能性就會越來越⼤,空閒位置會越來越少,線性探測的時間就會越來越久。極端情況下,我們可能需要探測整個雜湊表,所以最壞情況下的時間複雜度為O(n)。同理,在刪除和查詢時,也有可能會線性探測整張雜湊表,才能找到要查詢或者刪除的資料。
對於開放定址衝突解決⽅法,除了線性探測⽅法之外,還有另外兩種⽐較經典的探測⽅法,⼆次探測(Quadratic probing)和雙重雜湊(Double hashing)。
所謂⼆次探測,跟線性探測很像,線性探測每次探測的步⻓是1,那它探測的下標序列就是hash(key)+0,hash(key)+1,hash(key)+2……⽽⼆次探測探測的步⻓就變成了原來的“⼆次⽅”,也就是說,它探測的下標序列就是hash(key)+0,hash(key)+12,hash(key)+22……
所謂雙重雜湊,意思就是不僅要使⽤⼀個雜湊函式。我們使⽤⼀組雜湊函式hash1(key),hash2(key),hash3(key)……我們先⽤第⼀個雜湊函式,如果計算得到的儲存位置已經被佔⽤,再⽤第⼆個雜湊函式,依次類推,直到找到空閒的儲存位置。
連結串列法
連結串列法是⼀種更加常⽤的雜湊衝突解決辦法,相⽐開放定址法,它要簡單很多。我們來看這個圖,在雜湊表中,每個“桶(bucket)”或者“槽(slot)”會對應⼀條連結串列,所有雜湊值相同的元素我們都放到相同槽位對應的連結串列中。
當插⼊的時候,我們只需要通過雜湊函式計算出對應的雜湊槽位,將其插⼊到對應連結串列中即可,所以插⼊的時間複雜度是O(1)。當查詢、刪除⼀個元素時,我們同樣通過雜湊函式計算出對應的槽,然後遍歷連結串列查詢或者刪除。那查詢或刪除操作的時間複雜度是多少呢?
實際上,這兩個操作的時間複雜度跟連結串列的⻓度k成正⽐,也就是O(k)。對於雜湊⽐較均勻的雜湊函式來說,理論上講,k=n/m,其中n表示雜湊中資料的個數,m表示雜湊表中“槽”的個數。
本作品採用《CC 協議》,轉載必須註明作者和本文連結