萬字長文,必須看懂的哈希表總結

以前給你們介紹了鏈表棧和隊列今天咱們來講一種新的數據結構散列(哈希)表,散列是應用很是普遍的數據結構,在咱們的刷題過程當中,散列表的出場率特別高。因此咱們快來一塊兒把散列表的內些事給整明白吧。文章框架以下程序員

腦圖

說散列表以前,咱們先設想如下場景。算法

袁廚穿越回了古代,憑藉從現代學習的作飯手藝,開了一個袁記菜館,正值開業初期,店裏生意十分火爆,可是顧客結帳時就犯難了,每當結帳的時候,老闆娘老是按照菜單一個一個找價格(遍歷查找),每次都要找半天,因此結帳的地方老是排起長隊,顧客們表示用戶體驗不咋滴。袁廚一想這不是辦法啊,讓顧客總是等着,太影響客戶體驗啦。因此袁廚就先把菜單按照首字母排序(二分查找),而後查找的時候根據首字母查找,這樣結帳的時候就能大大提升檢索效率啦!可是呢?工做日顧客很少,老闆娘徹底應付的過來,可是每逢節假日,仍是會排起長隊。那麼有沒有什麼更好的辦法呢?對呀!咱們把全部的價格都背下來不就能夠了嗎?每一個菜的價格咱們都瞭如指掌,結帳的時候咱們只需簡單相加便可。因此袁廚和老闆娘加班加點的進行背誦。下次再結帳的時候一說吃了什麼菜,咱們立馬就知道價格啦。自此之後收銀臺再也沒有出現過長隊啦,袁記菜館開着開着一不當心就成了天下第一飯店了。數組

下面咱們來看一下袁記菜館老闆娘進化史。微信

image-20201117132633797

上面的後期結帳的過程則模擬了咱們的散列表查找,那麼在計算機中是如何使用進行查找的呢?數據結構

散列表查找步驟

散列表-------最有用的基本數據結構之一。是根據關鍵碼的值兒直接進行訪問的數據結構,散列表的實現經常叫作散列(hasing)。散列是一種用於以常數平均時間執行插入、刪除和查找的技術,下面咱們來看一下散列過程。框架

咱們的整個散列過程主要分爲兩步dom

(1)經過散列函數計算記錄的散列地址,並按此散列地址存儲該記錄。就比如麻辣魚咱們就讓它在川菜區,糖醋魚,咱們就讓它在魯菜區。可是咱們須要注意的是,不管什麼記錄咱們都須要用同一個散列函數計算地址,再存儲。函數

(2)當咱們查找時,咱們經過一樣的散列函數計算記錄的散列地址,按此散列地址訪問該記錄。由於咱們存和取得時候用的都是一個散列函數,所以結果確定相同。性能

剛纔咱們在散列過程當中提到了散列函數,那麼散列函數是什麼呢?學習

咱們假設某個函數爲 f,使得

存儲位置 = f (關鍵字)

輸入:關鍵字 輸出:存儲位置(散列地址)

那樣咱們就能經過查找關鍵字不須要比較就可得到須要的記錄的存儲位置。這種存儲技術被稱爲散列技術。散列技術是在經過記錄的存儲位置和它的關鍵字之間創建一個肯定的對應關係 f ,使得每一個關鍵字 key 都對應一個存儲位置 f(key)。見下圖

image-20201117145348616

這裏的 f 就是咱們所說的散列函數(哈希)函數。咱們利用散列技術將記錄存儲在一塊連續的存儲空間中,這塊連續存儲空間就是咱們本文的主人公------散列(哈希)表

上圖爲咱們描述了用散列函數將關鍵字映射到散列表,可是你們有沒有考慮到這種狀況,那就是將關鍵字映射到同一個槽中的狀況,即 f(k4) = f(k3) 時。這種狀況咱們將其稱之爲衝突k3k4則被稱之爲散列函數 f同義詞,若是產生這種狀況,則會讓咱們查找錯誤。幸運的是咱們能找到有效的方法解決衝突。

首先咱們能夠對哈希函數下手,咱們能夠精心設計哈希函數,讓其儘量少的產生衝突,因此咱們建立哈希函數時應遵循如下規則

(1)必須是一致的,假設你輸入辣子雞丁時獲得的是在看,那麼每次輸入辣子雞丁時,獲得的也必須爲在看。若是不是這樣,散列表將毫無用處。

