數據結構與算法18—哈希表(散列表)

哈希表的概念數組

哈希表(Hash Table)是一種特殊的數據結構,它最大的特色就是能夠快速實現查找、插入和刪除。數據結構

咱們知道,數組的最大特色就是:尋址容易,插入和刪除困難;而鏈表正好相反,尋址困難,而插入和刪除操做容易。那麼若是可以結合二者的優勢,作出一種尋址、插入和刪除操做一樣快速容易的數據結構,那該有多好。這就是哈希表建立的基本思想,而實際上哈希表也實現了這樣的一個「夙願」,哈希表就是這樣一個集查找、插入和刪除操做於一身的數據結構。函數

 

哈希表(Hash Table):也叫散列表,是根據關鍵碼值(key-value)而直接進行訪問的數據結構,也就是咱們經常使用到的map。加密

哈希函數:也稱爲是散列函數,是Hash表的映射函數,它能夠把任意長度的輸入變換成固定長度的輸出,該輸出就是哈希值。哈希函數能使對一個數據序列的訪問過程變得更加迅速有效,經過哈希函數,數據元素可以被很快的進行定位。spa

 

哈希表和哈希函數的標準定義:若關鍵字爲k,則其值存放在h(k)的存儲位置上。由此,不需比較即可直接取得所查記錄。稱這個對應關係f爲哈希函數,按這個思想創建的表爲哈希表。設計

 

設計出一個簡單、均勻、存儲利用率高的散列函數是散列技術中最關鍵的問題。
可是,通常散列函數都面臨着衝突的問題。兩個不一樣的關鍵字,因爲散列函數值相同,於是被映射到同一表位置上。該現象稱爲衝突(Collision)或碰撞。發生衝突的兩個關鍵字稱爲該散列函數的同義詞(Synonym)。
3d

 

設m和n分別表示表長和表中填入的結點數,則將α=n/m定義爲散列表的裝填因子(Load Factor)。α越大,表越滿,衝突的機會也越大。一般取α≤1。指針

 

哈希表的實現方法

哈希表的實現就是映射函數構造,看某個元素具體屬於哪個類別。如何構造咱們要考慮兩個問題:code

  • n個數據原僅佔用n個地址,雖然散列查找是以空間換時間,但仍但願散列的地址空間儘可能小。
  • 不管用什麼方法存儲,目的都是儘可能均勻地存放元素,以免衝突。blog

因此,我哈希表的映射函數構造方法也有不少,常見的有:直接定址法、 除留餘數法、 乘餘取整法、 數字分析法、 平方取中法、 摺疊法、 隨機數法等

 

一、直接定位法

Hash(key) = a·key + b (a、b爲常數)

優勢:以關鍵碼key的某個線性函數值爲哈希地址,不會產生衝突.

缺點:要佔用連續地址空間,空間效率低。

例:關鍵碼集合爲{100,300,500,700,800,900}, 選取哈希函數爲Hash(key)=key/100, 則存儲結構(哈希表)以下:

 

二、除留餘數法

Hash(key) = key mod p (p是一個整數)

特色:以關鍵碼除以p的餘數做爲哈希地址。

關鍵:如何選取合適的p?

技巧:若設計的哈希表長爲m,則通常取p≤m且爲質數 (也能夠是不包含小於20質因子的合數)。

 

三、乘餘取整法

Hash(key) = [B*( A*key mod 1 ) ]下取整  (A、B均爲常數,且0<A<1,B爲整數)

特色:以關鍵碼key乘以A,取其小數部分,而後再放大B倍並取整,做爲哈希地址。

例:欲以學號最後兩位做爲地址,則哈希函數應爲: H(k)=100*(0.01*k % 1 ) 其實也能夠用法2實現: H(k)=k % 100

 

四、數字分析法

特色:某關鍵字的某幾位組合成哈希地址。所選的位應當是:各類符號在該位上出現的頻率大體相同。

例:有一組(例如80個)關鍵碼,其樣式以下:

 

五、平方取中法

特色:對關鍵碼平方後,按哈希表大小,取中間的若干位做爲哈希地址。

理由:由於中間幾位與數據的每一位都相關。

:2589的平方值爲6702921,能夠取中間的029爲地址。

 

六、摺疊法

