1、散列表(Hash table,也叫哈希表),是根據關鍵碼值(Key value)而直接進行訪問的數據結構。提供了快速的插入和查找操作,其基於數組實現。其基本思想就是將關鍵字key均勻映射到散列表下標0~TableSize-1這個范圍之內的某個數。
2、散列函數構造方法:
1>直接定址法:所謂直接定址法就是說,取關鍵字的某個線性函數值為散列地址,即
優點:簡單、均勻,也不會產生沖突。 缺點:需要事先知道關鍵字的分布情況,適合查找表較小且連續的情況。
由於這樣的限制,在現實應用中,此方法雖然簡單,但卻並不常用。
2>數字分析法:如果關鍵字時位數較多的數字,比如11位的手機號"130****1234",其中前三位是接入號;中間四位是HLR識別號,表示用戶號的歸屬地;后四為才是真正的用戶號。如下圖所示。
如果現在要存儲某家公司的登記表,若用手機號作為關鍵字,極有可能前7位都是相同的,選擇后四位成為散列地址就是不錯的選擇。若容易出現沖突,對抽取出來的數字再進行反轉、右環位移等。總的目的就是為了提供一個散列函數,能夠合理地將關鍵字分配到散列表的各個位置。
數字分析法通過適合處理關鍵字位數比較大的情況,如果事先知道關鍵字的分布且關鍵字的若干位分布比較均勻,就可以考慮用這個方法。
3>平方取中法: 這個方法計算很簡單,假設關鍵字是1234,那么它的平方就是1522756,再抽取中間的3位就是227,用做散列地址。
平方取中法比較適合不知道關鍵字的分布,而位數又不是很大的情況。
4>
3、哈希化:(1)直接將關鍵字作為索引;(2)將字符串轉換成索引:將字符串轉換成ascii碼,然后進行相加;冪的連乘;壓縮可選值。
4、處理散列沖突的方法:
4.1 開放定址法:所謂的開放定址法就是一旦發生了沖突,就去尋找下一個空的散列地址,只要散列表足夠大,空的散列地址總能找到,並將記錄存入。
它的公式為:
比如說,關鍵字集合為{12, 67, 56, 16, 25, 37, 22, 29, 15, 47, 48, 34},表長為12。散列函數f(key) = key mod 12。
當計算前5個數{12, 67, 56, 16, 25}時,都是沒有沖突的散列地址,直接存入,如下表所示。
計算key = 37時,發現f(37) = 1,此時就與25所在的位置沖突。於是應用上面的公式f(37) = (f(37) + 1) mod 12 =2,。於是將37存入下標為2的位置。如下表所示。
接下來22,29,15,47都沒有沖突,正常的存入,如下標所示。
到了48,計算得到f(48) = 0,與12所在的0位置沖突了,不要緊,我們f(48) = (f(48) + 1) mod 12 = 1,此時又與25所在的位置沖突。於是f(48) = (f(48) + 2) mod 12 = 2,還是沖突......一直到f(48) = (f(48) + 6) mod 12 = 6時,才有空位,如下表所示。
把這種解決沖突的開放定址法稱為線性探測法。
考慮深一步,如果發生這樣的情況,當最后一個key = 34,f(key) = 10,與22所在的位置沖突,可是22后面沒有空位置了,反而它的前面有一個空位置,盡管可以不斷地求余后得到結果,但效率很差。因此可以改進di=12, -12, 22, -22.........q2, -q2(q<= m/2),這樣就等於是可以雙向尋找到可能的空位置。對於34來說,取di = -1即可找到空位置了。另外,增加平方運算的目的是為了不讓關鍵字都聚集在某一塊區域。稱這種方法為二次探測法。
還有一種方法,在沖突時,對於位移量di采用隨機函數計算得到,稱之為隨機探測法。
既然是隨機,那么查找的時候不也隨機生成di 嗎?如何取得相同的地址呢?這里的隨機其實是偽隨機數。偽隨機數就是說,如果設置隨機種子相同,則不斷調用隨機函數可以生成不會重復的數列,在查找時,用同樣的隨機種子,它每次得到的數列是想通的,相同的di 當然可以得到相同的散列地址。
總之,開放定址法只要在散列表未填滿時,總是能找到不發生沖突的地址,是常用的解決沖突的方法。
public void insert(Info info) {
//獲得關鍵字
String key = info.getKey();
//關鍵字所自定的哈希數
int hashVal = hashCode(key);
//如果這個索引已經被占用,而且里面是一個未被刪除的數據
while(arr[hashVal] != null && arr[hashVal].getName() != null) {
//進行遞加
++hashVal;
//循環
hashVal %= arr.length;
}
arr[hashVal] = info;
}
public Info find(String key) {
int hashVal = hashCode(key);
while(arr[hashVal] != null) {
if(arr[hashVal].getKey().equals(key)) {
return arr[hashVal];
}
++hashVal;
hashVal %= arr.length;
}
return null;
}
public Info delete(String key) {
int hashVal = hashCode(key);
while(arr[hashVal] != null) {
f(arr[hashVal].getKey().equals(key)) {
Info tmp = arr[hashVal];
tmp.setName(null);
return tmp;
}
++hashVal;
hashVal %= arr.length;
}
return null;
}
4.2 再散列函數法:
對於散列表來說,可以事先准備多個散列函數。
這里RHi 就是不同的散列函數,可以把前面說的除留余數、折疊、平方取中全部用上。每當發生散列地址沖突時,就換一個散列函數計算。
這種方法能夠使得關鍵字不產生聚集,但相應地也增加了計算的時間。
4.3 鏈地址法:
將所有關鍵字為同義詞的記錄存儲在一個單鏈表中,稱這種表為同義詞子表,在散列表中只存儲所有同義詞子表前面的指針。對於關鍵字集合{12, 67, 56, 16, 25, 37, 22, 29, 15, 47, 48, 34},用前面同樣的12為余數,進行除留余數法,可以得到下圖結構。
此時,已經不存在什么沖突換地址的問題,無論有多少個沖突,都只是在當前位置給單鏈表增加結點的問題。
鏈地址法對於可能會造成很多沖突的散列函數來說,提供了絕不會出現找不到地址的保證。當然,這也就帶來了查找時需要遍歷單鏈表的性能損耗。
總結:開放地址法將所有結點均存放在散列表(Hash)T[0..m-1]中,鏈址法將互為同義詞的結點鏈成一個但鏈表,而將此鏈表的頭指針放在散列表T[0..m-1]中。與開放地址相比 鏈地址法有如下優點:1,鏈地址法處理沖突簡單,且無堆積現象,即非同義詞決不會發生沖突,因此平均查找長度較短。2,鏈地址法中鏈表的結點是動態申請的,故它更適合造表前無法確定表長的情況,3,開放定址法為了減少沖突要求填充因子較小,故結點規模較大時會浪費很多空間,而鏈地址法中填充因子可以大於1且結點較大時,拉鏈法中增加的指針域可以忽略不計,因此節省空間,4,鏈地址法構造的散列表刪除結點很方便,只需簡單的刪去鏈表上相應的結點即可。
public class HashTable {
private LinkList[] arr;
public HashTable() {
arr = new LinkList[100];
}
public HashTable(int maxSize) {
arr = new LinkList[maxSize];
}
public void insert(Info info) {
//獲得關鍵字
String key = info.getKey();
//關鍵字所自定的哈希數
int hashVal = hashCode(key);
if(arr[hashVal] == null) {
arr[hashVal] = new LinkList();
}
arr[hashVal].insertFirst(info);
}
public Info find(String key) {
int hashVal = hashCode(key);
return arr[hashVal].find(key).info;
}
public Info delete(String key) {
int hashVal = hashCode(key);
return arr[hashVal].delete(key).info;
}
public int hashCode(String key) {
//int hashVal = 0;
//for(int i = key.length() - 1; i >= 0; i--) {
//int letter = key.charAt(i) - 96;
//hashVal += letter;
//}
//return hashVal;
BigInteger hashVal = new BigInteger("0");
BigInteger pow27 = new BigInteger("1");
for(int i = key.length() - 1; i >= 0; i--) {
int letter = key.charAt(i) - 96;
BigInteger letterB = new BigInteger(String.valueOf(letter));
hashVal = hashVal.add(letterB.multiply(pow27));
pow27 = pow27.multiply(new BigInteger(String.valueOf(27)));
}
return hashVal.mod(new BigInteger(String.valueOf(arr.length))).intValue();
}
}
public class LinkList {
//頭結點
private Node first;
public LinkList() {
first = null;
}
public void insertFirst(Info info) {
Node node = new Node(info);
node.next = first;
first = node;
}
public Node deleteFirst() {
Node tmp = first;
first = tmp.next;
return tmp;
}
public Node find(String key) {
Node current = first;
while(!key.equals(current.info.getKey())) {
if(current.next == null) {
return null;
}
current = current.next;
}
return current;
}
public Node delete(String key) {
Node current = first;
Node previous = first;
while(!key.equals(current.info.getKey())) {
if(current.next == null) {
return null;
}
previous = current;
current = current.next;
}
if(current == first) {
first = first.next;
} else {
previous.next = current.next;
}
return current;
}
}
public class Node {
//數據域
public Info info;
//指針域
public Node next;
public Node(Info info) {
this.info = info;
}
}
參考鏈接:http://blog.chinaunix.net/uid-26548237-id-3480645.html