讀完這篇,你一定能真正理解什麼是雜湊表
本文全是乾貨,讀完希望對你有所幫助~
雜湊表也稱為散列表,是根據關鍵字值(key value)而直接進行訪問的資料結構。也就是說,它通過把關鍵字值對映到一個位置來訪問記錄,以加快查詢的速度。這個對映函式稱為雜湊函式(也稱為雜湊函式),對映過程稱為雜湊化,存放記錄的陣列叫做散列表。比如我們可以用下面的方法將關鍵字對映成陣列的下標:
arrayIndex=hugeNumber%arraySize
在這裡我還是要推薦下我自己建的大資料學習交流裙:805127855, 裙 裡都是學大資料開發的,如果你正在學習大資料 ,小編關的),包括我自己整理的一份2018最新的大資料進階資料和高階開歡迎你加入,大家都是軟體開發黨,不定期分享乾貨(只有大資料開發相發教程,歡迎進階中和進想深入大資料的小夥伴。
列地址,即同一個陣列下標,這種現象稱為衝突,那麼我們該如何去處理衝突呢?一種方法是開放地址法,即通過系統的方法找到陣列的另一個空位,把資料填入,而不再用雜湊函式得到的陣列下標,因為該位置已經有資料了;另一種方法是建立一個存放連結串列的陣列,陣列內不直接儲存資料,這樣當發生衝突時,新的資料項直接接到這個陣列下標所指的連結串列中,這種方法叫做鏈地址法。下面針對這兩種方法進行討論。
1.開放地址法
1.1 線性探測法
所謂線性探測,即線性地查詢空白單元。我舉個例子,如果21是要插入資料的位置,但是它已經被佔用了,那麼就是用22,然後23,以此類推。陣列下標一直遞增,直到找到空白位。下面是基於線性探測法的雜湊表實現程式碼:
-
public class HashTable {
-
private DataItem[] hashArray; //DateItem類是資料項,封裝資料資訊
-
private int arraySize;
-
private int itemNum; //陣列中目前儲存了多少項
-
private DataItem nonItem; //用於刪除項的
-
public HashTable() {
-
arraySize = 13;
-
hashArray = new DataItem[arraySize];
-
nonItem = new DataItem(-1); //deleted item key is -1
-
}
-
public boolean isFull() {
-
return (itemNum == arraySize);
-
}
-
public boolean isEmpty() {
-
return (itemNum == 0);
-
}
-
public void displayTable() {
-
System.out.print("Table:");
-
for(int j = 0; j < arraySize; j++) {
-
if(hashArray[j] != null) {
-
System.out.print(hashArray[j].getKey() + " ");
-
}
-
else {
-
System.out.print("** ");
-
}
-
}
-
System.out.println("");
-
}
-
public int hashFunction(int key) {
-
return key % arraySize; //hash function
-
}
-
-
public void insert(DataItem item) {
-
if(isFull()) {
-
//擴充套件雜湊表
-
System.out.println("雜湊表已滿,重新雜湊化..");
-
extendHashTable();
-
}
-
int key = item.getKey();
-
int hashVal = hashFunction(key);
-
while(hashArray[hashVal] != null && hashArray[hashVal].getKey() != -1) {
-
++hashVal;
-
hashVal %= arraySize;
-
}
-
hashArray[hashVal] = item;
-
itemNum++;
-
}
-
/*
-
* 陣列有固定的大小,而且不能擴充套件,所以擴充套件雜湊表只能另外建立一個更大的陣列,然後把舊陣列中的資料插到新的陣列中。但是雜湊表是根據陣列大小計算給定資料的位置的,所以這些資料項不能再放在新陣列中和老陣列相同的位置上,因此不能直接拷貝,需要按順序遍歷老陣列,並使用insert方法向新陣列中插入每個資料項。這叫重新雜湊化。這是一個耗時的過程,但如果陣列要進行擴充套件,這個過程是必須的。
-
*/
-
public void extendHashTable() { //擴充套件雜湊表
-
int num = arraySize;
-
itemNum = 0; //重新記數,因為下面要把原來的資料轉移到新的擴張的陣列中
-
arraySize *= 2; //陣列大小翻倍
-
DataItem[] oldHashArray = hashArray;
-
hashArray = new DataItem[arraySize];
-
for(int i = 0; i < num; i++) {
-
insert(oldHashArray[i]);
-
}
-
}
-
public DataItem delete(int key) {
-
if(isEmpty()) {
-
System.out.println("Hash table is empty!");
-
return null;
-
}
-
int hashVal = hashFunction(key);
-
while(hashArray[hashVal] != null) {
-
if(hashArray[hashVal].getKey() == key) {
-
DataItem temp = hashArray[hashVal];
-
hashArray[hashVal] = nonItem; //nonItem表示空Item,其key為-1
-
itemNum--;
-
return temp;
-
}
-
++hashVal;
-
hashVal %= arraySize;
-
}
-
return null;
-
}
-
-
public DataItem find(int key) {
-
int hashVal = hashFunction(key);
-
while(hashArray[hashVal] != null) {
-
if(hashArray[hashVal].getKey() == key) {
-
return hashArray[hashVal];
-
}
-
++hashVal;
-
hashVal %= arraySize;
-
}
-
return null;
-
}
-
}
-
class DataItem {
-
private int iData;
-
public DataItem (int data) {
-
iData = data;
-
}
-
public int getKey() {
-
return iData;
-
}
-
}
線性探測有個弊端,即資料可能會發生聚集。一旦聚集形成,它會變得越來越大,那些雜湊化後落在聚集範圍內的資料項,都要一步步的移動,並且插在聚集的最後,因此使聚集變得更大。聚集越大,它增長的也越快。這就導致了雜湊表的某個部分包含大量的聚集,而另一部分很稀疏。
為了解決這個問題,我們可以使用二次探測:二次探測是防止聚集產生的一種方式,思想是探測相隔較遠的單元,而不是和原始位置相鄰的單元。線性探測中,如果雜湊函式計算的原始下標是x, 線性探測就是x+1, x+2, x+3, 以此類推;而在二次探測中,探測的過程是x+1, x+4, x+9, x+16,以此類推,到原始位置的距離是步數的平方。二次探測雖然消除了原始的聚集問題,但是產生了另一種更細的聚集問題,叫二次聚集:比如講184,302,420和544依次插入表中,它們的對映都是7,那麼302需要以1為步長探測,420需要以4為步長探測, 544需要以9為步長探測。只要有一項其關鍵字對映到7,就需要更長步長的探測,這個現象叫做二次聚集。
二次聚集不是一個嚴重的問題,但是二次探測不會經常使用,因為還有好的解決方法,比如再雜湊法。
1.2 再雜湊法
為了消除原始聚集和二次聚集,現在需要的一種方法是產生一種依賴關鍵字的探測序列,而不是每個關鍵字都一樣。即:不同的關鍵字即使對映到相同的陣列下標,也可以使用不同的探測序列。再雜湊法就是把關鍵字用不同的雜湊函式再做一遍雜湊化,用這個結果作為步長,對於指定的關鍵字,步長在整個探測中是不變的,不同關鍵字使用不同的步長、經驗說明,第二個雜湊函式必須具備如下特點:
-
和第一個雜湊函式不同;
-
不能輸出0(否則沒有步長,每次探索都是原地踏步,演算法將進入死迴圈)。
專家們已經發現下面形式的雜湊函式工作的非常好:
stepSize=constant-key%constant
其中 constant 是質數,且小於陣列容量。
再雜湊法要求表的容量是一個質數,假如表長度為15(0-14),非質數,有一個特定關鍵字對映到0,步長為5,則探測序列是 0,5,10,0,5,10,以此類推一直迴圈下去。演算法只嘗試這三個單元,所以不可能找到某些空白單元,最終演算法導致崩潰。如果陣列容量為13, 質數,探測序列最終會訪問所有單元。即 0,5,10,2,7,12,4,9,1,6,11,3,一直下去,只要表中有一個空位,就可以探測到它。下面看看再雜湊法的程式碼:
-
public class HashDouble {
-
private DataItem[] hashArray;
-
private int arraySize;
-
private int itemNum;
-
private DataItem nonItem;
-
public HashDouble() {
-
arraySize = 13;
-
hashArray = new DataItem[arraySize];
-
nonItem = new DataItem(-1);
-
}
-
public void displayTable() {
-
System.out.print("Table:");
-
for(int i = 0; i < arraySize; i++) {
-
if(hashArray[i] != null) {
-
System.out.print(hashArray[i].getKey() + " ");
-
}
-
else {
-
System.out.print("** ");
-
}
-
}
-
System.out.println("");
-
}
-
public int hashFunction1(int key) { //first hash function
-
return key % arraySize;
-
}
-
-
public int hashFunction2(int key) { //second hash function
-
return 5 - key % 5;
-
}
-
-
public boolean isFull() {
-
return (itemNum == arraySize);
-
}
-
public boolean isEmpty() {
-
return (itemNum == 0);
-
}
-
public void insert(DataItem item) {
-
if(isFull()) {
-
System.out.println("雜湊表已滿,重新雜湊化..");
-
extendHashTable();
-
}
-
int key = item.getKey();
-
int hashVal = hashFunction1(key);
-
int stepSize = hashFunction2(key); //用hashFunction2計算探測步數
-
while(hashArray[hashVal] != null && hashArray[hashVal].getKey() != -1) {
-
hashVal += stepSize;
-
hashVal %= arraySize; //以指定的步數向後探測
-
}
-
hashArray[hashVal] = item;
-
itemNum++;
-
}
-
public void extendHashTable() {
-
int num = arraySize;
-
itemNum = 0; //重新記數,因為下面要把原來的資料轉移到新的擴張的陣列中
-
arraySize *= 2; //陣列大小翻倍
-
DataItem[] oldHashArray = hashArray;
-
hashArray = new DataItem[arraySize];
-
for(int i = 0; i < num; i++) {
-
insert(oldHashArray[i]);
-
}
-
}
-
public DataItem delete(int key) {
-
if(isEmpty()) {
-
System.out.println("Hash table is empty!");
-
return null;
-
}
-
int hashVal = hashFunction1(key);
-
int stepSize = hashFunction2(key);
-
while(hashArray[hashVal] != null) {
-
if(hashArray[hashVal].getKey() == key) {
-
DataItem temp = hashArray[hashVal];
-
hashArray[hashVal] = nonItem;
-
itemNum--;
-
return temp;
-
}
-
hashVal += stepSize;
-
hashVal %= arraySize;
-
}
-
return null;
-
}
-
public DataItem find(int key) {
-
int hashVal = hashFunction1(key);
-
int stepSize = hashFunction2(key);
-
while(hashArray[hashVal] != null) {
-
if(hashArray[hashVal].getKey() == key) {
-
return hashArray[hashVal];
-
}
-
hashVal += stepSize;
-
hashVal %= arraySize;
-
}
-
return null;
-
}
-
}
2. 鏈地址法
在開放地址法中,通過再雜湊法尋找一個空位解決衝突問題,另一個方法是在雜湊表每個單元中設定連結串列(即鏈地址法),某個資料項的關鍵字值還是像通常一樣對映到雜湊表的單元,而資料項本身插入到這個單元的連結串列中。其他同樣對映到這個位置的資料項只需要加到連結串列中,不需要在原始的陣列中尋找空位。下面看看鏈地址法的程式碼:
在這裡我還是要推薦下我自己建的大資料學習交流裙:805127855, 裙 裡都是學大資料開發的,如果你正在學習大資料 ,小編歡迎你加入,大家都是軟體開發黨,不定期分享乾貨(只有大資料開發相關的),包括我自己整理的一份2018最新的大資料進階資料和高階開發教程,歡迎進階中和進想深入大資料的小夥伴。
-
public class HashChain {
-
private SortedList[] hashArray; //陣列中存放連結串列
-
private int arraySize;
-
public HashChain(int size) {
-
arraySize = size;
-
hashArray = new SortedList[arraySize];
-
//new出每個空連結串列初始化陣列
-
for(int i = 0; i < arraySize; i++) {
-
hashArray[i] = new SortedList();
-
}
-
}
-
public void displayTable() {
-
for(int i = 0; i < arraySize; i++) {
-
System.out.print(i + ": ");
-
hashArray[i].displayList();
-
}
-
}
-
public int hashFunction(int key) {
-
return key % arraySize;
-
}
-
public void insert(LinkNode node) {
-
int key = node.getKey();
-
int hashVal = hashFunction(key);
-
hashArray[hashVal].insert(node); //直接往連結串列中新增即可
-
}
-
public LinkNode delete(int key) {
-
int hashVal = hashFunction(key);
-
LinkNode temp = find(key);
-
hashArray[hashVal].delete(key);//從連結串列中找到要刪除的資料項,直接刪除
-
return temp;
-
}
-
-
public LinkNode find(int key) {
-
int hashVal = hashFunction(key);
-
LinkNode node = hashArray[hashVal].find(key);
-
return node;
-
}
-
}
下面是連結串列類的程式碼,用的是有序連結串列:
-
public class SortedList {
-
private LinkNode first;
-
public SortedList() {
-
first = null;
-
}
-
public boolean isEmpty() {
-
return (first == null);
-
}
-
public void insert(LinkNode node) {
-
int key = node.getKey();
-
LinkNode previous = null;
-
LinkNode current = first;
-
while(current != null && current.getKey() < key) {
-
previous = current;
-
current = current.next;
-
}
-
if(previous == null) {
-
first = node;
-
}
-
else {
-
node.next = current;
-
previous.next = node;
-
}
-
}
-
public void delete(int key) {
-
LinkNode previous = null;
-
LinkNode current = first;
-
if(isEmpty()) {
-
System.out.println("chain is empty!");
-
return;
-
}
-
while(current != null && current.getKey() != key) {
-
previous = current;
-
current = current.next;
-
}
-
if(previous == null) {
-
first = first.next;
-
}
-
else {
-
previous.next = current.next;
-
}
-
}
-
public LinkNode find(int key) {
-
LinkNode current = first;
-
while(current != null && current.getKey() <= key) {
-
if(current.getKey() == key) {
-
return current;
-
}
-
current = current.next;
-
}
-
return null;
-
}
-
public void displayList() {
-
System.out.print("List(First->Last):");
-
LinkNode current = first;
-
while(current != null) {
-
current.displayLink();
-
current = current.next;
-
}
-
System.out.println("");
-
}
-
}
-
class LinkNode {
-
private int iData;
-
public LinkNode next;
-
public LinkNode(int data) {
-
iData = data;
-
}
-
public int getKey() {
-
return iData;
-
}
-
public void displayLink() {
-
System.out.print(iData + " ");
-
}
-
}
在沒有衝突的情況下,雜湊表中執行插入和刪除操作可以達到O(1)的時間級,這是相當快的,如果發生衝突了,存取時間就依賴後來的長度,查詢或刪除時也得挨個判斷,但是最差也就O(N)級別。
雜湊表就分享這麼多,本文建議收藏,在等班車的時候、吃飯排隊的時候可以拿出來看看。利用碎片化時間來學習!
關注微信公眾號:程式設計師交流互動平臺!獲取資料學習!