紅黑樹
紅黑樹是一種自平衡的二叉樹;
紅黑樹必須要遵循的規則:
-
1.節點是紅色或黑色;
-
2.根節點為黑色;
-
3.每個葉子節點都是黑色的空節點;
-
4.紅色節點不能有紅色的父節點或子節點
-
5.從任一節點到葉子節點的黑色節點數必須一致;
紅黑樹的兩大操作:變色和旋轉
-
變色:將節點的顏色由黑變紅,或者由紅變黑
-
旋轉:通過有兩種旋轉方式
-
左旋轉:將右孩子替換父節點成為新的父節點,原父節點順勢左轉成為新的父節點的左孩子;
-
右旋轉:將左孩子替換父節點變成新的父節點,原父節點順勢向右成為新的父節點的右孩子;
-
備註:新插入的節點預設是紅色的;
紅黑樹插入新節點操作
情況1:新插入節點的父節點和父節點同一水平的叔叔節點,都是紅色時
-
將父節點和叔叔節點變成黑色;
-
祖父節點變成紅色;
-
如果祖父節點是根節點時,變成黑色;
情況2:新插入節點的父節點為紅色,父節點同一水平的叔叔節點為黑色
這種要根據新插入的節點,在根節點的插入位置不同,有不同的情況
-
最小子樹的左節點增加左子節點
- 進行右旋操作
-
最小子樹的左節點增加右子節點
-
先進行左旋,調整節點
-
再進行右旋,調整節點顏色
-
-
最小子樹的右節點增加左子節點
-
進行右旋操作
-
再進行左旋操作
-
-
最小子樹的右節點增加右子節點
- 進行左旋操作
案例
- 逐個增加10 70 32 34 13 56 56 21 3 62 4
具體可以看我的另一篇部落格:
https://www.cnblogs.com/perferect/p/13577085.html
實現程式碼(Java版本)
java中Map集合,在JDK1.8之後,Entry陣列,同一個HASH的值儲存改成了紅黑樹儲存。以ConcurrentHashMap程式碼進行紅黑樹相關帶啊分析;
以下程式碼的git地址:https://gitee.com/wuzhiaite/cs_java/blob/master/src/test/java/com/wuzhiaite/javaweb/base/bitree/RedBlackTree.java
-
TreeNode 程式碼
- 節點三個連線,一個顏色標記
class RedBlackNode< V extends Comparable<V>> {
RedBlackNode<V> parent; // 紅黑樹的連線
RedBlackNode<V> left;
RedBlackNode<V> right;
boolean red;
volatile V val;
RedBlackNode(V val,
RedBlackNode<V> parent) {
this.val = val ;
this.parent = parent ;
}
}
-
紅黑樹的結構
- 需要存放根節點
public final class RedBlackTree<V extends Comparable<V>> {
RedBlackNode root;
}
- 紅黑樹的插入操作
/**
* 1.查詢插入值的父節點
* 2.進行值插入和紅黑樹自平衡
* @param v
* @return
*/
public RedBlackNode putTreeVal(V v){
boolean searched = false;
for (RedBlackNode<V> p = root;;) {
int dir = 0;V pv;
if (p == null) {//根節點
root = new RedBlackNode(v,null);
break;
}
else if ((pv = p.val).compareTo(v) < 0) //根節點值小於新插入值
dir = -1;
else if (pv.compareTo(v) > 0 )//跟節點值大於新插入節點
dir = 1;
else if (pv == v || (pv != null && pv.equals(pv)))//判斷是否已經插入過
return p;
else{
//查詢是否已經存在該節點,如果存在則返回該節點
//根的左子樹進行查詢判斷
//根的右子樹進行查詢判斷
if (!searched) {
RedBlackNode<V> q, ch;
searched = true;
if (((ch = p.left) != null &&
(q = ch.findTreeNode(v)) != null) ||
((ch = p.right) != null &&
(q = ch.findTreeNode(v)) != null))
return q;
}
}
//1.提前將p的值賦值給一個新的變數px
//2.根據上面值判斷結果,如果新插入值小於節點p,則p賦值為左子樹,否則賦值為右子樹
//3.如果p為null,表示是可以插入值的節點
RedBlackNode<V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
RedBlackNode<V> x;
x = new RedBlackNode<V>(v,xp);//生成新的節點,父節點賦值為上面查詢出來的可連線點
// if (x != null)
// f.prev = x;
//根據dir值,關聯新生節點和父節點
if (dir <= 0)
xp.left = x;
else
xp.right = x;
if (!xp.red)//如果父節點是黑色的,則表明
x.red = true;
else {
root = balanceInsertion(root, x);//進行平衡插入資料
}
break;
}
}
return null;
}
/**
* 插入資料
* @param root
* @param x
* @return
*/
private RedBlackNode<V> balanceInsertion(RedBlackNode<V> root, RedBlackNode<V> x) {
x.red = true;
for (RedBlackNode<V> xp, xpp, xppl, xppr;;) {
//如果插入節點的父節點為空,則表示當前節點為根節點,變色並返回當前節點
if ((xp = x.parent) == null) {
x.red = false;
return x;
}
//如果插入值的祖父節點空,或父節點是黑色時,返回父節點
else if (!xp.red || (xpp = xp.parent) == null)
return root;
/**
* xpp
* / \
* xp xppr
* /
* x
*下面的情況時父節點為紅色,且祖父節點不為空時
*父節點是祖父節點的左節點時
*/
if (xp == (xppl = xpp.left)) {
//判斷叔叔節點如果是紅色,則父節點和叔叔節點變色
if ((xppr = xpp.right) != null && xppr.red) {
xppr.red = false;
xp.red = false;
xpp.red = true;//祖父節點變紅
x = xpp;//將祖父節點賦值給當前節點,用於判斷祖父節點和祖父節點的父節點是否都為紅色
}
//父節點和叔叔節點顏色不同時
else {
/**
* xpp
* / \
* xp xppr
* \
* x
*
* 如果要插入的節點是父節點的右節點時進行左轉
* 即,新的節點給最小子樹的左節點,新增右子節點時進行左轉
*/
if (x == xp.right) {
root = rotateLeft(root, x = xp);//這裡x=xp是因為,左旋是x替換xp的過程,所以用x=xp
xpp = (xp = x.parent) == null ? null : xp.parent;
}
/**
* xpp
* / \
* xp xppr
* /
* x
* 先變色再旋轉
**/
if (xp != null) {
xp.red = false;
if (xpp != null) {
xpp.red = true;
root = rotateRight(root, xpp);
}
}
}
}
/**
* 插入節點的父節點為祖父節點的右節點時
*/
else {
/**
* 父節點和兄弟節點為紅色時,同時變得,並將x變更為祖父節點
* 判斷祖父節點是否符合紅黑樹規則
*/
if (xppl != null && xppl.red) {
xppl.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
/**
* 異色進行旋轉
*/
else {
/**
* xpp
* / \
* xpl xp
* /
* x
* 進行右旋
* xpp
* / \
* xpl xp
* \
* x
*/
if (x == xp.left) {
root = rotateRight(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
/**
* 1.xp非空的時候,先進行變色,再進行左旋
* xp
* / \
* xpp x
* /
* xpl
*/
if (xp != null) {
xp.red = false;
if (xpp != null) {
xpp.red = true;
root = rotateLeft(root, xpp);
}
}
}
}
}
}
- 紅黑樹插入操作中設計到左旋和右旋操作
左旋操作
/**
*
* 進行左旋轉
* @param root
* @param p
* @return
*/
private RedBlackNode<V> rotateLeft(RedBlackNode<V> root, RedBlackNode<V> p) {
RedBlackNode<V> r, pp, rl;
/**
* pp
* / \
* p ppr
* \
* r
* / \
* rl rr
* 左旋操作:
* 1.r節點的左節點rl變成p節點的左節點
* 2.r節點的父節點變成p的父節點
* 3.p的父節點變成r
* pp
* / \
* r ppr
* / \
* p rr
* /
* rl
*
*/
if (p != null && (r = p.right) != null) {
if ((rl = p.right = r.left) != null)
rl.parent = p;
if ((pp = r.parent = p.parent) == null)//如果pp節點為空,也即r為根節點,顏色變黑
(root = r).red = false;
else if (pp.left == p)
pp.left = r;
else
pp.right = r;
r.left = p;
p.parent = r;
}
return root;
}
右旋操作
/**
* 進行右旋
* @param root
* @param p
* @return
*/
private RedBlackNode<V> rotateRight(RedBlackNode<V> root, RedBlackNode<V> p) {
/**
* p
* / \
* l pr
* / \
* ll lr
* 右旋操作:
* 1.p節點的左節點變成l的右節點
* 2.l的父節點變成p的父節點
* 3.p的父節點變成l
* l
* / \
* ll p
* / \
* lr pr
*/
RedBlackNode<V> l, pp, lr;
if (p != null && (l = p.left) != null) {
if ((lr = p.left = l.right) != null)
lr.parent = p;
if ((pp = l.parent = p.parent) == null)
(root = l).red = false;
else if (pp.right == p)
pp.right = l;
else
pp.left = l;
l.right = p;
p.parent = l;
}
return root;
}
其他操作-查詢節點
- 查詢紅黑樹中的節點,和所有二叉樹一樣進行二分法進行查詢。程式碼也比較簡單
//用於查詢節點的父節點
final RedBlackNode<V> findTreeNode(V v) {
if (v != null) {
RedBlackNode<V> p = this;
do {
int dir; V pv; RedBlackNode<V> q;
RedBlackNode<V> pl = p.left, pr = p.right;
if ((pv = p.val).compareTo(v) > 0)//從左子樹查詢
p = pl;
else if ((pv = p.val).compareTo(v) < 0)//從右子樹查詢
p = pr;
else if ((pv = p.val) == v || (pv != null && p.val.equals(pv)))//如果剛好和當前節點相同,則返回
return p;
else if (pl == null)
p = pr;
else if (pr == null)
p = pl;
else if ((q = pr.findTreeNode(v)) != null)//如果沒有,則迴圈遍歷,知道查詢的父節點P為null
return q;
else
p = pl;
} while (p != null);
}
return null;
}
其他操作-刪除節點(比較複雜,瞭解)
-
紅黑樹節點刪除涉及的情況比較多也比較複雜,大致分這幾種:
-
刪除節點是根節點的;
-
刪除節點有左右節點的情況;
-
刪除節點只有左節點情況
-
刪除節點只有右節點情況。
-
-
刪除的思路:
-
找到刪除節點的臨界節點,以上四種情況分別有不同處理,找到合適的臨界節點做替換節點
-
調整樹結構,將刪除節點移位。
-
斷開刪除節點與左右父節點的關聯,替換節點關聯刪除父節點
-
替換節點變色和進行左右旋操作,紅黑樹進行自平衡
-
-
具體程式碼如下:
/**
* 刪除節點
* @param p
* @return
*/
public boolean removeTreeNode(RedBlackNode<V> p){
RedBlackNode<V> r, rl;
if ((r = root) == null || r.right == null ||
(rl = r.left) == null || rl.left == null)
return true;
try {
/**
* sp
* \
* p
* / \
* pl pr
* /
* s
* \
* sr
*/
RedBlackNode<V> replacement;
RedBlackNode<V> pl = p.left;
RedBlackNode<V> pr = p.right;
if (pl != null && pr != null) {
RedBlackNode<V> s = pr, sl;
/**
* 1.刪除節點p的右節點pr
* 2.pr的左節點迴圈找到最小左子樹的左節點s
*/
while ((sl = s.left) != null)
s = sl;
boolean c = s.red; s.red = p.red; p.red = c; //顏色交換
RedBlackNode<V> sr = s.right;
RedBlackNode<V> pp = p.parent;
/**
* pr
* \
* p
* 當pr==s時
* 要刪除的節點只有一個右節點時,進行交換
*/
if (s == pr) {
p.parent = s;
s.right = p;
}
else {
/**
* 以下的資料處理邏輯是這樣子的
* pp pp pp
* \ / /
* p s s
* / \ / \ / \
* pl pr ————> pl pr ————> pl pr
* / / /
* s p sr
* \ \
* sr sr
*
* 1.找到要刪除節點p的右節點pr;
* 2.迴圈遍歷pr的所有子節點的左節點,找到最小子樹的左節點s;
* 3.p節點指向s的父節點,p成為s父節點的左子樹;
* 4.p的右子樹成為s節點的有子節點;
* 5.s的右子節點sr成為p的右子節點
* 6.s的成為p的左節點
*/
RedBlackNode<V> sp = s.parent;
if ((p.parent = sp) != null) {
if (s == sp.left)
sp.left = p;
else
sp.right = p;
}
if ((s.right = pr) != null)
pr.parent = s;
}
p.left = null;
if ((p.right = sr) != null)
sr.parent = p;
if ((s.left = pl) != null)
pl.parent = s;
if ((s.parent = pp) == null)
r = s;
else if (p == pp.left)
pp.left = s;
else
pp.right = s;
if (sr != null)
replacement = sr;
else
replacement = p;
}
/**
* 這個replacement分四種情況:
* 1.p有左右節點,且p為根節點的最小值子樹的左節點(s),有sr右節點。則為右節點
* 2.上面沒有右節點,則為p;
* 3.p只有左節點pl,replacement=pl
* 4.p只有右節點pr,replacement=pr
*/
else if (pl != null)
replacement = pl;
else if (pr != null)
replacement = pr;
else
replacement = p;
/**
* 用replacement元素替換要刪除的p節點
*/
if (replacement != p) {
RedBlackNode<V> pp = replacement.parent = p.parent;//pr
if (pp == null)
r = replacement;
else if (p == pp.left)
pp.left = replacement;
else
pp.right = replacement;
p.left = p.right = p.parent = null;
}
/**
* 判斷刪除後的節點是否需要進行自平衡
*/
root = (p.red) ? r : balanceDeletion(r, replacement);
/**
* 刪除p父親節點和p節點的關聯關係
*/
if (p == replacement) {
RedBlackNode<V> pp;
if ((pp = p.parent) != null) {
if (p == pp.left)
pp.left = null;
else if (p == pp.right)
pp.right = null;
p.parent = null;
}
}
} finally {
}
return false;
}
/**
* 節點刪除
* @param r
* @param x
* @return
*/
private RedBlackNode balanceDeletion(RedBlackNode<V> r, RedBlackNode<V> x) {
for (RedBlackNode<V> xp, xpl, xpr;;) {
/**
* 判斷是否為根節點
*/
if (x == null || x == root)
return root;
else if ((xp = x.parent) == null) {
x.red = false;
return x;
}
else if (x.red) {
x.red = false;
return root;
}
/**
* xp xp
* / \ /
* xpl(x) xpr x
* / \
* sl sr
*
*
* 已知:1.x為xp左節點且為黑色
* 2.xp節點肯定和x同色(因為紅黑樹不能有連續紅色節點,刪除掉中間節點後,只能都是紅色或黑色,上面判斷了x不能是紅色)
* 操作:1.x的兄弟節點異色,進行左旋
*
* 上面分析了x(replacement)節點有5種情況
*/
else if ((xpl = xp.left) == x) {
if ((xpr = xp.right) != null && xpr.red) {
xpr.red = false;
xp.red = true;
root = rotateLeft(root, xp);
xpr = (xp = x.parent) == null ? null : xp.right;
}
if (xpr == null)
x = xp;
else {
RedBlackNode<V> sl = xpr.left, sr = xpr.right;
if ((sr == null || !sr.red) &&
(sl == null || !sl.red)) {
xpr.red = true;
x = xp;
}
else {
if (sr == null || !sr.red) {
if (sl != null)
sl.red = false;
xpr.red = true;
root = rotateRight(root, xpr);
xpr = (xp = x.parent) == null ?
null : xp.right;
}
if (xpr != null) {
xpr.red = (xp == null) ? false : xp.red;
if ((sr = xpr.right) != null)
sr.red = false;
}
if (xp != null) {
xp.red = false;
root = rotateLeft(root, xp);
}
x = root;
}
}
}
else { // symmetric
if (xpl != null && xpl.red) {
xpl.red = false;
xp.red = true;
root = rotateRight(root, xp);
xpl = (xp = x.parent) == null ? null : xp.left;
}
if (xpl == null)
x = xp;
else {
RedBlackNode<V> sl = xpl.left, sr = xpl.right;
if ((sl == null || !sl.red) &&
(sr == null || !sr.red)) {
xpl.red = true;
x = xp;
}
else {
if (sl == null || !sl.red) {
if (sr != null)
sr.red = false;
xpl.red = true;
root = rotateLeft(root, xpl);
xpl = (xp = x.parent) == null ?
null : xp.left;
}
if (xpl != null) {
xpl.red = (xp == null) ? false : xp.red;
if ((sl = xpl.left) != null)
sl.red = false;
}
if (xp != null) {
xp.red = false;
root = rotateRight(root, xp);
}
x = root;
}
}
}
}
}