散列表查找定義
散列技術是在記錄的存儲位置和它的關鍵字之間建立一個確定的對應關系f,是的每個關鍵字key對應一個存儲位置f(key)。查找時,根據這個確定的對應關系找到給定值的key的對應f(key)。
我們把這種對應關系f稱為散列函數,又稱哈希(Hash)函數,按這個思想,采用散列技術將記錄存儲在一塊連續的存儲空間中,這塊連續存儲空間成為散列表或哈希表。關鍵字對應的記錄存儲位置我們成為散列地址。
查找時的步驟:
- 在存儲時,通過散列函數計算記錄的散列地址,並按散列地址存儲該記錄。
- 當查找記錄時,通過同樣的散列函數計算記錄的散列地址,按散列地址訪問該記錄
所以說,散列技術既是一種存儲方法,又是一種查找方法,散列技術的記錄之間不存在什么邏輯關系,它只與關鍵字有關。散列技術最適合的求解問題就是查找和給定值相等的記錄。
散列沖突:當兩個關鍵字
!=
,但是卻有f(
) =f(
),這種現象我們叫做散列沖突,並把
和
稱為這個散列函數的同義詞。
散列函數的構造方法
散列函數的構造方法遵循兩個原則:
- 計算簡單
- 散列地址分布均勻
直接定址法
比如我們現在要統計80后出生年份的人口數,那么我們對出生年份這個關鍵字可以用年份減去1980來作為地址。此時f(key)=key-1980。
地址 | 出生年份 | 人數 |
---|---|---|
0 | 1980 | 1500萬 |
01 | 1981 | 1600萬 |
02 | 1982 | 1300萬 |
… | … | … |
2000 | 2000 | 800萬 |
直接定址法就去取關鍵字的某個線性函數值為散列地址
f(key)=a * key +b
這樣的散列函數的優點就是簡單均勻,也不會產生沖突,但問題是需要事先知道關鍵字的分布情況,適合查找表較小且連續的情況。所以這個方法並不常用。
數字分析法
如果我們的關鍵字是位數較多的數字,比如用11位的手機號"130xxxx1234",其中前三位是接入號,中間四位是HLR識別號,后四位才是真正的用戶號,那么我們選擇后四位作為散列地址就是不錯的選擇如果害怕存在沖突現象,我們還可以進行對數字的翻轉,右環位移等方法。
數字分析法通常適合處理關鍵字位數比較大的情況,如果事先知道關鍵字的分布且關鍵字的若干位分布均勻,可以考慮使用這個方法。
平方取中法
假設關鍵字是1234,那么它的平方就是1522756,在抽取中間的三位就是227,用作散列地址。
平方取中法適合不知道關鍵字的分布,而位數又不是很大的情況
折疊法
折疊法是將關鍵字從左到右分割成位數相等的幾部分(注意最后一部分位數不夠可以短些),然后將這幾部分進行疊加求和,並按散列表表長,取后幾位作為散列地址。
比如我們的關鍵字是9876543210,散列表表長為3位,我們將它分為4組,987|654|321|0,然后將他們疊加求和為987+654+321+0=1962,在求后面的三位為散列地址962。
折疊法實現不需要知道關鍵字的分布,適合關鍵字較多的情況。
除留取余法
此方式為最常用的構造函數方法。對於散列表長為m的散列函數公式為:
f(key) = key mod p (p <= m)
mod是取模(求余數)的意思,該方法的關鍵就是在於選取合適的p,p如果選的不好,就可能容易出現同義詞。
一般來說,若散列表表長為m,通常p為小於或等於表長(最好接近m)的最小質數或不包含小於20質因子的合數。
隨機數法
選擇一個隨機數,取關鍵字的隨機函數值為它的散列地址。也就是f(key)=random(key)。這里random是隨機函數,如果關鍵字長度不等的情況下,采用這個方法構造散列函數是比較合適的。
對於字符串的處理,可以將其轉化為ASCII或Unicode碼。
總結
在選取散列函數時應從一下因素考慮:
- 計算散列地址所需的時間
- 關鍵字的長度
- 散列表的大小
- 關鍵字的分布情況
- 記錄查找的頻率
綜合這些因素,才能決策選擇哪種散列函數更合適
處理散列沖突的方法
開放定址法
開放定址法就是一旦發生了沖突,就去尋找下一個空的散列地址,只要散列表足夠大,空的散列地址總能尋找到,並將記錄記下。常用的開放定址法有:線性探查法、二次探查法、隨機探測法
線性探測法
設映射函數為f,表的規模為m,被映射的關鍵字是key。如果在表中散列位置f(key)上發生沖突,那么線性探查法依次檢查位置(f(key) + )mod m, i=1,2,…,直到某個(f(key) + )是空位置,或者(f(key) + )mod m = f(key)結束。
(key) = (f(key) + ) MOD m ( =1, 2, 3, 4, 5, …, m-1)
線性探測法存在一個問題:考慮最壞情況下,所有的存儲值都同一位置存在沖突,每次尋找一個新的位置存儲數據,第一次沖突尋找1次,第二次沖突2次,直到第N-1次沖突,需要尋找N-1次。像這種本來不是同義詞卻需要爭奪一個地址的情況,我們稱為堆積。堆積的出現,使得我們需要不斷的處理沖突,這種堆積效應使得插入和查找的復雜度都變為O(N)。
二次探測法
加入一個散列表最后的key=34,f(key)=10,與它之前的22所在位置沖突,但是22后面沒有空位置了,反而它的前面有一個空位置,盡管我們可以不斷的求余數后得到結果,但是效率很差,因此我們可以改進
=
,
,
,
,…,
,
,(q<=m/2)
這樣就等於可以雙向尋找可能的空位置,另外增加平方運算的目的是為了不讓關鍵字都聚集在某個快區域,我們稱這種方法叫做二次探測法:
設散列函數為f,表的規模為m,要散列的關鍵字為key。那么,如果在散列位置f(key)發生沖突,二次探查法依次檢查位置(f(key) + ),直到某個位置是個空位置,或者已經檢查過的位置。
(key) = (f(key) + ) MOD m ( = , , , ,…, , ,(q<=m/2))
相對線性探查法,二次探查確實可以一定程度避免堆積。但二次探查法最壞情況下,即所有關鍵字在同一個位置沖突下,數組的利用率為1/2。可以證明,對於任意素數N,一旦一個位置被檢查兩次,那么之后的所有位置都是被已檢查過的位置。
//設在i和j結束於相同位置
(h+ i2) mod N = (h+j2) mod N
→ (i+j)(i-j) mod N = 0
//因為N是素數,它必須整除因子(i+j)或(i-j),只有做了N次探查,N才能整除(i-j);同時,使得N整除(i+j)的最小(i+j)為N。
→ i+j = N → j = N - i
//故而不同的探查位置數只能是N/2。
最壞情況的搜索和插入運行時間依舊是O(N)。
隨機探測法
在沖突時,對於位移量 采用隨機函數計算得到的,我們稱之為隨機探測法
(key)= (f(key) + ) MOD m ( 是一個隨機數列)
再散列函數法
對於散列表,可以准備多個散列函數
(key) = (key) (i=1,2,3,…,k)
就是不同的散列函數,每當發生散列地址沖突時,就換一個散列函數計算,這種方法能夠使得關鍵字不產生聚集,但是相應的也增加了計算時間。
鏈地址法(封閉尋址法)
將所有關鍵字為同義詞的記錄存儲在一個單鏈表中,我們稱這種表為同義詞字表,在散列表中只存儲所有同義詞字表的頭指針。在java中java.util.HashMap就采用這樣的設計。java中HashMap是一種字典結構,實現了散列表的功能,存儲(key,value)鍵值對,至少支持get(key)、put(key,value)、delete(key)方法。廣義上來說,列表和二叉查找樹都是字典。
同開放尋址法,最壞的插入和搜索的時間復雜度都是O(n),當然如果是對關鍵字完美散列的散列函數,時間復雜度都是O(1)。
公共溢出法
為所有沖突的關鍵字建立一個公共的溢出區來進行存放。在查找時,對給定值通過散列函數計算出散列地址后,先與基本表的相應位置進行對比,如果相等則查找成功;如果不懂,則到溢出表中進行順序查找,在沖突的數據很少情況下,公共出去的結構對查找性能來說還是很高的。
散列表的查找實現
定義基本結構:
int[] elem; //散列表數據存儲數組
public int count; //散列表實際存儲數據量
private int maxSize = 20; //散列表的最大容量
public final int NULLKEY = -32769; //散列表初始值
public final int SUCCESS = 1;
public final int UNSUCCESS = 0;
對散列表進行初始化:
public HashTable() {
this.elem = new int[maxSize];
this.initHashTable();
}
public HashTable(int maxsize) {
this.maxSize = maxsize;
this.elem = new int[maxSize];
this.initHashTable();
}
public void initHashTable() {
for (int i = 0; i < maxSize; i++) {
this.elem[i]= NULLKEY;
}
}
散列函數:
/** * 散列函數 * 保留余數法 * @param key * @return */
public int Hash(int key) {
return key % maxSize;
}
散列表的插入操作:
public void insertHash(int key) {
int addr = Hash(key); //求散列地址
while(this.elem[addr] != NULLKEY) {
addr = Hash(addr + 1); //開放定址法的線性探測
}
this.elem[addr] = key;
++count;
}
代碼中插入關鍵字首先要計算散列地址,如果當前地址不為空關鍵字,則說明存在沖突,此時我們應該進行重新尋址。
查找記錄:
public int searchHash(int key) {
int addr = Hash(key);
while(this.elem[addr] != key) {
addr = Hash(addr + 1); //開放定址法的線性探測
if(this.elem[addr] == NULLKEY || addr == Hash(key)) { //如果循環回到原點
return UNSUCCESS; //說明關鍵字不存在
}
}
return SUCCESS;
}
測試代碼:
public static void main(String[] args) {
HashTable h = new HashTable();
h.insertHash(5);
h.insertHash(4);
h.insertHash(3);
h.insertHash(6);
h.insertHash(8);
h.insertHash(9);
h.insertHash(1);
h.insertHash(7);
System.out.println("插入數據數量:" + h.count);
System.out.println("是否存在關鍵字為0的值:" + h.searchHash(0));
System.out.println("是否存在關鍵字為9的值:" + h.searchHash(9));
}
結果:
插入數據數量:8
是否存在關鍵字為0的值:0
是否存在關鍵字為9的值:1
散列表查找性能分析
理想情況下散列查找的效率最高為O(1),當然只是理想情況下。。。
散列查找的平均查找長度取決於下面的因素:
- 散列函數是否均勻
散列函數的好壞直接影響着出現沖突的頻繁程度,但是由於不同的散列函數對同一組隨機的關鍵字,產生沖突的可能性是相同的。所以一般不考慮它對平均查找長度的影響。 - 處理沖突的方法
一般來說,線性探測處理沖突可能會產生堆積,顯然沒有二次尋址法好,而鏈地址法處理沖突不會產生任何堆積,因而具有更佳的平均查找性能。 - 散列表的填裝因子
設填裝因子為a,填入表的記錄個個數為m,散列表的長度為n,則:
a = m / n
填裝因子標志着散列表的裝滿的程度,當填入表的記錄越多,a越大,產生沖突的可能性就越大。無論記錄個數m有多大,我們總可以選擇一個合適的填裝因子以便將平均查找長度限定在一個范圍之內,通常我們都是將散列表的空間設置的比查找集合大,雖然浪費一定的空間,但是查找效率大大提升。