天天看點

Java Core系列之TreeMap實作詳解

因為看ehcache中溢出檔案的管理代碼,它用到了aa-tree作為檔案中的磁盤管理,因而決定先複習以下紅黑樹(rbt, red black tree),順便看看treemap的代碼。關于紅黑樹,網上已經有各種相關的文章了,因而屬于多一篇不多,少一篇不少的狀态,因而我純粹當作是我自己了解的紀錄,加深印象,如果能對部分思路和我類似的人有一些幫助,那就最好了。基于這樣的目的,我并不打算深入,如果想看更深入的或者更好的,可以讀讀我最後的參考連結。

紅黑樹引入的目的 

首先要從對有序序列的查找問題開始,對一個靜态的有序序列數組,隻需要二分查找即可得到o(log(n))的查找效率;然而實作并不會顯得那麼“靜态”,需要實作動态的插入、删除同時保持序列的有序性,并且盡量提高插入、删除、查找的效率。

為實作動态插入、删除,最簡單的實作是二叉排序樹(bst, binary sort tree),它具有以下性質:

1. 它可以是一個空樹。

2. 若它的左子樹不為空,則它的左子樹所有節點的值均小于根節點的值。

3. 若它的右子樹不為空,則它的右子樹所有節點的值均大于根節點的值。

4. 它的左子樹和右子樹都是一個二叉排序樹。

根據以上性質,

對查找,比較查找數和目前節點,如果查找數和目前節點相等,則找到傳回;如果查找數小于目前節點,查找其左子樹,如果查找數大于目前節點,查找其右子樹,直到找到或直到葉子節點為null,傳回null。

對插入,先查找這棵樹,如果找到已存在的節點,更新節點值,否則把新值插入到最後一個為null的節點中。

對删除,首先找到要删除的節點,a)如果找到的節點p沒有子節點,直接删除即可;b)如果找到的節點p隻有左子樹或右子樹,删除該節點,并将其父節點原來的指針指向它唯一的左子樹、或右子樹即可;c)如果找到的節點p既有左子樹又有右子樹,可以有兩種做法:i)删除節點p,把節點p的父節點原來指向p的指針指向節點p的左位元組點,而将節點p的右節點插入到節點p右節點的最右葉子節點上(如果節點p是其父節點的右節點,則将節點p的父節點原來指向p節點的指針指向p節點的右子樹,而将節點p的左子樹插入到節點p最左葉子節點上),ii)将節點p替換成節點p的直接前驅(或直接後繼),然後删除節點p的直接前驅(或直接後繼)(注:直接前驅查找:節點p左子樹的最右節點,直接後繼查找:節點p右子樹的最左節點)。

二叉排序樹實作比較簡單,但是它查找效率卻不理想,最好的效率是o(log(n)),最會效率為o(n)。

為了提高查找效率,後來有人提出了平衡二叉樹(avl樹),它具有二叉排序樹的所有特點,并添加了以下性質:

1. 左子樹和右子樹的深度之差絕對值不超過1。

2. 左子樹和右子樹都是平衡二叉樹。

為了實作平衡二叉樹,需要在沒個節點上添加平衡因子字段,在插入後,如果發現平衡因子不符合性質1,則需要經過旋轉以調整。平衡二叉樹可以保證其查找的最好、最壞查找時間複雜度都為o(log(n))。

紅黑樹是平衡二叉樹的變種,它是一種自平衡的二叉排序樹,也需要通過一些旋轉調整以保持它的性質,它的名字是在 leo j. guibas 和 robert sedgewick 于1978年寫的一篇論文中獲得的。不同于平衡二叉樹的高度平衡,紅黑樹隻能保證從根到葉子節點的最長路徑不超過最短路徑的2倍,因而它的最壞查找時間複雜度為o(2log(n + 1)),然而有研究表明它的統計性能要好于平衡二叉樹,因而它具有更廣泛的應用。

紅黑樹的性質

一棵樹需要滿足以下性質才能保證它是一顆紅黑樹:

1. 它是一顆二叉查找樹(bst)。

2. 它的所有節點是紅色或黑色。

3. 它的根節點是黑色。

4. 它的葉子節點是黑色(空節點視為葉子節點)。

5. 它的紅節點的子節點必須是黑色的;而黑節點的位元組點可以是黑色或紅色。

6. 它任一節點到其後代的葉子節點路徑上有相同的黑色節點數。

一般的文章都是把性質列出來,然後根據這些性質來寫代碼實作(我也一樣:)),但是如何得出這些性質呢?多問幾個為什麼總是好事,這個問題需要去讀上面提到的論文,我沒讀過也不打算讀,這貌似不是我能涉及的,那就提出問題不回答了。。。

treemap中紅黑樹的節點

對一顆樹的節點,最基礎是該節點的值、左節點指針、右節點指針。對treemap,因為它存儲的是鍵值對,因而它包含了key、value,為了紀錄節點的顔色,它還需要有color字段:

    private static final boolean red   = false;

    private static final boolean black = true;

    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;

        ....

    }

