前端數據結構–散列表(哈希表)

散列表的由來

前面說了數組、鏈表,他們各自有自己的特點:

  1. 數組:具有隨機訪問的特點,可以快速的根據下標訪問到數據,缺點是插入、刪除需要移動數據
  2. 鏈表:插入、刪除只需要改變結點之間的引用,缺點是查找數據需要從根結點遍歷訪問

 散列表是組合了數組和鏈表的優勢,規避它們的不足而產生新的一種數據結構。散列表是一種常用的數據存儲技術,散列後的數據可以快速地插入或取用。

什麼是散列表

  散列表英文叫 Hash table,也叫哈希表,是根據關鍵碼值(Key value)而直接進行訪問的數據結構。也就是說,它通過把關鍵碼值映射到表中一個位置來訪問記錄,以加快查找速度。這個映射函數叫做散列函數,存放記錄的數組叫散列表 如下圖所示

上面的定義可能不那麼清晰,可以嘗試這樣理解, 散列表就是通過散列函數(也叫哈希函數)將元素的鍵映射為數組下標(轉化後的值叫做散列值或哈希值),然後在對應下標的位置存儲記錄值、或者查找記錄值,這種數據結構稱為散列表。

  如圖散列表用的是數組支持下標隨機訪問特性,所以散列表其實就是數組的一種擴展,由數組演化而來。可以說,如果沒有數組,就沒有散列表。我們通過散列函數把元素的鍵值映射為下標,然後將數據存儲在數組中對應下標的位置。當我們按照鍵值查詢元素時,我們用同樣的散列函數,將鍵值轉化數組下標,從對應的數組下標的位置取數據。

散列函數

  從上面可以看出散列函數在散列表中起着非常核心的作用,散列函數,顧名思義,它是一個函數。我們可以把它定義成 hash(key),其中 key 表示鍵值,hash(key) 的值表示經過散列函數計算得到的散列值,即數組的下標。

基本特點

  1. 散列函數計算得到的散列值是一個非負整數
  2. 如果 key1 = key2,那 hash(key1) == hash(key2)
  3. 如果 key1 ≠ key2,那 hash(key1) ≠ hash(key2)

第一點:因為數組下標是從 0 開始的,所以散列函數生成的散列值也要是非負整數。

第二點:相同的 key,經過散列函數得到的散列值也應該是相同的。

第三點:理論上key和散列值是一一對應的,但是種現實是很有可能一個key對應了多個散列值的情況,這就會存在衝突的情況,這取決於散列函數的設計。

設計散列函數

  散列函數設計的好壞,決定了散列表衝突的概率大小,也直接決定了散列表的性能。一個好的散列函數基本滿足兩個原則

1、計算hash值簡單

  過於複雜的散列函數,會消耗很多計算時間,也就間接地影響到散列表的性能,因此散列涵的計算要簡單、快速。

2、散列函數計算出來的地址要分佈均勻

  散列函數生成的值要儘可能隨機並且均勻分佈,這樣才能避免或者最小化散列衝突,即便出現衝突,散列到每個槽里的數據也會比較平均,這樣可以保證存儲空間的合理使用。

實際工作中,我們還需要綜合考慮各種因素。這些因素有關鍵字的長度、特點、分佈、還有散列表的大小等。

常用設計散列函數基本思路:

1、直接地址法

1 hash(key) = a * key + b  // a、b為常數

這種方法計算最簡單,不會產生衝突,適合關鍵字的分佈比較連續,而且長度較小的情況,如果關鍵字不連續,空位就會比較多,就會造成存儲空間的浪費。

假如我們有 20 名選手參加學校運動會。為了方便記錄成績,每個選手胸前都會貼上自己的參賽號碼。這 20 名選手的號碼依次是 1 到 20。現在希望實現這樣一個功能,通過號碼快速找到對應的選手信息。

我們可以把號碼為 1 的選手,我們放到數組中下標為 1 的位置;號碼為 2 的選手,我們放到數組中下標為 2 的位置。以此類推,號碼為 k 的選手放到數組中下標為 k 的位置。即我們的哈希函數只要返回對應的key 即可;

1 function hash (key) {
2    return key
3 }

2、數字分析法

  在上面號碼太簡單了,如果把1-20的號碼增加了年級、班級,如1 變成了202103001, 2 變成了202103002,那麼此時我們上面那個哈希函數就不適用了。儘管我們不能直接把號碼作為數組下標,我們可以用號碼的後兩位做為數組的下標,即我們的哈希函數可以改為
1 function hash (key) {
2    return String(key).substring(6)
3 }

3、平方取中法

4、摺疊法

5、除留餘數法

  除留餘數法是使用的比較多的一種,公式為:

1 hash(key) = key % p  

如果散列表的表長為m,p為小於等於m的最大的質數,在一般情況下,對質數取余會讓衝突更少,數據元素在散列表分佈的更均勻。

質數又稱素數,除了1和自身,不能被其他自然數整除的數 如(2,3,5,7,11,13,17,…)

如有數據 { 10,15,20,25,30,35,40,45,50 },表長為10,那麼我們對 7 取余如下,其中 ^ 表示為空的鏈表:

6、隨機數法

選擇一個隨機函數,用關鍵字作為隨機函數的種子,返回值作為散列地址,即

hash(key) = radmom(key) 

可結合除留餘數

總結散列函數基本設計原則

散列函數設計沒有固定的方法,需要結合實際情況考慮如下因素:

  1. 要清楚關鍵字分佈的情況、範圍、規律,結合上面常用幾種方法,寫出散列函數
  2. 散列表的大小要合理,太大浪費空間太小則容易產生衝突
  3. 散列表的數據分佈要均勻,不要一些下標中有很多元素,其他的沒有或者很少
  4. 散列函數代碼要精簡,追求的是簡單高效、分佈均勻

散列衝突

  再好的散列函數也無法避免散列衝突,因為散列值是非負整數,總量是有限的,但是現實世界中要處理的鍵值是無限的,將無限的數據映射到有限的集合,肯定避免不了衝突。即便像業界著名的MD5、SHA、CRC等哈希算法,也無法完全避免這種散列衝突。而且,因為數組的存儲空間有限,也會加大散列衝突的概率。

  我們常用的散列衝突解決方法有兩類,開放尋址法(open addressing)和鏈表法(chaining)。下面簡單介紹下鏈表法

鏈表法

  鏈表法是一種更加常用的散列衝突解決辦法,在散列表中,每個下標會對應一條鏈表,所有散列值相同的元素我們都放到相同槽位對應的鏈表中。

每一個數組下標對應的鏈表可以是單鏈表也可以是雙鏈表。

當插入的時候,我們只需要通過散列函數計算出對應的下標,將其插入到對應鏈表中即可,所以插入的時間複雜度是 O(1)。

當查找、刪除一個元素時,我們同樣通過散列函數計算出對應下標,然後遍歷鏈表查找或者刪除。

前端哈希數據結構

  javascript 中的ObjectSetWeakSetMapWeakMap 都是哈希結構。