轉載:哈希函數 形象說明

哈希表與哈希函數

 

     hash表,有時候也被稱爲散列表。個人認爲,hash表是介於鏈表和二叉樹之間的一種中間結構。鏈表使用十分方便,但是數據查找十分麻煩;二叉樹中的數據嚴格有序,但是這是以多一個指針作爲代價的結果。hash表既滿足了數據的查找方便,同時不佔用太多的內容空間,使用也十分方便。

    打個比方來說,所有的數據就好像許許多多的書本。如果這些書本是一本一本堆起來的,就好像鏈表或者線性表一樣,整個數據會顯得非常的無序和凌亂,在你找到自己需要的書之前,你要經歷許多的查詢過程;而如果你對所有的書本進行編號,並且把這些書本按次序進行排列的話,那麼如果你要尋找的書本編號是n,那麼經過二分查找,你很快就會找到自己需要的書本;但是如果你每一個種類的書本都不是很多,那麼你就可以對這些書本進行歸類,哪些是文學類,哪些是藝術類,哪些是工科的,哪些是理科的,你只要對這些書本進行簡單的歸類,那麼尋找一本書也會變得非常簡單,比如說如果你要找的書是計算機方面的書,那麼你就會到工科一類當中去尋找,這樣查找起來也會顯得麻煩。

 

 

       哈希(Hash)算法就是單向散列算法,它把某個較大的集合P映射到另一個較小的集合Q中,假如這個算法叫H,那麼就有Q = H(P)。對於P中任何一個值p都有唯一確定的q與之對應,但是一個q可以對應多個p。作爲一個有用的Hash算法,H還應該滿足:H(p)速度比較快;給出一個q,很難算出一個p滿足q = H(p);給出一個p1,很難算出一個不等於p1的p2使得 H(p1)=H(p2)。

       數學原理聽起來很抽象,在網上找到一個很生動的描述。我們有很多的小豬,每個的體重都不一樣,假設體重分佈比較平均(我們考慮到公斤級別),我們按照體重來分,劃分成100個小豬圈。 然後把每個小豬,按照體重趕進各自的豬圈裏,記錄檔案。

       好了,如果我們要精確找到某個小豬怎麼辦呢?我們需要每個豬圈,每個小豬的比對嗎? 當然不需要了。 我們先看看要找的這個小豬的體重,然後就找到了對應的豬圈了。 在這個豬圈裏的小豬的數量就相對很少了。 我們在這個豬圈裏就可以相對快的找到我們要找到的那個小豬了。

        對應回hash算法:就是按照hashcode分配不同的豬圈,將hashcode相同的豬放到一個豬圈裏。 查找的時候,先找到hashcode對應的豬圈,然後在逐個比較裏面的小豬。

       關鍵就是建造多少個豬圈比較合適。如果每個小豬的體重全部不同(考慮到毫克級別),每個都建一個豬圈,那麼我們可以最快速度的找到這頭豬。缺點就是,建造那麼多豬圈的費用有點太高了。 如果我們按照10公斤級別進行劃分,那麼建造的豬圈只有幾個吧,那麼每個圈裏的小豬就很多了。我們雖然可以很快的找到豬圈,但從這個豬圈裏逐個確定那頭小豬也是很累的。 所以,好的hashcode,可以根據實際情況,根據具體的需求,在時間成本(更多的豬圈,更快的速度)和空間本(更少的豬圈,更低的空間需求)之間平衡。

         所以一個簡單的定義:哈希算法其本質上就是將一個數據映射成另一個數據,通常情況下原數據的長度比hash後的數據容量大。這種映射的關係我們叫做哈希函數或者散列函數。散列函數能使對一個數據序列的訪問過程更加迅速有效,通過散列函數,數據元素將被更快地定位。常見的構造散列函數的方法有:

  • 直接尋址法:取關鍵字或關鍵字的某個線性函數值爲散列地址。即H(key)=key或H(key) = a×key + b,其中a和b爲常數(這種散列函數叫做自身函數)
  • 數字分析法
  • 摺疊法
  • 隨機數法
  • 求模取餘法

