1. 程式人生 > >Java 基於紅黑樹的TreeMap,TreeSet實現原理

Java 基於紅黑樹的TreeMap,TreeSet實現原理

TreeSet and TreeMap

總體介紹

之所以把TreeSetTreeMap放在一起講解,是因為二者在Java裡有著相同的實現,前者僅僅是對後者做了一層包裝,也就是說TreeSet裡面有一個TreeMap(介面卡模式)**。因此本文將重點分析TreeMap

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

TreeMap底層通過紅黑樹(Red-Black tree)實現,也就意味著containsKey()

get()put()remove()都有著log(n)的時間複雜度。其具體演算法實現參照了《演算法導論》。

TreeMap_base.png

出於效能原因,TreeMap是非同步的(not synchronized),如果需要在多執行緒環境使用,需要程式設計師手動同步;或者通過如下方式將TreeMap包裝成(wrapped)同步的:

SortedMap m = Collections.synchronizedSortedMap(new TreeMap(...));

紅黑樹是一種近似平衡的二叉查詢樹,它能夠確保任何一個節點的左右子樹的高度差不會超過二者中較低那個的一陪。具體來說,紅黑樹是滿足如下條件的二叉查詢樹(binary search tree):

  1. 每個節點要麼是紅色,要麼是黑色。
  2. 根節點必須是黑色
  3. 紅色節點不能連續(也即是,紅色節點的孩子和父親都不能是紅色)。
  4. 對於每個節點,從該點至null(樹尾端)的任何路徑,都含有相同個數的黑色節點。

在樹的結構發生改變時(插入或者刪除操作),往往會破壞上述條件3或條件4,需要通過調整使得查詢樹重新滿足紅黑樹的約束條件。

預備知識

前文說到當查詢樹的結構發生改變時,紅黑樹的約束條件可能被破壞,需要通過調整使得查詢樹重新滿足紅黑樹的約束條件。調整可以分為兩類:一類是顏色調整,即改變某個節點的顏色;另一類是結構調整,集改變檢索樹的結構關係。結構調整過程包含兩個基本操作:左旋(Rotate Left),右旋(RotateRight)

左旋

左旋的過程是將x的右子樹繞x逆時針旋轉,使得x的右子樹成為x的父親,同時修改相關節點的引用。旋轉之後,二叉查詢樹的屬性仍然滿足。

TreeMap_rotateLeft.png

TreeMap中左旋程式碼如下:

//Rotate Left
private void rotateLeft(Entry<K,V> p) {
    if (p != null) {
        Entry<K,V> r = p.right;
        p.right = r.left;
        if (r.left != null)
            r.left.parent = p;
        r.parent = p.parent;
        if (p.parent == null)
            root = r;
        else if (p.parent.left == p)
            p.parent.left = r;
        else
            p.parent.right = r;
        r.left = p;
        p.parent = r;
    }
}

右旋

右旋的過程是將x的左子樹繞x順時針旋轉,使得x的左子樹成為x的父親,同時修改相關節點的引用。旋轉之後,二叉查詢樹的屬性仍然滿足。

TreeMap_rotateRight.png

TreeMap中右旋程式碼如下:

//Rotate Right
private void rotateRight(Entry<K,V> p) {
    if (p != null) {
        Entry<K,V> l = p.left;
        p.left = l.right;
        if (l.right != null) l.right.parent = p;
        l.parent = p.parent;
        if (p.parent == null)
            root = l;
        else if (p.parent.right == p)
            p.parent.right = l;
        else p.parent.left = l;
        l.right = p;
        p.parent = l;
    }
}

尋找節點後繼

對於一棵二叉查詢樹,給定節點t,其後繼(樹中比大於t的最小的那個元素)可以通過如下方式找到:

  1. t的右子樹不空,則t的後繼是其右子樹中最小的那個元素。
  2. t的右孩子為空,則t的後繼是其第一個向左走的祖先。

後繼節點在紅黑樹的刪除操作中將會用到。

TreeMap_successor.png

TreeMap中尋找節點後繼的程式碼如下:

// 尋找節點後繼函式successor()
static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) {
    if (t == null)
        return null;
    else if (t.right != null) {// 1. t的右子樹不空,則t的後繼是其右子樹中最小的那個元素
        Entry<K,V> p = t.right;
        while (p.left != null)
            p = p.left;
        return p;
    } else {// 2. t的右孩子為空,則t的後繼是其第一個向左走的祖先
        Entry<K,V> p = t.parent;
        Entry<K,V> ch = t;
        while (p != null && ch == p.right) {
            ch = p;
            p = p.parent;
        }
        return p;
    }
}

方法剖析

get()

get(Object key)方法根據指定的key值返回對應的value,該方法呼叫了getEntry(Object key)得到相應的entry,然後返回entry.value。因此getEntry()是演算法的核心。演算法思想是根據key的自然順序(或者比較器順序)對二叉查詢樹進行查詢,直到找到滿足k.compareTo(p.key) == 0entry

TreeMap_getEntry.png

具體程式碼如下:

//getEntry()方法
final Entry<K,V> getEntry(Object key) {
    ......
    if (key == null)//不允許key值為null
        throw new NullPointerException();
    Comparable<? super K> k = (Comparable<? super K>) key;//使用元素的自然順序
    Entry<K,V> p = root;
    while (p != null) {
        int cmp = k.compareTo(p.key);
        if (cmp < 0)//向左找
            p = p.left;
        else if (cmp > 0)//向右找
            p = p.right;
        else
            return p;
    }
    return null;
}

put()

put(K key, V value)方法是將指定的keyvalue對新增到map裡。該方法首先會對map做一次查詢,看是否包含該元組,如果已經包含則直接返回,查詢過程類似於getEntry()方法;如果沒有找到則會在紅黑樹中插入新的entry,如果插入之後破壞了紅黑樹的約束條件,還需要進行調整(旋轉,改變某些節點的顏色)。

public V put(K key, V value) {
	......
    int cmp;
    Entry<K,V> parent;
    if (key == null)
        throw new NullPointerException();
    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);
    Entry<K,V> e = new Entry<>(key, value, parent);//建立並插入新的entry
    if (cmp < 0) parent.left = e;
    else parent.right = e;
    fixAfterInsertion(e);//調整
    size++;
    return null;
}

上述程式碼的插入部分並不難理解:首先在紅黑樹上找到合適的位置,然後建立新的entry並插入(當然,新插入的節點一定是樹的葉子)。難點是調整函式fixAfterInsertion(),前面已經說過,調整往往需要1.改變某些節點的顏色,2.對某些節點進行旋轉。

TreeMap_put.png

調整函式fixAfterInsertion()的具體程式碼如下,其中用到了上文中提到的rotateLeft()rotateRight()函式。通過程式碼我們能夠看到,情況2其實是落在情況3內的。情況4~情況6跟前三種情況是對稱的,因此圖解中並沒有畫出後三種情況,讀者可以參考程式碼自行理解。

//紅黑樹調整函式fixAfterInsertion()
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);              // 情況1
                setColor(y, BLACK);                        // 情況1
                setColor(parentOf(parentOf(x)), RED);      // 情況1
                x = parentOf(parentOf(x));                 // 情況1
            } else {
                if (x == rightOf(parentOf(x))) {
                    x = parentOf(x);                       // 情況2
                    rotateLeft(x);                         // 情況2
                }
                setColor(parentOf(x), BLACK);              // 情況3
                setColor(parentOf(parentOf(x)), RED);      // 情況3
                rotateRight(parentOf(parentOf(x)));        // 情況3
            }
        } else {
            Entry<K,V> y = leftOf(parentOf(parentOf(x)));
            if (colorOf(y) == RED) {
                setColor(parentOf(x), BLACK);              // 情況4
                setColor(y, BLACK);                        // 情況4
                setColor(parentOf(parentOf(x)), RED);      // 情況4
                x = parentOf(parentOf(x));                 // 情況4
            } else {
                if (x == leftOf(parentOf(x))) {
                    x = parentOf(x);                       // 情況5
                    rotateRight(x);                        // 情況5
                }
                setColor(parentOf(x), BLACK);              // 情況6
                setColor(parentOf(parentOf(x)), RED);      // 情況6
                rotateLeft(parentOf(parentOf(x)));         // 情況6
            }
        }
    }
    root.color = BLACK;
}

remove()

remove(Object key)的作用是刪除key值對應的entry,該方法首先通過上文中提到的getEntry(Object key)方法找到key值對應的entry,然後呼叫deleteEntry(Entry<K,V> entry)刪除對應的entry。由於刪除操作會改變紅黑樹的結構,有可能破壞紅黑樹的約束條件,因此有可能要進行調整。

