1. 程式人生 > 其它 >資料結構是雜湊表(hashTable)(一)

資料結構是雜湊表(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,一直下去,只要表中有一個空位,就可以探測到它。