資料結構是雜湊表(hashTable)(一)
雜湊化之後難免會產生一個問題,那就是對不同的關鍵字,可能得到同一個雜湊地址,即同一個陣列下標,這種現象稱為衝突,那麼我們該如何去處理衝突呢?一種方法是開放地址法,即通過系統的方法找到陣列的另一個空位,把資料填入,而不再用雜湊函式得到的陣列下標,因為該位置已經有資料了;另一種方法是建立一個存放連結串列的陣列,陣列內不直接儲存資料,這樣當發生衝突時,新的資料項直接接到這個陣列下標所指的連結串列中,這種方法叫做鏈地址法。下面針對這兩種方法進行討論。
1.開放地址法
線性探測法
所謂線性探測,即線性地查詢空白單元。如果21是要插入資料的位置,但是它已經被佔用了,那麼就是用22,然後23,以此類推。陣列下標一直遞增,直到找到空白位。下面是基於線性探測法的雜湊表實現程式碼:
public class HashTable { private DataItem[] hashArray; // DateItem類是資料項,封裝資料資訊 private int arraySize=10; private int itemNum; // 陣列中目前儲存了多少項 private DataItem nonItem; // 用於刪除項的 public HashTable() { 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,一直下去,只要表中有一個空位,就可以探測到它。