數據結構 - 哈希

原文連接: blog.wangriyu.wang/2018/06-Has…html

索引

在 MySQL 中,主要有四種類型的索引,分別爲:B-Tree 索引,Hash 索引,Fulltext 索引和 R-Tree 索引。前一節已經講了 B 類樹的結構特色,此次講哈希索引,至於後面的全文索引R 樹索引感興趣本身看吧。python

以前講 B 樹時提到過哈希索引能夠支持動態長度,並且因爲 Hash 索引結構的特殊性,其檢索效率很是高,最好的狀況能夠一次定位,查詢效率遠大於 B 樹,可是實際運用中主要仍是用 B 樹作索引,由於哈希索引有如下侷限性:算法

(1)Hash 索引適合定值查詢,不能作範圍查詢數據庫

因爲 Hash 索引比較的是進行 Hash 運算以後的 Hash 值,因此它只能用於等值的過濾,不能用於基於範圍的過濾,由於通過相應的 Hash 算法處理以後的 Hash 值的大小關係,並不能保證和 Hash 運算前徹底同樣。數組

(2)Hash 索引沒法用於排序操做緩存

因爲 Hash 索引中存放的是通過 Hash 計算以後的 Hash 值,並且 Hash 值的大小關係並不必定和 Hash 運算前的鍵值徹底同樣,因此數據庫沒法利用索引的數據來進行任何排序運算;安全

(3)Hash 索引不能利用部分索引鍵查詢服務器

好比 (a,b,c) 形式的組合索引,查詢中只用到了 a 和 b 也是能夠查詢的,若是使用 hash 表,組合索引會將幾個字段合併 hash,沒辦法支持部分索引網絡

(4)Hash 索引遇到大量 Hash 值相等的狀況後性能並不必定就會比 B-Tree 索引高數據結構

數據庫支持非惟一的 Hash 索引,若是遇到非惟一值,存儲引擎會將他們連接到同一個 hash 鍵值下以一個鏈表的形式存在,而後在取得實際鍵值的時候再過濾不符合的鍵

Hash 索引在 MySQL 中使用的並非不少,目前主要是 Memory 和 NDB 引擎會使用;而經常使用的 Innodb 存儲引擎和 MyISAM 引擎使用的索引仍是 B+ 樹爲主

哈希表

哈希表 (Hash Table),也叫散列表,是根據鍵(Key)轉換而直接訪問在存儲地址的數據結構。

定義

好比咱們有關鍵字 k,則其值存放在 f(k) 的存儲位置上,所以不須要比較就能夠找到記錄。這個映射關係 f(x) 稱爲哈希函數(散列函數),以此創建的表就是哈希表

哈希表中存着的是包含存儲單元的數組,這些存儲單元咱們稱做槽 (slot) 或者桶 (bucket) ,每一個存儲單元能夠容納一個或多個關鍵字信息。理想狀況下,完美的散列函數能爲關鍵字找到惟一的獨佔的桶,但大多數狀況下用到的散列函數都是不完美的,會存在衝突;

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

若對於關鍵字集合中的任一個關鍵字,經散列函數映象到地址集合中任何一個地址的機率是相等的,則稱此類散列函數爲均勻散列函數(Uniform Hash function),這就是使關鍵字通過散列函數獲得一個「隨機的地址」,從而減小衝突

原理

image

輸入一個關鍵字 "lies",通過散列獲得值 9,對應哈希表獲得咱們要的地址 20

從關鍵字到索引值的轉換過程使用的就是散列函數,散列函數有不少種,好比一種簡單的散列方式以下:

image

取關鍵字每一個字符的 ASCII 碼,相加,即 lies -> 108+105+101+115 -> 429

可是咱們的表只有 30 項,那麼能夠再把上面的值模 30: 429 mod 30 = 9

一個散列就完成了

可是繼續輸入發現:

image

關鍵字 "foes" 散列後也對應表下標 9,這就是衝突,"lies" 和 "foes" 是同義詞

