最近在復習算法和數據結構 ,這章把hash表的概念和相關題目進行匯總。 ? ? ? ? ? ??
1.1、哈希表和數組、以及鏈表的對比:
(1).數組的特點:尋址容易,插入和刪除困難;數組存儲連續,查找一個元素的時間復雜度為O(1);
(2).鏈表的特點:尋址困難,插入和刪除容易。鏈表存儲區是離散的,遍歷鏈表的元素的時間復雜度為O(N)。
(3).hash-table是根據關鍵值(key-value)來直接進行訪問的數據結構,它結合了數組和鏈表的優點。hash表的難點
在于設計hash函數,以及解決沖突。這里我們會在后面提及;
這里是一些聯系人的信息,如果要存儲這些信息你會怎么做?我們比較直觀的想法是,設計一個結構體,用鏈表來存儲。結構體里面包含一個char型數組存放名字,char字符串存放電話號碼,和一個結構體指針用來存放下個結構體的地址。
[cpp]view plaincopy
張三?13980593357
李四?15828662334
王五?13409821234
張帥?13890583472
當要查找”王五 15828662334“這條記錄是否在這張鏈表中時,可能會從鏈表的頭結點開始遍歷,依次將每個結點中的姓名同”李四“進行比較,直到查找成功或者失敗為止,這種做法的時間復雜度為O(n)。即使采用二叉排序樹進行存儲,也最多為O(logn)。假設能夠通過”王五“這個信息直接獲取到該記錄在表中的存儲位置,就能省掉中間關鍵字比較的這個環節,復雜度直接降到O(1)。Hash表就能夠達到這樣的效果。
Hash表采用一個映射函數 f : key —> address 將關鍵字映射到該記錄在表中的存儲位置,從而在想要查找該記錄時,可以直接根據關鍵字和映射關系計算出該記錄在表中的存儲位置,通常情況下,這種映射關系稱作為Hash函數,而通過Hash函數和關鍵字計算出來的存儲位置(注意這里的存儲位置只是表中的存儲位置,并不是實際的物理地址)稱作為Hash地址。比如上述例子中,假如聯系人信息采用Hash表存儲,則當想要找到“李四”的信息時,直接根據“李四”和Hash函數計算出Hash地址即可。
常見的hash函數有三種,分別是:直接取余法、乘積取整法、平方取中法。下面一一介紹:
直接取余法根據字面意思我們就能理解到,它的基本實現是用關鍵字直接除以散列表的大小(我們一般取跟元素個數最
接近的質數作為散列表的大小)。如果知道Hash表的最大長度為m,可以取不大于m的最大質數p,然后對關鍵字進
行取余運算,h(key)=key%p。很多的書上認為,哈希表的大小最好是選擇一個大的質數,并且最好不要和2的整數冪接近。最不好的選擇是哈希表的大小恰好是2的整數冪。
這里可以這么認為:計算機是用二進制存儲的,當一個二進制數除以一個2的整數冪的時候,結果就是這個二進制數的后幾位,前面的位都丟失了,也就意味著丟失了一部分信息,進而導致哈希表中的元素分布不均勻。為了避免產生沖突,我們可以采用加、乘法、移位等等運算關系來進行處理,然后再取余數,獲得哈希地址。
下面是個例子。
[cpp]view plaincopy
staticintadditiveHash(String?key,intprime)//prime為我們選取的hash表大小。
{
inthash,?i;
for(hash?=?key.length(),?i?=?0;?i?<?key.length();?i++)
?hash?+=?key.charAt(i);
return(hash?%?prime);
}
關鍵字k乘以一個在(0,1)中的實數(最好是無理數),得到一個(0,1)之間的實數;取出其小數部分,乘以m,再取整數部分,即得K在Hash表中的位置。
對關鍵字進行平方運算,然后取結果的中間幾位作為Hash地址。假如有以下關鍵字序列{421,423,436},平
方之后的結果為{177241,178929,190096},那么可以取{72,89,00}作為Hash地址。
2.1"One-Way Hash"算法
這個算法是Blizzard的創作,是一個非常高效的把字符串轉換成整數的算法,舉個例子,字符串"unitneutralacritter.grp",通過這個算法得到的結果是0xA26067F3。
[cpp]view plaincopy
unsignedlongHashString(char*lpszFileName,?unsignedlongdwHashType)
{
unsignedchar*key?=?(unsignedchar*)lpszFileName;
unsignedlongseed1?=?0x7FED7FED,?seed2?=?0xEEEEEEEE;
intch;
while(*key?!=?0)
{
ch?=?toupper(*key++);//toupper是轉換為大寫
seed1?=?cryptTable[(dwHashType?<<?8)?+?ch]?^?(seed1?+?seed2);
seed2?=?ch?+?seed1?+?seed2?+?(seed2?<<?5)?+?3;
}
returnseed1;
}
運用上面的函數就可以把字符串轉化為整數,接下來我們用這個整數就可以通過hash函數產生hash地址了。
[cpp]view plaincopy
intGetHashTablePos(char*lpszString,?SOMESTRUCTURE?*lpTable,intnTableSize)
{
intnHash?=?HashString(lpszString),?nHashPos?=?nHash?%?nTableSize;
if(lpTable[nHashPos].bExists?&&?!strcmp(lpTable[nHashPos].pString,?lpszString))
returnnHashPos;
else
return-1;//Error?value
}
其他的字符串轉換成整數算法,可以查閱相關書籍,這不再深入分析。
最常用的一種解決哈希沖突的方法,我們可以理解為“鏈表的數組”,如圖:
左邊很明顯是個數組,數組的每個成員包括一個指針,指向一個鏈表的頭,當然這個鏈表可能為空,也可能元素很多。我們根據元素的一些特征把元素分配到不同的鏈表中去,也是根據這些特征,找到正確的鏈表,再從鏈表中找出這個元素。
這里給個例子:設有 m = 5 , H(K) = K mod 5 ,關鍵字值序例 5 , 21 , 17 , 9 , 15 , 36 , 41 , 24 ,按外鏈地址法所建立的哈希表如下圖所示:
用開放定址法解決沖突的做法是:當沖突發生時,使用某種探查(亦稱探測)技術在散列表中形成一個探查(測)序列。沿此序列逐個單元地查找,直到找到給定 的關鍵字,或者碰到一個開放的地址(即該地址單元為空)為止(若要插入,在探查到開放的地址,則可將待插入的新結點存人該地址單元)。查找時探查到開放的 地址則表明表中無待查的關鍵字,即查找失敗。
注意:
①用開放定址法建立散列表時,建表前須將表中所有單元(更嚴格地說,是指單元中存儲的關鍵字)置空。
②空單元的表示與具體的應用相關。
按照形成探查序列的方法不同,可將開放定址法區分為線性探查法、線性補償探測法、隨機探測等。
該方法的基本思想是:
將散列表T[0..m-1]看成是一個循環向量,若初始探查的地址為d(即h(key)=d),則最長的探查序列為
d,d+l,d+2,…,m-1,0,1,…,d-
即:探查時從地址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 中刪除一個記錄,按理應將這個記錄所
在位置置為空,但我們不能這樣做,而只能標上已被刪除的標記,否則,將會影響以后的查找。
③ 線性探測法很容易產生堆聚現象。所謂堆聚現象,就是存入哈希表的記錄在表中連成一片。按照線性探測法處
理沖突,如果生成哈希地址的連續序列愈長 ( 即不同關鍵字值的哈希地址相鄰在一起愈長 ) ,則當新的記錄加入該
表時,與這個序列發生沖突的可能性愈大。因此,哈希地址的較長連續序列比較短連續序列生長得快,這就意味
著,一旦出現堆聚 ( 伴隨著沖突 ) ,就將引起進一步的堆聚。
線性補償探測法的基本思想是:
將線性探測的步長從 1 改為 Q ,即將上述算法中的 j = (j + 1) % m 改為: j = (j + Q) % m ,而且要求 Q 與
m 是互質的,以便能探測到哈希表中的所有單元。
【例】 PDP-11 小型計算機中的匯編程序所用的符合表,就采用此方法來解決沖突,所用表長 m = 1321 ,選用
Q = 25 。
隨機探測的基本思想是:
將線性探測的步長從常數改為隨機數,即令: j = (j + RN) % m ,其中 RN 是一個隨機數。在實際程序中應預先
用隨機數發生器產生一個隨機序列,將此序列作為依次探測的步長。這樣就能使不同的關鍵字具有不同的探測次
序,從而可以避 免或減少堆聚。基于與線性探測法相同的理由,在線性補償探測法和隨機探測法中,刪除一個記
錄后也要打上刪除標記。