(2)計算簡單,假設咱們設計了一個算法,能夠保證全部關鍵字都不會衝突,可是這個算法計算複雜,會耗費不少時間,這樣的話就大大下降了查找效率,反而得不償失。因此我們散列函數的計算時間不該該超過其餘查找技術與關鍵字的比較時間,否則的話咱們幹嗎不使用其餘查找技術呢?

(3)散列地址分佈均勻咱們剛纔說了衝突的帶來的問題,因此咱們最好的辦法就是讓散列地址儘可能均勻分佈在存儲空間中,這樣即保證空間的有效利用,又減小了處理衝突而消耗的時間。

如今咱們已經對散列表,散列函數等知識有所瞭解啦,那麼咱們來看幾種經常使用的散列函數構造規則。這些方法的共同點爲都是將原來的數字按某種規律變成了另外一個數字。

散列函數構造方法

直接定址法

若是咱們對盈利爲0-9的菜品設計哈希表,咱們則直接能夠根據做爲地址,則 f(key) = key;

即下面這種狀況。

直接定址法

有沒有感受上面的圖很熟悉,沒錯咱們常常用的數組其實就是一張哈希表,關鍵碼就是數組的索引下標,而後咱們經過下標直接訪問數組中的元素。

另外咱們假設每道菜的成本爲50塊,那咱們還能夠根據盈利+成原本做爲地址,那麼則 f(key) = key + 50。也就是說咱們能夠根據線性函數值做爲散列地址。

f(key) = a * key + b a,b均爲常數

優勢:簡單、均勻、無衝突。

應用場景:須要事先知道關鍵字的分佈狀況,適合查找表較小且連續的狀況

數字分析法

該方法也是十分簡單的方法,就是分析咱們的關鍵字,取其中一段,或對其位移,疊加,用做地址。好比咱們的學號,前 6 位都是同樣的,可是後面 3 位都不相同,咱們則能夠用學號做爲鍵,後面的 3 位作爲咱們的散列地址。若是咱們這樣仍是容易產生衝突,則能夠對抽取數字再進行處理。咱們的目的只有一個,提供一個散列函數將關鍵字合理的分配到散列表的各位置。這裏咱們提到了一種新的方式,抽取,這也是在散列函數中常常用到的手段。

image-20201117161754010

優勢:簡單、均勻、適用於關鍵字位數較大的狀況

應用場景:關鍵字位數較大,知道關鍵字分佈狀況且關鍵字的若干位較均勻

摺疊法

其實這個方法也很簡單,也是處理咱們的關鍵字而後用做咱們的散列地址,主要思路是將關鍵字從左到右分割成位數相等的幾部分,而後疊加求和,並按散列表表長,取後幾位做爲散列地址。

好比咱們的關鍵字是123456789,則咱們分爲三部分 123 ,456 ,789 而後將其相加得 1368 而後咱們再取其後三位 368 做爲咱們的散列地址。

優勢:事先不須要知道關鍵字狀況

應用場景:適合關鍵字位數較多的狀況

除法散列法

在用來設計散列函數的除法散列法中,經過取 key 除以 p 的餘數,將關鍵字映射到 p 個槽中的某一個上,對於散列表長度爲 m 的散列函數公式爲

f(k) = k mod p (p <= m)

例如,若是散列表長度爲 12,即 m = 12 ,咱們的參數 p 也設爲12,那 k = 100時 f(k) = 100 % 12 = 4

因爲只須要作一次除法操做,因此除法散列法是很是快的。

由上面的公式能夠看出,該方法的重點在於 p 的取值,若是 p 值選的很差,就可能會容易產生同義詞。見下面這種狀況。咱們哈希表長度爲6,咱們選擇6爲p值,則有可能產生這種狀況,全部關鍵字都獲得了0這個地址數。image-20201117191635083

那咱們在選用除法散列法時選取 p 值時應該遵循怎樣的規則呢?

  • m 不該爲 2 的冪,由於若是 m = 2^p ,則 f(k) 就是 k 的 p 個最低位數字。例 12 % 8 = 4 ,12的二進制表示位1100,後三位爲100。
  • 若散列表長爲 m ,一般 p 爲 小於或等於表長(最好接近m)的最小質數或不包含小於 20 質因子的合數。

合數:合數是指在大於1的整數中除了能被1和自己整除外,還能被其餘數(0除外)整除的數。

質因子:質因子(或質因數)在數論裏是指能整除給定正整數的質數。

質因子

這裏的2,3,5爲質因子

仍是上面的例子,咱們根據規則選擇 5 爲 p 值,咱們再來看。這時咱們發現只有 6 和 36 衝突,相對來講就行了不少。

