最近學習了二叉搜索樹中的紅黑樹,感覺收獲頗豐,在此寫一篇文章小結一下學到的知識,順便手寫一下Java代碼。
1.引言
先來講講什么是二叉搜索樹,二叉搜索樹有如下特點:他是以一顆二叉樹(最多有兩個子結點)來組織的,對於樹中的某個節點,其左子樹的所有元素均小於該節點,其右子樹的元素均大於該節點。我們知道一顆有N個節點的二叉樹的高度至少為lgN,然后在樹上的操作都與其高度有關,因此限制樹的高度就顯得非常有必要。當一個二叉搜索樹的高度是lgN時,在該樹上的插入刪除搜索等操作均為O(lgN)的時間復雜度,但當二叉搜索樹不小心插入成了鏈表,高度為N的時候,在樹上的操作就變為O(N)了。因此我們有許多種平衡二叉樹通過特定的方法來限制樹的高度,紅黑樹就是其中的一種。紅黑樹(Red Black Tree) 是一種自平衡二叉查找樹,是在計算機科學中用到的一種數據結構,它在每個節點上增加了一個存儲位來表示節點的顏色,可以為紅色或黑色。通過對任何一條從根到葉子的簡單路徑上各個節點的顏色進行約束,紅黑樹確保沒有一條路徑會比其他路徑長出2倍,因此是近似於平衡的。
2.紅黑樹的性質
一顆紅黑樹是滿足以下紅黑性質的二叉搜索樹:
- 每個節點是紅色或黑色
- 根是黑色
- 葉節點(null)是黑色的
- 紅色的節點的兩個子結點均為黑色
- 對於每個節點,從該節點到其所有后代的簡單路徑上,均包含相同數目的黑色節點(我們把到葉節點的黑色節點數稱為黑高)
Java中樹的節點類如下:
- // 顏色枚舉
- enum RBColor {
- RED, BLACK;
- }
- // 樹節點類
- class RBTreeNode {
- RBTreeNode p = nullNode; // 父節點
- RBTreeNode left = nullNode; // 左子節點
- RBTreeNode right = nullNode; // 右子節點
- int val; // 值
- RBColor color; // 顏色
- public RBTreeNode() {};
- RBTreeNode(int val) {
- this.val = val;
- }
- @Override
- public String toString() {
- return " (" + val + " " + color + ") ";
- }
- // 用於表示空葉節點的靜態變量
- public static RBTreeNode nullNode = new RBTreeNode() {
- {
- color = RBColor.BLACK; // 葉結點為黑色
- }
- @Override
- public String toString() {
- return " (null " + color + ") ";
- }
- };
- }
3.旋轉
紅黑樹的關鍵操作在於其插入和刪除操作,但在講解這兩步關鍵操作之前,我們得定義一些輔助方法來讓我們更好的完成任務,該輔助方法就是樹的旋轉,示意圖如下:

