數據結構 哈希表(Hash Table)_哈希概述


哈希表支持一種最有效的檢索方法:散列。

從根來上說,一個哈希表包含一個數組,通過特殊的索引值(鍵)來訪問數組中的元素。

哈希表的主要思想是通過一個哈希函數,在所有可能的鍵與槽位之間建立一張映射表。哈希函數每次接受一個鍵將返回與鍵相對應的哈希編碼或哈希值。鍵的數據類型可能多種多樣,但哈希值的類型只能是整型。

計算哈希值和在數組中進行索引都只消耗固定的時間,因此哈希表的最大亮點在於它是一種運行時間在常量級的檢索方法。當哈希函數能夠保證不同的鍵生成的哈希值互不相同時,就說哈希表能直接尋址想要的結果。但這只是理想狀態,在實際運用過程中,能夠直接尋址結果的情況非常少。

通常與各種各樣的鍵相比,哈希表的條目數相應較少。因此,絕大多數的哈希函數會將一些不同的鍵映射到表中相同的槽位上。當兩個鍵映射到一個相同的槽位上時,它們就產生了沖突。一個好的哈希函數能最大限度的減少沖突,但沖突不能完全消除,我們仍要想辦法處理這些沖突。

鏈式哈希表的描述

鏈式哈希表從根本上說是由一組鏈表構成。每個鏈表都可以看做是一個“桶”,我們將所有的元素通過散列的方式放到具體的不同的桶中。插入元素時,首先將其鍵傳入一個哈希函數(該過程稱為哈希鍵),函數通過散列的方式告知元素屬於哪個“桶”,然后在相應的鏈表頭插入元素。查找或刪除元素時,用同們的方式先找到元素的“桶”,然后遍歷相應的鏈表,直到發現我們想要的元素。因為每個“桶”都是一個鏈表,所以鏈式哈希表並不限制包含元素的個數。然而,如果表變得太大,它的性能將會降低。

 

解決沖突

當哈希表中兩個鍵散列到一個相同的槽位時,這兩個鍵之間將會產生沖突。鏈式哈希表解決沖突的方法非常簡單:當沖突發生時,它就將元素放到已經准備好的“桶”中。但這同樣會帶來一個問題,當過多的沖突發生在同一槽位時,此位置的“桶”將會變得越來越深,從而造成訪問這個位置的元素所需要的時間越來越多。

在理想情況下,我們希望所有的“桶"以幾乎同樣的速度增長,這樣它們就可以盡可能的保持小的容量和相同的大小。換句話說,我們的目標就是盡可能的均勻和隨機地分配表中的元素,這種情況在理論上稱為 均勻散列,而在實際中,我們只能盡可能近似達到這種狀態。

如果想插入表中的元素數量遠大於表中的“桶‘的數量,那么即使是在一個均勻散列的過程中,表的性能也會迅速下降。在這種情況下,表中所有的”桶“都變得越來越深。因此,我們必須要特別注意一個哈希表的負載因子,其定義為:

a=n / m

其中n是表中的元素的個數,m是桶的個數。在均勻散列的情況下,鏈式哈希表的負載因子告訴我們表中的”桶“能裝下元素個數的最大值。

例如,有一個鏈式哈希表,其”桶“的數量是m=1699,元素的數量n=3198,其負載因子a=3198/1699=2。所以在這種情況下,當查找元素時,可能每個”桶“里面的元素個數不超過兩個。當有一個表的負載因子小於1時,這個表每個位置所包含的元素不超過1個。當然,由於均勻散列是一個理想的近似的情況,因此在實際情況中我們往往會檢索超過負載因子建議的數值。如何達到更接近於均勻散列的情況,最終取決於如何選擇哈希函數。

 選擇哈希函數

一個好的哈希函數旨在均勻散列,也就是,盡可能以均勻和隨機的方式散布一個哈希表中的元素。定義一個哈希函數,它將鍵k映射到哈希表中的位置x。x稱為k的哈希編碼,正式的表述為:

h(k) = x