treemap中紅黑樹節點插入

紅黑數的插入分以下步驟:

1. 如果目前為空樹,插入節點直接作為根節點,并将該節點顔色比較

2. 以二叉排序樹的查找算法查找目前樹,如果在目前樹中找到已存在的節點,更新節點的值,并傳回。

3. 否則,建立一個新節點,将其插入到最後一個查找到的葉子節點上,其初始顔色為紅色。

4. 如果新插入節點的父節點是黑節點,則沒有破壞目前紅黑樹的性質,直接傳回。

5. 如果新插入節點的父節點是紅節點,則需要做一些調整。

在treemap中,key值的比較可以通過構造treemap傳入的comparator執行個體,如果沒有comparator,則key必須實作comparable接口作為比較邏輯,key不可以為null。以二叉排序樹的算法插入新節點的代碼比較簡單:

    public v put(k key, v value) {

        entry<k,v> t = root;

        if (t == null) {

            compare(key, key); // type (and possibly null) check

            root = new entry<>(key, value, null);

            size = 1;

            modcount++;

            return null;

        }

        int cmp;

        // split comparator and comparable paths

        comparator<? super k> cpr = comparator;

        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();

            comparable<? super k> k = (comparable<? super k>) key;

                cmp = k.compareto(t.key);

        entry<k,v> e = new entry<>(key, value, parent);

        if (cmp < 0)

            parent.left = e;

        else

            parent.right = e;

        fixafterinsertion(e);

        size++;

        modcount++;

        return null;

    private void fixafterinsertion(entry<k,v> x) {

        x.color = red;

Java Core系列之TreeMap實作詳解

..

        root.color = black;

treemap中紅黑樹新節點插入後調整

紅黑樹的調整比較複雜,首先它會從目前節點向上查找,直到目前節點為null,或是root節點,或者目前節點的父節點顔色不是紅色,然後根據以下不同情況做處理(設目前節點為c(紅色),其父節點為p(紅色),其祖先節點為a(黑色),其叔叔節點為u(待定)):

1. p是a的左子樹,u節點顔色為紅色,此時不管c是節點是p的左子樹還是右子樹,隻需要将p和u設為黑色,a設為紅色,則可保證目前局部樹符合紅黑樹定義,把a作為新插入節點重新調整,如果目前樹已經是整棵樹,則因為根節點為紅色,不符合紅黑樹定義,此時隻需要将根節點顔色設定為黑色即可,即fixafterinsertion()最後一句代碼的作用。

2. p是a的左子樹,u節點顔色為黑色,c是p的左子樹,将p設定為黑色,a設定為紅色,并對a做右旋操作。此時c的父節點已變為黑色,循環可以直接退出。

3. p是a的左子樹,u節點顔色為黑色,c是p的右子樹,此時隻需要先對p左旋,然後設定c為黑色,a為紅色,并對a右旋,此時p的父節點已變為黑色,循環可以直接退出。

如下圖所示:

Java Core系列之TreeMap實作詳解

代碼:

        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);

                    }

                    rotateright(parentof(parentof(x)));

                }

            } else {

                ....

            }

4. p是a的右子樹,u節點顔色為紅色,此時不管c是節點是p的左子樹還是右子樹,隻需要将p和u設為黑色,a設為紅色,則可保證目前局部樹符合紅黑樹定義,把a作為新插入節點重新調整,如果目前樹已經是整棵樹,則因為根節點為紅色,不符合紅黑樹定義,此時隻需要将根節點顔色設定為黑色即可,即fixafterinsertion()最後一句代碼的作用。

5. p是a的右子樹,u節點顔色為黑色,c是p的右子樹,将p設定為黑色,a設定為紅色,并對a做左旋操作。此時c的父節點以變為黑色,循環可以直接退出。

6. p是a的右子樹,u節點顔色為黑色,c是p的左子樹,此時隻需要先對p左旋,然後設定c為黑色,a為紅色,并對a右旋,此時p的父節點已為黑色,循環可以直接退出。