最經典的莫過於求模取餘法。我們知道,任給一個整數A,將自然數1,2,3,4,…依次除以A,所得的餘數總是循環出現,呈週期性變化, 所以,我們可以取關鍵字被某個不大於散列表表長m的數p除後所得的餘數爲散列地址。即 H(key) = key % p, p<=m。

假設我們有一個很大集合A中有{496,387,184,21,96,31,.....}等等元素,回憶我們上面提到的小豬問題,我們可以將大的集合A(小豬)映射到一個小的集合B(豬圈)(假設B只有16個元素,請參考下圖)。我們對元素A的每一個元素採用求模算法,得到: 496 % 16 = 0, 所以我們把496填入集合B的0號位置,387 % 16 = 3,那麼387被填入集合B的3號位置。

 

當我們查詢140是否在集合A中時,我們可以對140進行同樣的求模算法,140 % 16=12 ,如果集合B的12號位置爲空,就可以推斷140不在集合A之中。但是,如果12號位置不爲空,是否可以確定140在集合A之中呢?答案是否定的,主要是由於求模算法會對數組長度進行取餘,因此其結果由於數組長度的限制必然會出現重複,比方說{108,12,140,28},這些元素用上面的算法得到的餘數都是12,所以就會有“衝突”這一問題。解決衝突的方法有很多種,最直觀的莫過於”拉鍊法“,即12號位置填入的不是元素本身,而是一個鏈表,所有餘數相同的元素,都寫入該鏈表。顯然鏈表中的元素要遠比集合A中的元素少了很多,這時就可以對鏈表做遍歷比較了。

  從上面的例子,我們知道對p的選擇很重要,一般取素數或m,若p選的不好,容易產生同義詞,即所謂的“衝突”或“碰撞”。發生“衝突”的概率可以用裝填因子來表示,裝填因子Load factor a=哈希表的實際元素數目(n)/ 哈希表的容量(m) a越大,哈希表衝突的概率越大,但是a越接近0,那麼哈希表的空間就越浪費。

一般情況下建議Load factor的值爲0-0.7,Java實現的HashMap默認的Load factor的值爲0.75,當裝載因子大於這個值的時候,HashMap會對數組進行擴張至原來兩倍大。

  哈希查找因使用哈希 (Hash) 函數而得名,哈希函數又叫散列函數,它是一種能把關鍵字映射成記錄存貯地址的函數。 
一.哈希表
①它是一種能把關鍵字映射成記錄存貯地址的函數。
②假定數組 HT[0 ~ m-1] 爲存貯記錄的地址空間, m 爲表長,哈希函數 H 以記錄的關鍵字 K爲自變量,計算出對應的函數值 H(K) ,並以它作爲關鍵字 K 所標識的記錄在表 HT 中的 ( 相對) 地址或索引號,這樣產生的記錄表 HT 叫做對應於哈希函數 H 的哈希表
③簡言之,在哈希表中,關鍵字爲 K 的記錄,存貯在 HT[H(K)] 位置。
④哈希函數值 H(K) 稱爲 K 的哈希地址或散列地址。

二.構造哈希表 
  構造哈希函數的方法很多,這裏只介紹一些常用的,計算簡便的方法。
1.平方取中法
  算出關鍵字值的平方,再取其中若干位作爲哈希函數值 ( 散列地址 ) 。
【例】假定表中各關鍵字是由字母組成的,用二位數字的整數 01 ~ 26 表示對應的 26 個英文字母在計算機中的內部編碼,則使用平方取中法計算 KEYA , KEYB , AKEY , BKEY 的散列地址可得:
關鍵字 K     K 的內部編碼            K 2           H(K)
 KEYA         11052501       122157778355001      778
 KEYB         11052502       122157800460004      800
 AKEY         01110525       001233265775625      265
 BKEY         02110525       004454315775625      315
平方之後,取左起第 7 ~ 9 位作爲散列地址。

2.除留餘數法
    這種方法是用模運算 (%) 得到的。設給出的關鍵字值爲 K ,存儲區單元數爲 m ,則用一個小於 m 的質數 P 去除 K ,得到的餘數爲 R ,即: R = K % P 。如果 R 落在存儲區地址範圍內,則 R 就取爲哈希函數值 ( 散列地址 ) ;否則,再用一個線性數求出哈希函數值。
