1. 程式人生 > >資料結構之雜湊表(HASH)

資料結構之雜湊表(HASH)

前言

   當我們在程式設計過程中,往往需要對線性表進行查詢操作。在順序表中查詢時,需要從表頭開始,依次遍歷比較a[i]與key的值是否相等,直到相等才返回索引i;在有序表中查詢時,我們經常使用的是二分查詢,通過比較key與a[i]的大小來折半查詢,直到相等時才返回索引i。最終通過索引找到我們要找的元素。
   但是,這兩種方法的效率都依賴於查詢中比較的次數。我們有一種想法,能不能不經過比較,而是直接通過關鍵字key一次得到所要的結果呢?這時,就有了散列表查詢(雜湊表)。

1、什麼是雜湊表

    要說雜湊表,我們必須先了解一種新的儲存方式—雜湊技術。
    雜湊技術是指在記錄的儲存位置和它的關鍵字之間建立一個確定的對應關係f,使每一個關鍵字都對應一個儲存位置。即:儲存位置=f(關鍵字)。這樣,在查詢的過程中,只需要通過這個對應關係f 找到給定值key的對映f(key)。只要集合中存在關鍵字和key相等的記錄,則必在儲存位置f(key)處。我們把這種對應關係f 稱為雜湊函式或雜湊函式。
    按照這個思想,採用雜湊技術將記錄儲存在一塊連續的儲存空間中,這塊連續的儲存空間稱為雜湊表。所得的儲存地址稱為雜湊地址或雜湊地址。

2、雜湊表查詢步驟

   ①、儲存資料時,將資料存入通過雜湊函式計算所得哪那個地址裡面。
   ②、查詢時,使用同一個雜湊函式通過關鍵字key計算出儲存地址,通過該地址即可訪問到查詢的記錄。

3、雜湊衝突

  在理想的情況下,每一個 關鍵字,通過雜湊函式計算出來的地址都是不一樣的。但是在實際情況中,我們常常會碰到兩個關鍵字key1≠key2,但是f(key1) = f(key2), 這種現象稱為衝突,並把key1和key2稱為這個雜湊函式的同義詞。
  衝突的出現會造成查詢上的錯誤,具體解決方法會在後文提到。

4、雜湊函式的構造方法

(1)、原則

  ①、計算簡單;
  ②、雜湊地址分佈均勻。

(2)、構造方法

  ①、直接定址法:不常用
    取關鍵字或關鍵字的某個線性函式值為雜湊地址:
    即:H(key) = key 或 H(key) = a*key+b
    優點:簡單,均勻,不會產生衝突;
    缺點:需要實現直到關鍵字的分佈情況,適合查詢表比較小且連續的情況。
  
  ②、數字分析法
   數字分析法用於處理關鍵字是位數比較多的數字,通過抽取關鍵字的一部分進行操作,計算雜湊儲存位置的方法。
   例如:關鍵字是手機號時,眾所周知,我們的11位手機號中,前三位是接入號,一般對應不同運營商的子品牌;中間四位是HLR識別號,表示使用者號的歸屬地;最後四位才是真正的使用者號,所以我們可以選擇後四位成為雜湊地址,對其在進行相應操作來減少衝突。
   數字分析法適合處理關鍵字位數比較大的情況,事先知道關鍵字的分佈且關鍵字的若干位分佈均勻。
   
  ③、平方取中法
   具體方法很簡單:先對關鍵字取平方,然後選取中間幾位為雜湊地址;取的位數由表長決定,適用於不知道關鍵字的分佈,而位數又不是很大的情況。
  
  ④、摺疊法
   將關鍵字分成位數相同的幾部分(最後一部分位數 可以不同),然後求這幾部分的疊加和(捨去進位),並按照散列表的表長,取後幾位作為雜湊地址。
   適用於關鍵字位數很多,而且關鍵字每一位上數字分佈大致均勻。
  
   ⑤、除留餘數法
   此方法為最常用的構造雜湊函式方法。對於雜湊表長為m的雜湊函式公式為:
   f(key) = key mod p (p <= m)
   此方法不僅可以對關鍵字直接取模,也可以在摺疊、平方取中之後再取模。
   所以,本方法的關鍵在於選擇合適的p,若是p選擇的不好,就可能產生 同義詞;根據前人經驗,若散列表的表長為m,通常p為小於或等於表長(最好接近m)的最小質數或不包含小於20質因子的合數。
  
   ⑥、隨機數法
   選擇一個隨機數,取關鍵字的隨機函式值作為他的雜湊地址。
   即:f(key) = random (key)
   當關鍵字的長度不等時,採用這個方法構造雜湊函式較為合適。當遇到特殊字元的關鍵字時,需要將其轉換為某種數字。

(3)、參考因素

   在實際應用過程中,應該視不同的情況採用不同的雜湊函式。下列是一些參考因素:
    ①計算雜湊地址所需的時間;
    ②關鍵字的長度;
    ③雜湊表的大小;
    ④關鍵字的分佈情況;
    ⑤查詢的頻率。
   選擇雜湊函式時,我們應該綜合以上因素,選擇合適的構建雜湊函式的方法。

