集合框架源碼分析(jdK1.7)(三)TreeMap

目錄

1.紅黑樹簡介

1.1二叉樹

1.2平衡二叉樹

1.3紅黑樹

1.4紅黑樹添加操作

1.5左旋、右旋

1.6刪除操作

2.TreeMap的數據結構

3.主要參數:

4.Treem主要構造方法:

5.TreeMap的Put方法

5.1 fixAfterInsertion(e)把二叉樹調整爲紅黑樹;

5.2rotateRight()左旋方法

5.3rotateRight()右旋轉方法

5.4 setColor()着色:

6remove(Object key)方法

7.總結


1.紅黑樹簡介

TreeMap的實現是紅黑樹算法的實現瞭解TreeMap首先要了解紅黑樹。

1.1二叉樹

概念:n(n≥0)個結點的有限集合,由一個根節點以及兩顆互不相交的、分別稱爲左子樹右子樹的二叉樹組成。

基本特徵:

每個結點最多隻有兩顆子樹(不存在度>2的結點)。

左子樹和右子樹次序不能顛倒(有序樹)。

1.2平衡二叉樹

概念:它是一棵空樹或它的左右兩個子樹的高度差的絕對值不超過1,並且左右兩個子樹都是一棵平衡二叉樹。

1.3紅黑樹

概念:  紅黑樹顧名思義就是節點是紅色或者黑色的平衡二叉樹,它通過顏色的約束來維持着二叉樹的平衡。

特點:

 1、每個節點都只能是紅色或者黑色

       2、根節點是黑色

       3、每個葉節點(NIL節點,空節點)是黑色的。 [注意:這裏葉子節點,是指爲空(NILNULL)的葉子節點!]

       4、如果一個結點是紅的,則它兩個子節點都是黑的。也就是說在一條路徑上不能出現相鄰的兩個紅色結點。

       5、從任一節點到其每個葉子的所有路徑都包含相同數目的黑色節點。

注意:

(01) 特性(3)中的葉子節點,是隻爲空(NILnull)的節點。

(02) 特性(5),確保沒有一條路徑會比其他路徑長出倆倍。因而,紅黑樹是相對是接近平衡的二叉樹

下面是一個紅黑樹所有葉子節點都爲NULL節點沒有表示出來

自動地址  https://sandbox.runjs.cn/show/2nngvn8w

 

1.4紅黑樹添加操作

首先,將紅黑樹當作一顆二叉查找樹,將節點插入;然後,將節點着色爲紅色;最後,通過旋轉和重新着色等方法來修正該樹,使之重新成爲一顆紅黑樹。詳細描述如下:

第一步: 將紅黑樹當作一顆二叉查找樹,將節點插入

      紅黑樹本身就是一顆二叉查找樹,將節點插入後,該樹仍然是一顆二叉查找樹。也就意味着,樹的鍵值仍然是有序的。此外,無論是左旋還是右旋,若旋轉之前這棵樹是二叉查找樹,旋轉之後它一定還是二叉查找樹。這也就意味着,任何的旋轉和重新着色操作,都不會改變它仍然是一顆二叉查找樹的事實。

第二步:將插入的節點着色爲"紅色"。

      爲什麼着色成紅色,而不是黑色呢?爲什麼呢?在回答之前,我們需要重新溫習一下紅黑樹的特性

(1) 每個節點或者是黑色,或者是紅色。

(2) 根節點是黑色。

(3) 每個葉子節點是黑色。 [注意:這裏葉子節點,是指爲空的葉子節點!]

(4) 如果一個節點是紅色的,則它的子節點必須是黑色的。

(5) 從一個節點到該節點的子孫節點的所有路徑上包含相同數目的黑節點。

      將插入的節點着色爲紅色,不會違背"特性(5)"!少違背一條特性,就意味着我們需要處理的情況越少。

第三步: 通過一系列的旋轉或着色等操作,使之重新成爲一顆紅黑樹。

      第二步中,將插入節點着色爲"紅色"之後,不會違背"特性(5)"。那它到底會違背哪些特性呢?

      對於"特性(1)",顯然不會違背了。因爲我們已經將它塗成紅色了。

      對於"特性(2)",顯然也不會違背。在第一步中,我們是將紅黑樹當作二叉查找樹,然後執行的插入操作。而根據二叉查找數的特點,插入操作不會改變根節點。所以,根節點仍然是黑色。

      對於"特性(3)",顯然不會違背了。這裏的葉子節點是指的空葉子節點,插入非空節點並不會對它們造成影響。

      對於"特性(4)",是有可能違背的

      那接下來,想辦法使之"滿足特性(4)",就可以將樹重新構造成紅黑樹了。

增加方案的解決方法:

根據被插入節點的父節點的情況,可以將"當節點z被着色爲紅色節點,並插入二叉樹"劃分爲三種情況來處理。

①情況說明:被插入的節點是根節點。

    處理方法:直接把此節點塗爲黑色。

②情況說明:被插入的節點的父節點是黑色。

    處理方法:什麼也不需要做。節點被插入後,仍然是紅黑樹。

③情況說明:被插入的節點的父節點是紅色。

處理方法:那麼,該情況與紅黑樹的“特性(5)”相沖突。這種情況下,被插入節點是一定存在非空祖父節點的;進一步的講,被插入節點也一定存在叔叔節點(即使叔叔節點爲空,我們也視之爲存在,空節點本身就是黑色節點)。理解這點之後,我們依據"叔叔節點的情況",

將這種情況進一步劃分爲3種情況(Case)。

 

 

現象說明

處理策略

Case 1

當前節點的父節點是紅色,且當前節點的祖父節點的另一個子節點(叔叔節點)也是紅色。

(01) 父節點設爲黑色。
(02) 叔叔節點設爲黑色。
(03) 祖父節點設爲紅色
(04) 祖父節點設爲當前節點”(紅色節點);即,之後繼續對當前節點進行操作。

Case 2

當前節點的父節點是紅色,叔叔節點是黑色,且當前節點是其父節點的右孩子

(01) 父節點作爲新的當前節點
(02) 新的當前節點爲支點進行左旋。

Case 3

當前節點的父節點是紅色,叔叔節點是黑色,且當前節點是其父節點的左孩子

(01) 父節點設爲黑色
(02) 祖父節點設爲紅色
(03) 祖父節點爲支點進行右旋。

 

1.5左旋、右旋

根據二叉樹的特點插入節點,插入結束後爲了維持平衡二叉樹採用左旋、右旋的方式如圖:

 

左右旋的原理相同

1.6刪除操作

將紅黑樹內的某一個節點刪除。需要執行的操作依次是:首先,將紅黑樹當作一顆二叉查找樹,將該節點從二叉查找樹中刪除;然後,通過"旋轉和重新着色"等一系列來修正該樹,使之重新成爲一棵紅黑樹。

刪除方案:

  按照待刪除節點有幾個兒子的情況來進行分類:

   情況一、無子節點(紅色節點)

       這種情況對該節點直接刪除即可,不會影響樹的結構。

       情況二、有一個子節點

       這種情況處理也是非常簡單的,用子節點替代待刪除節點,然後刪除子節點即可。

       情況三、有兩個子節點

       這種情況可能會稍微有點兒複雜。它需要找到一個替代待刪除節點(N)來替代它,然後刪除N即可。它主要分爲四種情況。

       1N的兄弟節點W爲紅色

       2N的兄弟w是黑色的,且w的倆個孩子都是黑色的。

       3N的兄弟w是黑色的,w的左孩子是紅色,w的右孩子是黑色。

       4N的兄弟w是黑色的,且w的右孩子時紅色的。

//todo

原理圖:http://www.cnblogs.com/yangecnu/p/Introduce-Red-Black-Tree.html

 

參考:https://blog.csdn.net/zhangyuan19880606/article/details/51234420/

http://www.cnblogs.com/skywang12345/p/3245399.html

2.TreeMap的數據結構

.TreeMap內部是有Entry<K,V>維護的,可以看出Entry<K,V>的結構就是一個紅黑樹的結構,

包含:左子樹右子樹,和父節點節點顏色(默認顏色爲黑色)

