程式設計師必讀:教你如何摸清哈希表(hash)的脾氣

1. 相關概念

在哈希表中,記錄的存儲位置 = f (關鍵字),通過查找關鍵字的存儲位置即可,不用進行比較。散列技術是在記錄的存儲位置和它的關鍵字之間建立一個明確的對應關係f 函數,使得每個關鍵字 key 對應一個存儲位置 f(key) 且這個位置是唯一的。這裡我們將這種對應關係 f 稱為散列函數,又稱為哈希(Hash)函數。採用散列技術將記錄存儲在一塊連續的存儲空間中,這塊連續存儲空間稱為散列表或哈希表(Hash table)。

當存儲記錄時,通過散列函數計算出記錄的散列地址;當查找記錄時,我們通過同樣的是散列函數計算記錄的散列地址,並按此散列地址訪問該記錄。散列技術即使一種存儲方法,也是一種查找方法;散列技術之間沒有關係,只有關鍵字和函數之間有關係,所以散列技術是一種面向查找的存儲技術

缺點是會存在關鍵字重複的問題,比如說男女為關鍵字的時候就不合適了。同樣不適合查找範圍的,比如說查找18-20歲之間的同學。散列表技術對於1對1的查找是適合的。

2. 構造散列函數

2.1 兩個基本原則

“好的散列函數 = 計算簡單 + 分佈均勻”。其中計算簡單指的是散列函數的計算時間不應該超過其他查找技術與關鍵字比較的時間,而分佈均勻指的是散列地址分佈均勻。

2.2 具體方法

2.2.1 直接定址法

即使用關鍵字本身作為函數值,即f(key) = key。假如有一個從1到100歲的人口數字統計表,其中,年齡作為關鍵字,哈希函數取關鍵字自身。

如,下圖所示

程序員必讀:教你如何摸清哈希表(hash)的脾氣

又假果現在要統計的是1980年以後出生的人口數,那麼我們對出生年份這個關鍵字可以變換為:用年份減去1980的值來作為地址。即:f(key) = key – 1980

程序員必讀:教你如何摸清哈希表(hash)的脾氣

所以直接定值法是取關鍵字的某個線性函數值為散列地址,即 f(key) = a*key + b。其優點是簡單、均勻,不會產生衝突;但缺點是需要知道關鍵字的分佈情況,希望數值是連續的。

2.2.2 數字分析法

數字分析法通常適合處理關鍵字位數比較大的情況,例如我們現在要存儲某家公司員工登記表,如果用手機號作為關鍵字,那麼我們發現抽取後面的四位數字作為散列地址是不錯的選擇,如下圖所示

程序員必讀:教你如何摸清哈希表(hash)的脾氣

2.2.3 平方取中法

平方取中法是將關鍵字平方之後取中間若干位數字作為散列地址。這種方法適用於不知道關鍵字的分佈,且數值的位數又不是很大的情況。

2.2.4 摺疊法

摺疊法是將關鍵字從左到右分割成位數相等的幾部分,然後將這幾部分疊加求和,並按散列表表長取後幾位作為散列地址。

2.2.5 除留餘數法

此方法為最常用的構造散列函數方法,對於散列表長為m的散列函數計算公式為:

f(key) = key mod p(p<=m)

事實上,這個方法不僅可以對關鍵字直接取模,也可以通過摺疊、平方取中後再取模。例如下表,我們對有12個記錄的關鍵字構造散列表時,就可以用f(key) = key mod 12的方法。

程序員必讀:教你如何摸清哈希表(hash)的脾氣

p的選擇是關鍵,如果對於這個表格的關鍵字,p還選擇12的話,那得到的情況未免也太糟糕了:

程序員必讀:教你如何摸清哈希表(hash)的脾氣

p的選擇很重要,如果我們把p改為11,那結果就另當別論啦:

程序員必讀:教你如何摸清哈希表(hash)的脾氣

當然在上述的這種情況中仍然是有衝突的情況,對於這種情況在後面中會介紹解決的方法。

2.2.6 隨機數法

選擇一個隨機數,取關鍵字的隨機函數值為它的散列地址。

f(key) = random(key)。

這裡的random是隨機函數,當關鍵字的長度不等時,採用這個方法構造散列函數是比較合適的。

