Hash表

Hash表

  Hash表也稱散列表,也有直接譯作哈希表,Hash表是一種特殊的數據結構,它同數組、鏈表以及二叉排序樹等相比較有很明顯的區別,它能夠快速定位到想要查找的記錄,而不是與表中存在的記錄的關鍵字進行比較來進行查找。這個源於Hash表設計的特殊性,它採用了函數映射的思想將記錄的存儲位置與記錄的關鍵字關聯起來,從而能夠很快速地進行查找。

1.Hash表的設計思想

  對於一般的線性表,比如鏈表,如果要存儲聯繫人信息: 

張三 13980593357
李四 15828662334
王五 13409821234
張帥 13890583472

  那麼可能會設計一個結構體包含姓名,手機號碼這些信息,然後把4個聯繫人的信息存到一張鏈表中。當要查找”李四 15828662334“這條記錄是否在這張鏈表中或者想要得到李四的手機號碼時,可能會從鏈表的頭結點開始遍歷,依次將每個結點中的姓名同”李四“進行比較,直到查找成功或者失敗爲止,這種做法的時間複雜度爲O(n)。即使採用二叉排序樹進行存儲,也最多爲O(logn)。假設能夠通過”李四“這個信息直接獲取到該記錄在表中的存儲位置,就能省掉中間關鍵字比較的這個環節,複雜度直接降到O(1)。Hash表就能夠達到這樣的效果。

  Hash表採用一個映射函數 f : key —> address 將關鍵字映射到該記錄在表中的存儲位置,從而在想要查找該記錄時,可以直接根據關鍵字和映射關係計算出該記錄在表中的存儲位置,通常情況下,這種映射關係稱作爲Hash函數,而通過Hash函數和關鍵字計算出來的存儲位置(注意這裏的存儲位置只是表中的存儲位置,並不是實際的物理地址)稱作爲Hash地址。比如上述例子中,假如聯繫人信息採用Hash表存儲,則當想要找到“李四”的信息時,直接根據“李四”和Hash函數計算出Hash地址即可。下面討論一下Hash表設計中的幾個關鍵問題。

1. Hash函數的設計

  Hash函數設計的好壞直接影響到對Hash表的操作效率。下面舉例說明:

  假如對上述的聯繫人信息進行存儲時,採用的Hash函數爲:姓名的每個字的拼音開頭大寫字母的ASCII碼之和。

  因此address(張三)=ASCII(Z)+ASCII(S)=90+83=173;

    address(李四)=ASCII(L)+ASCII(S)=76+83=159;

    address(王五)=ASCII(W)+ASCII(W)=87+87=174;

    address(張帥)=ASCII(Z)+ASCII(S)=90+83=173;

  假如只有這4個聯繫人信息需要進行存儲,這個Hash函數設計的很糟糕。首先,它浪費了大量的存儲空間,假如採用char型數組存儲聯繫人信息的話,則至少需要開闢174*12字節的空間,空間利用率只有4/174,不到5%;另外,根據Hash函數計算結果之後,address(張三)和address(李四)具有相同的地址,這種現象稱作衝突,對於174個存儲空間中只需要存儲4條記錄就發生了衝突,這樣的Hash函數設計是很不合理的。所以在構造Hash函數時應儘量考慮關鍵字的分佈特點來設計函數使得Hash地址隨機均勻地分佈在整個地址空間當中。通常有以下幾種構造Hash函數的方法:

  1)直接定址法

  取關鍵字或者關鍵字的某個線性函數爲Hash地址,即address(key)=a*key+b;如知道學生的學號從2000開始,最大爲4000,則可以將address(key)=key-2000作爲Hash地址。

  2)平方取中法

  對關鍵字進行平方運算,然後取結果的中間幾位作爲Hash地址。假如有以下關鍵字序列{421,423,436},平方之後的結果爲{177241,178929,190096},那麼可以取{72,89,00}作爲Hash地址。

  3)摺疊法

  將關鍵字拆分成幾部分,然後將這幾部分組合在一起,以特定的方式進行轉化形成Hash地址。假如知道圖書的ISBN號爲8903-241-23,可以將address(key)=89+03+24+12+3作爲Hash地址。

  4)除留取餘法

  如果知道Hash表的最大長度爲m,可以取不大於m的最大質數p,然後對關鍵字進行取餘運算,address(key)=key%p。

  在這裏p的選取非常關鍵,p選擇的好的話,能夠最大程度地減少衝突,p一般取不大於m的最大質數。

2.Hash表大小的確定

  Hash表大小的確定也非常關鍵,如果Hash表的空間遠遠大於最後實際存儲的記錄個數,則造成了很大的空間浪費,如果選取小了的話,則容易造成衝突。在實際情況中,一般需要根據最終記錄存儲個數和關鍵字的分佈特點來確定Hash表的大小。還有一種情況時可能事先不知道最終需要存儲的記錄個數,則需要動態維護Hash表的容量,此時可能需要重新計算Hash地址。

