數據結構與算法(十四)深刻理解紅黑樹和JDK TreeMap和TreeSet源碼分析

本文主要包括如下內容:node

  1. 什麼是2-3樹
  2. 2-3樹的插入操做
  3. 紅黑樹與2-3樹的等價關係
  4. 《算法4》和《算法導論》上關於紅黑樹的差別
  5. 紅黑樹的5條基本性質的分析
  6. 紅黑樹與2-3-4樹的等價關係
  7. 紅黑樹的插入、刪除操做
  8. JDK TreeMap、TreeSet分析

今天咱們來介紹下很是重要的數據結構:紅黑樹。算法

不少文章或書籍在介紹紅黑樹的時候直接上來就是紅黑樹的5個基本性質、插入、刪除操做等。本文不是採用這樣的介紹方式,在介紹紅黑樹以前,咱們要了解紅黑樹是怎麼發展出來的,進而就能知道爲何會有紅黑樹的5條基本性質。bash

這樣的介紹方式也是《算法4》的介紹方式。這也不奇怪,《算法4》的做者 Robert Sedgewick 就是紅黑樹的做者之一。在介紹紅黑樹以前,咱們先來看下2-3樹數據結構

什麼是2-3樹

在介紹紅黑樹以前爲何要先介紹 2-3樹 呢?由於紅黑樹是 完美平衡的2-3樹 的一種實現。因此,理解2-3樹對掌握紅黑樹是相當重要的。源碼分析

2-3樹 的一個Node可能有多個子節點(可能大於2個),並且一個Node能夠包含2個鍵(元素)性能

能夠把 紅黑樹(紅黑二叉查找樹) 看成 2-3樹 的一種二叉結構的實現。測試

在前面介紹的二叉樹中,一個Node保存一個值,在2-3樹中把這樣的節點稱之爲 2- 節點ui

若是一個節點包含了兩個(能夠看成兩個節點的融合),在2-3樹中把這樣的節點稱之爲 3- 節點。 完美平衡的2-3樹全部空連接到根節點的距離都應該是相同的spa

下面看下《算法4》對 2-3-節點的定義:3d

  • 2- 節點,含有一個鍵(及其對應的值)和兩條連接。該節點的左連接小於該節點的鍵;該節點的右連接大於該節點的鍵
  • 3- 節點,含有兩個鍵(及其對應的值)和三條連接。左連接小於該節點的左鍵;中連接在左鍵和右鍵之間;右連接大於該節點右鍵

以下面一棵 完美平衡的2-3樹

完美平衡的2-3 tree

2-3樹 是一棵多叉搜索樹,因此數據的插入相似二分搜索樹

2-3樹的插入操做

紅黑樹是對 完美平衡的2-3樹 的一種實現,因此咱們主要介紹完美平衡的2-3樹的插入過程

完美平衡的2-3樹插入分爲如下幾種狀況(爲了方便畫圖默認把空連接去掉):

向 2- 結點中插入新鍵

向2-結點中插入新鍵

向一棵只含有一個3-結點的樹中插入新鍵

由於2-3樹中節點只能是2-節點或者3-節點

往3-點中再插入一個鍵就成了4-節點,須要對其進行分解,以下所示:

向一棵只含有一個3-結點的樹中插入新鍵

向一個父結點爲 2- 結點的 3- 結點插入新鍵

往3-點中再插入一個鍵就成了4-節點,須要對其進行分解,對中間的鍵向上融合

因爲父結點是一個 2- 結點 ,融合後變成了 3- 結點,而後把 4- 結點的左鍵變成該 3- 節點的中間子結點

向一個父結點爲2-結點的3-結點插入新鍵

向一個父結點爲3- 結點的 3- 結點中插入新鍵

在這種狀況下,向3- 結點插入新鍵造成暫時的4- 結點,向上分解,父節點又造成一個4- 結點,而後繼續上分解

向一個父結點爲3-結點的3-結點中插入新鍵

一個 4- 結點分解爲一棵2-3樹6種狀況

一個4- 結點分解爲一棵2-3樹6種狀況

紅黑樹(RedBlackTree)

完美平衡的2-3樹和紅黑樹的對應關係

上面介紹完了2-3樹,下面來看下紅黑樹是怎麼來實現一棵完美平衡的2-3樹的

紅黑樹的背後的基本思想就是用標準的二分搜索樹和一些額外的信息來表示2-3樹的

這額外的信息指的是什麼呢?由於2-3樹不是二叉樹(最多有3叉),因此須要把 3- 結點 替換成 2- 結點

額外的信息就是指替換3-結點的方式

將2-3樹的連接定義爲兩種類型:黑連接、紅連接

黑連接 是2-3樹中普通的連接,能夠把2-3樹中的 2- 結點 與它的子結點之間的鏈看成黑連接

紅連接 2-3樹中 3- 結點分解成兩個 2- 結點,這兩個 2- 結點之間的連接就是紅連接

那麼如何將2-3樹和紅黑樹等價起來,咱們規定:紅連接均爲左連接

根據上面對完美平衡的2-3樹紅連接的介紹能夠得出結論:沒有一個結點同時和兩個紅連接相連

根據上面對完美平衡的2-3樹黑連接的介紹能夠得出結論:完美平衡的2-3樹是保持完美黑色平衡的,任意空連接到根結點的路徑上的黑連接數量相同

據此,咱們能夠得出3條性質:

  1. 紅連接均爲左連接
  2. 沒有一個結點同時和兩個紅連接相連
  3. 完美平衡的2-3樹是保持完美黑色平衡的,任意空連接到根結點的路徑上的黑連接數量相同

在紅黑樹中,沒有一個對象來表示紅連接和黑連接,經過在結點上加上一個屬性(color)來標識紅連接仍是黑連接,color值爲red表示結點是紅結點,color值爲black表示結點是黑結點。

黑結點 2-3樹中普通的 2-結點 的顏色 紅結點 2-3樹中 3- 結點 分解出兩個 2-結點 的最小 2-結點

下面是2-3樹和紅黑樹的一一對應關係圖:

image.png

紅黑樹的5個基本性質的分析

介紹完了2-3樹和紅黑樹的對應關係後,咱們再來看下紅黑樹的5個基本性質:

  1. 每一個結點要麼是紅色,要麼是黑色
  2. 根結點是黑色
  3. 每一個葉子結點(最後的空節點)是黑色
  4. 若是一個結點是紅色的,那麼他的孩子結點都是黑色的
  5. 從任意一個結點到葉子結點,通過的黑色結點是同樣的

2-3樹和紅黑樹的對應關係後咱們也就知道了紅黑樹的5個基本性質是怎麼來的了

紅黑樹的第一條性質:每一個節點要麼是紅色,要麼是黑色

由於咱們用結點上的屬性來表示紅鏈仍是黑鏈,因此紅黑樹的結點要麼是紅色,要麼是黑色是很天然的事情

紅黑樹的第二條性質:根結點是黑色

紅色節點的狀況是 3- 結點分解出兩個 2- 結點的最小節點是紅色,根節點沒有父節點因此只能是黑色

紅黑樹的第三條性質:每一個葉子結點(最後的空節點)是黑色

葉子節點也就是2-3樹中的空鏈,若是空鏈是紅色說明下面仍是有子結點的,可是空鏈是沒有子結點的;另外一方面若是 空鏈是紅色,空鏈指向的父結點結點若是也是紅色就會出現兩個連續的紅色連接,就和上面介紹的 「沒有一個結點同時和兩個紅連接相連」 相違背

紅黑樹的第四條性質:若是一個結點是紅色的,那麼他的孩子結點都是黑色的

上面介紹的‘沒有一個結點同時和兩個紅連接相連’,因此一個結點是紅色,那麼他的孩子結點都是黑色

紅黑樹的第五條性質:從任意一個結點到葉子結點,通過的黑色結點是同樣的

在介紹完美平衡的2-3樹和黑連接咱們得出的結論:‘完美平衡的2-3樹是保持完美黑色平衡的,任意空連接到根結點的路徑上的黑連接數量相同’, 因此從任意一個結點到葉子結點,通過的黑色結點數是同樣的

紅黑樹實現2-3樹過程當中的結點旋轉和顏色翻轉

顏色翻轉

爲何要顏色翻轉(flipColor)?在插入的過程當中可能出現以下狀況:兩個左右子結點都是紅色

顏色翻轉

根據咱們上面的描述,紅鏈只容許是左鏈(也就是左子結點是紅色)

因此須要進行顏色轉換:把該結點的左右子結點設置爲黑色,本身設置爲黑色

private void flipColor(Node<K, V> node) {
	node.color = RED;
	node.left.color = BLACK;
	node.right.color = BLACK;
}

複製代碼

左旋轉

左旋狀況大體有兩種:

結點是右子結點且是紅色

左旋轉1

顏色翻轉後,結點變成紅色且它是父結點的右子節點

左旋轉2

private Node<K, V> rotateLeft(Node<K, V> node) {
    Node<K, V> x = node.right;
    node.right = x.left;

    x.left = node;
    x.color = node.color;

    node.color = RED;
    return x;
}

複製代碼

右旋轉

須要右旋的狀況:連續出現兩個左紅色連接

右旋轉

private Node<K, V> rotateRight(Node<K, V> node) {
    Node<K, V> x = node.left;
    node.left = x.right;
    x.right = node;

    x.color = node.color;
    node.color = RED;

    return x;
}

複製代碼

紅黑樹實現2-3樹插入操做

經過咱們上面對紅黑樹和2-3樹的介紹,紅黑樹實現2-3樹插入操做就很簡單了

只要知足不出現 兩個連續左紅色連接右紅色連接左右都是紅色連接 的狀況就能夠了

因此僅僅須要處理三種狀況便可:

  1. 若是出現右側紅色連接,須要左旋
  2. 若是出現兩個連續的左紅色連接,須要右旋
  3. 若是結點的左右子連接都是紅色,須要顏色翻轉
private Node<K, V> _add(Node<K, V> node, K key, V value) {
    //向葉子結點插入新結點
	if (node == null) {
		size++;
		return new Node<>(key, value);
	}

	//二分搜索的過程
	if (key.compareTo(node.key) < 0)
		node.left = _add(node.left, key, value);
	else if (key.compareTo(node.key) > 0)
		node.right = _add(node.right, key, value);
	else
		node.value = value;

	//1,若是出現右側紅色連接,左旋
	if (isRed(node.right) && !isRed(node.left)) {
		node = rotateLeft(node);
	}

	//2,若是出現兩個連續的左紅色連接,右旋
	if (isRed(node.left) && isRed(node.left.left)) {
		node = rotateRight(node);
	}

	//3,若是結點的左右子連接都是紅色,顏色翻轉
	if (isRed(node.left) && isRed(node.right)) {
		flipColor(node);
	}
}

public void add(K key, V value) {
    root = _add(root, key, value);
    root.color = BLACK;
}

複製代碼

這樣下來紅黑樹依然保持着它的五個基本性質,下面咱們來對比下JDK中的TreeMap的插入操做

先按照上面的紅黑樹插入邏輯插入三個元素 [14, 5, 20],流程以下:

image.png

使用Java TreeMap來插入上面三個元素,流程以下:

image.png

經過對比咱們發現二者的插入後的結果不同,並且Java TreeMap是容許左右子結點都是紅色結點!

這就和咱們一直在說的用完美平衡的2-3樹做爲紅黑樹實現的基礎結構相違背了,咱們一直在強調不容許右節點是紅色,也不容許兩個連續的紅色左節點,不容許左右結點同時是紅色

這也是《算法4》在講到紅黑樹時遵循的。可是JDK TreeMap(紅黑樹)是容許右結點是紅色,也容許左右結點同時是紅色,Java TreeMap的紅黑樹實現從它的代碼註釋(From CLR)說明它的實現來自《算法導論》

說明《算法4》和《算法導論》中的所介紹的紅黑樹產生了一些「出入」,給咱們理解紅黑樹增長了一些困惑和難度

《算法4》在介紹紅黑樹以前先給咱們詳細介紹了2-3樹,而後接着講到完美平衡的2-3樹和紅黑樹的對應關係(紅黑樹就等於完美平衡的2-3樹),讓咱們知道紅黑樹是怎麼來的,根據這些介紹你本身是能夠解釋紅黑樹的的5個基本性質爲何是這樣的。

而在《算法導論》中介紹紅黑樹的時候沒有說起2-3樹,直接就是紅黑樹的5個基本性質,以及紅黑樹的插入、刪除操做,感受對初學者是不太合適的,由於你不知道爲何是這樣的,只是知道有這個五個性質,也許這就是爲何它叫導論的緣由吧

並且在《算法4》中做者最後好像也沒有明確的給出紅黑樹的五個基本性質,在《算法導論》中在紅黑樹章節一開始就貼出了5條性質,感受像是一種遞進和昇華

這兩本書除了對紅黑樹講解的方式存在差別外,咱們還發現《算法4》和《算法導論》在紅黑樹的實現上也是有差別的,就如咱們上面插入三個元素 [14, 5, 20] 產生不一樣的結果

在解釋這些差別以前,咱們再來看些2-3-4樹,上面提到完美平衡的2-3樹和紅黑樹等價,更準確的說是2-3-4樹和紅黑樹等價

2-3-4樹

2-3-4樹2-3樹 很是相像。2-3樹容許存在 2- 結點3- 結點,相似的2-3-4樹容許存在 2- 結點3- 結點4- 結點

2-3-4-結點

向2-結點、3-結點插入元素

2-結點插入元素,這個和上面介紹的2-3樹是同樣的,在這裏就不敘述了

3-結點插入元素,造成一個4-結點,由於2-3-4樹容許4-結點的存在,因此不須要向上分解

向4-結點插入元素

向4-結點插入元素,須要分解4-結點, 由於2-3-4樹最多隻容許存在4-結點,如:

4-結點插入元素

若是待插入的4-結點,它的父結點也是一個4-結點呢?以下圖的2-3-4樹插入結點K:

父結點也是4-結點

主要有兩個方案:

  1. Bayer於1972年提出的方案:使用相同的辦法去分解父結點的4-結點,直到不須要分解爲止,方向是自底向上
  2. GuibasSedgewick於1978年提出的方案:自上而下的方式,也就是在二分搜索的過程,一旦遇到4-結點就分解它,這樣在最終插入的時候永遠不會有父結點是4-結點的狀況

Bayer全名叫作Rudolf Bayer(魯道夫·拜爾),他在1972年發明的 對稱二叉B樹(symmetric binary B-tree) 就是 紅黑樹(red black tree) 的前身。 紅黑樹 這個名字是由 Leo J. GuibasRobert Sedgewick 於1978年的一篇論文中提出來的, 對該論文感興趣的能夠查看這個連接:professor.ufabc.edu.br/~jesus.mena…

下面的圖就是 自上而下 方案的流程圖

自上而下流程圖

2-3-4樹和紅黑樹的等價關係

在介紹2-3樹的時候咱們也講解了2-3樹和紅黑樹的等價關係,因爲2-3樹和2-3-4樹很是相似,因此2-3-4樹和紅黑樹的等價關係也是相似的。不一樣的是2-3-4的 4-結點 分解後的結點顏色變成以下形式:

4-結點分解圖

因此能夠得出下面一棵2-3-4樹和紅黑樹的等價關係圖:

2-3-4樹和紅黑樹的等價

上面在介紹紅黑樹實現2-3樹的時候講解了它的插入操做:

private Node<K, V> _add(Node<K, V> node, K key, V value) {
    //向葉子結點插入新結點
	if (node == null) {
		size++;
		return new Node<>(key, value);
	}

	//二分搜索的過程
	if (key.compareTo(node.key) < 0)
		node.left = _add(node.left, key, value);
	else if (key.compareTo(node.key) > 0)
		node.right = _add(node.right, key, value);
	else
		node.value = value;

	//1,若是出現右側紅色連接,左旋
	if (isRed(node.right) && !isRed(node.left)) {
		node = rotateLeft(node);
	}

	//2,若是出現兩個連續的左紅色連接,右旋
	if (isRed(node.left) && isRed(node.left.left)) {
		node = rotateRight(node);
	}

	//3,若是結點的左右子連接都是紅色,顏色翻轉
	if (isRed(node.left) && isRed(node.right)) {
		flipColor(node);
	}
}
複製代碼

咱們能夠很輕鬆的把它改爲2-3-4的插入邏輯(只須要把顏色翻轉的邏輯提到二分搜索的前面便可):

