資料結構和演算法之——散列表上
散列表的英文叫 “Hash Table”,我們也叫它 “雜湊表” 或者 “Hash 表”。
1. 雜湊思想?
散列表用的是陣列支援按照下標隨機訪問資料的特性,所以散列表其實就是陣列的一種擴充套件,由陣列演化而來。
假如我們有 100 名選手參加運動會,參賽號碼從 0~99。為了方便記錄查詢成績,我們將參賽號碼為 0 的選手的成績放在陣列下標為 0 的位置,參賽號碼為 1 的選手的成績放在陣列下標為 1 的位置,以此類推。
這樣,當我們想要查詢某個選手的成績時,我們只需要取出陣列中該選手參賽號碼對應下標的數值即可,時間複雜度為 ,效率非常高。
在這個例子中,參賽號碼是自然數,並且與陣列的下標形成一一對映,這其實就有了雜湊的思想。
但事實上,有時候我們不能直接將編號作為陣列下標,比如參賽選手的編號可能為 051167,05 表示年級,11 表示班級,67 表示序號。
這時候,我們可以通過擷取參賽編號的後兩位作為下標,當查詢選手資訊的時候,我們用同樣的方法,取出後兩位數字,作為陣列下標來讀取資料。
這就是典型的雜湊思想。其中,參賽選手的編號我們叫作鍵(key)或關鍵字,我們用它來標識一個選手。而把參賽編號轉化為陣列下標的對映方法就叫作雜湊函式(或 “Hash 函式”,“雜湊函式”),而雜湊函式計算得到的值就叫作雜湊值(或 “Hash 值”,“雜湊值”)。
散列表其實就是通過雜湊函式把元素的鍵值對映為下標,然後將資料儲存在陣列中對應下標的位置。當我們按照鍵值查詢元素的時候,我們用同樣的雜湊函式,將鍵值轉化為陣列下標,從對應下標位置的陣列中取資料。
2. 雜湊函式?
雜湊函式在散列表中起著非常關鍵的作用。
上面兩個例子中的雜湊函式都比較簡單,也很容易理解。但如果參賽選手的編號是隨機生成的 6 位數字,又或者是字元時,我們該如何構造雜湊函式呢?
雜湊函式有以下三個基本要求:
- 雜湊函式計算得到的雜湊值是一個非負整數
- 如果
- 如果
第一點和第二點都非常好理解,第三點要求看起來合情合理,但在真實情況下,要想找到一個不同 key 值對應的雜湊值都不一樣的雜湊函式,幾乎是不可能的。而且,因為陣列的儲存空間有限,也會加大雜湊衝突的概率。因此,我們需要通過其他途徑來解決雜湊衝突問題。
3. 雜湊衝突?
再好的雜湊函式也無法避免雜湊衝突,常用的解決蛋類衝突解決方法有兩類,開放定址法(open addressing)和連結串列法(chaining)。
3.1. 開放定址法
開放定址發的核心思想就是,如果出現了雜湊衝突,我們就重新探測一個空閒位置,將其插入。
線性探測(Linear Probing) 就是當我們往散列表中插入資料時,如果計算得到的雜湊值對應的位置已經被佔用了,我們就從當前位置開始,依次往後查詢,看是否有空閒位置,直到找到為止。
看下面的例子,橙色表示已經有元素,黃色表示空閒。當計算新插入的 x 的雜湊值為 7 時,我們發現數組中下標為 7 的地方已經有資料了,於是我們就依次向後查詢,遍歷到尾部都沒有找到空閒位置。我們再從頭開始查詢,直到找到陣列第 2 個位置空閒,我們就將 x 插入到這個地方。
在散列表中查詢元素的過程與插入類似,我們通過雜湊函式求出要查詢元素的鍵值對應的雜湊值,然後比較陣列中下標為雜湊值的元素和要查詢的元素。如果相等,那說明就是我們要查詢的元素;否則就順序往後依次查詢,若遍歷到陣列中的空閒位置還沒有找到,說明要查詢的元素並沒有在散列表中。
散列表跟陣列一樣,不僅支援插入、查詢操作,還支援刪除操作。對於使用線性探測解決衝突的散列表,刪除操作稍微有點特別,我們不能單純地把要刪除的元素設定為空。
因為在查詢的過程中,一旦我們遍歷到陣列中的空閒位置,我們就認定資料不在散列表中。但如果這個空閒位置是我們後來刪除的,就會導致我們的查詢演算法失效,本來存在的資料也會被認定為不存在。
我們可以將刪除的元素特殊標記為 deleted,然後當我們查詢到標記為 deleted 的位置時,我們不是停下來,而是繼續往下探測。
線性探測存在很大的問題,當散列表中插入的資料越來越多時,雜湊衝突的可能性就會越來越大,空閒位置越來越少,線性探測的時間也會越來越久。
除了線性探測,還有另外兩種比較經典的探測方法,二次探測(Quadratic Probing)和雙重探測(Double Probing)。
所謂二次探測,就是說每次探測的步長變成了原來的二次方,也就是說,它探測的下標序列變為 。
所謂雙重探測,就是說每次不僅僅使用一個雜湊函式,當第一個雜湊函式計算得到的儲存位置被佔用的時候,再使用第二個雜湊函式,以此類推,直到找到空閒的位置。
不管採用哪種探測方法,當散列表中的空閒位置不多時,雜湊衝突的概率就會大大提高。我們引入一個**裝載因子(load factor)**來表示散列表中空位的多少 散列表的裝載因子 = 填入表中的元素個數 / 散列表的長度
。裝載因子越大,說明空閒位置越少,衝突越多,散列表的效能會下降。
3.2. 連結串列法
連結串列法是一種更加常用的散列表衝突解決方法,相比開放定址法,它要簡單很多。
在散列表中,每個桶(bucket)或者槽(slot)會對應一條連結串列,所有雜湊值相同的元素會放到相同槽位對應的連結串列中。
向散列表中插入資料的時間複雜度為 ,而查詢或者刪除的時間複雜度則與連結串列的長度 k 成正比。
4. Word 文件中單詞拼寫檢查功能是如何實現的?
常見的英文單詞有 20 萬個左右,我們可以將這些常見單詞建立起一個散列表。當用戶輸入某個英語單詞時,我們拿使用者輸入的單詞去散列表中查詢,如果查到則說明拼寫正確,如果沒有查到,則說明拼寫可能有誤給予提示。
獲取更多精彩,請關注「seniusen」!