紅黑樹紅色和黑色的來源及HashMap源碼分析

哈希表:

又稱爲散列表,是一個使用關鍵碼值就可以直接映射到相應位置的數據結構,在不發生哈希衝突的情況下,哈希表不需要經過任何的對關鍵字的比較就能一次定位到數據的位置效率非常的高,其時間複雜度是O(1)。哈希表和數組很像將數據存儲在一個帶有索引標識的數據空間中,當然你也可以理解它爲一個數組,但是它和hasmap一樣也有鍵值對<key,value>,利用哈希函數將key關鍵詞轉爲一個int類型的整數也就是哈希值,然後對數組的長度取餘,得到的結果就是就是數組的索引下標,該value值就存儲在該索引值下標的數組位置中。

哈希碰撞:

哈希表維護的關係就是:index=f(key),index就是key的哈希值(整數),也是存儲位置的記錄,f就是哈希函數,在理想狀態下通過這個關係,哈希表每次都會得到一個不一樣的哈希值,但是現實總是不理想的,有時不同的key會得到一樣的哈希值時,這個情況就叫做哈希衝突也叫做哈希碰撞,由於哈希算法的原理就是將大範圍的區域映射到小範圍內,在空間有限的情況下再好的算法也避免不了衝突。

在java中兩個不同的對象通過hashCode()計算的值可能相等,使用hashCode()比較兩個對象是否相等時有可能出現true,但是使用equals()比較兩個不同的對象一定爲false,但是hashCode()的效率比equals()高,通過hashCode()計算出兩個不同的值的情況下使用equals()一定是false,所以在比較兩個對象是否相等時可以先使用hashCode(),如果計算出兩個不同的值那一定是兩個不同的對象,如果是得出相同的值,在使用equals()比較

鏈地址法:

解決衝突的方法由很多,其中一個就是鏈地址法,HashMap就是採用了鏈地址法,鏈地址法結構就是數組+鏈表,通過鏈表把數組的同一位置衝突元素一個個連接起來

各個數據結構時間複雜度對比:

數組:

指定下標的查找,時間複雜度爲O(1);通過給定值進行查找,需要遍歷數組,逐一比對給定關鍵字和數組元素,時間複雜度爲O(n),當然,對於有序數組,則可採用二分查找,插值查找,斐波那契查找等方式,可將查找複雜度提高爲O(logn);對於一般的插入刪除操作,涉及到數組元素的移動,其平均複雜度也爲O(n)

線性鏈表:

對於鏈表的新增,刪除等操作(在找到指定操作位置後),僅需處理結點間的引用即可,時間複雜度爲O(1),而查找操作需要遍歷鏈表逐一進行比對,複雜度爲O(n)

哈希表:

相比上述幾種數據結構,在哈希表中進行添加,刪除,查找等操作,性能十分之高,不考慮哈希衝突的情況下,僅需一次定位即可完成,時間複雜度爲O(1)

二叉樹:

對一棵相對平衡的有序二叉樹,對其進行插入,查找,刪除等操作,平均複雜度均爲O(logn)。

紅黑樹:

紅黑樹也是二叉查找樹的一種,二叉查找樹是一種相對簡單的數據結構,當二叉查找樹處於平衡狀態時,保持使用二分查找法,其查詢效率會很高,但是如果二叉查找樹在某些特殊情況下進行了按序插入的話,就會退化成爲一個鏈表結構,這樣就喪失了二分查找法的高效查詢,查詢的效率就會大大減低

例如上圖利用平衡的二叉樹查找4節點,從根節點3出發,查找節點大於當前節點,根據二叉查找樹的特性,左子樹的節點比右子樹的節點小,所以從3節點的右邊出發到了5節點,查找節點比5小,然後5節點的右邊出發,然後找到4節點,就完成任務,中間只比較了三次,而從鏈表順序找卻要5次,可以對比出保持二叉樹的平衡可以大大的提升查找效率

平衡二叉樹:

爲了使二叉樹保持平衡,我們可以對二叉樹的構成限定規則:二叉樹任意節點的左右子樹深度差不能超過1。例如上圖的退化成鏈表的二叉樹,左子樹深度爲0,右子樹深度爲4,相差達到了4,所以是一個不平衡的二叉樹。

我們在來觀察這兩個二叉樹是否平衡,左邊的二叉樹3節點右子樹深度是2,左子樹深度是零,所以也不是一個平衡二叉樹,轉化成右邊才平衡。

平衡二叉樹的方法有兩種左旋和右旋:

左旋:


右旋

紅黑樹:

紅黑樹也是一個二叉查找樹,它是23樹的表現,保持黑樹的絕對平衡性,犧牲了紅樹的平衡,也就是說紅黑樹犧牲了一部分的查詢效率,但是卻提升了插入和刪除效率,在查詢和修改之間做了擇中,以下是構成紅黑樹幾點:

  • 1、根節點必須是黑色的

  • 2、各個節點只能由黑色和紅色構成

  • 3、每個葉子節點(NIL)都是黑色的空節點

  • 4、紅色節點的子節點必須是黑色的

  • 5、任意節點的左右子樹到葉子節點的途中遇到的黑色節點的數量必須是一樣的

有些人會疑問,既然平衡二叉樹的都已經保持平衡了爲什麼還要引進紅黑樹呢,原因就是因爲平衡二叉樹太注重平衡,必須讓得把自己折騰成一個絕對完美的平衡二叉樹,其頻繁的旋轉調整會使平衡樹的性能大打折扣,而且其複雜度是要比紅黑樹要高的多的,實現起來很麻煩,而紅黑樹就避開了平衡二叉樹的這些問題,由於紅黑樹在增刪中不會頻繁的破壞紅黑樹的規則,所以紅黑樹不必要頻繁的做出調整,但是如果比起查詢性能還是平衡樹的比較高,紅黑樹在性能消耗和速度方面做出了平衡,紅黑樹就算是一個不完美的平衡樹

紅黑樹爲什麼是紅色和黑色的?紅色和黑色都代表着什麼意思?

紅黑樹是等價於一種絕對平衡的數據結構23樹,不懂23樹的同學自己去補課,23樹規定的是一個節點最多可以存放兩個數據,節點的連線是節點數據量加一,也就是存放兩個數據的節點有三條腿稱之爲3節點,存放一個數據的節點最多有兩條腿稱之爲二節點,如圖:存放42的是二節點,它最多有兩條腿,而存放17和33的是三節點,它最多有三條腿。

在向23樹添加數據時不可以向空節點添加,只能由一個節點如果變成了四節點(也就是節點數據有3個的時候)就會分裂,可以向下分裂成新的節點,也可以向上合併節點然後繼續分裂,下圖就是節點變成四節點的時候就會向下分裂,

下圖繼續添加元素,滿四節點就分裂,分裂成下圖的第三個樹時,這時候顯然不是絕對平衡的樹,然後就會向上合併,變成下圖的第四顆樹

繼續添加元素,添加4元素的時候形成了下圖的第二棵樹,由於變成了四節點然後就會向下分裂又形成了一顆不平衡的樹,下面其實畫少了一棵不平衡的樹,然後既然不平衡就得向上融合,然後就變成了下圖的第三課樹,顯然第三棵樹有節點變成了四節點然後就得向下分裂,變成了下圖的第四顆樹,這樣就無論怎麼添加,這個23樹都是絕對的平衡的

紅黑樹其實就是一種23樹的體現,在紅黑樹中紅色代表了和他的父親節點是融合在一起的,在23樹的節點中如果是個三節點那麼最大的就是另外那個數據的父親,如果是四節點那個在中間的就是父親,下圖就是紅黑樹對應的23樹的結構圖,分別是2節點和3節點對應的紅黑樹的結構

如下圖紅黑樹的這種情況就對應着四節點

下圖是一個完整的23樹類比紅黑樹的結構,左邊轉成紅黑樹就是如右邊所示:

由於紅黑樹中有一條超級重要的定義就是:任意節點的左右子樹到葉子節點的途中遇到的黑色節點的數量必須是一樣的

所以紅黑樹保持了絕對的黑平衡,由於紅黑樹只是保證了黑色的絕對平衡,所以查找數據的速度比平衡二叉樹要慢,但是紅黑樹不用平衡二叉樹那樣每添加或者刪除數據都得耗費時間去保持平衡,所以在頻繁的添加刪除修改的操作的場景,紅黑樹是比平衡二叉樹更加有效率,所以紅黑樹是對平衡二叉樹的一種查詢速度和增刪改綜合效率的折中優化

HashMap源碼解讀:

在JDK 1.7中HashMap是一個介於數組加鏈表的結構,用的就是哈希表的鏈地址法,其查詢性能會跟着哈希碰撞的增加而下降,從O(1)下降到了O(n),所以爲了解決這個問題,JDK1.8中加入了數組+鏈表+紅黑樹的結構解決哈希衝突帶來的性能問題提,點開JDK1.8中的HashMap看看HashMap的實現源碼:

由構造函數我們知道HashMap在new出來時不會立馬創建數組,而是在使用時纔開始創建,其加載因子是0.75,加載因子的作用主要是爲了哈希儘量不衝突,加載因子越大意味着數組的空間使用率就越大,但是相應的會發生哈希衝突的概率就越高,相反如果加載因子變小,空間使用率就會低,哈希衝突就小。如果發生了哈希衝突,會導致數組擴容或者節點轉化爲鏈表或者鏈表轉化爲紅黑樹,進而影響性能。所以加載因子默認值我們最好不要改

public HashMap() {
//加載因子0.75
        this.loadFactor = DEFAULT_LOAD_FACTOR;
    }

我們看這幾個重要的參數,DEFAULT_LOAD_FACTOR 加載因子0.75,TREEIFY_THRESHOLD =8和MIN_TREEIFY_CAPACITY=64是鏈表轉化成紅黑樹的兩個閾值, 如果一個節點上的鏈表達到了8並且數組容量到達了64,就會觸發鏈表轉化爲紅黑樹,但是如果鏈表上的節點僅僅只達到了8數組容量還沒達到64,就會觸發擴容,將容量擴容至原來的兩倍然後重新計算各個節點的數組下標,UNTREEIFY_THRESHOLD = 6當某個節點上的紅黑樹節點小於等於6就會觸發紅黑樹退化爲鏈表,DEFAULT_INITIAL_CAPACITY =1<<4就是數組初始化長度16

//數組初始化長度16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 
    static final int MAXIMUM_CAPACITY = 1 << 30;
//加載因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
//觸發轉化紅黑樹的鏈表閾值
    static final int TREEIFY_THRESHOLD = 8;
//紅黑樹退化鏈表的閾值
    static final int UNTREEIFY_THRESHOLD = 6;
//觸發轉化紅黑樹的數組閾值
    static final int MIN_TREEIFY_CAPACITY = 64;

從這兩個方法,我們可以看到key是通過這個Set組織的,value是通過Collection組織的,所以key是不可重複的,value可以重複,它們都是允許null值

 public Set<K> keySet() {
        Set<K> ks = keySet;
        if (ks == null) {
            ks = new KeySet();
            keySet = ks;
        }
        return ks;
    }
public Collection<V> values() {
        Collection<V> vs = values;
        if (vs == null) {
            vs = new Values();
            values = vs;
        }
        return vs;
    }

這是內部的裝載數據的節點結構,會存放k,v,hash,及轉換爲鏈表及樹時的指向next,在jdk1.7直接用的是Entry<K,V>,而jdk1.8改成了Node<K,V>,改成node主要是爲有必要時轉成紅黑樹

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

這個HashMap計算下標的方法得到一個哈希值,再通過這個(n - 1) & hash計算取模,得到的值都是落到哈希桶長度爲n內的

  static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

putVal是HashMap核心方法,
非哈希衝突:
1、當數組空時會調用resize()進行初始化,初始化容量默認是16,resize()除了初始化外還有擴容的功能
2、如果通過key的哈希值得到的數組下標在數組找到的是空值,代表數組還未發生哈希衝突則通過newNode直接往數組增加新的值,
3、如果通過key的哈希值得到的數組下標在數組找到的不是空值,則證明很有可能發生了哈希衝突
哈希衝突:
1、判斷hash和key是否和集合中的重複,重複則覆蓋。判斷是否是紅黑樹,是則直接往紅黑樹添加節點、不是則添加到鏈表中
2、調用treeifyBin方法看看是否需要轉爲紅黑樹
3、最後添加完數據後判斷是否超過擴容閾值而去擴容

  public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
//當數組空時會調用resize()進行初始化
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
//如果通過key的哈希值得到的數組下標在數組找到的p節點是空值,代表數組還未發生哈希衝突則通過newNode直接往數組增加新的值
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
//這裏很有可能是發生哈希衝突了
            Node<K,V> e; K k;
//這裏是put進去的值如果hash、key和p節點的相等,就判斷是重複了,舊值將其覆蓋
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
//判斷如果p節點是樹類型的,就使其插入紅黑樹
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
//遍歷table[i]所對應的鏈表,直到最後一個節點的next爲null或者有重複的key值
                for (int binCount = 0; ; ++binCount) {
//next爲null
                    if ((e = p.next) == null) {
//往鏈表的最後一個節點添加新節點
                        p.next = newNode(hash, key, value, null);
//超過閾值TREEIFY_THRESHOLD -1,其實就是鏈表長度到達或超過了8
                        if (binCount >= TREEIFY_THRESHOLD - 1) 
//treeifyBin方法主要判斷是否有必要轉化成紅黑樹
                            treeifyBin(tab, hash);
                        break;
                    }
//put進去的值如果hash、key和鏈表上的值有重複值就將其覆蓋
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }

//這裏主要是預留給子類去實現
            if (e != null) { 
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
//這裏是預留給子類去實現的方法
                afterNodeAccess(e);
                return oldValue;
            }
        }
//modCount是記錄被修改的次數
        ++modCount;
//哈希桶中節點的數據超過了   加載因子*數組容量   就觸發擴容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

這裏的擴容方法也是數組的初始化方法,一般來說數組容量是初始化至16,擴容閾值初始化至12,如果擴容的話會將數組和閾值同時擴容至2倍

  final Node<K,V>[] resize() {
//獲取舊的數組
        Node<K,V>[] oldTab = table;
//這裏是初始化用的
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
//這個是舊的觸發擴容的閾值,初始化時  0.75*16=12
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
//判斷舊的數組是否是達到或超過了最大容量,是則擴展到Integer.MAX_VALUE
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
//沒有超過最大容量MAXIMUM_CAPACITY,就將數組容量newCap擴大兩倍、閾值threshold  (默認是12)也擴大至2倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }

        else if (oldThr > 0) 
//將舊閾值賦值給newCap 
            newCap = oldThr;
        else {            
//這裏是初始化時使用的,數組容量初始爲16,將newThr 閾值初始化爲DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY 默認是12
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
//這裏是初始化時使用的,將newThr 閾值初始化爲DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY 默認是12
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
//閾值newThr 賦值給threshold 
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
//new出擴容至的新數組
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
//變量數組
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)
//重新計算數組下標,並存到新數組中
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
//如果有紅黑樹,重新移位,將紅黑樹放到新的節點
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
               //...........................省略
        return newTab;
    }

這裏會判斷數組長度是否小於64,小於64就不走轉紅黑樹的方法,就調用resize()方法進行擴容,大於64才進行轉紅黑樹

final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
//這裏會判斷數組長度是否小於64,小於64就不走轉紅黑樹的方法,就調用resize()方法進行擴容
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            do {
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }

這裏我們總結一下初始化、擴容、轉鏈表、及轉換成紅黑樹、紅黑樹退化的條件

  • 初始化:哈希桶16,擴容閾值12,加載因子0.75
  • 擴容:哈希桶超過擴容閾值時 擴容閾值和哈希桶同時擴容至兩倍,鏈表大於等於8的情況下哈希桶小於64 哈希桶和擴容閾值同時擴容至兩倍
  • 轉鏈表:發生哈希衝突時轉鏈表
  • 轉紅黑樹:鏈表節點大於8並且哈希桶容量大於等於64
  • 紅黑樹退化:鏈表節點小於等於6
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章