static final class Entry<K,V> implements Map.Entry<K,V> {
   
K key;
   
V value;
   
Entry<K,V> left = null;
   
Entry<K,V> right = null;
   
Entry<K,V> parent;
   
boolean color = BLACK;

   
/**
    
* Make a new cell with given key, value, and parent, and with
     * {@code null} child links, and BLACK color.
     */
   
Entry(K key, V value, Entry<K,V> parent) {
       
this.key = key;
       
this.value = value;
       
this.parent = parent;
   
}

 

 

3.主要參數:

//比較器,因爲TreeMap是有序的,通過comparator接口我們可以對TreeMap的內部排序進行精密的控制

private final Comparator<? super K> comparator;

//TreeMap-黑節點,爲TreeMap的內部類
private transient Entry<K,V> root = null;

//容器大小
private transient int size = 0;

//TreeMap修改次數
private transient int modCount = 0;

 

 

4.Treem主要構造方法:

public TreeMap() {

//始化的時候先初始化一個爲Null的比較器
    comparator = null;
}

 

TreeMap是紅黑樹無初始化容量等參數

5.TreeMap的Put方法

public V put(K key, V value) {

    //用t表示二叉樹的當前節點
    Entry<K,V> t = root;

    //t爲null表示一個空樹,即TreeMap中沒有任何元素,直接插入
    if (t == null) {

        //比較key值(不是很理解)
        compare(key, key); // type (and possibly null) check

        //將新的key-value鍵值對創建爲一個Entry節點,並將該節點賦予給root
        root = new Entry<>(key, value, null);

        //容器的size = 1,表示TreeMap集合中存在一個元素
        size = 1;

        modCount++; //修改次數 + 1
        return null;
   
}


    int cmp; //cmp表示key排序的返回結果

Entry<K,V> parent; //父節點
    // split comparator and comparable paths
   
Comparator<? super K> cpr = comparator; //指定的排序算法

//如果cpr不爲空,則採用既定的排序算法進行創建TreeMap集合
    if (cpr != null) {
       
do {
           
parent = t; //parent指向上次循環後的t
            cmp = cpr.compare(key, t.key); //比較新增節點的key和當前節點key的大小

        //cmp返回值小於0,表示新增節點的key小於當前節點的key,則以當前節點的左子節點作爲新的當前節點
            if (cmp < 0)
               
t = t.left;

        //cmp返回值大於0,表示新增節點的key大於當前節點的key,則以當前節點的右子節點作爲新的當前節點
            else if (cmp > 0)
               
t = t.right;

        //cmp返回值等於0,表示兩個key值相等,則新值覆蓋舊值,並返回新值
            else
               
return
t.setValue(value);
       
} while (t != null);
   
}

//如果cpr爲空,則採用默認的排序算法進行創建TreeMap集合(默認構造方法時cpr爲null)
    else {
       
if (key == null) //key值爲空拋出異常
            throw new NullPointerException();

/* 下面處理過程用compareTo比較和上面一樣 */
        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);
   
}

    //將新增節點當做parent的子節點
    Entry<K,V> e = new Entry<>(key, value, parent);

//如果新增節點的key小於parent的key,則當做左子節點
    if (cmp < 0)
       
parent.left = e;
   
else

  //如果新增節點的key小於parent的key,則當做左子節點
        parent.right = e;

    /*

     上面已經完成了排序二叉樹的的構建,將新增節點插入該樹中的合適位置

     下面fixAfterInsertion()方法就是對這棵樹進行調整、平衡

     */
    fixAfterInsertion(e);
    size++; //TreeMap元素數量 + 1
    modCount++; //TreeMap容器修改次數 + 1
    return null;
}

 

 

5.1 fixAfterInsertion(e)把二叉樹調整爲紅黑樹;

上面代碼中do{}代碼塊是實現排序二叉樹的核心算法,通過該算法我們可以確認新增節點在該樹的正確位置。找到正確位置後將插入即可,這樣做了其實還沒有完成,因爲我知道TreeMap的底層實現是紅黑樹,紅黑樹是一棵平衡排序二叉樹,普通的排序二叉樹可能會出現失衡的情況,所以下一步就是要進行調整。fixAfterInsertion(e); 調整的過程務必會涉及到紅黑樹的左旋、右旋、着色三個基本操作。代碼如下:

private void fixAfterInsertion(Entry<K,V> x) { // Entry<K,V> x表示新增節點
    x.color = RED; //新增節點的顏色爲紅色

   //循環 直到 x不是根節點,且x的父節點不爲紅色
    while (x != null && x != root && x.parent.color == RED) {
       
 //如果X的父節點(P)是其父節點的父節點(G)的左節點

         if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {

             //獲取X的叔節點(U)
            Entry<K,V> y = rightOf(parentOf(parentOf(x)));

            //如果X的叔節點(U) 爲紅色(情況三)
            if (colorOf(y) == RED) {

             //將X的父節點(P)設置爲黑色
                setColor(parentOf(x), BLACK);

             //將X的叔節點(U)設置爲黑色
                setColor(y, BLACK);

             //將X的父節點的父節點(G)設置紅色
                setColor(parentOf(parentOf(x)), RED);
               
x = parentOf(parentOf(x));

             //如果X的叔節點(U爲黑色);這裏會存在兩種情況(情況四、情況五)
            } else {

             //如果X節點爲其父節點(P)的右子樹,則進行左旋轉(情況四)
                if (x == rightOf(parentOf(x))) {

                     //將X的父節點作爲X
                    x = parentOf(x);

                     //右旋轉
                    rotateLeft(x);
                }

              //(情況五)

               //將X的父節點(P)設置爲黑色
                setColor(parentOf(x), BLACK);

               //將X的父節點的父節點(G)設置紅色
                setColor(parentOf(parentOf(x)), RED);

               //以X的父節點的父節點(G)爲中心右旋轉
                rotateRight(parentOf(parentOf(x)));
            }

        //如果X的父節點(P)是其父節點的父節點(G)的右節點
        } else {

//獲取X的叔節點(U)
            Entry<K,V> y = leftOf(parentOf(parentOf(x)));
           
if (colorOf(y) == RED) {

//將X的父節點(P)設置爲黑色
                setColor(parentOf(x), BLACK);

//將X的叔節點(U)設置爲黑色
                setColor(y, BLACK);

                //將X的父節點的父節點(G)設置紅色
                setColor(parentOf(parentOf(x)), RED);
               
x = parentOf(parentOf(x));
            }

//如果X的叔節點(U爲黑色);這裏會存在兩種情況(情況四、情況五)

             else {

//如果X節點爲其父節點(P)的右子樹,則進行左旋轉(情況四)
                if (x == leftOf(parentOf(x))) {

//將X的父節點作爲X
                    x = parentOf(x);

//右旋轉
                    rotateRight(x);
                }

                 //(情況五)

                //將X的父節點(P)設置爲黑色
                setColor(parentOf(x), BLACK);

//將X的父節點的父節點(G)設置紅色
                setColor(parentOf(parentOf(x)), RED);

//以X的父節點的父節點(G)爲中心右旋轉
                rotateLeft(parentOf(parentOf(x)));
            }
        }
    }

//將根節點G強制設置爲黑色
    root.color = BLACK;
}

 

 

 

