散列

散列的基本概念

散列方法的主要思想是根據結點的關鍵碼值來確定其存儲地址:以關鍵碼值K爲自變量,通過一定的函數關係h(K)(稱爲散列函數),計算出對應的函數值來,把這個值解釋爲結點的存儲地址,將結點存入到此存儲單元中。檢索時,用同樣的方法計算地址,然後到相應的單元裏去取要找的結點。通過散列方法可以對結點進行快速檢索。散列(hash,也稱“哈希”)是一種重要的存儲方式,也是一種常見的檢索方法

按散列存儲方式構造的存儲結構稱爲散列表(hash table)。散列表中的一個位置稱爲槽(slot)。散列技術的核心是散列函數(hash function)。 對任意給定的動態查找表DL,如果選定了某個“理想的”散列函數h及相應的散列表HT,則對DL中的每個數據元素X。函數值h(X.key)就是X在散列表HT中的存儲位置。插入(或建表)時數據元素X將被安置在該位置上,並且檢索X時也到該位置上去查找。由散列函數決定的存儲位置稱爲散列地址。 因此,散列的核心就是:由散列函數決定關鍵碼值(X.key)與散列地址h(X.key)之間的對應關係,通過這種關係來實現組織存儲並進行檢索。

一般情況下,散列表的存儲空間是一個一維數組HT[M],散列地址是數組的下標。設計散列方法的目標,就是設計某個散列函數h,0<=h( K ) < M;對於關鍵碼值K,得到HT[i] = K。 在一般情況下,散列表的空間必須比結點的集合大,此時雖然浪費了一定的空間,但換取的是檢索效率。設散列表的空間大小爲M,填入表中的結點數爲N,則稱爲散列表的負載因子(load factor,也有人翻譯爲“裝填因子”)。建立散列表時,若關鍵碼與散列地址是一對一的關係,則在檢索時只需根據散列函數對給定值進行某種運算,即可得到待查結點的存儲位置。但是,散列函數可能對於不相等的關鍵碼計算出相同的散列地址,我們稱該現象爲衝突(collision),發生衝突的兩個關鍵碼稱爲該散列函數的同義詞。在實際應用中,很少存在不產生衝突的散列函數,我們必須考慮在衝突發生時的處理辦法。

因此,採用散列技術時需要考慮的兩個首要問題是: 
(1)如何構造(選擇)使結點“分佈均勻”的散列函數? 
(2)一旦發生衝突,用什麼方法來解決?

二)散列函數

在以下的討論中,我們假設處理的是值爲整型的關鍵碼,否則我們總可以建立一種關鍵碼與正整數之間的一一對應關係,從而把該關鍵碼的檢索轉化爲對與其對應的正整數的檢索;同時,進一步假定散列函數的值落在0到M-1之間。散列函數的選取原則是:運算儘可能簡單;函數的值域必須在散列表的範圍內;儘可能使得結點均勻分佈,也就是儘量讓不同的關鍵碼具有不同的散列函數值。需要考慮各種因素:關鍵碼長度、散列表大小、關鍵碼分佈情況、記錄的檢索頻率等等。下面我們介紹幾種常用的散列函數。 
1、除餘法 
顧名思義,除餘法就是用關鍵碼x除以M(往往取散列表長度),並取餘數作爲散列地址。除餘法幾乎是最簡單的散列方法,散列函數爲: h(x) = x mod M。

2、乘餘取整法 
使用此方法時,先讓關鍵碼key乘上一個常數A (0< A < 1),提取乘積的小數部分。然後,再用整數n乘以這個值,對結果向下取整,把它做爲散列的地址。散列函數爲: hash ( key ) = _LOW( n × ( A × key % 1 ) )。 其中,“A × key % 1”表示取 A × key 小數部分,即: A × key % 1 = A × key - _LOW(A × key), 而_LOW(X)是表示對X取下整。

3、平方取中法 
由於整數相除的運行速度通常比相乘要慢,所以有意識地避免使用除餘法運算可以提高散列算法的運行時間。平方取中法的具體實現是:先通過求關鍵碼的平方值,從而擴大相近數的差別,然後根據表長度取中間的幾位數(往往取二進制的比特位)作爲散列函數值。因爲一個乘積的中間幾位數與乘數的每一數位都相關,所以由此產生的散列地址較爲均勻。

4、數字分析法 
設有 n 個 d 位數,每一位可能有 r 種不同的符號。這 r 種不同的符號在各位上出現的頻率不一定相同,可能在某些位上分佈均勻些,每種符號出現的機率均等; 在某些位上分佈不均勻,只有某幾種符號經常出現。可根據散列表的大小,選取其中各種符號分佈均勻的若干位作爲散列地址。

5、基數轉換法

將關鍵碼值看成另一種進制的數再轉換成原來進制的數,然後選其中幾位作爲散列地址。

