雜湊表(一)

來源:互聯網
上載者:User

雜湊表是一種資料結構,它可以提供快速的插入和刪除操作。無論雜湊表有多少資料,插入、刪除只需要接近常量的時間,即 O(1) 的時間級。明顯比樹還快,樹的操作通常需要O(N)的時間級。
缺點:它是基於數組的,數組建立之後難以維護。某些雜湊表被基本填滿時,效能下降非常嚴重。而且也沒有提供一種方法可以以任何一種順序(例如從大到小)遍曆表中資料項目。
若需把單詞當做key(數組下標)擷取value(資料),可以把單詞分解成字母組合,把字母轉化為它們的數字代碼(a-1,b-2,c-3……z-26,空格-27),每個數字乘以相應的27(因為字母有27種可能,包括空格)的冪,然後結果相加,就可以每個單詞對應一個獨一無二的數字。

例如 cats 轉換數字:3*273 + 1*272 + 20*271 + 19*270 = 60337

這種方案會使得數組的長度太大,而且只有很少的一部分下標是有資料的。

雜湊化

arrayIndex = hugeNumber % arraySize,這是一種雜湊函數,它把一個大範圍的數字雜湊(轉化)成一個小數位範圍。
使用取餘操作符(%),把巨大的整數範圍轉換為兩倍於要儲存內容的數組下標範圍。下面是雜湊函數的例子:
arraySize = wordNumber * 2;  
arrayIndex = hugeNumber % arraySize;
期待的數組應該有這樣的特點:平均起來,每兩個數組單元,就有一個數值,有些單元沒有數值,但有些單元可能有多個數值。

衝突

把巨大的數字空間壓縮為較小的數字空間,必然要付出代價,即不能保證每個單詞都映射到數組的空白單元。假設在數組中需要插入單詞zoo,雜湊化之後得到它的下標,發現該單元已經有了其它另一個不同的單詞,這個情況叫做“衝突”。

解決方案1 - 開放地址法

前面已經提過指定的數組大小兩倍於需要儲存的資料量,因此還有一半單元是空白的。當發生衝突的時候,通過系統的方法找到數組的一個空位,並把單詞放進去,而不再用雜湊函數得到的數組下標,這種方法叫做“開放地址法”。

解決方案2 - 鏈地址法

建立一個存放單詞鏈表的數組,數組內不直接儲存單詞,這樣,但發生衝突的時候,新的資料項目直接接到這個數組下標所指的鏈表當中。這種方法叫做“鏈地址法”。
開放地址法
尋找數組的其它位置有三種方法:線性探測、二次探測、再雜湊法。
1)線性探測

線性尋找空白單元,如果 21 是要插入資料的位置,它已經被佔用了。那麼就使用 22 ,然後是 23 ,以此類推,數組下標一直遞增,直到找到空位為止。

插入(insert)
當資料項目的數目占雜湊表的一半,或最多三分之二時,雜湊表的效能是最好的。可以看出已填充的單元分布不均勻,有時一串空白單元,有時有一串已填充的單元。

在雜湊表中,一串聯續的已填充單元叫做“以數列填滿”。增加越來越多的資料項目時,以數列填滿變得越來越長,這叫做“聚集”。

刪除(Delete)
在雜湊表中,尋找演算法是以雜湊化的關鍵字開始,沿著數組一個一個尋找,如果在尋找到關鍵字之前遇到一個空白單元,說明尋找失敗。

delete不是簡單地把某個單元的資料項目變為空白(null),因為在一個以數列填滿中間有個空白,尋找演算法就會中途放棄尋找。因此需要一個有特殊關鍵字的資料項目代替要被delete的資料項目。標記資料項目不存在。

public class DataItem {    private int i;    public DataItem(int i) {        this.i = i;    }    public int getKey() {        return i;    }    public void printf() {        System.out.println("data -> " + i);    }}

public class HashTable {    private DataItem[] itemArray;    private int arraySize;    private DataItem nonItem; // for deleted items    public HashTable(int size) {        this.arraySize = size;        itemArray = new DataItem[arraySize];        nonItem = new DataItem(-1);    }    public void display() {        for (DataItem data : itemArray) {            if (data != null) {                data.printf();            }        }    }    public int hashFuc(int key) {        return key % arraySize;    }    public void insert(DataItem item) {        int key = item.getKey();        int hashVal = hashFuc(key);        DataItem tItem;        while ((tItem = itemArray[hashVal]) != null && tItem.getKey() != -1) {            if (tItem.getKey() == key) {                itemArray[hashVal] = item;                return;            }            hashVal++; // go to next cell            hashVal %= arraySize; // wraparound if necessary        }        itemArray[hashVal] = item;    }    public DataItem delete(int key) {        int hashVal = hashFuc(key);        DataItem item;        while ((item = itemArray[hashVal]) != null) { // until empty cell            if (item.getKey() == key) {                itemArray[hashVal] = nonItem;                return item;            }            hashVal++;            hashVal %= arraySize;        }        return null;    }    public DataItem find(int key) {        int hashVal = hashFuc(key);        DataItem item;        while ((item = itemArray[hashVal]) != null) { // until empty cell            if (item.getKey() == key) {                return item;            }            hashVal++;            hashVal %= arraySize;        }        return null;    }}

public static void main(String[] args) {        HashTable t = new HashTable(10);        t.insert(new DataItem(39));        t.insert(new DataItem(51));        t.insert(new DataItem(23));        t.insert(new DataItem(25));        t.insert(new DataItem(23));        t.insert(new DataItem(10));        t.insert(new DataItem(9));        t.delete(25);        t.insert(new DataItem(79));        t.insert(new DataItem(81));        t.display();    }

列印結果:
data -> 10
data -> 51
data -> 9
data -> 23
data -> 79
data -> 81
data -> 39

擴充數組

當雜湊表太滿,需要擴充數組。只能建立一個新的更大的數組,然後把舊的數組所有資料項目插入到新的數組。由於雜湊函數是根據數組的大小計算資料項目的位置,所以不能簡單把一個資料項目插入新的數組,需要按順序遍曆舊的數組,然後調用 insert()向新的數組插入每個資料項目。這叫做“重新雜湊化”。

擴充後的數組容量通常是原來的兩倍,實際上數組的容量應該是一個質數,所以新的數組要比兩倍容量多一點。

好的HASH函數需要把未經處理資料均勻地分布到HASH數組裡,比如大部分是偶數,這時候如果HASH數組容量是偶數,容易使未經處理資料HASH後不會均勻分布:

2 4 6 8 10 12這6個數,如果對 6 取餘 得到 2 4 0 2 4 0 只會得到3種HASH值,衝突會很多。如果對 7 取餘 得到 2 4 6 1 3 5 得到6種HASH值,沒有衝突。

同樣地,如果資料都是3的倍數,而HASH數組容量是3的倍數,HASH後也容易有衝突,用一個質數則會減少衝突的機率,更分散。

以下是求質數的代碼:

private int getPrime(int min) {        for (int j = min;; j++) {            if (isPrime(j)) {                return j;            }        }    }    private boolean isPrime(int num) {        for (int j = 2; j * j <= num; j++) {            if (num % j == 0) {                return false;            }        }        return true;    }

3)二次探測