【例】有一組關鍵字從 000001 到 859999 ,指定的存儲區地址爲 1000000 ~ 1005999 ,即 m= 6000 ,可選 P = 599 ,若要轉換關鍵字 K = 172148 ,則有:
                R = 172148 % 599 = 4176
因 R 不在指定的地址範圍內,所以,取哈希函數爲:
                  H(K) = 1000000 + R
故有:
                H(K) = H(172148) = 1004176
這樣就把關鍵字 K 直接轉換成存儲地址了。

三.可能產生的衝突:

(1)衝突
     不同的關鍵字值,具有相同的哈希地址,因而被映射到同一表位置上。該現象稱爲衝突(Collision)或碰撞。
   【例】上圖中的k2≠k5,但h(k2)=h(k5),故k2和K5所在的結點的存儲地址相同。

(2)安全避免衝突的條件
    如何避免衝突發生,則取決於哈希函數的構造。 
    使散列地址均勻地分佈在哈希表的整個地址區間內,這樣可以避免或減少發生衝突。
    哈希函數的構造,與關鍵字的長度、哈希表的大小、關鍵字的實際取值狀況等許多因素有關,而且有的因素事前不能確定。所以,避免衝突這並非是件容易做到的事。
(3)衝突不可能完全避免
     由於關鍵字的值域往往比哈希表的個數大的多,所以哈希函數是一種壓縮映射,碰撞是難免的。
   【例】存貯 100 個學生記錄,儘管安排 120 個地址空間,但由於學生名 ( 假設不超過 10 個英文字母 ) 的理論個數超過 2610 ,要找到一個哈希函數把 100 個任意的學生名映射成 [0 ,119] 內的不同整數,實際上是不可能的。
   注意:問題在於一旦發生了衝突應如何處理。

四.解決衝突的主要方法 
   雖然我們不希望發生衝突,但實際上發生衝突的可能性仍是存在的。當關鍵字值域遠大於哈希表的長度,而且事先並不知道關鍵字的具體取值時。衝突就難免會發 生。另外,當關鍵字的實際取值大於哈希表的長度時,而且表中已裝滿了記錄,如果插入一個新記錄,不僅發生衝突,而且還會發生溢出。因此,處理衝突和溢出是 哈希技術中的兩個重要問題。
1、開放定址法
     用開放定址法解決衝突的做法是:當衝突發生時,使用某種探查(亦稱探測)技術在散列表中形成一個探查(測)序列。沿此序列逐個單元地查找,直到找到給定的關鍵字,或者碰到一個開放的地址(即該地址單元爲空)爲止(若要插入,在探查到開放的地址,則可將待插入的新結點存人該地址單元)。查找時探查到開放的地址則表明表中無待查的關鍵字,即查找失敗。
  注意:
 ①用開放定址法建立散列表時,建表前須將表中所有單元(更嚴格地說,是指單元中存儲的關鍵字)置空。
 ②空單元的表示與具體的應用相關。
     按照形成探查序列的方法不同,可將開放定址法區分爲線性探查法、線性補償探測法、隨機探測等。
(1)線性探查法(Linear Probing)
該方法的基本思想是:
     將散列表T[0..m-1]看成是一個循環向量,若初始探查的地址爲d(即h(key)=d),則最長的探查序列爲:
        d,d+l,d+2,…,m-1,0,1,…,d-1
     即:探查時從地址d開始,首先探查T[d],然後依次探查T[d+1],…,直到T[m-1],此後又循環到T[0],T[1],…,直到探查到T[d-1]爲止。
探查過程終止於三種情況:
     (1)若當前探查的單元爲空,則表示查找失敗(若是插入則將key寫入其中);
     (2)若當前探查的單元中含有key,則查找成功,但對於插入意味着失敗;
     (3)若探查到T[d-1]時仍未發現空單元也未找到key,則無論是查找還是插入均意味着失敗(此時表滿)。
利用開放地址法的一般形式,線性探查法的探查序列爲:
        hi=(h(key)+i)%m 0≤i≤m-1 //即di=i