解釋:對這段代碼的研究我們發現,其處理過程完全符合紅黑樹新增節點的處理過程。所以在看這段代碼的過程一定要對紅黑樹的新增節點過程有了解。在這個代碼中還包含幾個重要的操作。左旋(rotateLeft())、右旋(rotateRight())、着色(setColor())。

 

左旋:rotateLeft(): 所謂左旋轉,就是將新增節點(N)當做其父節點(P),將其父節點P當做新增節點(N)的左子節點。

即:G.left ---> N ,N.left ---> P

右旋:P.right ---> G、G.parent ---> P

5.2rotateRight()左旋方法

private void rotateLeft(Entry<K,V> p) {

        if (p != null) {

            //獲取P的右子節點,其實這裏就相當於新增節點N(情況四而言)

            Entry<K,V> r = p.right;

            //R的左子樹設置爲P的右子樹

            p.right = r.left;

            //R的左子樹不爲空,則將P設置爲R左子樹的父親

            if (r.left != null)

                r.left.parent = p;

            //P的父親設置R的父親

            r.parent = p.parent;

            //如果P的父親爲空,則將R設置爲跟節點

            if (p.parent == null)

                root = r;

            //如果P爲其父節點(G)的左子樹,則將R設置爲P父節點(G)左子樹

            else if (p.parent.left == p)

                p.parent.left = r;

            //否則R設置爲P的父節點(G)的右子樹

            else

                p.parent.right = r;

            //P設置爲R的左子樹

            r.left = p;

            //R設置爲P的父節點

            p.parent = r;

        }

}

 

 

5.3rotateRight()右旋轉方法

private void rotateRight(Entry<K,V> p) {

        if (p != null) {

            //L設置爲P的左子樹

            Entry<K,V> l = p.left;

            //L的右子樹設置爲P的左子樹

            p.left = l.right;

            //L的右子樹不爲空,則將P設置L的右子樹的父節點

            if (l.right != null)

                l.right.parent = p;

            //P的父節點設置爲L的父節點

            l.parent = p.parent;

            //如果P的父節點爲空,則將L設置根節點

            if (p.parent == null)

                root = l;

            //P爲其父節點的右子樹,則將L設置爲P的父節點的右子樹

            else if (p.parent.right == p)

                p.parent.right = l;

            //否則將L設置爲P的父節點的左子樹

            else

                p.parent.left = l;

            //P設置爲L的右子樹

            l.right = p;

            //L設置爲P的父節點

            p.parent = l;

        }

    }

 

 

5.4 setColor()着色:

着色就是改變該節點的顏色,在紅黑樹中,它是依靠節點的顏色來維持平衡的

private static <K,V> void setColor(Entry<K,V> p, boolean c) {
   
if (p != null)
       
p.color = c;
}

 


6remove(Object key)方法

//todo

7.總結

1.TreeMap和HashMap,HashTable的數據結構完全不同所以性質完全不同

2. TreeMap爲紅黑樹,所有節點是有順序的,(按順序插入)

3.無初始容量和擴容

4.思考爲什麼不用平衡二叉樹實現呢?

紅黑樹的性質5中說從一個節點出發到自己子孫節點存在相同數量的黑色節點,這就保證了沒有一條路徑會比其他路徑長兩倍,而平衡二叉樹中必須保證度相差不大於1,所以平衡二叉樹降低了要求,有維持了查找性能

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