旋轉由兩種,分別是左旋和右旋,相信上圖已經表示的非常明確,這里就不再細說,值得注意的是:在旋轉操作中只有指針的改變,其他屬性都保持不變。對旋轉前后的樹使用中序遍歷將得到相同的結果。
下面是旋轉的Java代碼:
- /**
- * 左旋操作
- * @param root 根結點引用
- * @param node 旋轉的節點
- * @return 根節點
- */
- public static RBTreeNode leftRotate(RBTreeNode root, RBTreeNode node) {
- if (node.right == RBTreeNode.nullNode)
- return root; // 左旋需要擁有右節點
- RBTreeNode right = node.right;
- // 旋轉節點的右子樹變為右節點的左子樹
- node.right = right.left;
- if (node.right != RBTreeNode.nullNode)
- node.right.p = node;
- // 用右節點代替旋轉節點位置
- if (node.p != RBTreeNode.nullNode) {
- right.p = node.p;
- if (node.p.left == node)
- node.p.left = right;
- else
- node.p.right = right;
- } else {
- root = right; // 沒有父節點的節點為根結點
- root.p = RBTreeNode.nullNode;
- }
- // 右節點的左子樹變為旋轉節點
- right.left = node;
- node.p = right;
- return root;
- }
- /**
- * 右旋操作
- * @param root 根結點引用
- * @param node 旋轉節點
- * @return 根節點
- */
- public static RBTreeNode rightRotate(RBTreeNode root, RBTreeNode node) {
- if (node.left == RBTreeNode.nullNode)
- return root; // 右旋需要有左節點
- RBTreeNode left = node.left;
- // 旋轉節點的左子樹變為左節點的右子樹
- node.left = left.right;
- if (node.left != RBTreeNode.nullNode)
- node.left.p = node;
- // 用左節點代替旋轉節點
- if (node.p != RBTreeNode.nullNode) {
- left.p = node.p;
- if (node.p.left == node)
- node.p.left = left;
- else
- node.p.right = left;
- } else {
- root = left;
- root.p = RBTreeNode.nullNode;
- }
- // 左節點的右子樹變為旋轉節點
- left.right = node;
- node.p = left;
- return root;
- }
4.插入
終於來到紅黑樹的第一個關鍵步驟了:插入操作。
對與插入操作我們利用如下思想解決:我們先把紅黑樹看成一個普通的二叉搜索樹,對其進行插入操作,插入完成后,我們把新加入的節點染成紅色,此時紅黑樹的紅黑性質被破壞,然后再通過特定的方法來維護紅黑樹的性質。
插入的Java代碼如下:
- /**
- * 紅黑樹插入操作
- * @param root 根結點引用
- * @param insertNode 要插入的新節點
- * @return 根節點
- */
- public static RBTreeNode rbInsert(RBTreeNode root, RBTreeNode insertNode) {
- RBTreeNode position = root, parent = RBTreeNode.nullNode; // position為插入位置,parent為該位置的父節點
- while (position != RBTreeNode.nullNode) {
- parent = position;
- if (insertNode.val < position.val) // 比該節點元素小的節點應該插入其左子樹
- position = position.left;
- else // 比該節點元素大的節點應該插入其右子樹
- position = position.right;
- }
- insertNode.p = parent;
- if (parent == RBTreeNode.nullNode) // 沒有父節點的節點為根結點
- root = insertNode;
- else if (insertNode.val < parent.val) // 插入為左節點
- parent.left = insertNode;
- else // 插入為右節點
- parent.right = insertNode;
- insertNode.color = RBColor.RED; // 把新插入的節點染成紅色
- return rbInsertFixup(root, insertNode); // 修復插入時紅黑樹性質
- }
好,終於來到重點了,紅黑樹的插入操作前半部分與一般二叉搜索樹別無二致,區別在於最后把新加入的節點染成紅色和恢復紅黑樹性質的部分。我們先來思考一下往紅黑樹插入一個紅節點會破壞紅黑樹的什么性質?首先性質1、3、5是不會受影響的,那么當我們插入的節點是紅黑樹的根結點時會影響性質2,根節點變成了紅色,此時我們把根節點染成黑色即可。當我們插入節點的父節點是紅色時會影響性質4,紅色節點有一個為紅色的子結點。對於以上這些影響我們分為3種情況來處理:
關鍵詞:叔節點
下面我們假設插入的節點為z(紅色),其父節點為x(紅色,為祖父節點的左節點,右節點情況鏡像處理即可),其叔節點為y(未知),祖父節點為w(黑色)
情況1:插入節點z的叔節點y為紅色
此時的情況如圖所示(下圖省略了部分不關鍵的子樹):