每種哈希表實現都要有處理衝突的方法,處理衝突也有不少方式,好比這裏能夠用簡單的鏈表來處理:

image

以前哈希表中索引對應的就是具體的地址,而如今改爲一個指向鏈表的指針,全部同義詞都存在相應鏈表中

可是咱們怎麼直到鏈表哪一個是 "lies" 哪一個是 "foes" 呢,能夠在鏈表每一個節點中添加關鍵字信息:

image

這樣衝突就算處理了,可是這種哈希表結構的最壞狀況是 O(n),不一樣於以前 O(1)。由於假如大部分關鍵字都是衝突的,那麼這些關鍵字就變爲鏈表查詢了。

所以一個均勻的散列函數和一個高效的處理衝突的方法對哈希表來講相當重要

下面會介紹一些常見的哈希函數和處理衝突的方法

哈希函數

  1. 直接定址法:取關鍵字或關鍵字的某個線性函數值爲散列地址。即 hash(k) = k 或 hash(k) = a * k + b,其中 a, b 爲常數(這種散列函數叫作自身函數)
  2. 數字分析法:假設關鍵字是以 r 爲基的數,而且哈希表中可能出現的關鍵字都是事先知道的,則可取關鍵字的若干數位組成哈希地址
  3. 平方取中法:取關鍵字平方後的中間幾位爲哈希地址。一般在選定哈希函數時不必定能知道關鍵字的所有狀況,取其中的哪幾位也不必定合適,而一個數平方後的中間幾位數和數的每一位都相關,由此使隨機分佈的關鍵字獲得的哈希地址也是隨機的。取的位數由表長決定。好比 {421,423,436},平方以後的結果爲{177241,178929,190096},那麼能夠取中間的兩位數{72,89,00}做爲 Hash 地址
  4. 摺疊法:將關鍵字分割成位數相同的幾部分(最後一部分的位數能夠不一樣),而後取這幾部分的疊加和(捨去進位)做爲哈希地址。好比圖書的 ISBN 號爲 8903-241-23,能夠將 address(key)=89+03+24+12+3 做爲 Hash 地址。
  5. 隨機數法
  6. 除留餘數法:取關鍵字被某個不大於散列表表長 m 的數 p 除後所得的餘數爲散列地址。即 hash(k) = k mod p, p <= m。不只能夠對關鍵字直接取模,也可在摺疊法、平方取中法等運算以後取模。對 p 的選擇很重要,通常取素數或 m,若 p 選擇很差,也容易產生衝突

衝突

影響因素

影響產生衝突多少有如下三個因素:

  • 散列函數是否均勻
  • 處理衝突的方法
  • 散列表的負載因子

散列表的負載因子定義爲:α = 填入表中的元素個數 / 散列表的長度。α 是散列表裝滿程度的標誌因子。 因爲表長是定值,α 與「填入表中的元素個數」成正比,因此 α 越大,代表填入表中的元素越多,產生衝突的可能性就越大。

處理方法

一、開放尋址 (open addressing/closed hashing): 一旦發生了衝突,經過探測尋找其餘空的位置,而後插入。

hash_i = (hash(k) + d_i) mod m, i = 1, 2, 3...m-1, 其中 hash(k) 爲散列函數,d_i 是增量序列,m 爲哈希表長,i 爲已發生衝突的次數。

hash(k) 是初始的探測位置,以後的探測位置 hash _1, hash_2, hash_3...hash _{m-1} 造成一個探測序列。

若是 d_i = i = 1, 2, 3...m-1,則爲線性探測,至關於逐一往下找表空閒位置

若是 d_i = \pm1^2, \pm2^2, \pm3^2...\pm k^2,則爲二次探測(或者叫平方探測),至關於探測間隔爲 \pm i^2

若是 d_i = i * rehash(k),rehash 是另外一個哈希函數,則爲雙散列探測

若是 d_i = 僞隨機數序列,則爲僞隨機探測