2.3 哈希表的選擇

現實中,我們應該視不同的情況採用不同的散列函數,這裡給大家一些參考方向:

(1) 計算散列地址所需的時間;

(2) 關鍵字的長度;

(3) 列表的大小;

(4) 關鍵字的分佈情況;

(5) 記錄查找的頻率。

3. 處理散列衝突的方法

3.1 開放定址法

所謂的開放定址法就是一旦發生了衝突,就去尋找下一個空的散列地址,只要散列表足夠大,空的散列地址總能找到,並將記錄存入。它的公式是:

fi(key) = (f(key)+di) MOD m (di=1,2,…,m-1)

例:假設關鍵字集合為{12, 67, 56, 16, 25, 37, 22, 29, 15, 47, 48, 34},使用除留餘數法(m=12)求散列表

程序員必讀:教你如何摸清哈希表(hash)的脾氣

也可以修改di的取值方式,例如使用平方運算來儘量解決堆積問題:

fi(key) = (f(key)+di) MOD m (di=1²,-1²,2²,-2²…,q²,-q²,q<=m/1)

還有一種方法是,在衝突時,對於位移量di採用隨機函數計算得到,我們稱之為隨機探測法:

fi(key) = (f(key)+di) MOD m (di是由一個隨機函數獲得的數列)

3.2 再散列函數法

同時準備多個散列函數,當第一個散列函數發生衝突的時候可以用備選的散列函數進行計算。

3.3 鏈地址法

例:假設關鍵字集合為{12, 67, 56, 16, 25, 37, 22, 29, 15, 47, 48, 34},同樣使用除留餘數法求散列表,如下圖所示

程序員必讀:教你如何摸清哈希表(hash)的脾氣

在上面個的鏈表中,如果沒有發生衝突的話,元素後面的地址為空;如果有衝突的話就將他鏈接到下一個元素。

3.4 公共溢出區法

例:假設關鍵字集合為{12, 67, 56, 16, 25, 37, 22, 29, 15, 47, 48, 34},同樣使用除留餘數法求散列表,如下圖所示

程序員必讀:教你如何摸清哈希表(hash)的脾氣

沒有衝突的元素放在左邊的表,有衝突的元素,將多餘的元素放在右邊的那個表。

4. 散列表查找的代碼實現

在這裡採用除留餘數法構造散列函數,代碼中還包括散列表的結構定義,散列表的初始化,插入關鍵字和查找關鍵字

#define HASHSIZE 12
#define NULLKEY -32768
// 定義一個散列表的結構
typedef struct
{
int *elem; // 數據元素的基址,動態分配數組
int count; // 當前數據元素的個數
}HashTable;
// 初始化散列表
int InitHashTable(HashTable *H)
{
H->count = HASHSIZE;
H->elem = (int *)malloc(HASHSIZE * sizeof(int));
if( !H->elem )
{
return -1; //申請空間失敗
}
for( i=0; i < HASHSIZE; i++ )
{
H->elem[i] = NULLKEY; //迭代進行初始化,其中的NULLKEY是一個默認值
}
return 0;
}
// 使用除留餘數法
int Hash(int key)
{
return key % HASHSIZE; //除數一般小於等於表長
}
// 插入關鍵字到散列表
void InsertHash(HashTable *H, int key)
{

int addr;
addr = Hash(key); //只是得到一個偏移地址
while( H->elem[addr] != NULLKEY ) // 如果不為空,則衝突出現
{
addr = (addr + 1) % HASHSIZE; // 開放定址法的線性探測
}
H->elem[addr] = key;
}
// 散列表查找關鍵字
int SearchHash(HashTable H, int key, int *addr)
{
*addr = Hash(key);
while( H.elem[*addr] != key )
{
*addr = (*addr + 1) % HASHSIZE;
if( H.elem[*addr] == NULLKEY || *addr == Hash(key) ) //後面那個條件說明循環回到原點
{
return -1;
}
}
return 0;
}

------------------------------------------

PS:碼字不容易,如果對您有幫助的話,幫忙轉發一下,謝謝啦~

會定期分享一些實用工具學習筆記~

程序員必讀:教你如何摸清哈希表(hash)的脾氣


分享到:


相關文章: