從原理了解Hash

Hash

它是什麼?

哈希表是又稱散列表,一種以 “key-value” 形式存儲數據的數據結構。所謂以 “key-value” 形式存儲數據,是指任意的 key 都唯一對應到內存中的某個位置。只需要輸入查找的值 key,就可以快速地找到其對應的 value。可以把哈希表理解爲一種高級的數組,這種數組的下標可以是很大的整數,浮點數,字符串甚至結構體。

爲什麼存在?

有時關鍵碼空間的數量級可能遠遠大於實際問題的空間,造成了巨大的浪費,我們使用桶(bucket)直接存放或間接指向一個詞條。


優缺點?

優點:
>>>空間利用率 :

問題空間N,關鍵碼空間R,桶數組(bucket array)或散列表(hash table),容量爲M,則:

N < M << R

空間 = O( N + M )=O(N)

M儘可能與N同階,所以至少與使用關鍵碼空間相比空間利用率大大的提高了。

>>>常數級的查找時間 :

因爲哈希表遵循的是循值訪問,所以查找時間只需要O(1),這是再好不過的了。

缺點:
>>>衝突:

hash(key) = key % M
衝突是無法避免的,但是我們可以儘量減少衝突,並從以下兩個方向入手:

  1. 精心設計散列表及散列函數,以儘可能降低衝突的概率;
  2. 制定可行的預案,以便在發生衝突時,能夠儘快予以排解。

怎麼用?

>>>循值訪問
>>>散列函數的設計:
  • 除餘法 :hash(key) = key % M

M應當選取素數。步長爲step,gcd(step,M)=G,當且僅當 G == 1時,足跡能夠遍佈整個散列表。又由於step不能確定,所以M應是一個素數。

  • MAD法 (multiply-add-divide) :hash(key) = ( a * key + b ) % M

除餘法有兩個缺陷。

一:它有不動點。無論表長M取值如何,總有hash(0) ≡ 0

二:零階均勻。[ 0 ,R)的關鍵碼,平均分配至M個桶;但相鄰的關鍵碼的散列地址也比相鄰。

取M爲素數,a > 0,b > 0,a % M != 0。hash(key) = ( a * key + b ) % M

  • 平方取中 (mid-square) :取 key^2的中間若干位,構成地址