特色:將關鍵碼自左到右分紅位數相等的幾部分(最後一部分位數能夠短些),而後將這幾部分疊加求和,並按哈希表表長,取後幾位做爲哈希地址。

適用於:每一位上各符號出現機率大體相同的狀況。

法1:移位法 ── 將各部分的最後一位對齊相加。

法2:間界疊加法──從一端向另外一端沿分割界來回摺疊後,最後一位對齊相加。

例:元素42751896, 用法1: 427+518+96=1041      用法2: 427 518 96—> 724+518+69 =1311

 

哈希表定址與解決衝突

Hash表解決衝突的方法主要有如下幾種:

開放定址法(開地址法)、 鏈地址法(拉鍊法)、 再哈希法(雙哈希函數法)、 創建一個公共溢出區,而最經常使用的就是開發定址法鏈地址法

 

一、開放定址法

若是兩個數據元素的哈希值相同,則在哈希表中爲後插入的數據元素另外選擇一個表項。當程序查找哈希表時,若是沒有在第一個對應的哈希表項中找到符合查找要求的數據元素,程序就會繼續日後查找,直到找到一個符合查找要求的數據元素,或者遇到一個空的表項。線性探測帶來的最大問題就是衝突的堆積,你把別人預約的坑佔了,別人也就要像你同樣去找坑。改進的辦法有二次方探測法和隨機數探測法。開放地址法包括線性探測二次探測以及雙重散列等方法。

設計思路:有衝突時就去尋找下一個空的哈希地址,只要哈希表足夠大,空的哈希地址總能找到,並將數據元素存入。

含義:一旦衝突,就找附近(下一個)空地址存入。

具體實現:

1) 線性探測法

Hi=(Hash(key)+di) mod m  ( 1≤i < m )    其中: Hash(key)爲哈希函數  m爲哈希表長度  di 爲增量序列 1,2,…m-1,且di=i

例:

關鍵碼集爲 {47,7,29,11,16,92,22,8,3},

設:哈希表表長爲m=11; 哈希函數爲Hash(key)=key mod 11; 擬用線性探測法處理衝突。建哈希表以下:

 

解釋:

 ① 4七、7(以及十一、1六、92)均是由哈希函數獲得的沒有衝突的哈希地址;

② Hash(29)=7,哈希地址有衝突,需尋找下一個空的哈希地址:由H1=(Hash(29)+1) mod 11=8,哈希地址8爲空,所以將29存入。

③ 另外,2二、八、3一樣在哈希地址上有衝突,也是由H1找到空的哈希地址的。其中3 還連續移動了兩次(二次彙集)

int FindHash(SeqList* pL, KeyType K) 
{
    int c=0;  int p=Hash(K); /*求得哈希地址*/
    while(pL->data[p].key!=NULL_KEY && K!=pL->data[p].key && ++c<MAXNUM)    p=Hash(K+c);
    if(K==pL->data[p].key) {
        printf("\n成功找到 %d", K);
        return p; /*查找成功,p返回待查數據元素下標*/
    }
    else if(pL->data[p].key==NULL_KEY) {
       printf("\n沒法找到 %d , 在位置 %d 插入。", K,p);
       pL->data[p].key = K;   pL->n++;
       return p;
    } else {
       printf("\n沒法找到 %d , 表已滿。", K);
       return -1;
    }
}

討論:

線性探測法的優勢只要哈希表未被填滿,保證能找到一個空地址單元存放有衝突的元素;

線性探測法的缺點:可能使第i個哈希地址的同義詞存入第i+1個哈希地址,這樣本應存入第i+1個哈希地址的元素變成了第i+2個哈希地址的同義詞,……,所以,可能出現不少元素在相鄰的哈希地址上「堆積」起來,大大下降了查找效率。

解決方案:可採用二次探測法或僞隨機探測法,以改善「堆積」問題。

 

2) 二次探測法

仍舉上例,改用二次探測法處理衝突,建表以下:

Hi=(Hash(key)±di) mod m   其中:Hash(key)爲哈希函數 m爲哈希表長度,m要求是某個4k+3的質數; di爲增量序列 12,-12,22,-22,…,q2