對開放尋址散列表性能的關鍵影響是載荷因子。隨着載荷係數增長到 100%,可能須要查找或插入給定關鍵字的探針數量急劇增長。一旦表格變滿,探測算法甚至可能沒法終止。即便具備良好的散列函數,也應嚴格限制載荷因子在 0.7-0.8 如下,好比 Java 的系統庫限制了荷載因子爲 0.75,超過此值將動態調整散列表大小。

二、單獨鏈表法。即上述原理用到的方法,也叫拉鍊法,鏈表的使用方式能夠分不少種,能夠見 WikiPedia 的事例,另外算法導論裏提到一種徹底散列的方式,至關於二維哈希表,每一個槽對應的是另外一個哈希列表,也值得借鑑

三、再散列: hash_i = rehash_i(k), i = 1, 2, 3...k, 在發生衝突時,使用其餘散列函數計算哈希值,直到衝突再也不發生。這種方法不易產生「彙集」,但增長了計算時間。

四、創建一個公共溢出區

動態哈希

前面的問題都是以哈希表定長爲基礎的,可是當關鍵字較多時,哈希表出現彙集時,性能會急劇降低。

即散列表的載荷因子較大時須要考慮擴充哈希表大小,若是是靜態 hash,則須要新建一張更大的表,而後將全部關鍵字從新散列到新的表中,以後刪除舊錶

而動態散列能夠在哈希表元素增加的同時,動態的調整 hash 桶的數目,動態 hash 不須要對原有元素進行從新插入(重組),而是在原基礎上,進行動態的桶擴展。

有如下三種方法能夠實現動態 hash:

  • 多 hash 表
  • 可擴展的動態散列
  • 線性散列

多 hash 表

多 hash 表就是採用多張哈希表來擴充原哈希表。

hash table

如圖,哈希函數爲 hash(k) = k % 5,每一個桶最多含 4 個關鍵字

當咱們要插入關鍵字 5 時,hash 值爲 0,應該插入 Bucket 1 中,並且 Bucket 1 有空閒位置,直接插入便可;

當咱們要插入關鍵字 3 時,hash 值爲 3,應該插入 Bucket 3 中,可是 Bucket 3 已經滿了,此時新建一張 hash 表來完成插入

hash table

對於此種方式,執行插入、查找、刪除操做時,均需先求得 hash 值 x。

  • 插入時,獲得當前的 hash 表的個數,並分別取得各個 hash 表的 x 位置上的索引項,若其中某個項指向的桶存在空閒位置,則插入之。同時,在插入時,可保持多個 hash 表在某個索引項上桶中元素的個數近似相等。若不存在空閒位置,則簡單的進行表擴充,即新建一個 hash 表,如上所示
  • 查找時,因爲某個記錄值可能存在當前 hash 結構的多個表中,所以需同時在多個 hash 表的同一位置上進行查找操做,等待全部的查找結束後,方可斷定該元素是否存在。因爲該種結構需進行屢次查找,當表元素很是多時,爲提升效率,在多處理器上可採用多線程,併發執行查找操做
  • 刪除操做,與上述過程基本相似。須要注意的是,若刪除操做致使某個 hash 表元素爲空,這時可將該表從結構中剔除

這種方式的優勢是設計和實現比較簡單的。缺點是佔用空間大,並且關鍵字較密集時空間利用率低。

可擴展的動態散列

引入一個僅存儲桶指針的索引數組,用翻倍的索引項數來取代翻倍的桶的數目,且每次只分裂有溢出的桶,從而減少翻倍的代價。

Extendible hashing

