1. 散列表的基本概念
元素的存儲位置和其關鍵字之間建立某種直接關系,這就是散列查找法。
(1) 散列函數(shù)和散列地址:在記錄的存儲位置p和其關鍵字key之間建立一個確定的對應關系H,使p=H(key),稱這對應關系H為散列函數(shù),p為散列地址。
(2) 散列表:一個有限連續(xù)的地址空間,用以存儲按散列函數(shù)計算得到相應散列地址的數(shù)據(jù)記錄。通常散列表是一個一維數(shù)組,散列地址是數(shù)組的下標。
(3) 沖突和同義詞:對不同的關鍵字可能得帶統(tǒng)一散列地址,即key1 != key2,而H(key1) = H(key2),這種現(xiàn)象稱為沖突。具有相同函數(shù)值得關鍵字對該散列函數(shù)來說稱作同義詞,key1 和key2互稱為同義詞。
2. 散列函數(shù)的構造方法
選取散列函數(shù)的參考:
- 計算散列地址所需的時間;
- 關鍵字長度;
- 散列表大小;
- 關鍵字的分布情況;
- 查找記錄的頻率。
(1) 直接定址法
其實就是直接通過取關鍵的字的某個線性值作為散列地址:f(key)=a*key+b,(a,b為常數(shù))
(2) 數(shù)字分析法
假設某公司的員工登記表以員工的手機號作為關鍵字。手機號一共11位。前3位是接入號,對應不同運營商 的子品牌;中間4位表示歸屬地;最后4位是用戶號。不同手機號前7位相同的可能性很大,所以可以選擇后4 位作為散列地址,或者對后4位反轉(zhuǎn)(1234 -> 4321)、循環(huán)右移(1234 -> 4123)、循環(huán)左移等等之后作為散列地址。
數(shù)字分析法通常適合處理關鍵字位數(shù)比較大的情況,如果事先知道關鍵字的分布且關鍵字的若干位分布比較 均勻,就可以考慮這個方法。
(3) 平方取中法
假設關鍵字是1234,平方之后是1522756,再抽取中間3位227,用作散列地址。平方取中法比較適合于不 知道關鍵字的分布,而位數(shù)又不是很大的情況。
(4) 折疊法
將關鍵字從左到右分割成位數(shù)相等的幾部分,最后一部分位數(shù)不夠時可以短些,然后將這幾部分疊加求和, 并按散列表表長,取后幾位作為散列地址。
比如關鍵字是9876543210,散列表表長是3位,將其分為四組,然后疊加求和:987 + 654 + 321 + 0 = 1962,取后3位962作為散列地址。
折疊法事先不需要知道關鍵字的分布,適合關鍵字位數(shù)較多的情況。
(5) 除留取余數(shù)法
f(key) = key mod p (p≤m),m為散列表長。
這種方法不僅可以對關鍵字直接取模,也可在折疊、平方取中
后再取模。根據(jù)經(jīng)驗,若散列表表長為m,通常p為小于或等于表長(最好接近m)的最小質(zhì)數(shù),可以更好的 減小沖突。
(6) 隨機數(shù)法
f(key) = random(key),這里random是隨機函數(shù)。
當關鍵字的長度不等時,采用這個方法構造散列函數(shù)是比較合適的。
3. 處理沖突的方法
(1) 開放地址法
開放地址就是一旦發(fā)生沖突,就去尋找下一個空的散列地址,只有散列表足夠大,空的散列地址總能找到,并且記錄它。
至于如何尋找下一個空的散列地址,有三種方法
1. 線性探測法
f(key)=(f(key)+d)%m ,其中d取(0,1,2,3,4.....,m-1),m為散列表的長度
如上圖所示,散列表的長度為12,而且我們現(xiàn)在已經(jīng)插入了部分數(shù)據(jù)了,下面我們繼續(xù)插入37,。然后,我們使用散列函數(shù)計算37的散列地址:
f(37)=f(37)%12=1
但是我們發(fā)現(xiàn)1這個位置已經(jīng)存放了25,那么我們就繼續(xù)尋找下一個空的散列地址。
f(37)=(f(37)+1)%12=2
發(fā)現(xiàn)2這個地址沒有內(nèi)容,所以把37插入到這個位置,得如下圖的結(jié)果:
線性探測來解決沖突問題,會造成沖突堆積。所謂的沖突堆積就是比如說剛才的37,它本來是屬于下標1的元素,現(xiàn)在卻占用了下標為2的空間,這會造成待會我們需要存放本來要放在下標為2的元素時,再次發(fā)生沖突,這個沖突會一直傳播下去,造成查找和插入效率都大大減低。
2. 二次探測法
f(key)=(f(key)+d)%m, ,其中d取(0^2,1^2,-1^2,2^2,-2^2,3^2,-3^2,4^2,-4^2...,q^2,-q^2),q<=m/2,m為散列表的長度
其實,這個是對線性探測的一個優(yōu)化,增加了平方可以不讓關鍵字聚集在某一塊區(qū)域。
例如,我們對剛才的那個散列表,插入一個元素:7,通過二次探測的散列函數(shù)計算得到的散列地址為:
f(7)=f(7)%12=7
但是,我發(fā)現(xiàn)下標為7的位置已經(jīng)存放了元素:67,所以我需要尋找下一個存儲地址:
f(7)=(f(7)+1^2)%12=8
突然發(fā)現(xiàn)下標為8的地址也存放了56這個元素,所以我們只能繼續(xù)往下尋找下一個存儲地址:
f(7)=(f(7)+(-1^2))%12=6
發(fā)現(xiàn)下標為6的這個地址空間還是空的,所以我就把7插入到這個位置,得到如下結(jié)果:
3. 隨機探測法
f(key)=(f(key)+d)%m, d為隨機數(shù)列,而m為表長
在實際程序中應預先用隨機數(shù)發(fā)生器產(chǎn)生一個隨機序列,將此序列作為依次探測的步長。這樣就能使不同的關鍵字具有不同的探測次序,從而可以避 免或減少堆聚。
(2) 鏈地址法
所謂的鏈地址法,其實就是當發(fā)生沖突時,我還是把它存放在當前的位置,只是每個位置都是使用鏈表來存放同義詞,這個思路和圖的鄰接表存儲方式很相似。如下圖所示:
4. 散列表的查找
開放地址散列表存儲表示
#define m 20 //散列表的長度
typedef struct{
KeyType key;
InfoType otherinfo;
}HashTable;
- 給定待查找的關鍵字key,根據(jù)造表時設定的散列函數(shù)計算H0=H(key)。
- 若單元H0為空,則所查找元素不存在。
- 若單元H0中元素關鍵字為key,則查找成功。
- 否則重復下述解決沖突的過程:
- 按處理沖突的方法,計算下一個散列地址Hi。
- 若單元Hi為空,則所查找元素不存在。
- 若單元Hi中元素的關鍵字為key,則查找成功。
#define NULLKEY 0
int SearchHash(HashTable HT,KeyType key)
{
H0 = H(key);//根據(jù)散列函數(shù)H(key)計算散列地址
if(HT[H0].key == NULLKEY) return -1;
else if(HT[H0].key == key ) return H0;
else
{
for(i=1;i<m;i++)
{
Hi = (H0+i)%m;//按照線性探測法計算下一個散列地址Hi
if(HT[Hi].key == NULLKEY) return -1;
else if(HT[Hi].key == key ) return Hi;
}
return -1;
}
}