3.衝突的解決

  在上述例子中,發生了衝突現象,因此需要辦法來解決,否則記錄無法進行正確的存儲。通常情況下有2種解決辦法:

  1)開放定址法

  即當一個關鍵字和另一個關鍵字發生衝突時,使用某種探測技術在Hash表中形成一個探測序列,然後沿着這個探測序列依次查找下去,當碰到一個空的單元時,則插入其中。比較常用的探測方法有線性探測法,比如有一組關鍵字{12,13,25,23,38,34,6,84,91},Hash表長爲14,Hash函數爲address(key)=key%11,當插入12,13,25時可以直接插入,而當插入23時,地址1被佔用了,因此沿着地址1依次往下探測(探測步長可以根據情況而定),直到探測到地址4,發現爲空,則將23插入其中。

  2)鏈地址法

   採用數組和鏈表相結合的辦法,將Hash地址相同的記錄存儲在一張線性表中,而每張表的表頭的序號即爲計算得到的Hash地址。如上述例子中,採用鏈地址法形成的Hash表存儲表示爲:   

   雖然能夠採用一些辦法去減少衝突,但是衝突是無法完全避免的。因此需要根據實際情況選取解決衝突的辦法。

4.Hash表的平均查找長度

  Hash表的平均查找長度包括查找成功時的平均查找長度和查找失敗時的平均查找長度。

  查找成功時的平均查找長度=表中每個元素查找成功時的比較次數之和/表中元素個數;

  查找不成功時的平均查找長度相當於在表中查找元素不成功時的平均比較次數,可以理解爲向表中插入某個元素,該元素在每個位置都有可能,然後計算出在每個位置能夠插入時需要比較的次數,再除以表長即爲查找不成功時的平均查找長度。

  下面舉個例子:

  有一組關鍵字{23,12,14,2,3,5},表長爲14,Hash函數爲key%11,則關鍵字在表中的存儲如下:

  地址     0     1     2     3      4     5    6   7   8    9  10   11   12    13

  關鍵字        23    12   14     2     3    5

 比較次數         1      2    1     3     3     2

  因此查找成功時的平均查找長度爲(1+2+1+3+3+2)/6=11/6;

  查找失敗時的平均查找長度爲(1+7+6+5+4+3+2+1+1+1+1+1+1+1)/14=38/14;

  這裏有一個概念裝填因子=表中的記錄數/哈希表的長度,如果裝填因子越小,表明表中還有很多的空單元,則發生衝突的可能性越小;而裝填因子越大,則發生衝突的可能性就越大,在查找時所耗費的時間就越多。因此,Hash表的平均查找長度和裝填因子有關。有相關文獻證明當裝填因子在0.5左右的時候,Hash的性能能夠達到最優。因此,一般情況下,裝填因子取經驗值0.5。

5.Hash表的優缺點

  Hash表存在的優點顯而易見,能夠在常數級的時間複雜度上進行查找,並且插入數據和刪除數據比較容易。但是它也有某些缺點,比如不支持排序,一般比用線性表存儲需要更多的空間,並且記錄的關鍵字不能重複。

 

代碼實現:

複製代碼
/*Hash表,採用數組實現,2012.9.28*/ 

#include<stdio.h>
#define DataType int
#define M 30
 
typedef struct HashNode    
{
    DataType data;    //存儲值 
    int isNull;           //標誌該位置是否已被填充 
}HashTable;

HashTable hashTable[M];

void initHashTable()     //對hash表進行初始化 
{
    int i;
    for(i = 0; i<M; i++)
    {
        hashTable[i].isNull = 1;    //初始狀態爲空 
    }
}

int getHashAddress(DataType key)    //Hash函數 
{
    return key % 29;     //Hash函數爲 key%29 
}

int insert(DataType key)    //向hash表中插入元素 
{
    int address = getHashAddress(key);       
    if(hashTable[address].isNull == 1)  //沒有發生衝突 
    {
        hashTable[address].data = key;
        hashTable[address].isNull = 0;
    }
    else    //當發生衝突的時候 
    {
        while(hashTable[address].isNull == 0 && address<M)
        {
            address++;     //採用線性探測法,步長爲1 
        }
        if(address == M)    //Hash表發生溢出 
            return -1;
        hashTable[address].data = key;
        hashTable[address].isNull = 0;
    }
    return 0;
}

int find(DataType key)      //進行查找 
{
    int address = getHashAddress(key);
    while( !(hashTable[address].isNull == 0 && hashTable[address].data == key && address<M))
    {
        address++;
    } 
    if( address == M)
        address = -1;
    return address;
}


int main(int argc, char *argv[])
{
    int key[]={123,456,7000,8,1,13,11,555,425,393,212,546,2,99,196};
    int i;
    initHashTable();
    for(i = 0; i<15; i++)
    {
        insert(key[i]);
    }
    
    for(i = 0; i<15; i++)
    {
        int address;
        address = find(key[i]);
        printf("%d %d\n", key[i],address);
    }
    return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章