如圖所示哈希表有一個標識表示全局位深度 (Global Depth),全局位深度表示取哈希值的低幾位做爲索引,好比 hash(k4) = 0100,全局位深度爲 2,則取低兩位 00 歸屬到 00 索引的桶中,並且哈希表索引項數始終等於 2 ^ Global Depth;桶中有一個本地位深度 (Local Depth),本地位深度表示當前桶中元素的低幾位是同樣的;圖中給出的桶中元素表示哈希後的值,好比桶 1 中 4 表示哈希值爲 0100 的關鍵字 k4

  • 插入時,計算出哈希值再根據全局位深度匹配索引項,若是找到的桶未滿,則直接插入便可;若是桶已滿,則判斷當前桶的本地位深度 L 與全局位深度 G 的大小關係: 若是 L = G,此時只有一個指針指向當前桶,則擴展索引,本地位深度和全局位深度均加一,索引項翻倍,重組當前桶的元素;若是 L < G,此時不止一個指針指向當前桶,故不須要翻倍索引項,只需分裂出一個桶,將本地位深度加一而後重組當前桶元素便可
  • 查找時,對於須要查找的關鍵字 x,hash(x) = y,根據當前 hash 表的全局位深度,決定對 y 取其後 G 位,位數不夠用 0 填充,找到對應的索引項,從而找到對應的桶,在桶中逐一進行比較
  • 刪除時,和查找操做相似,先定位元素,刪除之。若刪除時發現桶爲空,則能夠考慮將該桶與其兄弟桶進行合併,並使局部位深度減一

插入實例:

當咱們插入某個關鍵字 k13,hash(k13) = 1101,對應索引 01 的桶 Bucket 2,桶未滿直接插入便可;

再插入某個關鍵字 k20,hash(k20) = 10100,對應索引 00 的桶 Bucket 1,桶已滿並且本地位深度等於全局位深度,須要擴展索引,全局位深度和本地位深度均加一,並重組當前桶的元素,變爲下圖:

Extendible hashing

繼續插入某個關鍵字 k25,hash(k25) = 11001,對應索引 001 的桶 Bucket 2,桶已滿並且本地位深度小於全局位深度,當前桶存在兩個指針,只須要分裂出一個桶便可並將桶的本地位深度加一,如圖:

Extendible hashing

這種方式的優勢是能夠可動態進行桶的增加,且增加的同時,用索引項的翻倍代替桶數翻倍的傳統作法,可用性更好。缺點是當散列的數據分佈不均或偏斜較大時,會使得索引項的數目很大,數據桶的利用率很低;還有索引的增加速度,是指數級增加,擴展較快

線性散列

線性散列能隨數據的插入和刪除,適當的對 hash 桶數進行調整,一次只分裂一個桶,線性散列不須要存放數據桶指針的專門索引項。

定義:

標識符 描述
h_0,h_1,h_2... 一系列哈希函數,後者的範圍老是前者的兩倍,i 爲下標,h_i(key) = hash(key) mod (N*2^i)
N 桶的初始個數,必須是 2 的冪次方
d_i 多少比特位用於表示 N,N = 2^{d_i}
Level 當前輪數,每輪的初始桶數等於 N*2^{Level}
Next 一個指針指向須要分裂的桶,每次發生分裂的桶老是由 Next 決定,與當前值被插入的桶已滿或溢出無關
Load Factor 負載因子,當桶中記錄數達到該值時進行分裂;也可選擇當桶滿時才進行分裂

當某個桶發生溢出時,能夠將溢出元素以鏈表的形式鏈在桶後; 能夠監控整張哈希表或者桶的負載因子,視狀況選擇是否分裂,好比全表的負載達到 0.75 能夠對當前桶進行分裂,也能夠選擇只有桶滿了才分裂;

實例:

初始狀態 N = 4, Level = 0, Next 指向第一個桶

Linear-hashing

插入某個關鍵字 k37,hash(k37) = 37 = 100101,h_0(k37) = 01(取低兩位),對應編號 1 的桶,此時桶未滿直接插入便可:

Linear-hashing

插入某個關鍵字 k43,hash(k43) = 43 = 101011,h_0(k43) = 11(取低兩位),對應編號 3 的桶,此時桶已滿須要分裂: 首先把 43 鏈在桶 3 後面,而後分裂 Next 指向的桶 0,產生一個新桶,新桶編號 = N + Next = 4 + 0 = 100,Next 指向下一個桶;最後把桶 0 的元素按 h1 散列重組

