紅黑樹Red-Black tree初步詳解(Java程式碼實現)
紅黑樹Red-Blacktree初步詳解
本部落格的參考資料:
演算法導論
http://blog.csdn.net/v_july_v/article/details/6105630
http://www.cnblogs.com/skywang12345/p/3624343.html
一、紅黑樹簡介
先來看下演算法導論對R-B Tree的介紹:紅黑樹,一種二叉查詢樹,但在每個結點上增加一個儲存位表示結點的顏色,可以是Red或Black。通過對任何一條從根到葉子的路徑上各個結點著色方式的限制,紅黑樹確保沒有一條路徑會比其他路徑長出倆倍,因而是接近平衡的。
紅黑樹,作為一棵二叉查詢樹,滿足二叉查詢樹的一般性質。下面,來了解下
二叉查詢樹
二叉查詢樹,也稱有序二叉樹(ordered binary tree),或已排序二叉樹(sorted binary tree),是指一棵空樹或者具有下列性質的二叉樹:
- 若任意節點的左子樹不空,則左子樹上所有結點的值均小於它的根結點的值;
- 若任意節點的右子樹不空,則右子樹上所有結點的值均大於它的根結點的值;
- 任意節點的左、右子樹也分別為二叉查詢樹。
- 沒有鍵值相等的節點(no duplicate nodes)。
因為一棵由n個結點隨機構造的二叉查詢樹的高度為lgn,所以順理成章,二叉查詢樹的一般操作的執行時間為O(lgn)。但二叉查詢樹若退化成了一棵具有n個結點的線性鏈後,則這些操作最壞情況執行時間為
紅黑樹雖然本質上是一棵二叉查詢樹,但它在二叉查詢樹的基礎上增加了著色和相關的性質使得紅黑樹相對平衡,從而保證了紅黑樹的查詢、插入、刪除的時間複雜度最壞為O(log n)。
但它是如何保證一棵n個結點的紅黑樹的高度始終保持在logn的呢?這就引出了紅黑樹的5個性質:
1. 每個結點要麼是紅的要麼是黑的。
2. 根結點是黑的。
3. 每個葉結點(葉結點即指樹尾端NIL指標或NULL結點)都是黑的。
4. 如果一個結點是紅的,那麼它的兩個兒子都是黑的。
5. 對於任意結點而言,其到葉結點樹尾端NIL指標的每條路徑都包含相同數目的黑結點。
正是紅黑樹的這5條性質,使一棵
(注:上述第3、5點性質中所說的NULL結點,包括wikipedia.演算法導論上所認為的葉子結點即為樹尾端的NIL指標,或者說NULL結點。然百度百科以及網上一些其它博文直接說的葉結點,則易引起誤會,因,此葉結點非子結點)
如下圖所示,即是一顆紅黑樹(下圖引自wikipedia:):
此圖忽略了葉子和根部的父結點。同時,上文中我們所說的 "葉結點"或"NULL結點",如上圖所示,它不包含資料而只充當樹在此結束的指示,這些節點在繪圖中經常被省略,望看到此文後的讀者朋友注意。
二、紅黑樹的時間複雜度和相關證明
紅黑樹的時間複雜度為: O(lgn)下面通過“數學歸納法”對紅黑樹的時間複雜度進行證明。
定理:一棵含有n個節點的紅黑樹的高度至多為2log(n+1).
證明: "一棵含有n個節點的紅黑樹的高度至多為2log(n+1)" 的逆否命題是 "高度為h的紅黑樹,它的包含的內節點個數至少為 2h/2-1個"。 我們只需要證明逆否命題,即可證明原命題為真;即只需證明 "高度為h的紅黑樹,它的包含的內節點個數至少為 2h/2-1個"。
從某個節點x出發(不包括該節點)到達一個葉節點的任意一條路徑上,黑色節點的個數稱為該節點的黑高度(x's black height),記為bh(x)。關於bh(x)有兩點需要說明:
第1點:根據紅黑樹的"特性(5) ,即從一個節點到該節點的子孫節點的所有路徑上包含相同數目的黑節點"可知,從節點x出發到達的所有的葉節點具有相同數目的黑節點。這也就意味著,bh(x)的值是唯一的!
第2點:根據紅黑色的"特性(4),即如果一個節點是紅色的,則它的子節點必須是黑色的"可知,從節點x出發達到葉節點"所經歷的黑節點數目">= "所經歷的紅節點的數目"。假設x是根節點,則可以得出結論"bh(x) >= h/2"。進而,我們只需證明 "高度為h的紅黑樹,它的包含的黑節點個數至少為
2bh(x)-1個"即可。
到這裡,我們將需要證明的定理已經由"一棵含有n個節點的紅黑樹的高度至多為2log(n+1)" 轉變成只需要證明"高度為h的紅黑樹,它的包含的內節點個數至少為 2bh(x)-1個"。
下面通過"數學歸納法"開始論證高度為h的紅黑樹,它的包含的內節點個數至少為 2bh(x)-1個"。
(01) 當樹的高度h=0時, 內節點個數是0,bh(x) 為0,2bh(x)-1 也為 0。顯然,原命題成立。
(02) 當h>0,且樹的高度為 h-1 時,它包含的節點個數至少為 2bh(x)-1-1。這個是根據(01)推斷出來的!
下面,由樹的高度為 h-1 的已知條件推出“樹的高度為 h 時,它所包含的節點樹為 2bh(x)-1”。
當樹的高度為 h 時, 對於節點x(x為根節點),其黑高度為bh(x)。 對於節點x的左右子樹,它們黑高度為 bh(x) 或者 bh(x)-1。 根據(02)的已知條件,我們已知 "x的左右子樹,即高度為 h-1 的節點,它包含的節點至少為 2bh(x)-1-1 個";
所以,節點x所包含的節點至少為 ( 2bh(x)-1-1 ) + ( 2bh(x)-1-1 ) + 1 = 2^bh(x)-1。即節點x所包含的節點至少為 2bh(x)-1。 因此,原命題成立。
由(01)、(02)得出,"高度為h的紅黑樹,它的包含的內節點個數至少為 2^bh(x)-1個"。 因此,“一棵含有n個節點的紅黑樹的高度至多為2log(n+1)”。
三、樹的旋轉知識
節點的資料結構:
package com.red_black;
/**
* @author 作者 : 李
* @data 建立時間 : 2017年7月20日 上午9:27:39
* @version 版本 : 1.0
* @function 功能 : 紅黑樹的節點的資料結構
*/
public class NodeStruct {
private boolean color;
private float nodeValue;
private NodeStruct parentNode;
private NodeStruct leftNode;
private NodeStruct rightNode;
public NodeStruct() {
// TODO Auto-generated constructor stub
}
/**
* @param color
* @param nodeValue
* @param parentNode
* @param leftNode
* @param rightNode
*/
public NodeStruct(boolean color, float nodeValue, NodeStruct parentNode, NodeStruct leftNode,
NodeStruct rightNode) {
super();
this.color = color;
this.nodeValue = nodeValue;
this.parentNode = parentNode;
this.leftNode = leftNode;
this.rightNode = rightNode;
}
}
//省略個get和set函式...
當在對紅黑樹進行插入和刪除等操作時,對樹做了修改可能會破壞紅黑樹的性質。為了繼續保持紅黑樹的性質,可以通過對結點進行重新著色,以及對樹進行相關的旋轉操作,即通過修改樹中某些結點的顏色及指標結構,來達到對紅黑樹進行插入或刪除結點等操作後繼續保持它的性質或平衡的目的。
樹的旋轉分為左旋和右旋,下面藉助圖來介紹一下左旋和右旋這兩種操作。
1.左旋
如上圖所示,當在某個結點pivot上,做左旋操作時,我們假設它的右孩子y不是NIL[T],pivot可以為任何不是NIL[T]的左子結點。左旋以pivot到Y之間的鏈為“支軸”進行,它使Y成為該子樹的新根,而Y的左孩子b則成為pivot的右孩子。
左旋的Java程式碼:
/**
*//所在包com.red_black.CommonUtil
* @param x
* 對某一node節點進行左旋
*/
public static NodeStruct leftRotate(NodeStruct root, NodeStruct x) {
// 將y節點設定為旋轉節點x的右子節點
NodeStruct y = x.getRightNode();
// 將x的右子節點設定為y的左子節點
x.setRightNode(y.getLeftNode());
// 將y的左子節點的父節點設定為x節點
y.getLeftNode().setParentNode(x);
//如果x的父節點為根節點,則將y設定為根節點
if (x.getParentNode() == null)
{
root = y;
}
else
{
//如果x是它父節點的左子節點,則將x的父節點的左子節點設定為y節點,否則將x的父節點的右子節點設定為y節點
if(x.getParentNode().getLeftNode() == x)
{
x.getParentNode().setLeftNode(y);
}else
{
x.getParentNode().setRightNode(y);
}
}
//將x的父親設定為y節點的父親
y.setParentNode(x.getParentNode());
//將y的左子節點設定為x節點
y.setLeftNode(x);
//將x的父親設定為y節點
x.setParentNode(y);
//返回插入節點node並左旋後的紅黑樹
return root;
}
2.右旋
右旋與左旋差不多,再此不做詳細介紹。樹在經過左旋右旋之後,樹的搜尋性質保持不變,但樹的紅黑性質則被破壞了,所以,紅黑樹插入和刪除資料後,需要利用旋轉與顏色重塗來重新恢復樹的紅黑性質。
右旋的Java程式碼:
/**
* //所在包com.red_black.CommonUtil
* @param x
* 對某一node節點進行右旋
*/
public static NodeStruct rightRotate(NodeStruct root, NodeStruct x) {
// 將y節點設定為旋轉節點x的左子節點
NodeStruct y = x.getLeftNode();
// 將x的左子節點設定為y的右子節點
x.setLeftNode(y.getRightNode());
// 將y的右子節點的父節點設定為x節點
y.getRightNode().setParentNode(x);
//如果x的父節點為根節點,則將y設定為根節點
if (x.getParentNode() == null)
{
root = y;
}
else
{
//如果x它父節點的左子節點,則將x的父節點的左子節點設定為y節點,否則將x的父節點的右子節點設定為y節點
if(x.getParentNode().getLeftNode() == x)
{
x.getParentNode().setLeftNode(y);
}else
{
x.getParentNode().setRightNode(y);
}
}
//將x的父親設定為y節點的父親
y.setParentNode(x.getParentNode());
//將y的右子節點設定為x節點
y.setRightNode(x);
//將x的父親設定為y節點
x.setParentNode(y);
//返回插入節點node並右旋後的紅黑樹
return root;
}
四、紅黑樹的插入
要真正理解紅黑樹的插入,還得先理解二叉查詢樹的插入。磨刀不誤砍柴工,咱們再來了解一下二叉查詢樹的插入和紅黑樹的插入。
如果要在二叉查詢樹中插入一個結點,首先要查詢到結點要插入的位置,然後進行插入。假設插入的結點為z的話,插入的Java程式碼如下:
/**
* //所在包com.red_black.CommonUtil
* @param node 插入的節點
*/
public static insertNode(NodeStruct root, NodeStruct node)
{
//插入點node的父節點
NodeStruct nodeParent = null;
//紅黑樹的根節點
NodeStruct indexNode = root;
//查詢插入的點node的父節點nodeParent
while(indexNode != null)
{
nodeParent = indexNode;
if(node.getNodeValue() < indexNode.getNodeValue())
{
indexNode = indexNode.getLeftNode();
}
else
{
indexNode = indexNode.getRightNode();
}
}
//將插入點的父節點設定為nodeParent
node.setParentNode(nodeParent);
//如果nodeParent為null,則將插入點node是根節點
if(nodeParent == null)
{
root = node;
}else if(node.getNodeValue() < nodeParent.getNodeValue())
{
//將插入點node設定為它父節點nodeParent的左子節點
nodeParent.setLeftNode(node);
}else
{
//將插入點node設定為它父節點nodeParent的右子節點
nodeParent.setRightNode(node);
}
//返回插入節點node後的紅黑樹
return root;
}
紅黑樹的插入和插入修復
現在我們瞭解了二叉查詢樹的插入,接下來,咱們便來具體瞭解下紅黑樹的插入操作。紅黑樹的插入相當於在二叉查詢樹插入的基礎上,為了重新恢復平衡,繼續做了插入修復操作。
假設插入的結點為z,紅黑樹的插入java程式碼具體如下所示:
/**
* //所在包com.red_black.CommonUtil
* @param node 插入的節點
*/
public static insertNode(NodeStruct root, NodeStruct node)
{
//插入點node的父節點
NodeStruct nodeParent = null;
//紅黑樹的根節點
NodeStruct indexNode = root;
//查詢插入的點node的父節點nodeParent
while(indexNode != null)
{
nodeParent = indexNode;
if(node.getNodeValue() < indexNode.getNodeValue())
{
indexNode = indexNode.getLeftNode();
}
else
{
indexNode = indexNode.getRightNode();
}
}
//將插入點的父節點設定為nodeParent
node.setParentNode(nodeParent);
//如果nodeParent為null,則將插入點node是根節點
if(nodeParent == null)
{
root = node;
}else if(node.getNodeValue() < nodeParent.getNodeValue())
{
//將插入點node設定為它父節點nodeParent的左子節點
nodeParent.setLeftNode(node);
}else
{
//將插入點node設定為它父節點nodeParent的右子節點
nodeParent.setRightNode(node);
}
//將插入點node節點的左右子節點設定為null,並將插入點設定為紅色
node.setLeftNode(null);
node.setRightNode(null);
node.setColor(RED);
//對插入點node進行修復,使它繼續滿足紅黑樹的五個性質
root = RB_Insert_FixUp(root, node);
//返回插入節點node後的紅黑樹
return root;
}
把上面這段紅黑樹的插入程式碼,跟之前看到的二叉查詢樹的插入程式碼比較一下可以看出,紅黑樹的插入程式碼多了幾行紅色標註的程式碼,最後為保證紅黑性質在插入操作後依然保持,呼叫一個輔助程式RB_Insert_FixUp來對結點進行重新著色,並旋轉。
換言之,如果插入的是根結點,由於原樹是空樹,此情況只會違反性質2,因此直接把此結點塗為黑色;如果插入的結點的父結點是黑色,由於此不會違反性質2和性質4,紅黑樹沒有被破壞,所以此時什麼也不做。
但當遇到下述3種情況時又該如何調整呢?
● 插入修復情況1:如果當前結點的父結點是紅色且祖父結點的另一個子結點(叔叔結點)是紅色
● 插入修復情況2:當前節點的父節點是紅色,叔叔節點是黑色,當前節點是其父節點的右子
● 插入修復情況3:當前節點的父節點是紅色,叔叔節點是黑色,當前節點是其父節點的左子
答案就是根據紅黑樹插入程式碼RB-INSERT(T, z)最後一行呼叫的RB-INSERT-FIXUP(T, z)函式所示的步驟進行操作,具體如下所示:
/**
* //所在包com.red_black.CommonUtil
* 紅黑樹的調整和修復
* @param root
* @param node
* @return
*/
private static NodeStruct RB_Insert_FixUp(NodeStruct root, NodeStruct node) {
// TODO Auto-generated method stub
//判斷插入點node的父節點是否為null
while(node.getParentNode() != null && node.getParentNode().getColor()==RED )
//如果node的父節點是node祖父節點的左子節點,則右旋
if(node.getParentNode() == node.getParentNode().getParentNode().getLeftNode())
{
//node的叔叔節點
NodeStruct nodeUncle = node.getParentNode().getParentNode().getRightNode();
if(nodeUncle.getColor() == RED)
{
node.getParentNode().setColor(BLACK);
nodeUncle.setColor(BLACK);
node.getParentNode().getParentNode().setColor(RED);
//設定當前節點
node = node.getParentNode().getParentNode();
}else if(node == node.getParentNode().getRightNode())
{
//設定當前節點的父節點
node = node.getParentNode();
//左旋
root = leftRotate(root, node);
}else if(node == node.getParentNode().getLeftNode())
{
//當前節點的父節點變為黑色,祖父節點變為紅色,在祖父節點為支點右旋
node.getParentNode().setColor(BLACK);
node.getParentNode().getParentNode().setColor(RED);
//右旋
root = rightRotate(root, node.getParentNode().getParentNode());
}
}else
{
//node的叔叔節點
NodeStruct nodeUncle = node.getParentNode().getParentNode().getLeftNode();
if(nodeUncle.getColor() == RED)
{
node.getParentNode().setColor(BLACK);
nodeUncle.setColor(BLACK);
node.getParentNode().getParentNode().setColor(RED);
//設定當前節點
node = node.getParentNode().getParentNode();
}else if(node == node.getParentNode().getLeftNode())
{
//設定當前節點
node = node.getParentNode();
//右旋
root = rightRotate(root, node);
}else if(node == node.getParentNode().getRightNode())
{
//當前節點的父節點變為黑色,祖父節點變為紅色,在祖父節點為支點右旋
node.getParentNode().setColor(BLACK);
node.getParentNode().getParentNode().setColor(RED);
//左旋
root = leftRotate(root, node.getParentNode().getParentNode());
}
}
}
//插入節點node的父節點為null,則插入節點node設為根節點,顏色設定為黑色
root.setColor(BLACK);
//返回插入節點node並且修正後的紅黑樹
return root;
}
下面,咱們來分別處理上述3種插入修復情況。
- 插入修復情況1:當前結點的父結點是紅色,祖父結點的另一個子結點(叔叔結點)是紅色。
如下程式碼所示:
//如果node的父節點是node祖父節點的左子節點,則右旋
if(node.getParentNode() == node.getParentNode().getParentNode().getLeftNode())
{
//node的叔叔節點
NodeStruct nodeUncle = node.getParentNode().getParentNode().getRightNode();
if(nodeUncle.getColor() == RED)
此時父結點的父結點一定存在,否則插入前就已不是紅黑樹。與此同時,又分為父結點是祖父結點的左孩子還是右孩子,根據對稱性,我們只要解開一個方向就可以了。這裡只考慮父結點為祖父左孩子的情況,如下圖所示。
對此,我們的解決策略是:將當前節點的父節點和叔叔節點塗黑,祖父結點塗紅,把當前結點指向祖父節點,從新的當前節點重新開始演算法。即如下程式碼所示:
{
node.getParentNode().setColor(BLACK);
nodeUncle.setColor(BLACK);
node.getParentNode().getParentNode().setColor(RED);
//設定當前節點
node = node.getParentNode().getParentNode();
}
所以,變化後如下圖所示:
於是,插入修復情況1轉換成了插入修復情況2。
- 插入修復情況2:當前節點的父節點是紅色,叔叔節點是黑色,當前節點是其父節點的右子
else if(node == node.getParentNode().getRightNode())
{
//設定當前節點的父節點
node = node.getParentNode();
//左旋
root = leftRotate(root, node);
}
所以紅黑樹由之前的:
變化成:
從而插入修復情況2轉換成了插入修復情況3。
- 插入修復情況3:當前節點的父節點是紅色,叔叔節點是黑色,當前節點是其父節點的左孩子
else if(node == node.getParentNode().getLeftNode())
{
//當前節點的父節點變為黑色,祖父節點變為紅色,在祖父節點為支點右旋
node.getParentNode().setColor(BLACK);
node.getParentNode().getParentNode().setColor(RED);
//右旋
root = rightRotate(root, node.getParentNode().getParentNode());
}
最後,把根結點塗為黑色,整棵紅黑樹便重新恢復了平衡。所以紅黑樹由之前的:
變化成:
//主函式
package com.red_black;
/**
* @author 作者 : 李
* @data 建立時間 : 2017年7月20日 上午9:10:50
* @version 版本 : 1.0
* @function 功能 : 紅黑樹的演算法實現
*/
public class RedAndBlack {
private static NodeStruct RBTreeBoot = null;
public static void main(String[] args) {
// TODO Auto-generated method stub
//插入節點
float[] nodeValue = {11, 2, 14, 1, 7, 15, 5, 8, 4, 13};
for(int i = 0; i < nodeValue.length; i++)
{
NodeStruct node = new NodeStruct();
node.setNodeValue(nodeValue[i]);
RBTreeBoot = CommonUtil.insertNode(RBTreeBoot, node);
}
if(RBTreeBoot !=null)
{
//深度優先遍歷或者前序遍歷的遞迴展示二叉樹
RedAndBlack.show(RBTreeBoot);
}else
{
System.out.println("一顆空的紅黑樹!");
}
}
public static void show(NodeStruct tree)
{
if(tree.getLeftNode() != null || tree.getRightNode() !=null)
{
if(tree.getLeftNode() != null)
{
float x = tree.getLeftNode().getNodeValue();
boolean color = tree.getLeftNode().getColor();
String colorStr = null;
if(color)
{
colorStr = "RED";
}else
{
colorStr = "BLACK";
}
String parentColor = tree.getColor() ? "RED" : "BLACK";
System.out.println("left: " + "color: " + colorStr + "-->" + x + " --父節點--> " + parentColor + " " + tree.getNodeValue());
show(tree.getLeftNode());
}
if(tree.getRightNode() != null)
{
float y = tree.getRightNode().getNodeValue();
boolean color = tree.getRightNode().getColor();
String colorStr = null;
if(color)
{
colorStr = "RED";
}else
{
colorStr = "BLACK";
}
String parentColor = tree.getColor() ? "RED" : "BLACK";
System.out.println("right: " + "color: " + colorStr + "-->" + y + " --父節點--> " + parentColor + " " + tree.getNodeValue());
show(tree.getRightNode());
}
}else
{
if(tree.getParentNode() == null)
{
System.out.println("該紅黑樹僅有根節點,且 “"+tree.getNodeValue()+ "” 為紅黑樹的根節點");
}
}
}
}