資料結構與演算法(十四)深入理解紅黑樹和JDK TreeMap和TreeSet原始碼分析

Chiclaim發表於2018-08-25

本文主要包括以下內容:

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

今天我們來介紹下非常重要的資料結構:紅黑樹。

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

這樣的介紹方式也是《演算法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- 節點

如果一個節點包含了兩個(可以當作兩個節點的融合),在2-3樹中把這樣的節點稱之為 3- 節點。 完美平衡的2-3樹所有空連結到根節點的距離都應該是相同的

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

  • 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. 叔叔結點為黑色,正在處理的結點是左孩子

調整策略:由於父結點是紅色,叔叔結點為黑色,那麼爺爺結點肯定是黑色
把父結點染黑,爺爺結點染紅,然後爺爺結點右旋

複製程式碼

Case3、Case4、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 兄弟結點為紅色

調整方案:兄弟設定為黑色,父結點設定為紅色,父結點進行左旋轉
轉化為 case5、case6、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》、《演算法導論》

相關文章