JAVA數據結構之哈希表

Hash表簡介: 

  Hash表是基於數組的,優勢是提供快速的插入和查找的操做,編程實現相對容易,缺點是一旦建立就很差擴展,當hash表被基本填滿的時候,性能降低很是嚴重(發生彙集引發的性能的降低),並且沒有簡便方法以任何一種順序來遍歷表中的數據項,若須要,則要考慮其餘的數據結構(選擇hash表存儲數據通常是不須要有序遍歷數據,能夠提早預測數據量的大小)java

  Hash化:在hash表中咱們通常經過hash函數將數據的關鍵字轉換成爲數組的下標(若關鍵字能夠直接做爲數組的下標則不須要hash函數轉換)。如今咱們假設要在內存中存儲50000個單詞,此時關鍵字單詞不能直接做爲咱們的數組的下標,因此咱們就須要將其用咱們的hash函數轉換一下獲得咱們的數組的下標,這裏咱們用1-26表明咱們真的字母a-z空格咱們用0表明,接下來咱們要探討如何將表明單個字母的數字組合成表明整個單詞的數字呢,有兩種具備表明性的方法,假設咱們約定單詞由十個字母組成,編程

  第一種是每一個字母對應的數字相加獲得數組的下標:數組

  空位是0那麼第一個單詞a就是0+0+0+0+0+0+0+0+0+1 = 1,最後一個單詞是zzzzzzzzzz對應的數組下標是26+26+26+26+26+26+26+26+26+26 = 260,這裏咱們能夠明顯地看到,咱們的存儲數組的大小是260,明顯不夠咱們存儲50000個單詞,因此這裏的每個數組項都要包含一個數組或鏈表,這樣就嚴重下降了存取的速度。數據結構

  第二種是使用冪的連乘:ide

  咱們將每一個字母對應的數字乘以適當的27的冪(因爲包括空格有27個字符),在字母的第一位乘以0次冪第二位乘以1次冪以此類推,而後將每個結果相加獲得咱們最終的數組的下標,例如咱們將cats依此轉換3*273+1*272+20*271+19*270 =60337這樣能夠爲每個單詞建立獨一無二的整數可是此時的問題是單詞zzzzzzzzzz計算出來的數值變得很是大279=7000000000000結果很是巨大其中大多數結果都是空的函數

  對於英語詞典咱們存儲50000個單詞大約須要多餘一倍的空間來容納這些單詞(因爲當hash填滿的時候對其性能影響很大),因此須要容量100000的數組,咱們須要將0到超過7000000000000的範圍,壓縮爲0-100000的範圍,使用上面經過計算獲得的數字(這個數字可能超出變量範圍)來對咱們的壓縮範圍取餘能夠獲得咱們最終壓縮後的數字,將這樣巨大的數字空間壓縮成較小的數字空間,因爲不能保證每一個單詞都映射到數組的空白單元,這時候就會發生衝突,解決這樣衝突咱們通常使用開放地址法鏈地址法性能

開放地址法經過系統的方法找到系統的空位(三種:線性探測、二次探測、再哈希法),並將插入的單詞填入,而再也不使用用hash函數獲得數字做爲數組的下標。this

  • 線性探測倘若當前要插入的位置已經被佔用了以後,沿數組下標遞增方向查找,直到找到空位爲止
  • 二次探測二次探測和線性探測的區別在於二次探測的步長是,若計算的原始下標是x則二次探測的過程是x+12,x+22,x+32,x+42,x+52隨着探測次數的增長,探測的步長是探測次數的二次方(所以名爲二次探測)。二次探測會產生二次彙集:即當插入的幾個數通過hash後的下標相同的話,那麼這一串數字插入的探測步長會增長很快
  • hash法:爲了消除原始彙集和二次彙集,把關鍵字用不一樣的hash函數再作一遍hash化,用過這個結果做爲探測的步長,這樣對於特定的關鍵字在整個探測中步長不變,可是不一樣的關鍵字會使用不一樣的步長。stepSize = constant - key % constant 這個hash函數求步長比較實用,constant是小於數組容量的質數。(注意:第二個hash函數必須和第一個hash函數不一樣,步長hash函數輸出的結果值不能爲0)

下面給出再hash的JAVA代碼spa

class DataItem {
    private int iData;

    public DataItem(int iData) {
        this.iData = iData;
    }

    public int getKey() {
        return iData;
    }
}

/***再hash法的java代碼*/
class HashTableDouble {
    private DataItem[] hashArray;
    private int arraySize;
    //用來標記刪除數據項
    private DataItem nonItem;

    HashTableDouble(int size) {
        arraySize = size;
        hashArray = new DataItem[size];
        nonItem = new DataItem(-1);
    }

    //此函數用來獲得關鍵字插入的腳標
    public int hashFunc1(int key) {
        return key % arraySize;
    }

    //此函數用來獲得探測的步長
    public int hashFunc2(int key) {
        return 5 - key % 5;
    }

    /***再hash法插入數據*/
    public void insert(int key) {
        int hashVal = hashFunc1(key);
        int stepSize = hashFunc2(key);
        while (hashArray[hashVal] != null && hashArray[hashVal].getKey() != -1) {
            //將hash值依次加上步長
            hashVal += stepSize;
            //當hash值超出範圍了以後壓縮到指定範圍,至關因而循環尋找數據項
            hashVal %= stepSize;
        }
        hashArray[hashVal] = new DataItem(key);
    }

    /***刪除*/
    public void delete(int key) {
        int hashVal = hashFunc1(key);
        int stepSize = hashFunc2(key);
        while(hashArray[hashVal] != null){
            if (hashArray[hashVal].getKey() == key){
                hashArray[hashVal] = nonItem;
                return;
            }
            hashVal += stepSize;
            hashVal %= stepSize;
        }
    }

    /***查找對應的關鍵字*/
    public DataItem find(int key) {
        int hashVal = hashFunc1(key);
        int stepSize = hashFunc2(key);
        while(hashArray[hashVal] != null){
            if (hashArray[hashVal].getKey() == key){
                return hashArray[hashVal];
            }
            hashVal += stepSize;
            hashVal %= stepSize;
        }
        return null;
    }
}
再哈希法JAVA代碼

 鏈地址法  建立一個存放單詞鏈表的數組,數組內不直接存儲單詞,而是存儲單詞的鏈表或數組。發生衝突的時候,數據項直接接到這個數組下標所指的鏈表中便可。code

  優點:填入過程容許重複,全部關鍵值相同的項放在同一鏈表中,找到全部項就須要查找整個是鏈表,稍微有點影響性能。刪除只須要找到正確的鏈表,從鏈表中刪除對應的數據便可。表容量是質數的要求不像在二次探測和再hash法中那麼重要,因爲沒有探測的操做,因此無需擔憂容量被步長整除,從而陷入無限循環中。接下來給出鏈地址法的JAVA代碼:

/**
 * Link         是鏈節點類
 * SortedList   是鏈表類
 * HashTable    是Hash表類
 */
class Link {
    private int iData;
    public Link next;
    public Link(int iData) {
        this.iData = iData;
    }
    public int getKey() {
        return iData;
    }

}

/**
 * 這裏建立的是一個有序的鏈表,可減小不成功搜索一半的時間,刪除的時間級也減小一半
 * 插入的時間變長了,因爲須要找到正確的插入位置
 */
class SortedList {

    private Link first;
    public SortedList() {
        first = null;
    }

    //插入
    public void insert(Link theLink) {
        int key = theLink.getKey();
        Link previous = null;
        Link current = first;
        //這裏循環找到鏈節點插入的位置
        while (current != null && key > current.getKey()) {
            previous = current;
            current = current.next;
        }
        //插入新的鏈節點
        if (previous == null) {
            first = theLink;
        } else {
            previous.next = theLink;
            theLink.next = current;
        }
    }

    //delete刪除
    public void delete(int key) {
        Link previous = null;
        Link current = first;
        //找到須要刪除的鏈節點的位置
        while (current != null && key != current.getKey()) {
            previous = current;
            current = current.next;
        }
        //刪除鏈節點
        if (previous == null) {
            first = first.next;
        } else {
            previous.next = current.next;
        }
    }

    //find查找
    public Link find(int key) {
        Link current = first;
        while (current != null && current.getKey() <= key){
            if (current.getKey() == key)
                return current;
            current = current.next;
        }
        return null;
    }
}

class HashTable {
    private SortedList[] hashArray;
    private int arraySize;
    public HashTable(int arraySize) {  //構造函數
        this.arraySize = arraySize;
        hashArray = new SortedList[arraySize];
        for (int j = 0; j<arraySize;j++){
            hashArray[j] = new SortedList();
        }
    }
    /***將關鍵字hash化*/
    public int hashFunc(int key) {
        return key % arraySize;
    }
    public void insert(Link theLink) {
        int key = theLink.getKey();
        //獲取關鍵字的hash值
        int hashVal = hashFunc(key);
        hashArray[hashVal].insert(theLink);
    }
    public void delete(int key) {  //刪除
        int hashVal = hashFunc(key);
        hashArray[hashVal].delete(key);
    }
    public Link find(int key) {  //查找
        int hashVal = hashFunc(key);
        return hashArray[hashVal].find(key);
    }
}
鏈地址法的JAVA代碼

 ps:桶:相似於鏈地址法,只是將鏈表換爲數組這樣的數組有時稱爲桶,可是桶的容量很差選擇。

Hash化的效率: 

  下圖顯示了不一樣的hash方法的效率狀況

 

  從上圖咱們能夠看到開放地址法隨着裝填因子變大,比較次數後期會急劇變大,因此使用開放地址法的時候儘可能保持裝填因子不能超過2/3,二次探測和再hash法的性能至關,他們都要比線性探測要好。當裝填因子爲0.5的時候成功和不成功的查找須要平均2次比較,當裝填因子爲0.8的時候分別須要2.9和5.0次查找,因此對於較高的裝填因子,能夠選擇二次探測和再hash法。鏈地址法的效率,全部的操做都須要1+nComps的時間,nComps表示關鍵字的比較次數,和裝填因子有關,裝填因子的大小影響每個鏈表的平均長度,從而會影響每一個關鍵字在鏈表中的比較次數,從而影響效率。

  若是在hash表建立的時候,填入的項數未知,鏈地址法好於開放地址法,當裝填因子變大的時候開放地址法的效率降低很快,而鏈地址法效率是線性地降低。兩種均可選的時候也推薦選擇鏈地址法,雖然要使用到鏈表類,可是當要增長比預期更多的數據的時候性能不會快速的降低

以上就是hash表數據結構的全部內容

相關文章
相關標籤/搜索