注:只有3這個關鍵碼的衝突處理與上例不一樣, Hash(3)=3,哈希地址上衝突,由 H1=(Hash(3)+12) mod 11=4,仍然衝突; H2=(Hash(3)-12) mod 11=2,找到空的哈希地址,存入。

 

 2、鏈地址法(拉鍊法)

基本思想:將具備相同哈希地址的記錄鏈成一個單鏈表,m個哈希地址就設m個單鏈表,而後用一個數組將m個單鏈表的表頭指針存儲起來,造成一個動態的結構。

注:有衝突的元素能夠插在表尾,也能夠插在表頭

:設{ 47, 7, 29, 11, 16, 92, 22, 8, 3, 50, 37, 89 }的哈希函數爲: Hash(key)=key mod 11, 用拉鍊法處理衝突,則建表以下圖所示。

 

三、再哈希法(雙哈希函數法)

Hi=RHi(key)     i=1, 2, …,k

RHi均是不一樣的哈希函數,當產生衝突時就計算另外一個哈希函數,直到衝突再也不發生。

優勢:不易產生彙集;

缺點:增長了計算時間。

 

4. 創建一個公共溢出區

思路:除設立哈希基本表外,另設立一個溢出向量表。 全部關鍵字和基本表中關鍵字爲同義詞的記錄,無論它們由哈希函數獲得的地址是什麼,一旦發生衝突,都填入溢出表。

這個方法其實就更加好理解,你不是衝突嗎? 好吧,凡是衝突的都跟我走,我給大家這些衝突找個地兒待着。這就如同孤兒院收留全部無家可歸的孩子 樣,咱們爲全部衝突的關鍵字創建了一個公共的溢出區來存放.

 

哈希表的查找及分析

哈希查找過程

哈希表的主要目的是用於快速查找,且插入和刪除操做都要用到查找。因爲散列表的特殊組織形式,其查找有特殊的方法。 設散列爲HT[0…m-1],散列函數爲H(key),解決衝突的方法爲R(x, i) ,則在散列表上查找定值爲K的記錄的過程如圖所示。

 

查找效率分析

明確:散列函數沒有「萬能」通式,要根據元素集合的特性而分別構造。

討論:哈希查找的速度是否爲真正的O(1)?

不是。因爲衝突的產生,使得哈希表的查找過程仍然要進行比較,仍然要以平均查找長度ASL來衡量。 通常地,ASL依賴於哈希表的裝填因子,它標誌着哈希表的裝滿程度。

 

討論:

1) 散列存儲的查找效率究竟是多少?

答:ASL與裝填因子α有關!既不是嚴格的O(1),也不是O(n)

2)「衝突」是否是特別討厭?

答:不必定!正由於有衝突,使得文件加密後沒法破譯(不可逆,是單向散列函數,可用於數字簽名)。 利用了哈希表性質:源文件稍稍改動,會致使哈希表變更很大。

 

 練習: 

給定關鍵字序列11,78,10,1,3,2,4,21,試分別用順序查找、二分查找、二叉排序樹查找、散列查找(用線性探查法和拉鍊法)來實現查找,試畫出它們的對應存儲形式(順序查找的順序表,二分查找的斷定樹,二叉排序樹查找的二叉排序樹及兩種散列查找的散列表),並求出每一種查找的成功平均查找長度。散列函數H(k)=k%11

 

1)  順序查找的成功平均查找長度爲: ASL=(1+2+3+4+5+6+7+8)/8=4.5

 

2)  二分查找的斷定樹(中序序列爲從小到大排列的有序序列)如圖所示

從上圖能夠獲得二分查找的成功平均查找長度爲: ASL=(1+2*2+3*4+4)/8=2.625

 

3)  二叉排序樹(關鍵字順序已肯定,該二叉排序樹應惟一)如圖 所示

從圖能夠獲得二叉排序樹查找的成功平均查找長度爲: ASL=(1+2*2+3*2+4+5*2)=3.125

 

4)  線性探查法解決衝突的散列表如圖所示

從圖能夠獲得線性探查法的成功平均查找長度爲: ASL=(1+1+2+1+3+2+1+8)/8=2.375

 

5)  拉鍊法解決衝突的散列表 如圖所示

從圖能夠獲得拉鍊法的成功平均查找長度爲: ASL=(1*6+2*2)/8=1.25

相關文章
相關標籤/搜索