getEntry()函式前面已經講解過,這裡重點放deleteEntry()上,該函式刪除指定的entry並在紅黑樹的約束被破壞時進行呼叫fixAfterDeletion(Entry<K,V> x)進行調整。

由於紅黑樹是一棵增強版的二叉查詢樹,紅黑樹的刪除操作跟普通二叉查詢樹的刪除操作也就非常相似,唯一的區別是紅黑樹在節點刪除之後可能需要進行調整。現在考慮一棵普通二叉查詢樹的刪除過程,可以簡單分為兩種情況:

  1. 刪除點p的左右子樹都為空,或者只有一棵子樹非空。
  2. 刪除點p的左右子樹都非空。

對於上述情況1,處理起來比較簡單,直接將p刪除(左右子樹都為空時),或者用非空子樹替代p(只有一棵子樹非空時);對於情況2,可以用p的後繼s(樹中大於x的最小的那個元素)代替p,然後使用情況1刪除s(此時s一定滿足情況1.可以畫畫看)。

基於以上邏輯,紅黑樹的節點刪除函式deleteEntry()程式碼如下:

// 紅黑樹entry刪除函式deleteEntry()
private void deleteEntry(Entry<K,V> p) {
    modCount++;
    size--;
    if (p.left != null && p.right != null) {// 2. 刪除點p的左右子樹都非空。
        Entry<K,V> s = successor(p);// 後繼
        p.key = s.key;
        p.value = s.value;
        p = s;
    }
    Entry<K,V> replacement = (p.left != null ? p.left : p.right);
    if (replacement != null) {// 1. 刪除點p只有一棵子樹非空。
        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) {
        root = null;
    } else { // 1. 刪除點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;
        }
    }
}

上述程式碼中佔據大量程式碼行的,是用來修改父子節點間引用關係的程式碼,其邏輯並不難理解。下面著重講解刪除後調整函式fixAfterDeletion()。首先請思考一下,刪除了哪些點才會導致調整?只有刪除點是BLACK的時候,才會觸發調整函式,因為刪除RED節點不會破壞紅黑樹的任何約束,而刪除BLACK節點會破壞規則4。

跟上文中講過的fixAfterInsertion()函式一樣,這裡也要分成若干種情況。記住,無論有多少情況,具體的調整操作只有兩種:1.改變某些節點的顏色,2.對某些節點進行旋轉。

TreeMap_fixAfterDeletion.png

上述圖解的總體思想是:將情況1首先轉換成情況2,或者轉換成情況3和情況4。當然,該圖解並不意味著調整過程一定是從情況1開始。通過後續程式碼我們還會發現幾個有趣的規則:a).如果是由情況1之後緊接著進入的情況2,那麼情況2之後一定會退出迴圈(因為x為紅色);b).一旦進入情況3和情況4,一定會退出迴圈(因為x為root)。

刪除後調整函式fixAfterDeletion()的具體程式碼如下,其中用到了上文中提到的rotateLeft()rotateRight()函式。通過程式碼我們能夠看到,情況3其實是落在情況4內的。情況5~情況8跟前四種情況是對稱的,因此圖解中並沒有畫出後四種情況,讀者可以參考程式碼自行理解。

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);                   // 情況1
                setColor(parentOf(x), RED);             // 情況1
                rotateLeft(parentOf(x));                // 情況1
                sib = rightOf(parentOf(x));             // 情況1
            }
            if (colorOf(leftOf(sib))  == BLACK &&
                colorOf(rightOf(sib)) == BLACK) {
                setColor(sib, RED);                     // 情況2
                x = parentOf(x);                        // 情況2
            } else {
                if (colorOf(rightOf(sib)) == BLACK) {
                    setColor(leftOf(sib), BLACK);       // 情況3
                    setColor(sib, RED);                 // 情況3
                    rotateRight(sib);                   // 情況3
                    sib = rightOf(parentOf(x));         // 情況3
                }
                setColor(sib, colorOf(parentOf(x)));    // 情況4
                setColor(parentOf(x), BLACK);           // 情況4
                setColor(rightOf(sib), BLACK);          // 情況4
                rotateLeft(parentOf(x));                // 情況4
                x = root;                               // 情況4
            }
        } else { // 跟前四種情況對稱
            Entry<K,V> sib = leftOf(parentOf(x));
            if (colorOf(sib) == RED) {
                setColor(sib, BLACK);                   // 情況5
                setColor(parentOf(x), RED);             // 情況5
                rotateRight(parentOf(x));               // 情況5
                sib = leftOf(parentOf(x));              // 情況5
            }
            if (colorOf(rightOf(sib)) == BLACK &&
                colorOf(leftOf(sib)) == BLACK) {
                setColor(sib, RED);                     // 情況6
                x = parentOf(x);                        // 情況6
            } else {
                if (colorOf(leftOf(sib)) == BLACK) {
                    setColor(rightOf(sib), BLACK);      // 情況7
                    setColor(sib, RED);                 // 情況7
                    rotateLeft(sib);                    // 情況7
                    sib = leftOf(parentOf(x));          // 情況7
                }
                setColor(sib, colorOf(parentOf(x)));    // 情況8
                setColor(parentOf(x), BLACK);           // 情況8
                setColor(leftOf(sib), BLACK);           // 情況8
                rotateRight(parentOf(x));               // 情況8
                x = root;                               // 情況8
            }
        }
    }
    setColor(x, BLACK);
}