用線性探測法處理衝突,思路清晰,算法簡單,但存在下列缺點:
  ① 處理溢出需另編程序。一般可另外設立一個溢出表,專門用來存放上述哈希表中放不下的記錄。此溢出表最簡單的結構是順序表,查找方法可用順序查找。
  ② 按上述算法建立起來的哈希表,刪除工作非常困難。假如要從哈希表 HT 中刪除一個記錄,按理應將這個記錄所在位置置爲空,但我們不能這樣做,而只能標上已被刪除的標記,否則,將會影響以後的查找。
  ③ 線性探測法很容易產生堆聚現象。所謂堆聚現象,就是存入哈希表的記錄在表中連成一片。按照線性探測法處理衝突,如果生成哈希地址的連續序列愈長 ( 即不同關鍵字值的哈希地址相鄰在一起愈長 ) ,則當新的記錄加入該表時,與這個序列發生衝突的可能性愈大。因此,哈希地址的較長連續序列比較短連續序列生長得快,這就意味着,一旦出現堆聚 ( 伴隨着衝突 ) ,就將引起進一步的堆聚。

(2)線性補償探測法 
線性補償探測法的基本思想是:
  將線性探測的步長從 1 改爲 Q ,即將上述算法中的 j = (j + 1) % m 改爲: j = (j +Q) % m ,而且要求 Q 與 m 是互質的,以便能探測到哈希表中的所有單元。
【例】 PDP-11 小型計算機中的彙編程序所用的符合表,就採用此方法來解決衝突,所用表長 m= 1321 ,選用 Q = 25 。

(3)隨機探測 
隨機探測的基本思想是:
  將線性探測的步長從常數改爲隨機數,即令: j = (j + RN) % m ,其中 RN 是一個隨機數。在實際程序中應預先用隨機數發生器產生一個隨機序列,將此序列作爲依次探測的步長。這樣就能使不同的關鍵字具有不同的探測次序,從而可以避免或減少堆聚。基於與線性探測法相同的理由,在線性補償探測法和隨機探測法中,刪除一個記錄後也要打上刪除標記。
2、拉鍊法
(1)拉鍊法解決衝突的方法
     拉鍊法解決衝突的做法是:將所有關鍵字爲同義詞的結點鏈接在同一個單鏈表中。若選定的散列表長度爲m,則可將散列表定義爲一個由m個頭指針組成的指針數組T[0..m-1]。凡是散列地址爲i的結點,均插入到以T[i]爲頭指針的單鏈表中。T中各分量的初值均應爲空指針。在拉鍊法中,裝填因子α可以大於1,但一般均取α≤1。

(2)拉鍊法的優點
與開放定址法相比,拉鍊法有如下幾個優點:
  ①拉鍊法處理衝突簡單,且無堆積現象,即非同義詞決不會發生衝突,因此平均查找長度較短;
  ②由於拉鍊法中各鏈表上的結點空間是動態申請的,故它更適合於造表前無法確定表長的情況;
  ③開放定址法爲減少衝突,要求裝填因子α較小,故當結點規模較大時會浪費很多空間。而拉鍊法中可取α≥1,且結點較大時,拉鍊法中增加的指針域可忽略不計,因此節省空間;
  ④在用拉鍊法構造的散列表中,刪除結點的操作易於實現。只要簡單地刪去鏈表上相應的結點即可。而對開放地址法構造的散列表,刪除結點不能簡單地將被刪結點的空間置爲空,否則將截斷在它之後填人散列表的同義詞結點的查找路徑。這是因爲各種開放地址法中,空地址單元(即開放地址)都是查找失敗的條件。因此在用開放地址法處理衝突的散列表上執行刪除操作,只能在被刪結點上做刪除標記,而不能真正刪除結點。

(3)拉鍊法的缺點
     拉鍊法的缺點是:指針需要額外的空間,故當結點規模較小時,開放定址法較爲節省空間,而若將節省的指針空間用來擴大散列表的規模,可使裝填因子變小,這又減少了開放定址法中的衝突,從而提高平均查找速度。

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