我不知道的hash表

前言

爲何要寫散列表(Hash Table)?!由於忘記,就算之前在大學的時候學得多好,分數考多高。成了前端狗以後一方面是本身對本身不夠嚴格,另外一方面工做方面也是用得少這方面的知識,平時大多都是一羣朋友吹牛皮的時候,亮出來嚇嚇人(也只能說本身記得的部分)。當夜深人靜時都會想一想本身吹過的牛皮~感嘆這些知識本身真的忘得差很少了,三分鐘熱度來寫個文章溫故知新!前端

正文

什麼是散列表

散列表(Hash Table),用一個映射函數將數據的特徵值與數據保存表的存儲位置創建聯繫,以便在尋找數據的時候能夠快速定位,這樣的表成爲散列表(Hash Table),上述映射函數成爲hash函數。java

根據設定的哈希函數和衝突處理方法將一組關鍵字映射到一個有限連續的地址集上,並以關鍵字映射獲得的值做爲關鍵字對應數據在表中的存儲位置,這種稱爲哈希表。數組

一個通俗的例子是,爲了查找電話簿中某人的號碼,能夠建立一個按照人名首字母順序排列的表(即創建人名 x 到首字母 F(x) 的一個函數關係),在首字母爲W的表中查找「王」姓的電話號碼,顯然比直接查找就要快得多。這裏使用人名做爲關鍵字,「取首字母」是這個例子中散列函數的函數法則 F(),存放首字母的表對應散列表。關鍵字和函數法則理論上能夠任意肯定。(摘自散列表維基百科bash

爲何須要散列表

通常來講hash表的數據會用一個一維數組存儲。而在低級語言中使用數組是須要預先申請數組的長度,便是內存大小。而在實際應用中,很難肯定最後應該申請多大的內存,過大浪費資源,太小則會形成內存溢出。在溢出前你就須要擴容,擴容的時候又涉及到內存拷貝的問題,由於數組是一組連續的地址集,你難保在這段地址集後面的地址沒有保存另外的數據。另外,單純用數組存儲數據,存儲的時候很爽,可是在尋找的時候就一身騷了~畢竟在數組必定規模的時候你遍歷一次或許就要作成千上萬次的對比數據,這仍是單純的一維數組而已,若是是個矩陣呢?複雜度是否是就蹭蹭往上漲!函數

如何構建散列表

哈希函數

如上文所述,哈希函數就是用來肯定數據特徵值與數據在哈希表存儲位置的映射關係。ui

對不一樣的關鍵字可能獲得同一散列地址,即k1≠k2,而f(k1)=f(k2),這種現象稱爲衝突。具備相同函數值的關鍵字對該散列函數來講稱作同義詞。this

經常使用的幾個用於構建哈希函數的方法以下:spa

  • 直接定址法:使用一個線性函數做爲映射函數;
  • 除留餘數法:經過模 一個小於或等於散列表長度的值,取其他數做爲散列表的存儲位置;
  • 數字分析法:肉眼觀測,剔除顯而易見的噪點;
  • 平方中值法:將數據特徵(通常是天然數)平方後取中間幾位數,取幾位取決於表的大小;
  • 摺疊法:將數據特徵值分隔成爲數相同的幾部分,而後取疊加和(捨去高位);
  • 僞隨機數法;

以上每一個方法不一一闡述,我只挑出「除留餘數法」淺析。一是它經常使用;二是我只想說它。指針

除留餘數法的數學表達式:code

 

hash( key ) = key mod p ( p ≤ m )
key: 數據特徵值; m: 散列表的長度;

 

下面用一例子對上述數學表達式進行實踐(注意,一下例子只是對除留餘數法的實踐,並未徹底達到理想的映射關係,僅僅是基本的關係):

假設現有一組數據

const names = ['Issac', 'Fanck', 'Sonia', 'Gary', 'Rick', 'Ryron', 'Emma'];
複製代碼

取以上人名的首字母的 ASCII碼 做爲數據的特徵值。

const nameKeys = names.map((name) => {
	const initial = name.slice(0, 1);
	const key = initial.charCodeAt();
	return key;
});

// print: [73, 70, 83, 71, 82, 82, 69]
複製代碼

而後,取 p = 7對p的選擇很重要,通常取素數(質數)或m,若p選擇很差,容易產生衝突(哈希衝突)。

const p = 7;
const indexs = nameKeys.map((nameKey) => {
    const index = nameKey % p;
    return index;
});

// print: [3, 0, 6, 1, 5, 5, 6]
複製代碼

根據 namesindexs 的值:

['Issac', 'Fanck', 'Sonia', 'Gary', 'Rick', 'Ryron', 'Emma']
[      3,       0,       6,      1,      5,       5,      6]
複製代碼

能夠獲得一個基本的關係表:

0 1 2 3 4 5 6
Fanck Gary - Issac - Rick Sonia
- - - - - Ryron Emma

由表中的數據你能夠看見三、五、6這三個位置都有一個以上的name映射到了同一個存儲位置上,顯然一個位置是不能存放多個數據,除非使用別的儲存方式。該當如何下節分解!

哈希衝突(碰撞)

前一小節中出現「多個name映射到了同一個存儲位置」的狀況就是典型的哈希衝突。 出現哈希衝突,一是更換其餘的哈希函數,二是對哈希函數的結果進行處理。要知道在數據足夠大後不管怎麼更換哈希函數都沒法避免哈希衝突,只能儘可能減少出現哈希衝突的概率。

經常使用方法有如下幾種:

  • 開放定址法;
  • 單獨鏈表法;
  • 再散列;
  • 雙散列;

如下淺析前兩種方法,緣由同上。

 

開放定址法

開放定址法的數學表達式:

 

Hash = (hash(key) + d i) mod m, i=1,2,3...k (k <= m-1)
m爲散列表長,di爲增量序列(函數),i爲已發生衝突的次數

 

增量序列的構建方法有如下幾種:

  • 線性探測:d(i) = i;
  • 平方探測:d(i) = i^2;
  • 僞隨機探測;

下面用「線性探測」編寫例子。

 

回顧哈希函數獲得的映射結果:

['Issac', 'Fanck', 'Sonia', 'Gary', 'Rick', 'Ryron', 'Emma']
[      3,       0,       6,      1,      5,       5,      6]
複製代碼

取哈希表的長度m = p = 7

映射 Ryron 時出現衝突

# i = 1, d(1) = 1,hash(key) = 5
(5 + 1) mod 7 = 6   # 衝突

# i = 2, d(2) = 2,hash(key) = 5
(5 + 2) mod 7 = 0   # 衝突

// ...

# i = 4, d(4) = 4,hash(key) = 5
(5 + 4) mod 7 = 2   # 命中
複製代碼
0 1 2 3 4 5 6
Fanck Gary Ryron Issac - Rick Sonia

映射 Emma 時繼續出現衝突

# i = 1, d(1) = 1,hash(key) = 6
(6 + 1) mod 7 = 0   # 衝突

# ...

# i = 5, d(5) = 5,hash(key) = 6
(6 + 5) mod 7 = 4   # 命中
複製代碼
0 1 2 3 4 5 6
Fanck Gary Ryron Issac Emma Rick Sonia

到此,衝突處理完畢,散列表構建完成!咱們能夠試着在散列表中查找數據。

假設如今要查找 IssacRyron:

# Issac
# key('I') = 73, Hash(key) = 3
hashTable(3)  # output 'Issac' => Hit


# Ryron
# key('R') = 82, Hash(key) = 5
hashTable(3)  # output 'Rick' => Miss

# d(1) = 1, Hash(key) = 6
hashTable(6)  # output 'Sonia' => Miss

# ...

# d(4) = 4, Hash(key) = 2
hashTable(2)  # output 'Ryron' => Hit
複製代碼

 

單獨鏈表法

使用單鏈表做爲存儲數據的方式,言外之意,hash(key)獲得的在哈希表的存儲位置再也不是數據的存儲位置,而是用於存儲數據所用鏈表的地址。

單鏈表(LinkList)形如:

class LinkList() {
    constructor(_data, _next) {
        this.head = new Entry();
    }
    
    insert(_data) {
        // ...
    }
    
    find(_data) {
        // ...
    }
    
    // ...
}
複製代碼

鏈表的每一個節點形如:

class Entry {
    constructor(_data, _next) {
        this.data = _data;
        this.next = _next;
    }
}
複製代碼

回顧哈希函數獲得的映射結果:

['Issac', 'Fanck', 'Sonia', 'Gary', 'Rick', 'Ryron', 'Emma']
[      3,       0,       6,      1,      5,       5,      6]
複製代碼
let key;
let index;
const hashTable = new Array(7);

// insert 'Issac'
key = getKey('Issac');    // 73
index = hash(key);    // 3, 命中
hashTable(index) = new LinkList();
hashTable(index).insert('Issac');

// insert 'Fanck'
key = getKey('Fanck');    // 70
index = hash(key);    // 0, 命中
hashTable(index) = new LinkList();
hashTable(index).insert('Fanck');

// insert 'Sonia'
key = getKey('Sonia');    // 83
index = hash(key);    // 6, 命中
hashTable(index) = new LinkList();
hashTable(index).insert('Sonia');

// insert 'Gary'
key = getKey('Gary');    // 71
index = hash(key);    // 1, 命中
hashTable(index) = new LinkList();
hashTable(index).insert('Gary');

// insert 'Rick'
key = getKey('Rick');    // 82
index = hash(key);    // 5, 命中
hashTable(index) = new LinkList();
hashTable(index).insert('Rick');

// insert 'Ryron'
key = getKey('Ryron');    // 82
index = hash(key);    // 5, 衝突
hashTable(index).insert('Ryron');

// insert 'Emma'
key = getKey('Emma');    // 69
index = hash(key);    // 6, 衝突
hashTable(index).insert('Emma');
複製代碼

平均查找長度

爲何要求取「平均查找長度」?從上文中你能夠發現,因爲哈希衝突的出現,用數據特徵並不能直接獲取數據的儲存位置,是須要進行一次以上的對比才能找到目標數據,也可能數據並不存在於哈希表中,即查找不成功。所以,就須要一個量度去衡量查找效率,即平均查找長度。

平均查找長度分爲下面兩類:

  • 成功的平均查找長度:
ASL = (d 0 + d 1 + d 2 + ... + d i) / n, (i = 0, 1, 2 ... n)
ASL: 平均查找長度, n: 哈希表的元素個數
di: 第 i 個元素查找成功所須要的查找(比對)次數
  • 不成功的平均查找長度:
ASL = (d 0 + d 1 + d 2 + ... + d i) / n, (i = 0, 1, 2 ... l)
l: 哈希表的長度, n: 哈希表的元素個數, ASL: 平均查找長度
di: 在第 i 個存儲位置開始查找直到肯定不存在所須要的查找(比對)次數

下面分別計算前一小節中哈希表的成功和不成功的平均查找長度。

# 成功的平均查找長度
Frank:1
Gary: 1
Issac: 1
Rick: 1
Ryron: 2
Sonia: 1
Emma: 2

ASL = (1 + 1 + 1 + 1 + 2 + 1 + 2) / 7 ≈ 1.2857
複製代碼

對於不成功的平均查找長度數學代數式中di有必要再細說一下。 好比如今要再上表中查找Fiona,這是不存在中的。

hash('F')  # output 0
複製代碼

而後,就是在 hashTable(0) 存儲的單鏈表中查找Fiona

# 第一次查找, 第一個節點的data存的是Frank,不匹配!
# 而且第一個的指針域爲空指針(沒有指向下一個節點),這樣就成功肯定Fiona不在表中,即查找失敗!
結果是hash(key)等於0的查找失敗長度爲1
複製代碼

由上面的例子就能夠知道:

# 不成功的平均查找長度
hash(key) => 0: 1
hash(key) => 1: 1
hash(key) => 2: 0
hash(key) => 3: 1
hash(key) => 4: 0
hash(key) => 5: 2
hash(key) => 6: 2

ASL = (1 + 1 + 0 + 1 + 0 + 2 + 2) / 7 = 1
複製代碼

 

載荷(裝填)因子

載荷因子指的是哈希表的裝滿程度,具體計算式是:

 

α = 表中元素個數 / 哈希表長度

 

從公式中你能夠直觀地感覺到,α 越大下次存入新元素的時候發生衝突的概率越大,由於表中空缺的位置已經很小了! 發生衝突的次數越多,也就是意味着查找長度就可能越大!想一想哈希表的存在乎義:爲了方便定位目標查找元素!查找長度足夠大時哈希表就不在方便了,也就失去存在乎義。而α就能夠直觀地指明當前哈希表已經不夠方便了,那麼這個時候就須要對哈希表進行擴容!沒錯,它就是標示什麼時候應該擴容,好比java中的hashMap則是α > 0.75 時後對當前哈希表進行擴容!

最後寫兩句

上文其實已經很早就寫好,剩下本身定的兩個問題原本想淺析一下,可是因爲自身緣由一拖再拖,也只能先在此留個坑,但願本身有生之年能夠回來填坑(滑稽)

  • 如何判斷一個哈希表的好壞?
  • 爲何除留餘數發中p通常取質數?

但願本身一如既往能夠對未知的知識保有好奇心~

相關文章
相關標籤/搜索