TreeSet

前面已經說過TreeSet是對TreeMap的簡單包裝,對TreeSet的函式呼叫都會轉換成合適的TreeMap方法,因此TreeSet的實現非常簡單。這裡不再贅述。

            
           

相關推薦

Java 基於TreeMap,TreeSet實現原理

TreeSet and TreeMap 總體介紹 之所以把TreeSet和TreeMap放在一起講解,是因為二者在Java裡有著相同的實現,前者僅僅是對後者做了一層包裝,也就是說TreeSet裡面有一個TreeMap(介面卡模式)**。因此本文將重點分析TreeMap。

【演算法】java演算法的完整實現及swing介面演示程式

【前言】 當初因為覺得資料結構及演算法是碼農的基礎(正如鋤頭對農民一樣)才決定話費時間來補習的,但是真正自行實現演算法及演算法的視覺化演示的時候才發現難度是如此之大。 演算法寫起來慢,swing介面寫起來也慢。 紅黑樹的結構最重要就是幾個規則: 1、根節點為黑色,NIL

的快速實現

紅黑樹的概述: 紅黑樹本質上是一種二叉查詢樹,但它在二叉查詢樹的基礎上額外添加了一個標記(顏色),同時具有一定的規則。這些規則使紅黑樹保證了一種平衡,插入、刪除、查詢的最壞時間複雜度都為 O(logn)。 紅黑樹的性質: 1、每個節點要麼是紅色,要麼是黑色; 2、根節點

的簡單實現

紅黑樹 LINDA 2018/9/25 前言 如果你還是對寫紅黑樹毫無頭緒,可以看一下我的思路,從普通二叉搜尋樹的插入操作是如何一步步“進化”為真正的紅黑樹的插入操作的。 紅黑樹的四個規則: (1) 每個結點要麼是紅的,要麼是黑的; (2) 根結點必須為黑

(三)之 Linux核心中的經典實現

