從Hash到散列表到HashMap

Hash

Hash 哈希、散列,一般咱們講的都是hash函數,是將任意長度的數據映射到有限長度的域上,做爲這段數據的特徵(指紋)。java

什麼是哈希算法,比較常見的有MDx系列(MD5等)、SHA-xxx系列(SHA-256等),對於哈希算法,通常須要知足兩點:算法

  • 抗碰撞能力:對於任意兩個不一樣的數據塊,其hash值相同的可能性極小;對於一個給定的數據塊,找到和它hash值相同的數據塊極爲困難。
  • 抗篡改能力:對於一個數據塊,哪怕只改動其一個比特位,其hash值的改動也會很是大。

最直接,則對應於jdk中的hashCode()函數,以String.hashCode()爲例數組

public int hashCode() {
    int h = hash; // default 0
    if (h == 0 && value.length > 0) {
        char val[] = value;

        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
        hash = h;
    }
    return h;
}

很簡潔的一個乘加迭代運算,將字符串映射成爲一個int值數據結構

Hash Table

Hash Table 散列表(也叫哈希表),是根據鍵值(Key value)而直接進行訪問的數據結構。也就是說,它經過把鍵值映射到表中一個位置來訪問記錄,以加快查找的速度。這個映射函數即是hash函數,存放數據的數組則是散列表。函數

散列函數

hash的做用即是將鍵值映射爲散列表的一個具體位置,即散列表數組的下標。散列表的長度是固定的,如散列表長度爲10,則其下標只能爲 0, 1, 2, ... , 8, 9 ,而通常常見hash函數並不能將任意的鍵值均映射到0~9之間。oop

爲了到達此效果,有不少構造散列值的算法,如除留餘數法、平方取中法、摺疊法、隨機數法、數學分析法等。咱們以最多見的除留餘數法,就是把鍵值經過一個固定的算法函數既所謂的hash函數轉換成一個整型數字,而後將該數字對散列表長度進行取餘,取餘結果就看成散列表的下標(散列值)。post

還以上爲例,散列表長度爲10,則計算散列值的hash函數能夠設計爲性能

static int hash(String s, int len) {
    return (s.hashCode() & 0x7fffffff) % len;
}

依次計算ManerFan Maner-Fan Maner·Fan對應的散列值優化

System.out.println(hash("ManerFan", 10)); // out 2
System.out.println(hash("Maner-Fan", 10)); // out 1
System.out.println(hash("Maner·Fan", 10)); // out 9

總的來說,要爲鍵值實現一個優秀的散列方法須要知足三個條件:spa

  • 一致性:等價的鍵必然產生相等的散列值
  • 高效性:計算簡便
  • 均勻性:均勻的散列全部的鍵

碰撞處理

對於有限長度的散列表,hash碰撞在所不免(不一樣鍵值經過hash計算出來的散列值相同)
解決碰撞的方法有不少種,如線性探測發、拉鍊法等等,這裏介紹一下拉鍊法

一種直接的辦法是,將散列表的每一個元素指向一條鏈表,鏈表的每一個節點都存儲了散列值爲該元素的索引的鍵值對,這樣,發生衝突的元素都被存儲在一條鏈表中。

盜一張百科的圖
拉鍊法

左側數組爲散列表,散列表每一個元素都對應一條鏈表。數組尋址容易、修改困難,鏈表尋址困難、修改簡單,這樣便以較好的性能解決了散列表的查詢、插入與刪除動做。

HashMap

關於HashMap的源碼已經有不少博文講解的很是詳盡,這裏再也不對源碼作過多的解析
推薦一篇文章 Java7/8 中的 HashMap 和 ConcurrentHashMap 全解析 (如下插圖均來自此文章)

HashMap的實現即是散列表,只是在其基礎上作了一些擴容等方面的優化

hashmap

容量(2的整數次冪)

HashMap的默認初始容量爲2^4(=16),即便建立時指定初始容量,HashMap內部也會計算一個不小於指定容量的、最接近的且爲2的整數次冪的一個值做爲初始容量

static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

爲何必須是2的整數次冪?
以上在介紹散列表時,提出了使用除留餘數法來計算某值對應的散列表下標。但直接取餘操做效率不高,HashMap採用了位與運算 (length - 1) & hash,但這跟2的整數次冪又有什麼關係?

優先應該明確,2的整數次冪減1所對應的二進制表示,是一串連續的1,好比16-1爲1111,8-1爲0111,等等。任何數字與(2^n - 1)相與,其值只能在[0, 2^n - 1]之間,且是連續的,這正好知足散列表求下標的條件。

爲何說以上計算結果是連續的,好比使用14 - 1,其二進制表示爲1101,任何數字與1101相與,其值只能在[0, 13]之間,且不包含二、三、六、七、十、11,這樣極大地浪費了散列表的節點空間。

節點結構(鏈表 | 紅黑樹)

以上在介紹碰撞處理時,提出了拉鍊法,即將發生衝突的元素存儲在一條鏈表中。

咱們知道,鏈表具備存儲、修改速度快等優勢,但檢索速度較慢。若是存在大量key,且在其進行(length - 1) & hash(key)後的值均相同,將其所有放入HashMap中,則HashMap將會退化成一條鏈表,此時若是進行大量查詢操做,則有可能佔用大部分CPU時間而形成拒絕服務攻擊。

紅黑樹(平衡二叉樹)平衡了存儲、修改及查詢的複雜度。java8中,當散列表某一entry上的節點數量大於8時,會將該entry的結構從鏈表升級爲紅黑樹,反之若是某一entry上的節點數量降到6如下時,會將該entry的結構從紅黑樹恢復爲鏈表。

紅黑樹

擴容

以上,已經介紹了HashMap的容量capacity,其老是爲2的整數次冪。
若是HashMap的容量大小老是保持不變,則隨着存放在HashMap中的key-value愈來愈多,每一個entry下的節點數量則會愈來愈大,不論對於鏈表仍是紅黑樹,不論對於修改仍是查詢,效率都將成爲一個問題。

這裏引入另外兩個參數:
loadFactor:負載因子,默認爲 0.75。
threshold:擴容的閾值,等於 capacity * loadFactor

當HashMap存儲的節點數量大於閾值threshold時會進行擴容操做,將當前enties數量擴容到當前的兩倍 capacity*2
負載因子loadFactor的做用在於,HashMap並不會等到節點數量到達總容量capacity以後再進行擴容,而是在「快要達到」總容量時便進行擴容

關於更多HashMap的細節,能夠閱讀jdk源碼或者各類技術博客

相關文章
相關標籤/搜索