此時的處理方法很簡單,我們只需把祖父節點的黑色“扒”下來放到父節點X和叔節點Y即可,此時對於節點Z就保持了紅黑樹的性質4,然而進行了此操作后我們還需要對祖父節點W進行繼續遍歷,因為此時祖父節點有可能違反了紅黑樹的性質。當我們遍歷的祖父節點為根結點時,把根結點變為黑色即可。
情況2:插入節點z的叔節點y是黑色的,且z是一個右孩子
左上角為情況2,此時叔節點w為黑色,且插入節點z為父節點x的右孩子,此時我們對父節點x進行一次左旋,然后交換x和z的引用,即可轉換為右上角的情況3.
右上角為情況3,此時叔節點w為黑色,且插入節點z為父節點x的左孩子,此時我們進行如下操作即可恢復紅黑樹的性質:
- 交換父節點x和祖父節點w的顏色
- 對祖父節點w進行右旋
上面的操作既修正了對性質4的違反,也沒有引起對其他紅黑樹性質的違反,因此我們此時可以結束對紅黑樹的性質修復工作。
下面給出紅黑樹插入時性質修復的Java代碼:
- /**
- * 修復插入時違反的紅黑樹性質
- * @param root 根節點引用
- * @param node 修復節點
- * @return 根節點
- */
- public static RBTreeNode rbInsertFixup(RBTreeNode root, RBTreeNode node) {
- // 修復節點不是根節點且為紅色時
- RBTreeNode parent = node.p, grandParent, parentBorther;
- while(parent != RBTreeNode.nullNode && parent.color == RBColor.RED) {
- grandParent = parent.p;
- if (grandParent.left == parent) { // 父節點為左節點
- parentBorther = grandParent.right; // 叔節點為右節點
- if (parentBorther != RBTreeNode.nullNode && parentBorther.color == RBColor.RED) { // case 1
- grandParent.color = RBColor.RED; // 祖父節點改為紅色
- parent.color = RBColor.BLACK; // 父節點和叔節點改為黑色
- parentBorther.color = RBColor.BLACK;
- node = grandParent; // 對祖父節點繼續遍歷
- } else {
- if (parent.right == node) { // case 2
- root = leftRotate(root, parent); // 對父節點左旋
- // 交換node和parent的引用
- RBTreeNode temp = node;
- node = parent;
- parent = temp;
- }
- // case 3
- grandParent.color = RBColor.RED; // 祖父染成紅色
- parent.color = RBColor.BLACK; // 父節點染成黑色
- root = rightRotate(root, grandParent); // 對祖父右旋
- node = root; // 把節點置為根節點退出修復
- }
- } else { // 父節點為右節點,鏡像處理
- parentBorther = grandParent.left;
- if (parentBorther != RBTreeNode.nullNode && parentBorther.color == RBColor.RED) { // case 1
- grandParent.color = RBColor.RED;
- parent.color = RBColor.BLACK;
- parentBorther.color = RBColor.BLACK;
- node = grandParent;
- } else {
- if (parent.left == node) { // case 2
- root = rightRotate(root, parent);
- RBTreeNode temp = node;
- node = parent;
- parent = temp;
- }
- // case 3
- grandParent.color = RBColor.RED;
- parent.color = RBColor.BLACK;
- root = leftRotate(root, grandParent);
- node = root;
- }
- }
- parent = node.p;
- }
- // 根節點染為黑色
- root.color = RBColor.BLACK;
- return root;
- }
講完插入,我們來講講刪除操作。與插入類似,再刪除前我們先把紅黑樹當成是一顆普通的二叉搜索樹來處理刪除節點的操作。但在把節點刪除過后,由於刪除節點會帶走一種顏色,因此我們需要記錄下被刪除的顏色和刪除顏色的位置,最后我們再考慮如何修復樹的紅黑性質。二叉搜索樹刪除節點分為三種情況,這里簡單提一下:
- 刪除節點沒有子節點:直接把刪除節點的位置置空即可
- 刪除節點有一個子節點:用該子節點頂替刪除節點的位置
- 刪除節點有兩個子節點:這是比較復雜的情況,此時我們要從刪除節點的兩邊子樹中尋找一個節點來頂替其位置,我們可以找右子樹的最小節點或左子樹的最大節點,本文給出的代碼為尋找右子樹的最小節點。同時在代碼中我們把刪除節點的顏色賦給頂替節點,從而使實際刪除顏色的節點為頂替節點。
Java代碼如下:
- /**
- * 紅黑樹刪除操作
- * @param root 根節點引用
- * @param deleteNode 要刪除的節點
- * @return 根節點
- */
- public static RBTreeNode rbDelete(RBTreeNode root, RBTreeNode deleteNode) {
- RBTreeNode replaceNode, fixNode = RBTreeNode.nullNode; // 頂替刪除節點的代替節點、需要修復顏色的節點位置
- RBTreeNode fixNodeParent = deleteNode.p;
- RBColor deleteColor = deleteNode.color; // 記錄被刪除節點的顏色
- if (deleteNode.left == RBTreeNode.nullNode && deleteNode.right == RBTreeNode.nullNode) // 刪除節點沒有任何子結點
- replaceNode = RBTreeNode.nullNode;
- else if (deleteNode.right == RBTreeNode.nullNode) { // 處理只有左子節點的情況
- replaceNode = deleteNode.left;
- fixNode = replaceNode;
- } else if (deleteNode.left == RBTreeNode.nullNode) { //處理只有右子節點的情況
- replaceNode = deleteNode.right;
- fixNode = replaceNode;
- } else { // 處理有兩個子節點的情況
- replaceNode = deleteNode.right;
- while (replaceNode.left != RBTreeNode.nullNode) // 找到右子樹的最小節點
- replaceNode = replaceNode.left;
- fixNode = replaceNode.right; // 修復節點位置變為原頂替節點位置
- if (replaceNode.p == deleteNode) { // 特殊情況,右子樹沒有左節點
- if (fixNode != RBTreeNode.nullNode) // 修復節點不為空
- fixNode.p = replaceNode;
- fixNodeParent = replaceNode;
- } else {
- replaceNode.p.left = fixNode; // 修復節點頂替該節點的位置
- if (fixNode != RBTreeNode.nullNode) // 修復節點不為空
- fixNode.p = replaceNode.p;
- fixNodeParent = replaceNode.p;
- replaceNode.right = deleteNode.right;
- }
- // 用刪除節點的顏色代替頂替節點的顏色,使得被刪除顏色的節點實際變為頂替節點
- deleteColor = replaceNode.color;
- replaceNode.color = deleteNode.color;
- replaceNode.left = deleteNode.left;
- }
- if (replaceNode != RBTreeNode.nullNode) // 存在頂替節點
- replaceNode.p = deleteNode.p;
- if (deleteNode.p == RBTreeNode.nullNode) // 刪除節點的父節點為空,是根節點
- root = replaceNode;
- else { // 刪除節點不是根節點
- if (deleteNode.p.left == deleteNode)
- deleteNode.p.left = replaceNode;
- else
- deleteNode.p.right = replaceNode;
- }
- if (deleteColor == RBColor.BLACK) // 如果刪除的顏色是黑色則需要進行修復
- root = rbDeleteFixup(root, fixNode, fixNodeParent);
- return root;
- }
首先,如果刪除的節點顏色為紅色,則不會影響任何紅黑性質。但如果刪除的顏色是黑色,則可能影響性質2(根節點是黑色的),也可能影響性質4(紅色的節點的兩個子結點均為黑色),也可能影響性質5(對於每個節點,從該節點到其所有后代的簡單路徑上,均包含相同數目的黑色節點)。那么當刪除的節點顏色為黑色時,對於如何修復刪除后的紅黑性質,我們采用以下思考方式:
我們假設修復位置的節點具有兩種顏色,該節點原來的顏色,以及我們被刪除的黑色。那么:
- 如果該節點原來為紅色,那么我們被刪除的黑色可以直接覆蓋其顏色不影響任何紅黑性質
- 如果該節點是黑色同時他也是根節點,那么我們可以簡單的“消除”掉節點上面的一層黑色
- 如果該節點是黑色,但不是根節點,我們只能通過旋轉和重新着色的方法轉換修復的位置或退出循環
以下把修復刪除紅黑性質的工作分為4中情況,此處假設修復位置節點為A(黑色,此處假設為父節點的左節點,右節點請鏡像處理),其父節點為B,兄弟節點為C,兄弟節點的左子節點為D,兄弟節點的右子節點為E。
關鍵詞:兄弟節點
情況1:A的兄弟節點為紅色