原理:將平方運算分解爲一系列的左移操作,以及若干次加法,思想類似於快速冪,如13^2=13 + (13)<<2 + (13)<<3。如果忽略進位,每個數位都是由原關鍵碼若干次求和得到的,因此兩側的數位是由更少的原數位累積而得,而越是居中的數位是由更多的原數位累積,截取居中的若干位,可以使得原關鍵碼各數位對最終地址的影響彼此更爲接近。

  • 多項式法 :
    hash(s=x0x1x2xn1)=x0an1+x1an2++xn2a1+xn1 hash( s = x_0,x_1,x_2,···,x_{n-1} ) = x_0*a^{n-1} + x_1*a^{n-2} + ··· + x_{n-2}*a^1 + x_{n-1}
    Karp-Rabin算法:串即是數的思想吻合。

  • 更多散列函數 :數字分析法(selecting digits),摺疊法(folding),位異或法(XOR),僞隨機數法(#!慎用此法)法等

>>>排解衝突 :
  • 獨立鏈(linker-list chaining / separate chaining)

每個桶存放一個指針,衝突的詞條,組成列表。封閉定址(closed addressing)策略,每個桶只能存放與這個桶單元的地址相沖突的詞條。

優點 :

1. 無需爲每個桶預留多個槽位

2. 任意多次的衝突都可解決

3. 刪除操作實現簡單、統一

但是 :

1.指針需要額外空間

2. 節點需要動態申請(時間成本要比常規操作高出兩個數量級)

3. 更重要的是系統緩存幾乎失效!每個桶內部的查找都是沿着對應的列表順序進行的,在此之前各節點的插入和刪除順序是隨機的,對於任何一個列表而言,其中的節點在物理空間上往往不是連續分佈的,無法利用有效的緩存加速查找,當散列表的長度非常大,要使用到IO時,這種問題會更加明顯。

  • 開放定址(open addressing ~ closed hashing) :

散列表所佔用的空間在物理上始終是與地址一致的,無需申請額外的空間。每一個詞條都可以存放在任何一個桶中。爲每個桶事先約定若干備用桶,它們構成一個查找鏈(probing sequence/chain)。

沿查找鏈查找,逐個轉向下一桶單元,直到

命中成功,或者抵達一個空桶失敗

線性探索(Linear probing) 一旦衝突,則試探後一緊鄰桶單元;

[ hash(key) + 1 ] % M
[ hash(key) + 2 ] % M
[ hash(key) + 3 ] % M
...

優點:

1. 無需附加的空間

2. 查找鏈具有局部性,可充分利用系統緩存,有效減少I/O

但是:

1. 操作時間大於O(1)

2. 衝突增多,以往的衝突,會導致後續的的衝突clustering。

懶惰刪除: 使用時需要特別注意刪除,如果直接刪除,後續詞條將丟失—明明存在卻訪問不到。這時需要進行懶惰刪除,對需要刪除的某一詞條進行標記,查找操作遇到標記轉向下一個繼續查找,插入操作遇到標記則直接將詞條插入在此處。

  • 平方試探(Quadratic probing) :

open addressing和closed addressing都屬於線性試探,而線性試探有一個問題就是試探位置間距太小,大部分的試探的位置都集中於某一個相對很小的局部。因此不妨適當的拉開各次試探的間距,平方試探就是這一思路的具體體現。

以平方數爲距離,確定下一試探桶單元

[ hash(key) + 1^2 ] % M
[ hash(key) + 2^2 ] % M
[ hash(key) + 3^2 ] % M
[ hash(key) + 4^2 ] % M

優點 :

數據聚集現象有所緩解,在查找鏈上,各桶間距線性遞增,一旦衝突,可聰明地跳離。


缺點 :

1. 若設計外存,I/O將激增。平方試探策略將在一定程度上破壞數據訪問的局部性,甚至系統緩存的功能會失效,不過通常情況下這個問題還不算很嚴重。不失一般性,取系統緩存頁面的大小爲1KB,如果桶單元只記錄相應的引用,大致需要4字節,每一個緩存頁面都可以容納至少256個桶單元,1KB4B=256=162\frac{1 KB}{4B}=256=16^2,也就是說要做一次額外的I/O對換,必須連續的發生16次衝突,機率其實是非常小的。

2. 可能會出現空桶。

如 { 0, 1, 2, 3, 4, 5, … }^2 % 12 = {0, 1, 4, 9}

只會涉及到其中的4個單元。沒有辦法找到剩下2/3的空桶。這裏M選的12是一個合數,借組數論的知識不難證明,只要表長M是合數,這種情況必然發生,因爲 n^2 % M 可能的取值必然少於[M/2]([]爲向上取整,()爲向下取整,之後不再說明)種。

將表長變爲素數,如 { 0, 1, 2, 3, 4, 5, … }^2 % 11 = {0, 1, 4, 9, 5, 3}。

M若爲素數: n^2 % M 可能的取值恰好會有 [M/2]種—此前,恰由查找鏈的前[M/2],因爲一般的素數M都是奇數,所以這個比例剛剛超過50%,這是情況可能到的最壞程度。

關於這一點的正面結論是:若M是素數,但裝填因子λ\lambda< 0.5,就一定能找出;否則,不一定。

反證法證明這個結論:

假設存在 0 <= a < b < [M/2] ,使得沿着查找鏈,第a項和第b項彼此衝突。
a2  b2M\therefore a^2 \; 和b^2 自然屬於M的某一同餘類,
a2  b2(modM)a^2 \; ≡ b^2 \quad (mod M)
b2    a2  =  (b+a)(ba)0(modM)b^2\;- \;a^2 \; = \; (b+a)*(b-a) ≡ 0 \quad (mod M)
然而
0  <  ba  <  b+a  <  M 0\;<\;b\,-\,a\;< \;b\,+\,a\; < \;M

1<b+a<M1< b+a < M
得到(b+a)竟然是M是一個非平凡因子,這與M是素數矛盾!

  • 雙向平方試探 :

一旦發生衝突,則交替的向前向後以遞增的平方數爲間隔逐一試探。

[ hash(key) + 1^2 ] % M
[ hash(key) - 1^2 ] % M
[ hash(key) + 2^2 ] % M
[ hash(key) - 2^2 ] % M
[ hash(key) + 3^2 ] % M
[ hash(key) - 3^2 ] % M
...

正向和逆向的子查找鏈,各包含[m/2]個互異的桶,但是有些素數會讓這兩個序列存在除0以外公共的桶。

結論:若表長取做素數 M = 4 * K +3,必然可以保證查找鏈的前M項均互異。

反證法證明:

M=4*K+3


設正向試探序列的第a步與逆向試探序列的第b步衝突,

而且應當是1<= b , a <= (M/2) 

衝突即 -b^2 和 a^2 是一個同餘類

-b^2 ≡ a ^2 (mod M)

設a^2 + b^2 =n
0 ≡ a^2 + b^2 = n (mod M)

所以M是n的一個素因子,

根據費馬雙平方定理的推論,

n不僅能被M整除,也能被M^2整除

所以M ^2 <= a^2 + b^2 

但是與b , a <= (M/2)矛盾,即不可能成立。
費馬雙平方定理---任一素數p可表示爲一對整數的平方和,當且僅當p%4=1

費馬雙平方定理的推論---任一自然數n可表示爲一對正數的平方和,當且僅當在其素分解中,
形如 M = 4*K+3的每一素因子均爲偶數次方。

extend

>>>什麼樣的哈希函數纔是更好的?

  1. 確定determinism : 同一關鍵碼總是被映射至同一單元。
  2. 快速efficiency :expected-O(1)
  3. 滿射surjection : 儘可能充分地覆蓋整個散列空間
  4. 均勻uniformity : 關鍵碼映射到散列表各位置的概率儘量接近(可有效避免聚集clostering現象)

>>> 關於哈希的題

哈希表的查找只需要O(1),我覺得強大的不只是哈希表,更主要是hash帶給我們的這種思想,比如說Karp-Rabin算法,而且我覺得狀壓也是有這種思想的,這些都是一種把信息壓縮的思想,充分利用信息。附上兩道這兩天的leetcode的打卡題,我覺得還是很不錯的。

leetcode974

leetcode287

內容是對鄧俊輝鄧老師學堂在線上的數據結構(下)(2020春)第九章詞典的總結。(鄧老師的課真的太好了)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章