前言
當我們在編程過程中,往往需要對線性表進行查找操作。在順序表中查找時,需要從表頭開始,依次遍歷比較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);如果沒有時間限制,那麼我們可以使用無序數組並進行順序查找,這樣只需要很少的內存。哈希表使用了適度的時間和空間來在這兩個極端之間找到了平衡。只需要調整哈希函數算法即可在時間和空間上做出取捨。