如上圖所示,此時我們先交換父節點B和兄弟節點C的顏色,然后對父節點B進行左旋,以上操作並不會影響紅黑樹性質,而我們也把情況1轉化為了別的情況。
情況2:A的兄弟節點為黑色,其子節點均為黑色(下圖灰色代表未知顏色)

此時的處理方法很簡單,因為A節點和其兄弟節點C均為黑色,且C的子節點也均為黑色,因此我們可以把A節點和C節點的黑色上移到父節點B上,再把修復位置換為父節點B,針對父節點B繼續進行修復。(如果父節點B是紅色或根節點就可以停止修復了~)
情況3:A的兄弟節點為黑色,兄弟節點的左子節點為紅色,右子節點為黑色

此時我們首先交換兄弟節點C與其左子紅色節點D的顏色,然后對兄弟節點C進行右旋,把情況3轉化為情況4繼續處理。
情況4:A的兄弟節點為黑色,兄弟節點的右子節點為紅色

此時我們進行如下變換操作:
- 把父節點B和兄弟節點的右子節點E染成黑色,兄弟節點C染成父節點顏色
- 對父節點B進行左旋
以上操作在沒有破壞紅黑樹性質的情況下,消除了節點A的一重黑色,因此至此修復過程可以結束了。
刪除時修復過程的Java代碼如下:
- /**
- * 修復刪除時破壞的紅黑樹性質
- * @param root 根引用
- * @param fixNode 修復位置
- * @param parent 修復位置的父節點(修復位置為葉結點時使用)
- * @return 根
- */
- public static RBTreeNode rbDeleteFixup(RBTreeNode root, RBTreeNode fixNode, RBTreeNode parent) {
- RBTreeNode brother;
- while (root != fixNode && fixNode.color == RBColor.BLACK) {
- parent = fixNode.p == null ? parent : fixNode.p; // 處理fixNode為nullNode情況
- if (fixNode == parent.left) { // 頂替位置在父節點左邊
- brother = parent.right;
- if (brother.color == RBColor.RED) { // case 1
- // 交換父節點和兄弟節點的顏色
- RBColor temp = brother.color;
- brother.color = parent.color;
- parent.color = temp;
- // 父節點進行左旋
- root = leftRotate(root, parent);
- } else if (brother == RBTreeNode.nullNode) { // case 2
- // 兄弟節點為空,即為黑色,只需繼續遍歷父節點即可
- fixNode = parent;
- } else if (brother.left.color == RBColor.BLACK &&
- brother.right.color == RBColor.BLACK) { // case 2
- brother.color = RBColor.RED;
- fixNode = parent; // 繼續遍歷父節點
- } else { // case 3 and case 4
- if (brother.left.color == RBColor.RED &&
- brother.right.color == RBColor.BLACK) { // case 3
- // 兄弟節點染成紅色,左子節點染成黑色
- brother.color = RBColor.RED;
- brother.left.color = RBColor.BLACK;
- // 兄弟節點右旋
- root = rightRotate(root, brother);
- brother = brother.p;
- }
- // case 4
- // 變色
- brother.color = parent.color;
- parent.color = RBColor.BLACK;
- brother.right.color = RBColor.BLACK;
- // 父節點左旋
- root = leftRotate(root, parent);
- break;
- }
- } else {
- brother = parent.left;
- if (brother.color == RBColor.RED) { // case 1
- // 交換父節點和兄弟節點的顏色
- RBColor temp = brother.color;
- brother.color = parent.color;
- parent.color = temp;
- // 父節點進行右旋
- root = rightRotate(root, parent);
- } else if (brother == RBTreeNode.nullNode) { // case 2
- // 兄弟節點為空,即為黑色,只需繼續遍歷父節點即可
- fixNode = parent;
- } else if (brother.left.color == RBColor.BLACK &&
- brother.right.color == RBColor.BLACK) { // case 2
- brother.color = RBColor.RED;
- fixNode = parent; // 繼續遍歷父節點
- } else { // case 3 and case 4
- if (brother.right.color == RBColor.RED &&
- brother.left.color == RBColor.BLACK) { // case 3
- // 兄弟節點染成紅色,左子節點染成黑色
- brother.color = RBColor.RED;
- brother.right.color = RBColor.BLACK;
- // 兄弟節點右旋
- root = leftRotate(root, brother);
- brother = brother.p;
- }
- // case 4
- // 變色
- brother.color = parent.color;
- parent.color = RBColor.BLACK;
- brother.left.color = RBColor.BLACK;
- // 父節點左旋
- root = rightRotate(root, parent);
- break;
- }
- }
- }
- fixNode.color = RBColor.BLACK;
- return root;
- };
這里給出本人用來測試和打印紅黑樹的Java函數:
- public static void main(String[] args) {
- int num[] = new int[]{5, 4, 1, 6, 3, 2};
- List<RBTreeNode> list = new ArrayList<>();
- RBTreeNode root = RBTreeNode.nullNode;
- // 插入測試
- for (int i = 0; i < num.length; i++) {
- list.add(new RBTreeNode(num[i]));
- root = rbInsert(root, list.get(i));
- printRBTree(root);
- System.out.println("");
- }
- // 刪除測試
- for (int i = 0; i < num.length; i++) {
- root = rbDelete(root, list.get(0));
- list.remove(0);
- printRBTree(root);
- System.out.println("");
- }
- }
- /**
- * 打印一顆紅黑樹
- * @param root 根節點的引用
- */
- public static void printRBTree(RBTreeNode root) {
- if (root == RBTreeNode.nullNode) {
- System.out.println("這是一顆空樹");
- return;
- }
- Queue<RBTreeNode> q = new LinkedList<>();
- boolean allNull = false; // 是否全為空節點
- q.add(root);
- while (!allNull) { // 該行不是全為葉結點
- allNull = true;
- Queue<RBTreeNode> rowQ = new LinkedList<>(); // 用於存儲一行的所有節點
- RBTreeNode node;
- while (!q.isEmpty()) {
- node = q.poll();
- System.out.print(node);
- if (node != RBTreeNode.nullNode) { // 該節點不是葉結點
- if (node.left != RBTreeNode.nullNode) {
- rowQ.add(node.left);
- allNull = false;
- } else
- rowQ.add(RBTreeNode.nullNode);
- if (node.right != RBTreeNode.nullNode) {
- rowQ.add(node.right);
- allNull = false;
- } else
- rowQ.add(RBTreeNode.nullNode);
- } else { // 該節點為葉節點
- rowQ.add(RBTreeNode.nullNode);
- rowQ.add(RBTreeNode.nullNode);
- }
- }
- q = rowQ;
- System.out.println("");
- }
- }
總結,沒寫不知道,一寫嚇一跳,用Java來實現紅黑樹還是有挺多麻煩點的:
- 在Java中不知道如何修改根引用,所以最后都在函數上補了返回值
- 剛開始沒考慮null葉節點其實是算黑色節點的情況,后來補充了一個靜態變量作為葉節點
- 用靜態變量當葉節點使得葉節點是共享的,不能修改葉節點的left,right,p指針,因此又再刪除時添加了fixParent變量
- 刪除時擁有兩個子樹,但右子樹沒有左節點的情況是個坑……
總而言之,紅黑樹的5個性質,3種插入情況,4種刪除情況記住就大概沒什么問題了~