6、摺疊法 
有時關鍵碼所含的位數很多,採用平方取中法計算太複雜,則可將關鍵碼分割成位數相同的幾部分(最後一部分的位數可以不同),然後取這幾部分的疊加和(捨去進位)作爲散列地址,這方法稱爲摺疊法。

7、ELFhash字符串散列函數 
ELFhash函數在UNIX系統V 版本4中的“可執行鏈接格式”( Executable and Linking Format,即ELF )中會用到,ELF文件格式用於存儲可執行文件與目標文件。ELFhash函數是對字符串的散列。它對於長字符串和短字符串都很有效,字符串中每個字符都有同樣的作用,它巧妙地對字符的ASCII編碼值進行計算,ELFhash函數對於能夠比較均勻地把字符串分佈在散列表中。

三)衝突解決的策略

儘管散列函數的目標是使得衝突最少,但實際上衝突是無法避免的。因此,我們必須研究衝突解決策略。衝突解決技術可以分爲兩類:開散列方法( open hashing,也稱爲拉鍊法,separate chaining )和閉散列方法( closed hashing,也稱爲開地址方法,open addressing )。這兩種方法的不同之處在於:開散列法把發生衝突的關鍵碼存儲在散列表主表之外,而閉散列法把發生衝突的關鍵碼存儲在表中另一個槽內。

開散列方法:

1、拉鍊法

開散列方法的一種簡單形式是把散列表中的每個槽定義爲一個鏈表的表頭。散列到一個特定槽的所有記錄都放到這個槽的鏈表中。圖9-5說明了一個開散列的散列表,這個表中每一個槽存儲一個記錄和一個指向鏈表其餘部分的指針。這7個數存儲在有11個槽的散列表中,使用的散列函數是h(K) = K mod 11。數的插入順序是77、7、110、95、14、75和62。有2個值散列到第0個槽,1個值散列到第3個槽,3個值散列到第7個槽,1個值散列到第9個槽。

2、桶式散列

桶式散列方法的基本思想是把一個文件的記錄分爲若干存儲桶,每個存儲桶包含一個或多個頁塊,一個存儲桶內的各頁塊用指針連接起來,每個頁塊包含若干記錄。散列函數h把關鍵碼值K轉換爲存儲桶號,即h(K)表示具有關鍵碼值K的記錄所在的存儲桶號。 圖9-6表示了一個具有B個存儲桶的散列文件組織。有一個存儲桶目錄表,存放B個指針,每個存儲桶一個,每個指針就是所對應存儲桶的第一個頁塊的地址。

有些存儲桶僅僅由一個頁塊組成,如下圖中的1號存儲桶。有的存儲桶由多個頁塊組成,每一個頁塊的塊頭上有一個指向下一個頁塊的指針,例如,如下圖中的第B-1號存儲桶由b4,b5,b6三個頁塊組成,每個存儲桶中最後一個頁塊的頭上爲空指針。

閉散列方法:

閉散列方法把所有記錄直接存儲在散列表中。每個記錄關鍵碼key有一個由散列函數計算出來的基位置,即h(key)。如果要插入一個關鍵碼,而另一個記錄已經佔據了R的基位置(發生碰撞),那麼就把R存儲在表中的其它地址內,由衝突解決策略確定是哪個地址。

閉散列表解決衝突的基本思想是:當衝突發生時,使用某種方法爲關鍵碼K生成一個散列地址序列d0,d1,d2,... di ,...dm-1。其中d0=h(K)稱爲K的基地址地置( home position );所有di(0< i< m)是後繼散列地址。當插入K時,若基地址上的結點已被別的數據元素佔用,則按上述地址序列依次探查,將找到的第一個開放的空閒位置di作爲K的存儲位置;若所有後繼散列地址都不空閒,說明該閉散列表已滿,報告溢出。相應地,檢索K時,將按同值的後繼地址序列依次查找,檢索成功時返回該位置di ;如果沿着探查序列檢索時,遇到了開放的空閒地址,則說明表中沒有待查的關鍵碼。刪除K時,也按同值的後繼地址序列依次查找,查找到某個位置di具有該K值,則刪除該位置di上的數據元素(刪除操作實際上只是對該結點加以刪除標記);如果遇到了開放的空閒地址,則說明表中沒有待刪除的關鍵碼。因此,對於閉散列表來說,構造後繼散列地址序列的方法,也就是處理衝突的方法。

形成探查的方法不同,所得到的解決衝突的方法也不同。下面是幾種常見的構造方法。

1、線性探查法

將散列表看成是一個環形表,若在基地址d(即h(K)=d)發生衝突,則依次探查下述地址單元:d+1,d+2,......,M-1,0,1,......,d-1直到找到一個空閒地址或查找到關鍵碼爲key的結點爲止。當然,若沿着該探查序列檢索一遍之後,又回到了地址d,則無論是做插入操作還是做檢索操作,都意味着失敗。 用於簡單線性探查的探查函數是: p(K,i) = i