Linear-hashing

繼續插入某個關鍵字 k29,hash(k29) = 29 = 11101,h_0(k29) = 01,對應編號 1 的桶,此時桶已滿須要分裂: 也是按剛纔的步驟處理,不過此次 Next 指向的桶就是當前桶

Linear-hashing

向桶 1 中連續插入 17,33,41,引發分裂:

Linear-hashing

繼續向桶 1 插入 57,引發分裂,須要注意的是這時 Next 指針已經遍歷過初始的四個桶,第一輪已結束,Level 加一,Next 指向第一個桶,第二輪初始爲八個桶(即 N = 8):

Linear-hashing

插入就是如上形式進行,接下來看怎麼查找:

假如要查找某個關鍵字 key 對應的位置,若是 Next \leq h _{Level}(key) \leq N-1,則查詢 h _{Level} 列的與低 d_i 位對應的桶,不然查詢 h _{Level+1} 列的與低 d_i +1 位對應的桶。

Linear-hashing

在此圖中 N = 4,Level = 0,Next = 2,d_0 = 2

好比查詢 k44,h_0(k44) = 00,00(= 0) 不在 2 和 3 之間,則查詢 h1 列與 k44 低三位 (100) 對應的桶,找到編號爲 4 的桶,再從桶找到對應位置;

假如查詢的是 k7,h_0(k7) = 11,11(= 3) 在 2 和 3 之間,則查詢 h0 列與 k7 低兩位 (11) 對應的桶,找到編號爲 3 的桶,再從桶中找到對應位置

Linear-hashing

在此圖中 N = 8,Level = 1,Next = 0,d_1 = 3

假如查詢 k44,h_1(k44) = 100,100(= 4) 在 0 和 7 之間,因此查詢 h1 列與 100 對應的桶,即編號爲 4 的桶;

其餘關鍵字的 h1 值都是在範圍 [Next, N] 內的,因此都是找 h1 列的

線性散列的刪除操做是插入操做的逆操做,若溢出塊爲空,則可釋放。若刪除致使某個桶元素變空,則 Next 指向上一個桶。當 Next 減小到 0,且最後一個桶也是空時,則 Next 指向 N/2 - 1 的位置,同時 Level 值減 1。

線性散列比可擴展動態散列更靈活,且不須要存放數據桶指針的專門索引項,節省了空間;但若是數據散列後分布不均勻,致使的問題可能會比可擴展散列還嚴重

分佈式哈希

分佈式哈希表 - DHT增長或移除節點只改變鄰近節點所擁有的關鍵值集合,而其餘節點的則不會被改變。傳統的散列表,若增長或移除一個位置,則整個關鍵值空間必須從新散列。

其中一致性哈希算法很是適用於分佈式哈希表,一致性哈希容許任意順序插入或刪除存儲桶,不一樣於線性散列那樣須要順序處理。

一致性哈希

應用場景

有 N 臺服務器提供緩存服務,須要對服務器進行負載均衡,將請求平均分發到每臺服務器上,每臺機器負責 1/N 的服務。

經常使用的算法是對 hash 結果模 N 取餘,好比 N = 5,hash = 103,餘數爲 3 則分發到編號爲 3 的服務器上。可是這樣的算法存在嚴重問題,若是有某臺機器宕機或者新添加服務器,都會形成大量緩存請求失效或者須要轉移重組數據。

而使用一致性哈希後,一樣增長或者刪除服務器節點只會對少許數據產生影響,不須要大規模遷移數據。好比分佈式緩存系統節點數多,並且容易變更,很是適合使用一致性哈希

原理

假設一致性哈希算法的結果是 32 位的 Key 值,將這 2^32 個數值映射在一個環形空間上,則對應有 2^32 個緩存區。好比下圖中五個 Key 值分佈在環形空間上