1 /* 2 Red Black Trees 3 (C) 1999 Andrea Arcangeli <[email protected]> 4 (C) 2002 David Woodhouse <[email protected

【資料結構】(如何實現及怎樣判斷)

      紅黑樹是一顆二叉搜尋樹,它在每個節點上增加了一個儲存位來表示節點的顏色,可以是red或black。通過對任何一條從根節點到葉子節點的簡單路徑上的顏色來約束,紅黑樹保證了最長路徑不超過最短路經的兩倍,因此近似於平衡。 紅黑樹的規則: 1、每個節點不是紅色就是

演算法的實現與剖析

 一、紅黑樹的介紹 先來看下演算法導論對R-BTree的介紹:紅黑樹,一種二叉查詢樹,但在每個結點上增加一個儲存位表示結點的顏色,可以是Red或Black。通過對任何一條從根到葉子的路徑上各個結點著色方式的限制,紅黑樹確保沒有一條路徑會比其他路徑長

詳解Linux核心演算法的實現

    開發平臺:Ubuntu11.04     核心原始碼:linux-2.6.38.8.tar.bz2     平衡二叉樹(BalancedBinary Tree或Height-Balanced Tree)又稱AVL樹。它或者是一棵空樹,或者是具有下列性質的二叉樹:

Java 集合 | | 前置知識

一、前言 0tnv1e.png 為啥要學紅黑樹吖? 因為筆者最近在趕專案的時候,不忘抽出時間來複習 Java 基礎知識,現在準備看集合的原始碼啦啦。聽聞,HashMap 在 jdk 1.8 的時候,底層的資料結構發生了變化,變成了陣列+連結串列+紅黑樹。很好,沒了解過紅黑樹,所以就趁今天閒暇學習一下啦 二

插入和刪除原理

紅黑樹本質是一顆二叉查詢樹,增加了著色以及相關的性質,使得紅黑樹的查詢,插入,刪除的時間複雜度最壞為O(log n)。 一、紅黑樹相對二叉查詢樹來說,有以下五個性質。 a.紅黑樹的節點不是紅色就是黑色 b.紅黑樹中根節點必是黑色。 c.紅黑樹上的節點時紅色,它的兩個子節

關於(R-B tree)原理,看這篇如何

學過資料資料結構都知道二叉樹的概念,而又有多種比較常見的二叉樹型別,比如完全二叉樹、滿二叉樹、二叉搜尋樹、均衡二叉樹、完美二叉樹等;今天我們要說的紅黑樹就是就是一顆非嚴格均衡的二叉樹,均衡二叉樹又是在二叉搜尋樹的基礎上增加了自動維持平衡的性質,插入、搜尋、刪除的效率都比較高。紅黑樹也是實現TreeMap儲存結

基於Java實現的基本操作

首先,在閱讀文章之前,我希望讀者對二叉樹有一定的瞭解,因為紅黑樹的本質就是一顆二叉樹。所以本篇部落格中不在將二叉樹的增刪查的基本操作了。 有隨機數節點組成的二叉樹的平均高度為logn,所以正常情況下二叉樹查詢的時間複雜度為O(logn)。但是,根據二叉樹的特性,在最壞的情況下,比如儲存的是一個有

結合java.util.TreeMap源碼理解

pen leaf tails 變化 col 般的 參考 some 解決 前言 本篇將結合JDK1.6的TreeMap源碼,來一起探索紅-黑樹的奧秘。紅黑樹是解決二叉搜索樹的非平衡問題。 當插入(或者刪除)一個新節點時,為了使樹保持平衡,必須遵循一定的規則,這個規則就是紅

數據結構 - (Red Black Tree)插入詳解與實現Java

啟示 dpa con 技術分享 節點數 src 通知 一點 this   最終還是決定把紅黑樹的篇章一分為二,插入操作一篇,刪除操作一篇,因為合在一起寫篇幅實在太長了,寫起來都覺得累,何況是閱讀並理解的讀者。       紅黑樹刪除操作請參考 數據結構 - 紅黑樹(Red

數據結構 - (Red Black Tree)刪除詳解與實現Java

replace ati 轉載 之前 9.png one com 四種 簡單   本篇要講的就是紅黑樹的刪除操作       紅黑樹插入操作請參考 數據結構 - 紅黑樹(Red Black Tree)插入詳解與實現(Java)   紅黑樹的刪除是紅黑樹操作中比較麻煩且比較有意

二叉java實現

二叉樹的java實現 public class BinaryTree { /** * 根節點 */ private static Node root; static class Node { int key; Node l

深入剖析以及JAVA實現

作者:美團技術團隊 連結:https://zhuanlan.zhihu.com/p/24367771 來源:知乎 著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。   紅黑樹是平衡二叉查詢樹的一種。為了深入理解紅黑樹,我們需要從二叉查詢樹開始講起。 BST

.集合Set,HashSet,TreeSet及其底層實現HashMap和;Collection總結

ONE.Set集合 one.Set集合的特點  無序,唯一 TWO.HashSet集合 1.底層資料結構是雜湊表(是一個元素為連結串列的陣列) 2.那麼HashSet如何來實現元素的唯一性的呢? 通過一HashSet新增字串的案例檢視HashSet中add()的原始碼,

根據的演算法來分析TreeMap實現

TreeMap的實現是紅黑樹演算法的實現,所以要了解TreeMap就必須對紅黑樹有一定的瞭解。通過這篇博文你可以獲得如下知識點:        1、紅黑樹的基本概念。     &nb

二、JAVA知識點之HashMap、TreeMap——精髓

4、JAVA中HashMap和TreeMap什麼區別?低層資料結構是什麼? 1)、使用層次上的區別: HashMap: a)、陣列+連結串列儲存key-value,1.8加入紅黑樹(優化連結串列查詢過長的問題) b)、允許null作為key和value,key不可以重複,value允許重複 c