線上性探測中會發生聚集,一旦聚集形成,它會越來越大,雜湊化後的落在聚集範圍內的資料項目都要一步步移動,效能越差。

裝填因子:已填入雜湊表的資料項目和表長的比率叫做裝填因子。loadFactor = nItems / arraySize ;

二次探測是防止聚集的產生,思想是探測相隔較遠的單元,而不是相鄰的單元。

步驟是步數的平方:假設雜湊表中原始下標是x,那麼線性探測是:x+1,x+2,x+3……;而在二次探測中,探測過程是:x+12,x+22,x+32……。

二次探測消除了線上性探測產生的聚集問題,這種聚集問題叫做“原始聚集”。然而二次探測產生了另外一種更細的聚集問題。之所以會發生,是因為所有映射到同一個位置的關鍵字在尋找空位時,探測的單元都是一樣的(步長總是固定的,都是:1、4、9、16、25、36……)。

4)再雜湊法

為了消除原始聚集和二次聚集,可使用另一種方法:再雜湊法。現在需要的一種方法產生一種依賴關鍵字的探測序列,而不是每個關鍵字都一樣,那麼,不同的關鍵字即使映射到相同的數組下標,也可以使用不同的探測序列。

方法是把關鍵字用不同的雜湊函數再做一遍雜湊化,用這個結果作為步長。對於指定的關鍵字,步長在整個探測是不變的,不過不同關鍵字使用不同步長。

經驗說明,第二雜湊函數必須具備以下條件:

與第一個雜湊函數不同
不能輸入0(否則沒有步長,每次探測都原地踏步,死迴圈。)
stepSize = constant - (key % constant),其中 constant 是質數,且小於數組容量。例如:stepSize = 5 - key % 5 ;

public class HashTable2 {    private DataItem[] itemArray;    private int arraySize;    private DataItem nonItem; // for deleted items    public HashTable2(int size) {        this.arraySize = size;        itemArray = new DataItem[arraySize];        nonItem = new DataItem(-1);    }    public void display() {        for (DataItem data : itemArray) {            if (data != null) {                data.printf();            }        }    }    public int hashFuc1(int key) {        return key % arraySize;    }    public int hashFuc2(int key) {        /*         * non-zero, less than array size, different from hashFuc1. array size         * must be relatively prime to 5, 4, 3, 2         */        return 5 - key % 5;    }    public void insert(DataItem item) {        int key = item.getKey();        int hashVal = hashFuc1(key);        int stepSize = hashFuc2(key);        DataItem tItem;        while ((tItem = itemArray[hashVal]) != null && tItem.getKey() != -1) {            if (tItem.getKey() == key) {                itemArray[hashVal] = item;                return;            }            hashVal += stepSize; // add the step            hashVal %= arraySize; // wraparound if necessary        }        itemArray[hashVal] = item;    }    public DataItem delete(int key) {        int hashVal = hashFuc1(key);        int stepSize = hashFuc2(key);        DataItem item;        while ((item = itemArray[hashVal]) != null) { // until empty cell            if (item.getKey() == key) {                itemArray[hashVal] = nonItem;                return item;            }            hashVal += stepSize;            hashVal %= arraySize;        }        return null;    }    public DataItem find(int key) {        int hashVal = hashFuc1(key);        int stepSize = hashFuc2(key);        DataItem item;        while ((item = itemArray[hashVal]) != null) { // until empty cell            if (item.getKey() == key) {                return item;            }            hashVal += stepSize;            hashVal %= arraySize;        }        return null;    }}

表的容量必須是一個質數

再雜湊法要求表的容量是一個質數。為什麼會有這個限制,假設表的容量不是質數,表長是15(座標 0 - 14),有一個特別關鍵字映射到0,步長為5,探測序列為0、5、10、0、5……,一直迴圈下去,演算法只會嘗試這三個單元,不可能找到其它空白單元,演算法崩潰。

如果數組容量是13,即一個質數,那麼探測序列會訪問到所有單元。即0、5、10、2、7、12、4、9、1、6、11、3,一直下去,只要表中有一個空位,就可以探測到它。用質數作為數組容量使得任何數想整除它是不可能的,因此探測序列最終會檢查到所有單元。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.