一般來說,大多數的散列方法都假設k為整數,這樣k能夠很容易地以數學方式修改,從而使得h能夠更均勻地將元素分布在表中。當k不是一個整數時,我們也可以很容易的將它強制置轉換為整型。

如何強制轉換一組鍵,很大程度上取決於鍵本身的特點。所以,在一個特定的應用中,盡可能地獲取鍵的特性尤為重要。例如,如果我們想對程序中的標識符進行散列,會發現程序中有很多相似的前綴和后綴,因為開發人員傾向於將變量聲明為類似sampleptr、simpleptr和sentryptr的名字。我們可以將鍵嚴格按照鍵的開頭和結尾字符來強制轉換,但這顯然不是一個好辦法,因為對於一個k會有多個整數與之對應。另一方面,我們不妨隨機地從4個位置來選擇字符,然后隨機地改變它們的順序,並將它們封裝到一個4字節的整數中。要記住,無論用什么樣的方法來強制轉換鍵,目的都是盡可能選擇一個能將鍵均勻、隨機地分布到表中的哈希函數。

取余法

 一種簡單地將整型k映射到m槽位的散列方法是計算k除以m所得到的余數。我們稱之為取余法,正式的表述為:

h(k) = k mod m 

如果有m=1699個位置,而要散列的鍵k = 25657,通過這種方法得到哈希編碼為25657 mod 1699 = 172。通常情況下,要避免m的值為2的冪。這是因為假如m=2p   ,那么h僅僅是k的p個最低階位。通常我們選擇的m會是一個素數,且不要太接近於2的冪,同時還要考慮存儲空間的限制和負載因子。

例如,如果我們想往一個鏈式哈希表中插入n=4500個左右的元素,會選擇m=1699(m是一個介於210~211之間的素數)。由此可以計算出它的負載因子a=4500/1699約等於2.6,根據均勻散列表述,這說明表中每個“桶”大概能容納2~3個元素。

乘法

與取余法不同的是乘法。它將整型k乘以一個常數A(0<A<1);取結果的小數部分;然后再乘以m取結果的整數部分。通常情況下,A取0.618,它由5的開平方減1再除以2得到。這個方法稱為乘法,正式的表述為:

h(k) = m(kA mod 1), 其中A約等於0.618

這個方法有個優點是,對於表中槽位個數m的選擇並不需要像取余法中那么慎重。例如:如果表有m=2000個位置,散列的鍵k=6341,那么得到的哈希編碼為2000X(6341X0.618 mod 1) = 2000X(3918.738 mod 1)=2000X0.738=1476。

在鏈式哈希表中,如果期待插入的元素個數n不超過4500個,可以讓m=2250。這樣得到的負載因子a=4500/2250=2,根據均勻散列的規則,在每個“桶”中存儲的元素個數一般不超過兩個。同時,這個散列方法可以讓我們更加靈活地選擇m,以便獲取我們可以接受的最大“桶”深。

示例1列舉了一個能夠較好的處理字符串的哈希函數。它通過一系列的位操作將鍵強制轉換為整數。所有這些整數都是通過取余法得到的。這個哈希函數針對哈希字符串執行的很好。

 示例1:一個適用於處理字符串的哈希函數

 

/*hashpjw.c*/
#include "hashpjw.h"

unsigned int hashpjw(const void *key)
{
    const char *ptr;
    unsigned int val;
    /*通過一系列的位操作,將鍵強制轉換為整數*/
    val=0;
    ptr=key;
    
    while(*ptr != '\0')
    {
        unsigned int tmp;
        val = (val << 4) + (*ptr);
        
        if(tmp = (val & oxf0000000))
        {
            val = val ^ (tmp >> 24);
            val = val ^ tmp;
        }
        
        ptr++;
    }
    /*在實際操作中,使用實際大小代替PRIME_TBLSIZ*/
    return val % PRIME_TBLSIZ;
}

另外一篇文章中,我們將詳細討論鏈式哈希表的接口定義與實現分析。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM