1. 程式人生 > >Java集合原始碼解析:TreeMap

Java集合原始碼解析:TreeMap

本文概要

  1. 二叉查詢樹的用處
  2. 二叉查詢樹,以及二叉樹帶來的問題
  3. 平衡二叉樹的好處
  4. 紅黑樹的定義以及構造
  5. 紅黑樹在TreeMap的運用

二叉樹的好處

可能許多人會有疑問,為什麼要使用二叉樹,有那麼多的資料結構,比如陣列、連結串列等

簡單看下陣列和連結串列的優缺點

陣列

  • 優勢:查詢快,通過索引直接定位資料。時間複雜度O(1)
  • 劣勢:刪除和插入元素比較麻煩,需要移動的元素比較多。時間複雜度O(n)

連結串列

  • 優勢:刪除和插入比較方便,直接修改指標,時間複雜度O(1)
  • 劣勢:查詢慢,需要沿著頭指標挨個去對比,時間複雜度O(n)

那麼二叉樹則是結合了上面兩種資料結構的優勢,並且它是有序的,而且在處理大批量的動態資料是比較有用的。它的時間複雜度O(logN)

二叉查詢樹

先來看看二叉查詢樹的定義:

  1. 要麼是一顆空樹,要麼就是一顆具有如下特性的二叉樹
  2. 左節點的值必須小於等於父節點的值
  3. 右節點的值必須大於等於父節點的值

每個節點都符合這個特性,所以它是有序的,也便於查詢,如下圖:

在這裡插入圖片描述

但是在一種極端的情況下,二叉查詢樹會出現不平衡。如果一棵二叉樹,只有左子樹或者右子樹,就變成了一個連結串列,查詢的效率就變的很慢,如下圖: 在這裡插入圖片描述

對於查詢而言,二叉查詢樹的查詢是跟樹的高度是有關係的,如果一棵樹的高度為N,那麼最多可以在N步內完成查詢,所以樹的高度越矮,那麼查詢的效率就越高。考慮到一般情況,左子樹和右子樹的高度不能相差太大,所以我們都希望二叉查詢樹兩邊子樹是平衡的,而不是隻有一邊子樹。為了優化因左右子樹高度不穩定對查詢效率的影響,於是出現了平衡二叉樹

平衡二叉樹

先看平衡二叉樹的定義:

  1. 它是一顆二叉樹
  2. 它的左子樹和右子樹的深度差的絕對值不超過1

在這裡插入圖片描述

在構造平衡二叉樹時,新增一個節點,可能會造成二叉樹的失衡,失衡調整主要是通過旋轉最小失衡樹來實現。

失衡調整主要分為4種情況:

  • LL型 LL型

當插入“7”節點,是最小失衡樹的左子樹的“8”左節點。很顯然,是“9”的左子樹過高,那麼以"9"節點為軸心右旋

  • LR型 LR型

當插入"8"節點,是最小失衡樹左子樹的“7”的右節點。首先以“7”為軸心,然後左旋,變成了LL型,然後以“9”為軸心右旋。

  • RR型 RR型 當插入“11”節點,是最小失衡樹的右子樹的“10”右節點。很顯然,是“9”的右子樹過高,那麼以"9"節點為軸心左旋

  • RL型 RL

當插入"11"節點,是最小失衡樹右子樹的“12”的左節點。首先以“12”為軸心,右旋,變成了RR型,然後以“10”為軸心右旋。

紅黑樹

先來看看紅黑樹的定義

  1. 每個節點要麼紅色,要麼黑色
  2. 根節點是黑色
  3. 所有葉子節點是黑色,即空節點(NIL)
  4. 每個紅色節點的兩個子節點都是黑色(從每個葉子到根的所有路徑上不能有兩個連續的紅色節點)
  5. 從一個節點到其所有葉子節點的所有路徑上包含相同數目的黑節點

注意:

  • 特性3中的葉子節點,是隻為空(NIL或null)的節點。
  • 特性5,確保沒有一條路徑會比其他路徑長出倆倍。因而,紅黑樹是相對是接近平衡的二叉樹。因此在最壞情況下,紅黑樹能保證時間複雜度為O( lgn )

在這裡插入圖片描述

當對紅黑樹進行插入和刪除時,可能會破壞紅黑樹的性質,那麼就需要通過修改某些節點顏色樹的旋轉來恢復紅黑樹的性質

樹的旋轉,分為左旋和右旋,如下圖

  • 左旋 在這裡插入圖片描述 對A節點進行左旋,首先找到A節點的右孩子節點B,讓B節點的左孩子節點C指向A節點的右孩子節點,再把A節點指向B節點的左孩子節點。

  • 右旋 在這裡插入圖片描述

對A節點進行右旋,首先找到A節點的左孩子節點B,讓B節點的右孩子節點D執行A節點的右孩子節點,在把A節點執行B節點的右孩子節點。

紅黑樹的插入

向一棵含有n個節點的紅黑樹插入一個新節點的操作可以在O(lgn)時間內完成。 在繼續插入操作分析前,再來複習下紅黑樹的特性:

  1. 每個節點要麼紅色,要麼黑色
  2. 根節點是黑色
  3. 所有葉子節點是黑色,即空節點(NIL)
  4. 每個紅色節點的兩個子節點都是黑色(從每個葉子到根的所有路徑上不能有兩個連續的紅色節點)
  5. 從一個節點到其所有葉子節點的所有路徑上包含相同數目的黑節點

規則:

  • 在紅黑樹插入節點時,節點的初始顏色是紅色,這樣可以儘量避免對樹的結構進行調整(參考第5個規則)
  • 但是插入紅色節點的時候,不會破壞第5個規則,但是可能會破壞第4個規則,所以這時候就需要通過修改某些節點的顏色、對某些節點進行旋轉,來維持紅黑樹的性質
  • 在刪除節點是時候,如果刪除的節點為黑色,可能會破壞第5個規則,那麼同樣需要修復樹的結構,以進行維護樹的性質

插入節點可以分為7種情況進行處理

  1. 空樹中插入節點
  2. 插入節點的父節點是黑色
  3. 插入節點的父節點是紅色,且叔叔節點是黑色,當前節點是左節點,父節點是左節點
  4. 插入節點的父節點是紅色,且叔叔節點是黑色,當前節點是左節點,父節點是右節點
  5. 插入節點的父節點是紅色,且叔叔節點是黑色,當前節點是右節點,父節點是右節點
  6. 插入節點的父節點是紅色,且叔叔節點是黑色,當前節點是右節點,父節點是左節點
  7. 插入節點的父節點是紅色,且叔叔節點也是紅色

情況一:空樹中插入節點

違反:性質2

修復策略:把插入節點修改為黑色即可

情況二:插入節點的父節點是黑色

違反:未違反任何性質

修復策略:什麼都不做

情況三: 插入節點的父節點是紅色,且叔叔節點是黑色,當前節點是左節點,父節點是左節點

違反:性質4

修復策略:

  1. 把父節點顏色修改為黑色
  2. 把祖父節點顏色修改為紅色
  3. 對祖父節點進行右旋

如下圖所示: 在這裡插入圖片描述

情況四: 插入節點的父節點是紅色,且叔叔節點是黑色,當前節點是左節點,父節點是右節點

違反:性質4

修復策略:

  1. 對父節點進行右旋
  2. 把自己節點顏色修改為黑色,祖父節點修改為紅色
  3. 對祖父節點進行左旋

如下圖: 在這裡插入圖片描述

情況五: 插入節點的父節點是紅色,且叔叔節點是黑色,當前節點是右節點,父節點是右節點

違反:性質4

修復策略:

  1. 把父節點顏色修改為黑色
  2. 把祖父節點顏色修改為紅色
  3. 對祖父節點進行左旋

圖就不畫,跟情況三型別

情況六: 插入節點的父節點是紅色,且叔叔節點是黑色,當前節點是右節點,父節點是左節點

違反:性質4

修復策略:

  1. 對父節點進行左旋
  2. 把自己節點修改為黑色,把祖父節點顏色修改為紅色
  3. 對祖父節點進行右旋

圖就不畫,跟情況四型別

情況七: 插入節點的父節點是紅色,且叔叔節點是紅色

違反:性質4

修復策略:

  1. 把父節點顏色和叔叔節點顏色修改為黑色
  2. 把祖父節點顏色修改為紅色
  3. 然後會變成情況一至七的情況,繼續按情況進行分析

刪除節點對節點的調整,我們在TreeMap在進行分析

TreeMap

Java TreeMap實現了SortedMap介面,也就是說會按照key的大小順序對Map中的元素進行排序,key大小的判定通過其本身自帶的自然排序,也可以通過構造器傳入Comparator比較器。

