數據結構_搜索之哈希

 

1.  哈希的概念

在進行順序查找或者二叉搜索樹查找時,元素存儲的位置和元素各個關鍵碼之間沒有對應關係,所以在查找一個元素時必須通過關鍵碼的比較才能判斷該關鍵碼所存放的位置,搜索的效率取決與在搜索過程中元素比較的次數。

因此,這個時候就想有這麼一種理想的查找算法,可以不經過任何比較,直接從存儲元素的表中得到想要查找的數據。

爲此,我們構造一種結構,通過某種函數使元素的存儲位置和存儲的值(關鍵碼)之間存在一一映射的關係,那麼我們就可以在查找元素時通過他們之間的關係來直接找到對應的元素。

當向該結構中插入元素時,由給定的函數(哈希函數)計算出該值因該在的位置,然後將元素插入到對應的位置

當我們要在該結構中搜索元素時,通過給定的函數,計算出它應該在的位置,然後直接返回對應位置的元素值。

以上所說的就是哈希(hash)(有叫散列),上面所提到的函數,就是哈希函數(又叫散列函數),通過哈希函數算出來的元素應該存放是位置又叫哈希地址,上面所說的構造出來的用來存放元素的結構,就叫哈希表(又叫散列表)

舉個栗子:

但是這個時候就會有一個問題:如果這個時候我還想插入一個12會出現什麼問題呢???

2.  哈希衝突

當有兩個元素 x 和 y ,x  != y,但是有Hash(x) = Hash(y),也就是說對於兩個值不相同的元素但是通過hash函數算出來的哈希地址相同,這種現象稱爲哈希衝突或者哈希碰撞;把具有不同的關鍵值而具有相同的哈希地址的兩個元素稱爲“同義詞”;

那麼定發生哈希衝突的時候該如何處理呢???首先我們可以想到的就是哈希函數,因爲哈希地址是有哈希函數計算出來的,因此我們先來看看哈希函數...

3.  哈希函數

引起哈希衝突的一個可能的原因就是哈希函數設計的不夠合理

3.1 哈希函數的設計原則:

  1. 哈希函數的定義域必須包括需要存儲的 全部關鍵碼,如果哈希表允許有m個哈希地址時,其哈希函數的值域必須在0到m-1之間
  2. 哈希函數計算出來的值能均勻分佈在整個空間中
  3. 哈希函數應該比較簡單