private Node<K, V> _add(Node<K, V> node, K key, V value) {
    //向葉子結點插入新結點
	if (node == null) {
		size++;
		return new Node<>(key, value);
	}
	
	//split 4-nodes on the way down
	if (isRed(node.left) && isRed(node.right)) {
		flipColor(node);
	}

	//二分搜索的過程
	if (key.compareTo(node.key) < 0)
		node.left = _add(node.left, key, value);
	else if (key.compareTo(node.key) > 0)
		node.right = _add(node.right, key, value);
	else
		node.value = value;

	//fix right-leaning reds on the way up
	if (isRed(node.right) && !isRed(node.left)) {
		node = rotateLeft(node);
	}

	//fix two reds in a row on the way up
	if (isRed(node.left) && isRed(node.left.left)) {
		node = rotateRight(node);
	}

}

複製代碼
//使用2-3-4樹插入數據 [E,C,G,B,D,F,J,A]

RB2_3_4Tree<Character, Character> rbTree = new RB2_3_4Tree<>();
rbTree.add('E', 'E');
rbTree.add('C', 'C');
rbTree.add('G', 'G');
rbTree.add('B', 'B');
rbTree.add('D', 'D');
rbTree.add('F', 'F');
rbTree.add('J', 'J');
rbTree.add('A', 'A');
rbTree.levelorder(rbTree.root);


//使用2-3樹插入數據 [E,C,G,B,D,F,J,A]

RBTree<Character, Character> rbTree = new RBTree<>();
rbTree.add('E', 'E');
rbTree.add('C', 'C');
rbTree.add('G', 'G');
rbTree.add('B', 'B');
rbTree.add('D', 'D');
rbTree.add('F', 'F');
rbTree.add('J', 'J');
rbTree.add('A', 'A');
rbTree.levelorder(rbTree.root);

複製代碼

下面是 2-3-4樹2-3樹 插入結果的對比圖:

image.png

因此咱們一開始用紅黑樹實現完美平衡的2-3樹,左右結點是不會都是紅色的 如今用紅黑樹實現2-3-4樹,左右結點的能夠同時是紅色的,這樣的紅黑樹效率更高。由於若是遇到左右結點是紅色,就進行顏色翻轉,還須要對紅色的父結點進行向上回溯,由於父結點染成紅色了,可能父結點的父結點也是紅色,可能須要進行結點旋轉或者顏色翻轉操做,因此說2-3-4樹式的紅黑樹效率更高。

因此回到上面咱們提到《算法4》和《算法導論》在實現上的差別的問題,就很好回答了,由於《算法4》是用紅黑樹實現2-3樹的,並非2-3-4樹。可是若是是用紅黑樹實現2-3-4樹就和《算法導論》上介紹的紅黑樹同樣嗎?不同。

下面繼續作一個測試,分別往上面紅黑樹實現的 2-3-4樹JDK TreeMap 中插入**[E, D, R, O, S, X]**

2-3-4樹和TreeMap插入結果比較

雖然兩棵樹都是紅黑樹,可是卻不同。而且TreeMap容許右節點是紅色,在2-3-4樹中最可能是左右子結點同時是紅色的狀況,不會出現左結點是黑色,右邊的兄弟結點是紅色的狀況,爲何會有這樣的差別呢?

從上面的2-3-4樹的插入邏輯能夠看出,若是右節點是紅色會執行左旋轉操做,因此不會出現單獨紅右結點的狀況 也就是說只會出現單獨的左結點是紅色的狀況,咱們把這種形式的紅黑樹稱之爲左傾紅黑樹(Left Leaning Red Black Tree),包括上面的紅黑樹實現的完美平衡的2-3樹也是左傾紅黑樹

爲何在《算法4》中,做者規定全部的紅色連接都是左連接,這只是人爲的規定,固然也能夠是右連接,規定紅連接都是左鏈,可使用更少的代碼來實現黑色平衡,須要考慮的狀況會更少,就如上面咱們介紹的插入操做,咱們只須要考慮3中狀況便可。

可是通常意義上的紅黑樹是不須要維持紅色左傾的這個性質的,因此爲何TreeMap是容許單獨右紅結點的

若是還須要維護左傾狀況,這樣的話就更多的操做,可能還須要結點旋轉和顏色的翻轉,性能更差一些,雖然也是符合紅黑樹的性質

