5,常見數據結構-散列表

想了解更多數據結構以及算法題,可以關注微信公衆號“數據結構和算法”,每天一題爲你精彩解答。也可以掃描下面的二維碼關注
在這裏插入圖片描述

基礎知識

散列表也叫哈希表,是根據鍵值對(key,value)進行訪問的一種數據結構。他是把一對(key,value)通過key的哈希值來映射到數組中的,也就是說,它通過把關鍵碼值映射到表中一個位置來訪問記錄,以加快查找的速度。這個映射函數叫做散列函數,存放記錄的數組叫做散列表。

1,HashMap

散列表中最常見的應該就是HashMap了,HashMap的實現原理非常簡單,他其實就是數組加鏈表的一種數據結構。如果映射在數組中出現了衝突,他會以鏈表的形式存在。我們來看一下他的數據結構是什麼樣的。
在這裏插入圖片描述
上面的圖有兩處非常明顯的錯誤,不知道大家有沒有發現,如果對HashMap源碼比較熟悉的估計一眼就能看的出來。首先是數組的長度必須是2的n次冪,這裏長度是9,明顯有錯,然後是entry的個數不能大於數組長度的75%,如果大於就會觸發擴容機制進行擴容,這裏明顯是大於75%。我爲什麼要畫這個錯誤的圖呢,因爲在網上確實看到過不少這樣不嚴謹的圖,希望大家能夠看清楚。那麼正確的圖應該是這樣的。
在這裏插入圖片描述
數組的長度即是2的n次冪,而他的size又不大於數組長度的75%。

HashMap的實現原理是先要找到要存放數組的下標,如果是空的就存進去,如果不是空的就判斷key值是否一樣,如果一樣就替換,如果不一樣就以鏈表的形式存在。

在java中1.7及以前的版本如果以鏈表的形式存在,在插入的時候採用的是頭插法。

在1.8是尾插法。並且在java1.8中如果鏈表的長度大於8的時候會轉爲紅黑樹。

在HashMap中,數組的大小是2n,無論你初始化的時候傳的值是多少,他都會初始化爲2n,並且這個2^n是大於等於你初始化值的最小值,比如初始化的時候傳的值是17,他會計算得到32。關於怎麼計算的,我們有3種方式,第一種就是通過while循環,我們來看下代碼

public static int tableSizeFor(int initialCapacity) {
    int capacity = 1;
    while (capacity < initialCapacity)
        capacity <<= 1;
    return capacity;
}

這種解法是最簡單的,一眼就能看懂,還有兩種解法我們也可以看下

public static int tableSizeFor(int i) {
    i--;
    i |= i >>> 1;
    i |= i >>> 2;
    i |= i >>> 4;
    i |= i >>> 8;
    i |= i >>> 16;
    return i + 1;
}

原理比較簡單,就是把最左邊的1往右全部鋪開,最後在加上1就是我們要求的結果。這裏第二行減1的目的是防止i等於2^n的時候結果會放大。比如當i=32的時候如果我們在第2行不減1,會導致結果爲64。我們再來看另一種寫法

 public static int tableSizeFor(int i) {
     if ((i & (i - 1)) == 0)
         return i;
     i |= (i >> 1);
     i |= (i >> 2);
     i |= (i >> 4);
     i |= (i >> 8);
     i |= (i >> 16);
     return (i - (i >>> 1)) << 1;
}

在第2-3行實現判斷是不是2^n, 如果是就直接返回,第4-8行也是把i最左邊的1往右全部鋪開,第9行i-(i>>>1)表示的是把i最左邊的1保留,其他的全部置爲0,通俗一點也就是他返回的是小於i的最大的2^n,然後再往左移一位就是我們要求的結果。我們就以i等於17爲例用最後一個方法來畫個圖分析一下。
在這裏插入圖片描述

2,ArrayMap

除了使用數組和鏈表以外,我們能不能只使用一種數據結構呢,比如數組,當然也是可以的。大家可能會懷疑,如果只使用一種數據結構的話,映射出現了衝突怎麼辦,其實也很好解決。ArrayMap的實現原理是使用兩個數組,一個存放hash值,一個存放key和value,其中存放key和value的數組長度是存放hash值數組長度的二倍,其中存放hash值的數組必須是排序的。如果hash值出現了衝突,說明hash值最終的計算是一樣的,那麼在hash數組中肯定是挨着的,所以查找的時候如果hash值有重複的就會把重複的也查找一遍。我們來看ArrayMap中的一段代碼

 int indexOf(Object key, int hash) {
     final int N = mSize;
 
     // Important fast case: if nothing is in here, nothing to look for.
     if (N == 0) {
         return ~0;
     }
 
     int index = binarySearchHashes(mHashes, N, hash);

    // If the hash code wasn't found, then we have no entry for this key.
    if (index < 0) {
        return index;
    }

    // If the key at the returned index matches, that's what we want.
    if (key.equals(mArray[index<<1])) {
        return index;
    }

    // Search for a matching key after the index.
    int end;
    for (end = index + 1; end < N && mHashes[end] == hash; end++) {
        if (key.equals(mArray[end << 1])) return end;
    }

    // Search for a matching key before the index.
    for (int i = index - 1; i >= 0 && mHashes[i] == hash; i--) {
        if (key.equals(mArray[i << 1])) return i;
    }

    // Key not found -- return negative value indicating where a
    // new entry for this key should go.  We use the end of the
    // hash chain to reduce the number of array entries that will
    // need to be copied when inserting.
    return ~end;
}

我們看到第23-30行,如果hash值一樣,在查找的時候不光往前查找而且還會往後查找。他的數據結構是這樣的。
在這裏插入圖片描述

3,SparseArray

在散列表中如果可以確定key值都是int類型,那麼又可以簡化,直接用key值當hash值存儲即可,和ArrayMap一樣只需要兩個數組即可,一個是存放key的,一個是存放value的,不同的是這兩個數組的長度都是一樣的。這種情況下就不會出現hash值一樣的問題了,因爲這個時候如果hash值一樣的話,那麼他們的key肯定是一樣的,而在散列表中是不可能存在了,假如在插入數據的時候有一樣的key,那麼他的value是要被替換掉的,所以不會出現兩個完全一樣的key。他的數據結構圖是這樣的
在這裏插入圖片描述

4,ThreadLocalMap

在java語言中還有一個關於散列表的,那就是ThreadLocalMap,這個類是ThreadLocal的一個靜態內部類,一般我們用不到。如果出現hash衝突的時候他的實現原理和上面的幾種也都不太一樣。存儲的時候他首先會根據hash值映射到指定的數組,如果當前位置爲空就直接存進去,如果不爲空就往後找,找他的下一個,我們來看其中的一段代碼

    /**
     * Increment i modulo len.
     */
    private static int nextIndex(int i, int len) {
        return ((i + 1 < len) ? i + 1 : 0);
    }

總結:
散列表大家第一個想到的就是HashMap,需要數組加鏈表的方式才能實現,通過我們上面的分析,其實我們不需要鏈表也能實現。散列表的實現原理其實很簡單。他的核心是當我們的hash值出現衝突的時候該怎麼解決。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章