1. 程式人生 > >資料結構和演算法之——散列表上

資料結構和演算法之——散列表上

散列表的英文叫 “Hash Table”,我們也叫它 “雜湊表” 或者 “Hash 表”

1. 雜湊思想?

散列表用的是陣列支援按照下標隨機訪問資料的特性,所以散列表其實就是陣列的一種擴充套件,由陣列演化而來。

假如我們有 100 名選手參加運動會,參賽號碼從 0~99。為了方便記錄查詢成績,我們將參賽號碼為 0 的選手的成績放在陣列下標為 0 的位置,參賽號碼為 1 的選手的成績放在陣列下標為 1 的位置,以此類推。

這樣,當我們想要查詢某個選手的成績時,我們只需要取出陣列中該選手參賽號碼對應下標的數值即可,時間複雜度為 O(1)O(1),效率非常高。

在這個例子中,參賽號碼是自然數,並且與陣列的下標形成一一對映,這其實就有了雜湊的思想。

但事實上,有時候我們不能直接將編號作為陣列下標,比如參賽選手的編號可能為 051167,05 表示年級,11 表示班級,67 表示序號。

這時候,我們可以通過擷取參賽編號的後兩位作為下標,當查詢選手資訊的時候,我們用同樣的方法,取出後兩位數字,作為陣列下標來讀取資料。

這就是典型的雜湊思想。其中,參賽選手的編號我們叫作鍵(key)或關鍵字,我們用它來標識一個選手。而把參賽編號轉化為陣列下標的對映方法就叫作雜湊函式(或 “Hash 函式”,“雜湊函式”),而雜湊函式計算得到的值就叫作雜湊值(或 “Hash 值”,“雜湊值”)

散列表其實就是通過雜湊函式把元素的鍵值對映為下標,然後將資料儲存在陣列中對應下標的位置。當我們按照鍵值查詢元素的時候,我們用同樣的雜湊函式,將鍵值轉化為陣列下標,從對應下標位置的陣列中取資料。

2. 雜湊函式?

雜湊函式在散列表中起著非常關鍵的作用。

上面兩個例子中的雜湊函式都比較簡單,也很容易理解。但如果參賽選手的編號是隨機生成的 6 位數字,又或者是字元時,我們該如何構造雜湊函式呢?

雜湊函式有以下三個基本要求:

  • 雜湊函式計算得到的雜湊值是一個非負整數
  • 如果 key1=key2hash(key1)=hash(key2)key1 = key2,那麼 hash(key1) = hash(key2)
  • 如果 key1̸=key2hash(key1)̸=hash(key2)key1 \not= key2,那麼 hash(key1) \not= hash(key2)

第一點和第二點都非常好理解,第三點要求看起來合情合理,但在真實情況下,要想找到一個不同 key 值對應的雜湊值都不一樣的雜湊函式,幾乎是不可能的。而且,因為陣列的儲存空間有限,也會加大雜湊衝突的概率。因此,我們需要通過其他途徑來解決雜湊衝突問題。

3. 雜湊衝突?

再好的雜湊函式也無法避免雜湊衝突,常用的解決蛋類衝突解決方法有兩類,開放定址法(open addressing)連結串列法(chaining)

3.1. 開放定址法

開放定址發的核心思想就是,如果出現了雜湊衝突,我們就重新探測一個空閒位置,將其插入。

線性探測(Linear Probing) 就是當我們往散列表中插入資料時,如果計算得到的雜湊值對應的位置已經被佔用了,我們就從當前位置開始,依次往後查詢,看是否有空閒位置,直到找到為止。

看下面的例子,橙色表示已經有元素,黃色表示空閒。當計算新插入的 x 的雜湊值為 7 時,我們發現數組中下標為 7 的地方已經有資料了,於是我們就依次向後查詢,遍歷到尾部都沒有找到空閒位置。我們再從頭開始查詢,直到找到陣列第 2 個位置空閒,我們就將 x 插入到這個地方。

在散列表中查詢元素的過程與插入類似,我們通過雜湊函式求出要查詢元素的鍵值對應的雜湊值,然後比較陣列中下標為雜湊值的元素和要查詢的元素。如果相等,那說明就是我們要查詢的元素;否則就順序往後依次查詢,若遍歷到陣列中的空閒位置還沒有找到,說明要查詢的元素並沒有在散列表中。

散列表跟陣列一樣,不僅支援插入、查詢操作,還支援刪除操作。對於使用線性探測解決衝突的散列表,刪除操作稍微有點特別,我們不能單純地把要刪除的元素設定為空

因為在查詢的過程中,一旦我們遍歷到陣列中的空閒位置,我們就認定資料不在散列表中。但如果這個空閒位置是我們後來刪除的,就會導致我們的查詢演算法失效,本來存在的資料也會被認定為不存在。

我們可以將刪除的元素特殊標記為 deleted,然後當我們查詢到標記為 deleted 的位置時,我們不是停下來,而是繼續往下探測。

線性探測存在很大的問題,當散列表中插入的資料越來越多時,雜湊衝突的可能性就會越來越大,空閒位置越來越少,線性探測的時間也會越來越久

除了線性探測,還有另外兩種比較經典的探測方法,二次探測(Quadratic Probing)雙重探測(Double Probing)

所謂二次探測,就是說每次探測的步長變成了原來的二次方,也就是說,它探測的下標序列變為 hash(key)+0,hash(key)+12,hash(key)+22hash(key) + 0, hash(key) + 1^2, hash(key) + 2^2 ……

所謂雙重探測,就是說每次不僅僅使用一個雜湊函式,當第一個雜湊函式計算得到的儲存位置被佔用的時候,再使用第二個雜湊函式,以此類推,直到找到空閒的位置。

不管採用哪種探測方法,當散列表中的空閒位置不多時,雜湊衝突的概率就會大大提高。我們引入一個**裝載因子(load factor)**來表示散列表中空位的多少 散列表的裝載因子 = 填入表中的元素個數 / 散列表的長度。裝載因子越大,說明空閒位置越少,衝突越多,散列表的效能會下降。

3.2. 連結串列法

連結串列法是一種更加常用的散列表衝突解決方法,相比開放定址法,它要簡單很多。

在散列表中,每個桶(bucket)或者槽(slot)會對應一條連結串列,所有雜湊值相同的元素會放到相同槽位對應的連結串列中

向散列表中插入資料的時間複雜度為 O(1)O(1),而查詢或者刪除的時間複雜度則與連結串列的長度 k 成正比。

4. Word 文件中單詞拼寫檢查功能是如何實現的?

常見的英文單詞有 20 萬個左右,我們可以將這些常見單詞建立起一個散列表。當用戶輸入某個英語單詞時,我們拿使用者輸入的單詞去散列表中查詢,如果查到則說明拼寫正確,如果沒有查到,則說明拼寫可能有誤給予提示。

獲取更多精彩,請關注「seniusen」!