Java Core系列之TreeMap實作詳解

                entry<k,v> y = leftof(parentof(parentof(x)));

                    if (x == leftof(parentof(x))) {

                        rotateright(x);

                    rotateleft(parentof(parentof(x)));

treemap中紅黑樹節點删除

紅黑樹的删除類似二叉查找樹删除邏輯類似,在對同時有左子樹和右子樹存在時,treemap選擇先将要删除的節點替換成其直接後繼節點,然後删除其直接後繼節點(其直接後繼節點不可能同時存在左子節點和右子位元組點)。對紅黑樹,由于紅色節點不影響路徑計算(性質6),因而對紅色節點可以直接删除。然而在删除黑色節點時,如果删除的節點不是樹的唯一節點,那麼在某些路徑上的黑色節點數會發生改變,破壞性質6;如果被删除的唯一子節點為紅色,而父節點也為紅色,那麼性質5被破壞,因為存在紅色節點的子節點為紅色;如果删除的是根節點,而它的唯一子節點是紅色,則性質3被破壞。因而需要做一些調整。

    private void deleteentry(entry<k,v> p) {

        size--;

        // if strictly internal, copy successor's element to p and then make p

        // point to successor.

        if (p.left != null && p.right != null) {

            entry<k,v> s = successor(p);

            p.key = s.key;

            p.value = s.value;

            p = s;

        } // p has 2 children

        // start fixup at replacement node, if it exists.

        entry<k,v> replacement = (p.left != null ? p.left : p.right);

        if (replacement != null) {

            // link replacement to parent

            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;

            // null out links so they are ok to use by fixafterdeletion.

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

            // fix replacement

            if (p.color == black)

                fixafterdeletion(replacement);

        } else if (p.parent == null) { // return if we are the only node.

            root = null;

        } else { //  no children. use self as phantom replacement and unlink.

                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;

treemap中紅黑樹删除黑色節點後調整

調整的邏輯分以下步驟來考慮(假設新替換的節點為c,即代碼中的x參數,c的父節點為p,c是p的左子節點,c的兄弟節點為s,s的左子樹為sl,s的右子樹為sr):

1. 如果c為紅色,直接将c标記為黑色即可,因為删除的黑節點數被該節點補上,該樹已經恢複成一顆紅黑樹。

2. 如果c為黑色,且c為根節點,直接傳回。

3. 如果c為黑色,且s為紅色,那麼節點p、sl、sr都為黑色,此時設定p為紅色,s為黑色,對p左旋,并重新計算s,即s變為sl,即把問題轉換為兄弟節點為黑色的情況。圖檔來自http://blog.csdn.net/v_july_v/article/details/6105630,自己畫太麻煩了,雖然圖檔的命名規則和我的不一樣,湊合的看把,囧。

Java Core系列之TreeMap實作詳解
Java Core系列之TreeMap實作詳解

4. 如果c為黑色,s為黑色,且sl、sr都為黑色,将s設定為紅色,p指派給c,重新計算。

Java Core系列之TreeMap實作詳解
Java Core系列之TreeMap實作詳解

5. 如果c為黑色,s為黑色,且sl為紅色,sr為黑色,那麼設定sl為黑色,s為紅色,對s右旋,重新設定s為sl。

Java Core系列之TreeMap實作詳解
Java Core系列之TreeMap實作詳解

6. 如果c為黑色,s為黑色,且sr為紅色,sl為任一顔色,則把s節點的顔色設定為p節點的顔色,設定p節點的顔色為黑色,sr節點的顔色為黑色,對p節點右旋,算法結束。

Java Core系列之TreeMap實作詳解
Java Core系列之TreeMap實作詳解

當c為p的右子節點時,其邏輯和以上對稱,不再贅述。

    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) == red) {

                    setcolor(sib, black);

                    setcolor(parentof(x), red);

                    rotateleft(parentof(x));

                    sib = rightof(parentof(x));

                if (colorof(leftof(sib))  == black &&

                    colorof(rightof(sib)) == black) {

                    setcolor(sib, red);

                    x = parentof(x);

                    if (colorof(rightof(sib)) == black) {

                        setcolor(leftof(sib), black);

                        setcolor(sib, red);

                        rotateright(sib);

                        sib = rightof(parentof(x));

                    setcolor(sib, colorof(parentof(x)));

                    setcolor(rightof(sib), black);

                    x = root;

            } else { // symmetric

                entry<k,v> sib = leftof(parentof(x));

                    rotateright(parentof(x));

                    sib = leftof(parentof(x));

                if (colorof(rightof(sib)) == black &&

                    colorof(leftof(sib)) == black) {

                    if (colorof(leftof(sib)) == black) {

                        setcolor(rightof(sib), black);

                        rotateleft(sib);

                        sib = leftof(parentof(x));

                    setcolor(leftof(sib), black);

        setcolor(x, black);

treemap中紅黑樹節點查找

紅黑樹的節點查找同二叉查找樹邏輯,不再贅述。這裡有一點不太明白:getentryusingcomparator()方法注釋中說它從getentry()方法提取出來是為了性能上的考慮,這是為什麼?

    /**

     * version of getentry using comparator. split off from getentry

     * for performance. (this is not worth doing for most methods,

     * that are less dependent on comparator performance, but is

     * worthwhile here.)

     */

    final entry<k,v> getentryusingcomparator(object key)

treemap中其他方法

treemap中其他方法實作比較直覺,隻要了解了紅黑樹,基本上很容易了解,不再贅述。

參考連結:

http://blog.csdn.net/v_july_v/article/details/6105630

http://blog.csdn.net/zhaojinjia/article/details/8120403

http://blog.csdn.net/eric491179912/article/details/6179908

http://dongxicheng.org/structure/red-black-tree/