TreeMap底層是通過紅黑樹實現,也就意味著containsKey(),get(),put(),remove()的時間複雜度都為O(log(n))

首先來看看TreeMap構造器

    public TreeMap() {
        comparator = null;
    }
    
    public TreeMap(Comparator<? super K> comparator) {
        this.comparator = comparator;
    }

	public TreeMap(Map<? extends K, ? extends V> m) {
        comparator = null;
        putAll(m);
    }
	
	public TreeMap(SortedMap<K, ? extends V> m) {
        comparator = m.comparator();
        try {
            buildFromSorted(m.size(), m.entrySet().iterator(), null, null);
        } catch (java.io.IOException cannotHappen) {
        } catch (ClassNotFoundException cannotHappen) {
        }
    }
    

TreeMap的成員變數

public class TreeMap<K,V>
    extends AbstractMap<K,V>
    implements NavigableMap<K,V>, Cloneable, java.io.Serializable
{
    // key的比較器
    private final Comparator<? super K> comparator;

	// 樹的根節點
    private transient Entry<K,V> root;

     // 樹的節點個數
    private transient int size = 0;

     // 對樹的修改次數
    private transient int modCount = 0;
    
    ````省略程式碼
}

下面我們依次來看get()、put()、remove()方法

get()

    public V get(Object key) {
    	// get方法實際上呼叫的是getEntry()
        Entry<K,V> p = getEntry(key);
        // 如果p節點存在,則返回p節點的value,否則返回null
        return (p==null ? null : p.value);
    }
    
    final Entry<K,V> getEntry(Object key) {
        // Offload comparator-based version for sake of performance
        // 如果建立TreeMap的時候傳入了比較器,那麼呼叫getEntryUsingComparator(key)        
		// getEntryUsingComparator(key)跟getEntry(key)邏輯差不多,只不過一個使用了自定義比較器去比較key,一個使用自身的比較器去比較key		       
        if (comparator != null)
            return getEntryUsingComparator(key);
        if (key == null)
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
        // 把key強轉為比較器
        Comparable<? super K> k = (Comparable<? super K>) key;
        // 獲取根節點
        Entry<K,V> p = root;
        while (p != null) {
       		// key與根節點的key進行比較
            int cmp = k.compareTo(p.key);
            if (cmp < 0)
            	// key小,則把左節點賦給p進行迴圈
                p = p.left;
            else if (cmp > 0)
            	// key大,則把右節點賦給p進行迴圈
                p = p.right;
            else
            	// 相等,直接返回p節點
                return p;
        }
        return null;
    }

get()方法還是比較簡單的,從根節點開始,依次對節點的key進行判斷,如果大於節點的key則繼續判斷節點的右孩子節點,以此類推,直到找到相等key的節點。上面都有註釋講的非常清楚了。

put()

    public V put(K key, V value) {
        Entry<K,V> t = root;
        // 如果根節點為空
        if (t == null) {
            compare(key, key); // type (and possibly null) check
			// 直接建立一個Entry,賦給根節點
            root = new Entry<>(key, value, null);
            // 樹節點的大小賦值為1
            size = 1;
            // 修改次數+1
            modCount++;
            return null;
        }
        int cmp;
        Entry<K,V> parent;
        // split comparator and comparable paths
        // 獲取比較器
        Comparator<? super K> cpr = comparator;
        // 判斷該key是否存在,如果存在直接找到該節點,把節點的值修改為新value,然後直接返回
        if (cpr != null) {
            do {
                parent = t;
                cmp = cpr.compare(key, t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        else {
            if (key == null)
                throw new NullPointerException();
            @SuppressWarnings("unchecked")
                Comparable<? super K> k = (Comparable<? super K>) key;
            do {
                parent = t;
                cmp = k.compareTo(t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        // 如果key不存在
        // 建立一個新的Entry節點
        Entry<K,V> e = new Entry<>(key, value, parent);
        // key與parent的key進行比較
        if (cmp < 0)
        	// key小,把新節點指向parent的左節點
            parent.left = e;
        else
        	// key大,把新節點指向parent的右節點
            parent.right = e;
        // 添加了一個紅色的新節點,可能會破壞原來的紅黑樹結構,那麼需要進行修復
        fixAfterInsertion(e);
        // 節點+1
        size++;
        // 修改次數+1
        modCount++;
        return null;
    }
    
    private void fixAfterInsertion(Entry<K,V> x) {
        x.color = RED;

        while (x != null && x != root && x.parent.color == RED) {
            if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
                Entry<K,V> y = rightOf(parentOf(parentOf(x)));
                if (colorOf(y) == RED) {
                    setColor(parentOf(x), BLACK);
                    setColor(y, BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    x = parentOf(parentOf(x));
                } else {
                    if (x == rightOf(parentOf(x))) {
                        x = parentOf(x);
                        rotateLeft(x);
                    }
                    setColor(parentOf(x), BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    rotateRight(parentOf(parentOf(x)));
                }
            } else {
                Entry<K,V> y = leftOf(parentOf(parentOf(x)));
                if (colorOf(y) == RED) {
                    setColor(parentOf(x), BLACK);
                    setColor(y, BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    x = parentOf(parentOf(x));
                } else {
                    if (x == leftOf(parentOf(x))) {
                        x = parentOf(x);
                        rotateRight(x);
                    }
                    setColor(parentOf(x), BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    rotateLeft(parentOf(parentOf(x)));
                }
            }
        }
        root.color = BLACK;
    }

put()方法總結:

  • 首先獲取樹根節點,如果根節點為空,那麼直接建立一個新節點指向根節點
  • 根節點不為空,然後判斷樹裡面是否存在節點的key與新增的key相等
  • 如果相等,那麼直接把該節點的value替換成新的value
  • 如果不相等,然後建立一個新的節點,新增到紅黑樹中
  • 添加了一個新的節點可能會破壞樹的結構,那麼呼叫fixAfterInsertion(),進行紅黑樹結構進行調整。

fixAfterInsertion()跟我們在上面講紅黑樹插入的情況,已經講的非常清楚了。

remove()

    public V remove(Object key) {
    	// 首先判斷該key是否存在
        Entry<K,V> p = getEntry(key);
        if (p == null)
        	// 不存在直接返回null
            return null;

        V oldValue = p.value;
        // 呼叫deleteEntry()刪除節點
        deleteEntry(p);
        return oldValue;
    }
    
    private void deleteEntry(Entry<K,V> p) {
    	// 修改次數+1
        modCount++;
        // 樹節點個數-1
        size--;
		
		// 如果p節點的左右孩子節點都不為空,那麼呼叫successor(p)尋找後繼節點
        if (p.left != null && p.right != null) {
        	// 尋找後繼節點邏輯很簡單
        	// 即為:p節點的右子樹的最小的那個元素,即為p的後繼節點
            Entry<K,V> s = successor(p);
            // 把p替換成後繼節點
            p.key = s.key;
            p.value = s.value;
            p = s;
        }        
        // 獲取p節點的左孩子節點,如果左孩子節點不存在,則獲取p節點的右孩子節點
        Entry<K,V> replacement = (p.left != null ? p.left : p.right);
			
		// 如果p節點的孩子節點不為空
        if (replacement != null) {
            replacement.parent = p.parent;
            if (p.parent == null)
                root = replacement;
            else if (p == p.parent.left)
                p.parent.left  = replacement;
            else
                p.parent.right = replacement;

            p.left = p.right = p.parent = null;

            if (p.color == BLACK)
                fixAfterDeletion(replacement);
        } else if (p.parent == null) { 
        	// 如果p的父節點為null,則為root節點
            root = null;
        } else { 
        	// 如果p節點沒有孩子節點
            if (p.color == BLACK)
                fixAfterDeletion(p);

            if (p.parent != null) {
                if (p == p.parent.left)
                    p.parent.left = null;
                else if (p == p.parent.right)
                    p.parent.right = null;
                p.parent = null;
            }
        }
    }

successor(),找到後繼節點

    static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) {
        if (t == null)
            return null;
        else if (t.right != null) {        	
            Entry<K,V> p = t.right;
            // 沿著t的右節點的左子樹找到最小的元素
            while (p.left != null)
                p = p.left;
            return p;
        } else {
            Entry<K,V> p = t.parent;
            Entry<K,V> ch = t;
            while (p != null && ch == p.right) {
                ch = p;
                p = p.parent;
            }
            return p;
        }
    }

找到後繼節點原理很簡單,就是沿著右孩子節點的左子樹找到最小的元素

fixAfterDeletion(),對刪除節點的樹,進行修復

    private void fixAfterDeletion(Entry<K,V> x) {
        while (x != root && colorOf(x) == BLACK) {
            if (x == leftOf(parentOf(x))) {
                Entry<K,V> sib = rightOf(parentOf(x));

                if (colorOf(sib) ==