image-20201117192738889

優勢:計算效率高,靈活

應用場景:不知道關鍵字分佈狀況

乘法散列法

構造散列函數的乘法散列法主要包含兩個步驟

  • 用關鍵字 k 乘上常數 A(0 < A < 1),並提取 k A 的小數部分
  • 用 m 乘以這個值,再向下取整

散列函數爲

f (k) = ⌊ m(kA mod 1) ⌋

這裏的 kA mod 1 的含義是取 keyA 的小數部分,即 kA - ⌊kA⌋

優勢:對 m 的選擇不是特別關鍵,通常選擇它爲 2 的某個冪次(m = 2 ^ p ,p爲某個整數)

應用場景:不知道關鍵字狀況

平方取中法

這個方法就比較簡單了,假設關鍵字是 321,那麼他的平方就是 103041,再抽取中間的 3 位就是 030 或 304 用做散列地址。再好比關鍵字是 1234 那麼它的平方就是 1522756 ,抽取中間 3 位就是 227 用做散列地址.

優勢:靈活,適用範圍普遍

適用場景:不知道關鍵字分佈,而位數又不是很大的狀況。

隨機數法

故名思意,取關鍵字的隨機函數值爲它的散列地址。也就是 f(key) = random(key)。這裏的random是 隨機函數。

優勢:易實現

適用場景:關鍵字的長度不等時

上面咱們的例子都是經過數字進行舉例,那麼若是是字符串可不能夠做爲鍵呢?固然也是能夠的,各類各樣的符號咱們均可以轉換成某種數字來對待,好比咱們常常接觸的ASCII 碼,因此是一樣適用的。

以上就是經常使用的散列函數構造方法,其實他們的中心思想是一致的,將關鍵字通過加工處理以後變成另一個數字,而這個數字就是咱們的存儲位置,是否是有一種間諜傳遞情報的感受。

一個好的哈希函數能夠幫助咱們儘量少的產生衝突,可是也不能徹底避免產生衝突,那麼遇到衝突時應該怎麼作呢?下面給你們帶來幾種經常使用的處理散列衝突的方法。

處理散列衝突的方法

咱們在使用 hash 函數以後發現關鍵字 key1 不等於 key2 ,可是 f(key1) = f(key2),即有衝突,那麼該怎麼辦呢?不急咱們慢慢往下看。

開放地址法

瞭解開放地址法以前咱們先設想如下場景。

袁記菜館內,鈴鈴鈴,鈴鈴鈴 電話鈴響了

大鵬:老袁,給我訂個包間,我今天要去帶幾個客戶去你那談生意。

袁廚:大鵬啊,你經常使用的那個包間被人訂走啦。

大鵬:老袁你這不仗義呀,咋沒給我留住呀,那你給我找個空房間吧。

袁廚:好滴老哥

哦,穿越回古代就沒有電話啦,那看來穿越的時候得帶着幾個手機了。

上面的場景其實就是一種處理衝突的方法-----開放地址法

開放地址法就是一旦發生衝突,就去尋找下一個空的散列地址,只要列表足夠大,空的散列地址總能找到,並將記錄存入,爲了使用開放尋址法插入一個元素,須要連續地檢查散列表,或稱爲探查,咱們經常使用的有線性探測,二次探測,隨機探測

線性探測法

下面咱們先來看一下線性探測,公式:

f,(key) = ( f(key) + di ) MOD m(di = 1,2,3,4,5,6....m-1)

咱們來看一個例子,咱們的關鍵字集合爲{12,67,56,16,25,37,22,29,15,47,48,21},表長爲12,咱們再用散列函數 f(key) = key mod 12。

咱們求出每一個 key 的 f(key)見下表

image-20201118121740324

咱們查看上表發現,前五位的 f(key) 都不相同,即沒有衝突,能夠直接存入,可是到了第六位 f(37) = f(25) = 1,那咱們就須要利用上面的公式 f(37) = f (f(37) + 1 ) mod 12 = 2,這其實就是咱們的訂包間的作法。下面咱們看一下將上面的全部數存入哈希表是什麼狀況吧。

image-20201118121801671

咱們把這種解決衝突的開放地址法稱爲線性探測法。下面咱們經過視頻來模擬一下線性探測法的存儲過程。

線性探測法

另外咱們在解決衝突的時候,會遇到 48 和 37 雖然不是同義詞,卻爭奪一個地址的狀況,咱們稱其爲堆積。由於堆積使得咱們須要不斷的處理衝突,插入和查找效率都會大大下降。