5、雜湊衝突的解決

   前文提到,雜湊衝突不能避免,所以我們需要找到方法來解決它。
   雜湊衝突的解決方案主要有四種:開放地址法;再雜湊;鏈地址法;公共溢位區法。

(1)、開放地址法  

   開放地址法就是指:一旦發生了衝突就去尋找下一個空的雜湊地址,只要雜湊表足夠大,空的雜湊地址總能找到,並將記錄存入。
   公式:Hi=(H(*key) + Di) mod m (i = 1,2,3,….,k k<=m-1)
   其中:H(key)為雜湊函式;m為雜湊表表長;Di為增量序列,有以下3中取法:
     ①Di = 1,2,3,…,m-1, 稱為線性探測再雜湊;
     ②Di = 1²,-1²,2²,-2²,。。。,±k²,(k<= m/2)稱為二次探測再雜湊
     ③Di = 偽隨機數序列,稱為偽隨機數探測再雜湊。
     例如:在長度為12的雜湊表中插入關鍵字為38的記錄:
     這裡寫圖片描述
     從上述線性探測再雜湊的過程中可以看出一個現象:當表中i、i+1位置上有記錄時,下一個雜湊地址為i、i+1、i+2的記錄都將填入i+3的位置,這種本不是同義詞卻要爭奪同一個地址的現象叫“堆積“。即在處理同義詞的衝突過程中又添加了非同義詞的衝突;但是,用線探測再雜湊處理衝突可以保證:只要雜湊表未填滿,總能找到一個不發生衝突的地方。

(2)、再雜湊法

     公式:Hi = RHi(key) i = 1,2,…,k
     RHi均是不同的雜湊函式,意思為:當繁盛衝突時,使用不同的雜湊函式計算地址,直到不衝突為止。這種方法不易產生堆積,但是耗費時間。

(3)、鏈地址法

     將所有關鍵字為同義字的記錄儲存在一個單鏈表中,我們稱這種單鏈表為同義詞子表,散列表中儲存同義詞子表的頭指標。
     如關鍵字集合為{19,14,23,01,68,20,84,27,55,11,10,79},按雜湊函式H(key) = key mod 13;
     這裡寫圖片描述
     鏈地址法解決了衝突,提供了永遠都能找到地址的保證。但是,也帶來了查詢時需要遍歷單鏈表的效能損耗。

(4)、公共溢位區法

     即設立兩個表:基礎表和溢位表。將所有關鍵字通過雜湊函式計算出相應的地址。然後將未發生衝突的關鍵字放入相應的基礎表中,一旦發生衝突,就將其依次放入溢位表中即可。
     在查詢時,先用給定值通過雜湊函式計算出相應的雜湊地址後,首先 首先與基本表的相應位置進行比較,如果不相等,再到溢位表中順序查詢。

6、雜湊表查詢演算法的實現

   首先定義一個散列表的結構以及一些相關的常數。其中,HashTables是散列表結構。結構當中的elem為一個動態陣列。

#define SUCCESS 1
#define UNSUCCESS 0
#define HASHSIZE 12    /*定義雜湊表長為陣列的長度*/
#define NULLKEY -32768
{
    int *elem;        /*陣列元素儲存基址,動態分配陣列*/
    int count;        /*當前資料元素的個數*/
}HashTable;
int m = 0;            

初始化雜湊表

/*初始化雜湊表*/
Status InitHashTable(HashTable *H)
{
    int i;
    m = HASHSIZE;
    H->count = m;
    H->elem = (int *)malloc(m*sizeof(int));
    for(i = 0;i<m;i++)
        H->elem[i] = NULLKEY;

    return OK;
}   

定義雜湊函式

/*雜湊函式*/
int Hash(int key)
{
    return key % m;     /*除留取餘法*/
}

插入操作

/*將關鍵字插入散列表*/
void InsertHash(HashTable *H,int key)
{
     int addr = Hash(Key);             /*求雜湊地址*/
     while(H->elem[addr] != NULLKEY)         /*如果不為空則衝突*/
         addr = (addr + 1) % m;           /*線性探測*/
     H->elem[addr] = key;            /*直到有空位後插入關鍵字*/        
}   

查詢操作

/*查詢*/
Status SearchHash(HashTable H,int key,int *addr)
{
    *addr = Hash(key);        /*求雜湊地址*/
    while(H.elem[*addr] != key)        /*若不為空,則衝突*/
    {
        *addr = (*addr + 1) % m;         /*線性探測*/
        if(H.elem[*addr) == NULLKEY || *addr == Hash(key))
        {/*如果迴圈回到原點*/
            return UNSUCCESS;        /*則說明關鍵字不存在*/
        }
    }
    return SUCCESS;
}   

7、總結

  1、雜湊表就是一種以鍵值對儲存資料的結構。
  2、雜湊表是一個在空間和時間上做出權衡的經典例子。如果沒有記憶體限制,那麼可以
直接將鍵作為陣列的索引。那麼所查詢的時間複雜度為O(1);如果沒有時間限制,那麼我們可以使用無序陣列並進行順序查詢,這樣只需要很少的記憶體。雜湊表使用了適度的時間和空間來在這兩個極端之間找到了平衡。只需要調整雜湊函式演算法即可在時間和空間上做出取捨。