3.2 常見的哈希函數:

  • 直接定製法

  1. 取關鍵值的某個線性函數爲散列地址 : Hash(key) = A * key + B;
  2. 優點:簡單,均勻
  3. 缺點:需要事先知道關鍵值的分佈情況
  4. 適合查找比較小且連續的情況(如編程題:找到字符串中第一個只出現一次的字符
  • 除留餘數法

  1. 設哈希表中允許的地址數爲m個,取最接近或者等於m的一個質數作爲除數,按照哈希函數:Hash(key) = key % p 計算出關鍵碼對應的哈希地址(其中p爲等於或者接近m的質數)
  • 平方取中法

  1. 假設關鍵字爲123,它的平方爲15129 ;可以取它的中間三位數521作爲哈希地址
  2. 再如關鍵字爲666,它的平方爲443556; 可以取它的平方中間的435或者355作爲哈希地址
  3. 適用:適合不知道關鍵字的分佈,而位數又不是很大的情況
  • 摺疊法

  1. 摺疊法是將關鍵字從左到右分割成長度相等的幾部分(最後一部分可以短些),然後將這幾部分疊加求和,根據散列表的長度取後幾位作爲哈希地址。
  2. 適用:適合事先不知道關鍵字序列,且關鍵字位數比較多的情況。
  • 隨機數法

  1. 選擇一個隨機函數,取關鍵字的隨機函數值作爲哈希地址,
  2. 通常用於關鍵字長度不等的情況
  • 數學分析法

  1. 設有d個n位數,每一位可能有r中不同的符號,這r中符號在每個位上出現的頻率不一定相同,每種符號出現的可能性均等,在某些位上出現的可能性不均等,只有某幾種符號經常出現,此時可以根據散列表的大小,選擇其中符號分佈均勻的幾位作爲散列地址。
  2. 例如:手機號前面的大多數都是一樣的,可以利用後幾位作爲判斷散列地址的條件,如果這樣還容易出現哈希衝突,可以將提取出來的數字在進行逆序,循環左移,循環右移,高兩位和低兩位求和等方法優化。

注意:哈希函數設計的越精妙,出現哈希衝突的可能性越低,但是不可能完全避免哈希衝突。那麼當我們遇到哈希衝突時,又該如何解決呢???

4.  處理哈希衝突

  • 閉散列

閉散列:也叫開放地址法,當發生哈希衝突時,如果表還沒有被填滿,就將當前這個值放入”下一個空位置“中去。

那麼問題又來了,怎麼找下一個空位置呢?主要有兩種方法:線性探測 和 二次探測

線性探測:當發生哈希衝突的時候,從發生衝突的位置開始一次向後探測

插入元素:如果插入位置沒有值,就將其直接插入;如果有值,比較要插入的值和當前值是否相同,相同則不用再次插入,不同就依次向後探測,直到有空的位置,將值插入;

查找元素:通過哈希函數算出哈西地址,比較該處的值和要找的值是否相同,相同就直接返回該值,不相同就依次向後找,直到找到該值或者找遍哈希表找不到該值。

二次探測:當遇到哈希衝突的時候,讓當前位置朝左右兩邊探測,找空位置;朝左右兩邊探測的方法爲:

左邊:           右邊:  (i = 1, 2, 3,……)

也就是說每次探測的時候從用哈希函數計算出來的位置開始,左邊和它距離爲i平方的位置,右邊和它距離爲i平方的位置(i從1遞增)

比如:

這時候還需要知道一個概念:哈希因子 

從上面的存儲過程可以看出來,當哈希表元素比較少的時候,還想碰撞機率很小,但是一旦當哈希錶快滿了,就會出現問題。比如:如果我們用線性探測,如果還剩下一個位置爲空,我們可能會將整個哈希表走一遍才能存下最後一個元素;同樣,如果使用二次探測來插入元素,本來這個表還剩下幾個空間,但是我們跳着跳着就把剩下的空間都錯過了,導致元素插入失敗。因此使用哈希表的時候,一般不會讓表存滿了,這時候定義一個數叫做哈希因子α,哈希因子是一個大於0小於1的數;當 哈希表中的元素個數 = 哈希表的大小 * 哈希因子 的時候就相當於‘哈希表存滿了’,此時就不應該在想哈希表中插入元素。我們可以理解爲到這個時候,如果我們在想其中插入元素,就會使哈希衝突產生的可能性增大。如果這個時候非要向其中插入元素,但是有不想影響哈希表的效率,可以先擴容,然後在將之前的元素在新擴容的空間中再次哈希(因爲哈希表中元素的存儲和哈希表的下標有關,因此擴容時必須還要使用哈希算法將之前的元素重新插入)存入,在插入新的元素。

散列表的哈希因子 = 已經存入的元素個數 / 哈希表的總大小

哈希因子的大小一般在0.8以下;像Java集合框架中的哈希因子爲0.75,一旦元素個數超了就會resize(擴容);

對於開放地址發而言,哈希因子一般在0.7 ~0,8以下,研究發現:如果超過0.8,查表時不命中率按照指數曲線上升;

對於二次探測而言,經過研究表明:當表的長度爲質數且哈希因子不超過0.5時,新的元素一定可以插入,而且任何一個位置都不會被探測兩次,因此當表中超過一半的空位置,就不存在裝滿的問題;在搜索時可以不用裝滿問題,但是在插入時如果哈希因子超過0.5,一定要考慮擴容問題;

  • 開散列

開散列法又叫鏈地址法。所謂的鏈地址法就是先通過哈希函數計算出各個關鍵碼所對應的地址,將所有地址相同的元素用鏈表連起來,哈希表中對應的位置只需要存放該鏈表的頭結點地址。

在查找元素的時候,只需要計算出對應的哈希地址,然後遍歷鏈表即可,具體如下圖(隨便舉的幾個例子):

使用鏈地址法看起來好像是多了些指針,佔了大量空間,閉散列必須保持大量的空閒空間來保證搜索的效率,比如哈希因子小於0.7之類的限制條件,而表項所需要的空間比指針的空間大得多,因此鏈地址法和閉散列相比,更加節省空間。

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