經過上面的視頻咱們應該瞭解了線性探測的執行過程了,那麼咱們考慮一下這種狀況,如果咱們的最後一位不爲21,爲 34 時會有什麼事情發生呢?

image-20201118133459372

此時他第一次會落在下標爲 10 的位置,那麼若是繼續使用線性探測的話,則須要經過不斷取餘後獲得結果,數據量小還好,要是很大的話那也太慢了吧,可是明明他的前面就有一個空房間呀,若是向前移動只需移動一次便可。不要着急,前輩們已經幫咱們想好了解決方法

二次探測法

其實理解了咱們的上個例子以後,這個一下就能整明白了,根本不用費腦子,這個方法就是更改了一下di的取值

線性探測: f,(key) = ( f(key) + di ) MOD m(di = 1,2,3,4,5,6....m-1)

二次探測: f,(key) = ( f(key) + di ) MOD m(di =1^2 , -1^2 , 2^2 , -2^2 .... q^2, -q^2, q<=m/2)

注:這裏的是 -1^2 爲負值 而不是 (-1)^2

因此對於咱們的34來講,當di = -1時,就能夠找到空位置了。

image-20201118142851095

二次探測法的目的就是爲了避免讓關鍵字彙集在某一塊區域。另外還有一種有趣的方法,位移量採用隨機函數計算獲得,接着往下看吧.

隨機探測法

你們看到這是不又有新問題了,剛纔咱們在散列函數構造規則的第一條中說

(1)必須是一致的,假設你輸入辣子雞丁時獲得的是在看,那麼每次輸入辣子雞丁時,獲得的也必須爲在看。若是不是這樣,散列表將毫無用處。

咦?怎麼又是在看哈哈,那麼問題來了,咱們使用隨機數做爲他的偏移量,那麼咱們查找的時候豈不是查不到了?由於咱們 di 是隨機生成的呀,這裏的隨機實際上是僞隨機數,僞隨機數含義爲,咱們設置隨機種子相同,則不斷調用隨機函數能夠生成不會重複的數列,咱們在查找時,用一樣的隨機種子它每次獲得的數列是相同的,那麼相同的 di 就能獲得相同的散列地址

隨機種子(Random Seed)是計算機專業術語,一種以隨機數做爲對象的以真隨機數(種子)爲初始條件的隨機數。通常計算機的隨機數都是僞隨機數,以一個真隨機數(種子)做爲初始條件,而後用必定的算法不停迭代產生隨機數

image-20201118154853554

image-20201118205305792

經過上面的測試是否是一下就秒懂啦,爲何咱們可使用隨機數做爲它的偏移量,理解那句,相同的隨機種子,他每次獲得的數列是相同的。

下面咱們再來看一下其餘的函數處理散列衝突的方法

再哈希法

這個方法其實也特別簡單,利用不一樣的哈希函數再求得一個哈希地址,直到不出現衝突爲止。

f,(key) = RH,( key ) (i = 1,2,3,4.....k)

這裏的RH,就是不一樣的散列函數,你能夠把咱們以前說過的那些散列函數都用上,每當發生衝突時就換一個散列函數,相信總有一個可以解決衝突的。這種方法能使關鍵字不產生彙集,可是代價就是增長了計算時間。是否是很簡單啊。

鏈地址法

下面咱們再設想如下情景。

袁記菜館內,鈴鈴鈴,鈴鈴鈴電話鈴又響了,那個大鵬又來訂房間了。

大鵬:老袁啊,我一會去你那吃個飯,仍是上回那個包間

袁廚:大鵬你下回能不能早點說啊,又沒人訂走了,這回是老王訂的

大鵬:老王這個老東西啊,反正也是熟人,你再給我整個桌子,我拼在他後面吧

很差意思啊各位同窗,信鴿最近太貴了還沒來得及買。上面的情景就是模擬咱們的新的處理衝突的方法鏈地址法。

上面咱們都是遇到衝突以後,就換地方。那麼咱們有沒有不換地方的辦法呢?那就是咱們如今說的鏈地址法。

還記得咱們說過得同義詞嗎?就是 key 不一樣 f(key) 相同的狀況,咱們將這些同義詞存儲在一個單鏈表中,這種表叫作同義詞子表,散列表中只存儲同義詞子表的頭指針。咱們仍是用剛纔的例子,關鍵字集合爲{12,67,56,16,25,37,22,29,15,47,48,21},表長爲12,咱們再用散列函數 f(key) = key mod 12。咱們用了鏈地址法以後就不再存在衝突了,不管有多少衝突,咱們只需在同義詞子表中添加結點便可。下面咱們看下鏈地址法的存儲狀況。