例9.7 已知一組關鍵碼爲(26,36,41,38,44,15,68,12,06,51,25),散列表長度M= 15,用線性探查法解決衝突構造這組關鍵碼的散列表。 因爲n=11,利用除餘法構造散列函數,選取小於M的最大質數P=13,則散列函數爲:h(key) = key%13。按順序插入各個結點: 26: h(26) = 0,36: h(36) = 10, 41: h(41) = 2,38: h(38) = 12, 44: h(44) = 5。 插入15時,其散列地址爲2,由於2已被關鍵碼爲41的元素佔用,故需進行探查。按順序探查法,顯然3爲開放的空閒地址,故可將其放在3單元。類似地,68和12可分別放在4和13單元中.

2、二次探查法

二次探查法的基本思想是:生成的後繼散列地址不是連續的,而是跳躍式的,以便爲後續數據元素留下空間從而減少聚集。二次探查法的探查序列依次爲:12,-12,22 ,-22,...等,也就是說,發生衝突時,將同義詞來回散列在第一個地址的兩端。求下一個開放地址的公式爲:

3、隨機探查法

理想的探查函數應當在探查序列中隨機地從未訪問過的槽中選擇下一個位置,即探查序列應當是散列表位置的一個隨機排列。但是,我們實際上不能隨機地從探查序列中選擇一個位置,因爲在檢索關鍵碼的時候不能建立起同樣的探查序列。然而,我們可以做一些類似於僞隨機探查( pseudo-random probing )的事情。在僞隨機探查中,探查序列中的第i個槽是(h(K) + ri) mod M,其中ri是1到M - 1之間數的“隨機”數序列。所有插入和檢索都使用相同的“隨機”數。探查函數將是 p(K,i) = perm[i - 1], 這裏perm是一個長度爲M - 1的數組,它包含值從1到M – 1的隨機序列。

4、雙散列探查法

僞隨機探查和二次探查都能消除基本聚集——即基地址不同的關鍵碼,其探查序列的某些段重疊在一起——的問題。然而,如果兩個關鍵碼散列到同一個基地址,那麼採用這兩種方法還是得到同樣的探查序列,仍然會產生聚集。這是因爲僞隨機探查和二次探查產生的探查序列只是基地址的函數,而不是原來關鍵碼值的函數。這個問題稱爲二級聚集( secondary clustering )。

爲了避免二級聚集,我們需要使得探查序列是原來關鍵碼值的函數,而不是基位置的函數。雙散列探查法利用第二個散列函數作爲常數,每次跳過常數項,做線性探查。

四)散列的檢索效率分析

我們可以根據完成一次操作,即插入、刪除和檢索操作,所需要的記錄訪問次數來衡量散列方法的性能。由於散列表的插入和刪除操作都是基於檢索進行的:在刪除一條記錄之前必須先找到該記錄,因此刪除一條記錄之前需要的訪問數等於成功檢索到它需要的訪問數;而插入一條記錄時,必須找到探查序列的尾部(對於不考慮刪除的情況,是尾部的空槽;對於考慮刪除的情況,也要找到尾部,才能確定是否有重複記錄),這等於對這條記錄進行一次不成功的檢索。因此,散列表的效率實質上還是平均檢索長度,而且我們需要區別對待成功的檢索與不成功的檢索。

當散列表比較空的時候,所插入的記錄比較容易插入到其空閒的基地址。如果散列表中的記錄比較多,插入記錄時,很可能要靠衝突解決策略來尋找探查序列中合適的另一個槽。而且,檢索記錄時,很多時候需要沿着探查序列逐個查找。隨着散列表記錄不斷增加,越來越多的記錄有可能放到離其基地址更遠的地方。

根據這些討論,我們可以看到散列方法預期的代價與負載因子α= N/M有關。其中,M是散列表存儲空間大小,N是表中當前的記錄數目。

從圖9-8可以看出,開散列方法的效率最好,實際系統中使用的散列大多都是開散列。開散列方法非常簡單、易於實現,它不會產生聚集現象(聚集導致更大的平均檢索長度),刪除也極爲方便。大部分數據結構教材用比較多的篇幅來討論閉散列方法,是因爲閉散列需要考慮的因素更多,因而更需要精心設計,閉散列在某些受限制的系統中(例如不能使用堆棧分配新空間)有獨到的用途。並且,經過精心設計的閉散列的效率比開散列穩定。

----

摘錄自:http://www.jpk.pku.edu.cn/pkujpk/course/sjjg/frame/index.html

 

轉自 http://www.cnblogs.com/huangfox/archive/2012/07/06/2578898.html

發佈了0 篇原創文章 · 獲贊 2 · 訪問量 5983
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章