介紹完了《算法4》上的紅黑樹,下面就來分析下通常意義上的紅黑樹的 插入刪除 操做,也就是《算法導論》上介紹的紅黑樹。

紅黑樹插入操做

插入操做有兩種狀況是很是簡單的,因此在這裏單獨說一下:

case 1. 若是插入的結點是根結點,直接把該結點設置爲黑色,整個插入操做結束

以下圖所示:

image.png

case 2. 若是插入的結點的父結點是黑色,也無需調整,整個插入操做結束

以下圖所示:

image.png

下面開始介紹比較複雜的狀況

紅黑樹插入操做,咱們只須要處理父結點是紅色的狀況,由於一開始紅黑樹確定是黑色平衡的,就是由於往葉子節點插入元素後可能出現兩個連續的紅色的結點

須要注意的是,咱們把新插入的結點默認設置爲紅色,初始的時候,正在處理的節點就是插入的結點,在不斷調整的過程當中,正在處理的節點會不斷的變化,且叔叔、爺爺、父結點都是相對於當前正在處理的結點來講的

case 3. 叔叔結點爲紅色,正在處理的節點能夠是左也能夠是右結點

調整策略:因爲父結點是紅色,叔叔結點是紅色,爺爺結點是黑色,執行顏色翻轉操做
而後把當前正在處理的結點設置爲爺爺結點,若是爺爺的父結點是黑色插入操做結束,若是是紅色繼續處理

複製代碼

case 4. 叔叔結點爲黑色,正在處理的結點是右結點

調整策略:因爲父結點是紅色,叔叔結點爲黑色,那麼爺爺結點確定是黑色
把正在處理的節點設置爲父結點,而後左旋,造成Case5狀況

複製代碼

case 5. 叔叔結點爲黑色,正在處理的結點是左孩子

調整策略:因爲父結點是紅色,叔叔結點爲黑色,那麼爺爺結點確定是黑色
把父結點染黑,爺爺結點染紅,而後爺爺結點右旋

複製代碼

Case三、Case四、Case5若是單獨來理解的話比較困難,就算單獨爲每個Case畫圖,我以爲也很難完整的理解,不少博客上都是這種方式,感受不太好理解。我將這三種狀況經過一張流程圖串聯起來,將這三個Case造成一個總體,藍色箭頭表示正在處理的結點,以下所示:

紅黑樹的插入操做流程圖

紅黑樹刪除操做

上面介紹完了紅黑樹的插入操做,接下來看下紅黑樹的刪除操做

紅黑樹的刪除操做比插入操做更加複雜一些

爲了描述方便,咱們把正在處理的結點稱之爲 X,父結點爲 P(Parent),兄弟節點稱之爲 S(Sibling),左侄子稱之爲 LN(Left Nephew),右侄子稱之爲 RN(Right Nephew)

若是刪除的結點是黑色,那麼就致使原本保持黑平衡的紅黑樹失衡了,從下圖能夠看出結點P到左子樹的葉子結點通過的黑節點數量爲4(2+2),到右子樹的葉子節點通過的黑色節點數量是5(2+3),以下圖所示:

image.png

紅黑樹的刪除操做,若是刪除的是黑色會致使紅黑樹就不能保持黑色平衡了,須要進行調整了; 若是刪除的是紅色,那麼就無需調整,直接刪除便可,由於沒有沒有破壞黑色平衡

刪除結點後,無需調整的狀況

case 1 刪除的結點是紅色結點,直接刪除便可

case 2 刪除的節點是黑色,若是當前處理的節點X是根結點

不管根結點是什麼顏色,都將根結點設置爲黑色
複製代碼

case 3 刪除的結點是黑色,若是當前處理的結點是紅色結點,將該結點設置爲黑色

由於刪除黑色結點後,就打破了黑色平衡,黑高少了1
因此把一個紅色節點設置爲黑色,這樣黑高又平衡了

複製代碼

刪除節點後,須要調整的狀況

正在處理的結點爲X,要刪除的結點是左結點,分爲4中狀況:

case 4 兄弟結點爲紅色

調整方案:兄弟設置爲黑色,父結點設置爲紅色,父結點進行左旋轉
轉化爲 case五、case六、case7
複製代碼

case 5 兄弟結點爲黑色,左侄子LN爲黑色,右侄子RN爲黑色

