HashMap數據結構之道

HashMap數據結構之道

問題1:HashMap的數據結構是什麼樣的?java


同窗1:嗯...數組+鏈表node

同窗2:數組+鏈表...程序員

同窗3:數組+鏈表...數組

同窗4:數組+鏈表+紅黑樹...安全

同窗n:.....數據結構


爲何答案會有兩種?難道你們學習的HashMap有兩個版本?我忽然想起馬克思哲學裏面的一句話,真理是相對的,不是絕對的,變化纔是惟一的真理。架構


不錯,對於Java這種語言排行榜常常排於榜首的高級語言,變化也是它的生存之道。Java在推出新版本的同時,不斷的完善重要class的數據結構,提高它的性能,穩固它的安全性。HashMap就是其中之一。ide


HashMap在Java1.7裏使用的是數組+鏈表的數據結構,在Java1.8裏使用的是數組+鏈表+紅黑樹。性能



問題2:HashMap爲何在1.8的版本,對它的數據結構進行了修改?學習


同窗1:嗯,有bug(標準的程序員思惟)

同窗2:有漏洞...(有***思惟的同窗)

同窗3:沒事,無聊,寂寞,想搞事情...(有創業者思惟的同窗)

同窗4:提高性能...(有架構師思惟的同窗)

......


Java在維護它全球頂級語言,近趨於霸主地位的時候,固然要從細節入手,從源碼入手,完善它的性能,修復它的漏洞。Java如此,其餘語言也是如此。


問題3:HashMap在1.7和1.8,性能上究竟有了多大的提高,咱們上代碼,看看速度如何?


import java.util.HashMap;public class Test100 {public static void main(String[] args) {
    System.out.println("java version:" + System.getProperty("java.version"));
    HashMap<String, String> hashMap = new HashMap<String, String>();
    long putTotalTime = 0, getTotalTime = 0;
    for (int i = 0; i < 100000; i++) {
        long putStartTime = System.currentTimeMillis();
        hashMap.put("yaoshen" + i, "yaoshen" + i);
        putTotalTime += System.currentTimeMillis() - putStartTime;

        long getStartTime = System.currentTimeMillis();
        hashMap.get("yaoshen" + i);
        getTotalTime += System.currentTimeMillis() - getStartTime;
    }
    System.out.println("10W data:put total time is :" + putTotalTime);
    System.out.println("10W data:get total time is :" + getTotalTime);}}


測試結果以下:


ae76e3d5c11ba78156e064e7a1d14114.jpeg



咱們能夠清楚的看見HashMap在1.8的版本,數據量很是大(10萬條)的時候,查詢的總時間明顯比較低,也就是說HashMap在1.8的版本查找速度很快,插入或者是刪除相對較慢。那麼爲何會這樣?


思考題:爲何HashMap在1.8的版本,查找速度有了大幅提高?


接下來,我將逐一帶你們進行全面剖析HashMap的數據結構。


一.  數據結構不一樣


1.  HashMap在1.7的版本數據結構以下:

數組+鏈表(單向鏈表)


a1fa84556dd63d3a1ef9ac1d42564b65.jpeg


1.1  從數據結構咱們來分析HashMap的put過程


插入元素的數據結構以下代碼:

static class Entry<K,V> implements Map.Entry<K,V> {
    final K key;
    V value;
    Entry<K,V> next;
    int hash;

    /**     * Creates new entry.     */
    Entry(int h, K k, V v, Entry<K,V> n) {
        value = v;
        next = n;
        key = k;
        hash = h;
    }}


第一步:計算key對應數組的index(索引),是經過hashcode & (length-1),就是hashcode值和(數組長度-1)的與運算。

第二步:將插入的元素放入數組index的位置,將next指針指向以前的元素。

圖解過程:


6abd37e310456e9e4cef7e366f12a7fc.jpeg


1.2  HashMap在1.7的版本的get過程


第一步:計算key對應數組的index(索引),找到數組的頭結點

第二步:從頭結點逐個向下遍歷,直到key的hash值與節點的hash值碰撞相等,而後取出value值。


思考一下:get過程的時間複雜度應該是O(n),試着想一下,若是咱們在插入的過程當中對節點進行一些變換,例如將單向鏈表變成二叉樹,或者是平衡二叉樹,是否是下次在查找的過程,就能減小遍歷的時間複雜度呢?


下面,咱們引入HashMap在Java1.8裏的數據結構


2.  HashMap在1.8的版本數據結構以下:


445198c283997b6ab162c6612708e3b2.jpeg