Consistent-hashing

每個緩存節點(Shard)也遵循一樣的 Hash 算法,好比利用 IP 作 Hash,映射到環形空間當中

Consistent-hashing

而後按順時針將 Key 歸屬到最近的節點上,好比 K五、K1 歸屬爲節點 N1 上

Consistent-hashing

增長節點 N4 時,咱們只須要改變 K5 的歸屬便可,不會對其餘節點和數據形成影響

Consistent-hashing

刪除節點 N1 時,咱們只須要將 K1 歸屬到 N2 上就行

Consistent-hashing

可是仍會有分佈不均的狀況,特別是服務器節點較少時,好比下圖大部分鍵值都流向某個單一節點

Consistent-hashing

爲了應對這種狀況,引入了虛擬節點的概念,將原來的一個物理節點映射出多個子節點,讓子節點代替物理節點放置在環形空間中

Consistent-hashing

好比本來的物理節點 N1 使用的關鍵字是本身的 IP 192.168.1.109,其位置是 hash("192.168.1.109")。如今爲其建立兩個虛擬節點 N1_1 和 N1_2,兩個子節點可使用 IP + 編號 的方式計算哈希,好比 hash("192.168.1.109#1") 和 hash("192.168.1.109#2"),同理爲 N2 也建立了兩個虛擬節點,

建立虛擬節點的好處是分佈更均勻

其餘常見散列函數

MD5

Message-Digest Algorithm 5 - 消息摘要算法,一種被普遍使用的密碼散列函數,能夠產生出一個 128 位的散列值

能夠應用於文件校驗,例如,服務器預先提供一個 MD5 校驗和,用戶下載完文件之後,用 MD5 算法計算下載文件的 MD5 校驗和,而後經過檢查這兩個校驗和是否一致,就能判斷下載的文件是否出錯

MD5 是輸入不定長度信息,輸出固定長度 128 位的算法。通過程序流程,生成四個 32 位數據,最後聯合起來成爲一個 128 位散列。基本方式爲,求餘、取餘、調整長度、與連接變量進行循環運算,得出結果。

可是 MD5 能夠被破解,不適合高度安全性的場景,好比不能用於密鑰認證和數字簽名

SHA

Secure Hash Algorithm - 安全散列算法是一個密碼散列函數家族,也能計算出一個數字消息所對應到的,長度固定的字符串(又稱消息摘要)的算法。安全性高於 MD5,好比數字簽名經常使用的就是 SHA-256。

SHA 分爲 SHA-0、SHA-一、SHA-二、SHA-3 四個大版本,其中 SHA-0 和 SHA-1 輸出的散列值爲 160 位;SHA-2 細分爲多種,好比 SHA-256 輸出的是 256 位散列值,另外還有 SHA-22四、SHA-38四、SHA-512 等等;SHA-3 是 2015 年正式發佈的,SHA-3 並非要取代 SHA-2,由於 SHA-2 目前並無出現明顯的弱點,因爲對 MD5 出現成功的破解,以及對 SHA-0 和 SHA-1 出現理論上破解的方法,NIST 感受須要一個與以前算法不一樣的,可替換的加密散列算法,也就是如今的 SHA-3

CRC

Cyclic redundancy check - 循環冗餘校驗是一種根據網絡數據包或計算機文件等數據產生簡短固定位數校驗碼的一種散列函數,主要用來檢測或校驗數據傳輸或者保存後可能出現的錯誤。通常來講,循環冗餘校驗的值都是 32 位的整數。

CRC 的計算過程網絡課程便講過,不算複雜,這裏不深刻。WikiPedia 上提供了一些 CRC 變體的描述,感興趣能夠了解一下。

儘管在錯誤檢測中很是有用,CRC 並不能可靠地校驗數據完整性,由於 CRC 多項式是線性結構,能夠很是容易地故意改變量據而維持 CRC 不變。通常使用Message authentication code校驗數據完整性

Reference

相關文章
相關標籤/搜索