在這種條件下,還有兩種狀況:父結點是紅色或黑色,不論是那種狀況,調整方案都是一致的

調整方案:將兄弟結點設置爲紅色,把當前處理的結點設置爲父結P
複製代碼

case 6 兄弟結點爲黑色,左侄子爲紅色,右侄子RN爲黑色

調整方案:將左侄子結點設置爲黑色,兄弟結點設置爲紅色,兄弟結點右旋轉,這樣就轉化成了case7
複製代碼

case 7 兄弟結點爲黑色,左侄子無論紅黑,右侄子爲紅色

處理方式:兄弟結點變成父結點的顏色,而後父結點設置黑色,右侄子設置黑色,父結點進行左旋轉
複製代碼

和插入操做同樣,下面經過一張流程圖把刪除須要調整的狀況串聯起來:

紅黑樹刪除操做流程圖

上面處理的全部狀況都是基於正在處理的結點是左結點 若是要調整正在處理的結點是右節點的狀況,就是上面的處理的鏡像。插入操做也是同理,因此就省略了

Java TreeMap、TreeSet源碼分析

TreeMap底層就是用紅黑樹實現的,它在插入後調整操做主要在fixAfterInsertion方法裏,我爲每種狀況都添加註釋,以下所示:

/** From CLR */
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)));
			//-----Case3狀況-----
			if (colorOf(y) == RED) {
				setColor(parentOf(x), BLACK);
				setColor(y, BLACK);
				setColor(parentOf(parentOf(x)), RED);
				x = parentOf(parentOf(x));
			} else {
				//-----Case4狀況-----
				if (x == rightOf(parentOf(x))) {
					x = parentOf(x);
					rotateLeft(x);
				}
				//-----Case5狀況-----
				setColor(parentOf(x), BLACK);
				setColor(parentOf(parentOf(x)), RED);
				rotateRight(parentOf(parentOf(x)));
			}
		} else {
			//省略鏡像狀況
		}
	}
	root.color = BLACK;
}

複製代碼

它的刪除後調整操做主要在fixAfterDeletion方法:

/** From CLR */
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));
			//-----Case4的狀況-----
			if (colorOf(sib) == RED) {
				setColor(sib, BLACK);
				setColor(parentOf(x), RED);
				rotateLeft(parentOf(x));
				sib = rightOf(parentOf(x));
			}
			//-----Case5的狀況-----
			if (colorOf(leftOf(sib))  == BLACK &&
				colorOf(rightOf(sib)) == BLACK) {
				setColor(sib, RED);
				x = parentOf(x);
			} else {
				//-----Case6的狀況-----
				if (colorOf(rightOf(sib)) == BLACK) {
					setColor(leftOf(sib), BLACK);
					setColor(sib, RED);
					rotateRight(sib);
					sib = rightOf(parentOf(x));
				}
				//-----Case7的狀況-----
				setColor(sib, colorOf(parentOf(x)));
				setColor(parentOf(x), BLACK);
				setColor(rightOf(sib), BLACK);
				rotateLeft(parentOf(x));
				x = root;
			}
		} else { // symmetric
			//省略鏡像的狀況
		}
	}
	setColor(x, BLACK);
}

複製代碼

TreeSet 底層就是用 TreeMap 來實現的,往TreeSet添加進的元素看成TreeMap的key,TreeMap的value是一個常量Object。掌握了紅黑樹,對於這兩個集合的原理就不難理解了。

最後

本文從一開始講的2-3樹和紅黑樹的對應關係,再到2-3-4樹和紅黑樹的對應關係,再到《算法4》和《算法導論》JDK TreeMap在紅黑樹上的差別 而後詳細介紹了紅黑樹的插入、刪除操做,最後分析了下Java中的TreeMap和TreeSet集合類。

人生當如紅黑樹,當過於自喜或過於自卑的時候,應當自我調整,尋求平衡。

我很醜,紅黑樹卻很美。 但願本文對你有 些許幫助。

下面是個人公衆號,乾貨文章不錯過,有須要的能夠關注下,很是感謝:

個人公衆號

參考資料

  1. www.cs.princeton.edu/~rs/talks/L…
  2. professor.ufabc.edu.br/~jesus.mena…
  3. 《算法4》、《算法導論》
相關文章
相關標籤/搜索