image-20201118161354566

鏈地址法雖然可以不產生衝突,可是也帶來了查找時須要遍歷單鏈表的性能消耗,有得必有失嘛。

公共溢出區法

下面咱們再來看一種新的方法,這回大鵬又要來吃飯了。

袁記菜館內.....

袁廚:呦,這是什麼風把你給刮來了,咋沒開你的大奔啊。

大鵬:哎呀媽呀,別那麼多廢話了,我快餓死了,你快給我找個位置,我要吃點飯。

袁廚:你來的,太不巧了,我們的店已經滿了,你先去旁邊的小屋看會電視,等有空了我再叫你。小屋裏面還有幾個和你同樣來晚的,大家一塊兒看吧。

大鵬:電視?看電視?

上面得情景就是模擬咱們的公共溢出區法,這也是很好理解的,你不是衝突嗎?那衝突的各位我先給你安排個地方呆着,這樣你就有地方住了。咱們爲全部衝突的關鍵字創建了一個公共的溢出區來存放。

溢出區法

那麼咱們怎麼進行查找呢?咱們首先經過散列函數計算出散列地址後,先於基本表對比,若是不相等再到溢出表去順序查找。這種解決衝突的方法,對於衝突不多的狀況性能仍是很是高的。

散列表查找算法(線性探測法)

下面咱們來看一下散列表查找算法的實現

首先須要定義散列列表的結構以及一些相關常數,其中elem表明散列表數據存儲數組,count表明的是當前插入元素個數,size表明哈希表容量,NULLKEY散列表初始值,而後咱們若是查找成功就返回索引,若是不存在該元素就返回元素不存在。

咱們將哈希表初始化,爲數組元素賦初值。

第一行

插入操做的具體步驟:

(1)經過哈希函數(除法散列法),將 key 轉化爲數組下標

(2)若是該下標中沒有元素,則插入,不然說明有衝突,則利用線性探測法處理衝突。詳細步驟見註釋

第二行

查找操做的具體步驟:

(1)經過哈希函數(同插入時同樣),將 key 轉成數組下標

(2)經過數組下標找到 key值,若是 key 一致,則查找成功,不然利用線性探測法繼續查找。

第三張

下面咱們來看一下完整代碼

第四張

散列表性能分析

若是沒有衝突的話,散列查找是咱們查找中效率最高的,時間複雜度爲O(1),可是沒有衝突的狀況是一種理想狀況,那麼散列查找的平均查找長度取決於哪些方面呢?

1.散列函數是否均勻

咱們在上文說到,能夠經過設計散列函數減小衝突,可是因爲不一樣的散列函數對一組關鍵字產生衝突可能性是相同的,所以咱們能夠不考慮它對平均查找長度的影響。

2.處理衝突的方法

相同關鍵字,相同散列函數,不一樣處理衝突方式,會使平均查找長度不一樣,好比咱們線性探測有時會堆積,則不如二次探測法好,由於鏈地址法處理衝突時不會產生任何堆積,於是具備最佳的平均查找性能

3.散列表的裝填因子

原本想在上文中提到裝填因子的,可是後來發現即便沒有說明也不影響咱們對哈希表的理解,下面咱們來看一下裝填因子的總結

裝填因子 α = 填入表中的記錄數 / 散列表長度

散列因子則表明着散列表的裝滿程度,表中記錄越多,α就越大,產生衝突的機率就越大。咱們上面提到的例子中 表的長度爲12,填入記錄數爲6,那麼此時的 α = 6 / 12 = 0.5 因此說當咱們的 α 比較大時再填入元素那麼產生衝突的可能性就很是大了。因此說散列表的平均查找長度取決於裝填因子,而不是取決於記錄數。因此說咱們須要作的就是選擇一個合適的裝填因子以便將平均查找長度限定在一個範圍以內。

各位若是能感受到這個文章寫的很用心的話,能給您帶來一丟丟幫助的話,能麻煩您給這個文章點個贊嗎?這樣我就巨有動力寫下去啦。

另外你們若是須要其餘精選算法題的動圖解析,你們能夠微信關注下 【袁廚的算法小屋】,我是袁廚一個酷愛作飯因此本身考取了廚師證的菜雞程序員,會一直用心寫下去的,感謝支持!另外我給你們整理了一些我用過的和我認爲不錯的資料,你們能夠回覆相應關鍵字按取所需。
QQ截圖20201214161931

相關文章
相關標籤/搜索