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

本文主要包括以下內容:

  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樹

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樹和紅黑樹的一一對應關係圖:

在這裏插入圖片描述

紅黑樹的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

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

顏色翻轉

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],流程如下:

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

流程

通過對比我們發現兩者的插入後的結果不一樣,而且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-結點呢?如下圖的2-3-4樹插入結點K:

在這裏插入圖片描述

主要有兩個方案:

  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年的一篇論文中提出來的,
對該論文感興趣的可以查看這個鏈接:http://professor.ufabc.edu.br/~jesus.mena/courses/mc3305-2q-2015/AED2-13-redblack-paper.pdf

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

自上而下

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樹 插入結果的對比圖:

2-3-4樹和2-3樹插入對比

所以我們一開始用紅黑樹實現完美平衡的 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. 如果插入的結點是根結點,直接把該結點設置爲黑色,整個插入操作結束

如下圖所示:

case 1

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

如下圖所示:

父節點是黑色

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

紅黑樹插入操作,我們只需要處理父結點是紅色的情況,因爲一開始紅黑樹肯定是黑色平衡的,就是因爲往葉子節點插入元素後可能出現兩個連續的紅色的結點

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

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),如下圖所示:

在這裏插入圖片描述

紅黑樹的刪除操作,如果刪除的是黑色會導致紅黑樹就不能保持黑色平衡了,需要進行調整了;
如果刪除的是紅色,那麼就無需調整,直接刪除即可,因爲沒有沒有破壞黑色平衡

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

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集合類。

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

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


下面是我的公衆號,乾貨文章不錯過,有需要的可以關注下,有任何問題可以聯繫我:

公衆號:  chiclaim

參考資料

  1. https://www.cs.princeton.edu/~rs/talks/LLRB/RedBlack.pdf
  2. http://professor.ufabc.edu.br/~jesus.mena/courses/mc3305-2q-2015/AED2-13-redblack-paper.pdf
  3. 《算法4》、《算法導論》
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章