HashMap中提到的散列是什么?
一)散列的基本概念
散列方法的主要思想是根據結點的關鍵碼值來確定其存儲地址:以關鍵碼值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可以看出,開散列方法的效率最好,實際系統中使用的散列大多都是開散列。開散列方法非常簡單、易于實現,它不會產生聚集現象(聚集導致更大的平均檢索長度),刪除也極為方便。大部分數據結構教材用比較多的篇幅來討論閉散列方法,是因為閉散列需要考慮的因素更多,因而更需要精心設計,閉散列在某些受限制的系統中(例如不能使用堆棧分配新空間)有獨到的用途。并且,經過精心設計的閉散列的效率比開散列穩定。?
總結
以上是生活随笔為你收集整理的HashMap中提到的散列是什么?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java面试绕不开的问题: Java中H
- 下一篇: HashMap与加载因子/负载因子loa