從源碼中分析:

 /** * The bin count threshold for using a tree rather than list for a * bin.  Bins are converted to trees when adding an element to  a * bin with at least this many nodes. The value must be greater * than 2 and should be at least 8 to mesh with assumptions in * tree removal about conversion back to plain bins upon * shrinkage. */static final int TREEIFY_THRESHOLD = 8;


咱們來翻譯這句話:

HashMap處理「碰撞」增長了紅黑樹這種數據結構,當碰撞結點較少時,採用鏈表存儲,當較大時(>8個),採用紅黑樹(特色是查詢時間是O(logn))存儲(有一個閥值控制,大於閥值(8個),將鏈表存儲轉換成紅黑樹存儲。


可能此時,對於數據結構,你會有知識斷層,那麼不要緊,我來爲你一一介紹這些數據結構。


1. 數組,帶有索引的容器,固定長度(ArrayList中數據結構,自動擴容)


7d78c5b677e406900719d983c0655a3f.jpeg


2. 雙向鏈表,以下圖(LinkedList)


eaaa11d36bcb8d338732e3b59e9b44c2.jpeg


3. 單向鏈表


d7bf487dcdd30c79a66acd8447dbb5f1.jpeg


4. 紅黑樹


1b577caab44f5f0f81a5967b3f4ba2f6.jpeg


特色:

1)每一個節點非紅即黑

2)根節點是黑的;

3)每一個葉節點(葉節點即樹尾端NULL指針或NULL節點)都是黑的;

4)如圖所示,若是一個節點是紅的,那麼它的兩兒子都是黑的;

5)對於任意節點而言,其到葉子點樹NULL指針的每條路徑都包含相同數目的黑節點;

6)每條路徑都包含相同的黑節點;


2.1 此時,咱們分析一下HashMap在1.8版本里面的put過程

插入元素包含以下

1)單向鏈表,代碼如上面對應的1.7版本

2)紅黑樹

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
    TreeNode<K,V> parent;  // red-black tree links    TreeNode<K,V> left;
    TreeNode<K,V> right;
    TreeNode<K,V> prev;    // needed to unlink next upon deletion    boolean red;
    TreeNode(int hash, K key, V val, Node<K,V> next) {
        super(hash, key, val, next);
    }}


分析put過程

第一步:計算key對應數組的index(索引),是經過hashcode & (length-1),就是hashcode值和(數組長度-1)的與運算。

第二步:當前索引所對應的單向鏈表長度<=8時,將插入的元素放入數組index的位置,將next指針指向以前的元素。反之,則把當前索引全部的元素轉化爲紅黑樹。


2.2 HashMap在1.8的版本的get過程


第一步:計算key對應數組的index(索引),找到數組的頭結點

第二步:若是頭節點是單向鏈表結構,則從頭結點逐個向下遍歷,知道key的hash值與節點的hash值碰撞相等,而後取出value值。若是是紅黑樹,則用紅黑樹的遍歷,碰撞hash值,而後取出value值。


二.  HashMap的擴容


當HashMap中的元素個數超過數組大小*loadFactor時,就會進行數組擴容,loadFactor的默認值爲0.75,也就是說,默認狀況下,數組大小爲16,那麼當hashmap中元素個數超過16*0.75=12的時候,就把數組的大小擴展爲2*16=32,即擴大一倍,而後從新計算每一個元素在數組中的位置,而這是一個很是消耗性能的操做,因此若是咱們已經預知hashmap中元素的個數,那麼預設元素的個數可以有效的提升hashmap的性能。


好比說,咱們有1000個元素new HashMap(1000), 可是理論上來說new HashMap(1024)更合適,不過上面已經說過,即便是1000,hashmap也自動會將其設置爲1024。 可是new HashMap(1024)還不是更合適的,由於0.75*1000 < 1000, 也就是說爲了讓0.75 * size > 1000, 咱們必須這樣new HashMap(2048)才最合適,既考慮了&的問題,也避免了resize的問題。


綜上所述,咱們得出結論:


一.  HashMap在Java1.7的版本是數組+單向鏈表存儲,在1.8的版本是數組+單向鏈表+紅黑樹(若是當前索引對應的單向鏈表長度小於等於8,則用單向鏈表,若是大於8,則轉化爲紅黑樹)


二.  HashMap在1.8的版本中,大數據量的查找,性能有了提高,是由於在put的過程當中,增長了紅黑樹的轉化,犧牲了put的時間和空間複雜度


三.  HashMap的擴容過程,是個很是消耗性能的,擴容後的HashMap,須要從新計算以前數組各個索引對應的頭結點(根節點)在新數組中對應的索引。

相關文章
相關標籤/搜索