一、概述
紅黑樹是一種自平衡二叉查找樹,最早由一位名叫Rudolf Bayer的德國計算機(jī)科學(xué)家于1972年發(fā)明。然而,最初的樹形結(jié)構(gòu)不是現(xiàn)在的紅黑樹,而是一種稱為B樹的結(jié)構(gòu),它是一種多叉樹,可用于在磁盤上存儲大量數(shù)據(jù)。
在1980年代早期,計算機(jī)科學(xué)家Leonard Adleman和Daniel Sleator推廣了紅黑樹,并證明了它的自平衡性和高效性。從那時起,紅黑樹成為了最流行的自平衡二叉查找樹之一,并被廣泛應(yīng)用于許多領(lǐng)域,如編譯器、操作系統(tǒng)、數(shù)據(jù)庫等。
紅黑樹的名字來源于紅色節(jié)點(diǎn)和黑色節(jié)點(diǎn)的交替出現(xiàn),它們的顏色是用來維護(hù)樹的平衡性的關(guān)鍵。它們的顏色具有特殊的意義:
- 黑色節(jié)點(diǎn)代表普通節(jié)點(diǎn),
- 紅色節(jié)點(diǎn)代表一個新添加的節(jié)點(diǎn),
它們必須滿足一些特定的規(guī)則才能維持樹的平衡性。
紅黑樹也是一種自平衡的二叉搜索樹,較之 AVL,插入和刪除時旋轉(zhuǎn)次數(shù)更少
二、紅黑樹的特性
- 每個節(jié)點(diǎn)要么是紅色,要么是黑色。
- 根節(jié)點(diǎn)是黑色。
- 所有葉子節(jié)點(diǎn)(NIL節(jié)點(diǎn))都是黑色。這些葉子節(jié)點(diǎn)通常表示空節(jié)點(diǎn)或沒有實(shí)際存儲值的節(jié)點(diǎn)。
- 紅色節(jié)點(diǎn)不能相連:如果一個節(jié)點(diǎn)是紅色,它的兩個子節(jié)點(diǎn)必須是黑色。
- 黑色完美平衡:從任何節(jié)點(diǎn)到其所有后代葉子節(jié)點(diǎn)的簡單路徑上,必須包含相同數(shù)量的黑色節(jié)點(diǎn)。這個屬性確保了紅黑樹的平衡性,避免了樹變得過高。
三、實(shí)現(xiàn)紅黑樹
3.1 節(jié)點(diǎn)定義
public class RedBlackTree {
/**
* 根節(jié)點(diǎn)
*/
private Node root;
/**
* 顏色枚舉
*/
enum Color {
RED, BLACK;
}
private static class Node {
int key;
Object value;
Node left;
Node right;
// 父節(jié)點(diǎn)
Node parent;
// 顏色 默認(rèn)為紅色
Color color = Color.RED;
/**
* 判斷節(jié)點(diǎn)是否是左孩子
*
* @return
*/
boolean isLeftChild() {
return parent != null && this == parent.left;
}
/**
* 獲取當(dāng)前節(jié)點(diǎn)的叔叔:爸爸的兄弟(前提必須有爺爺存在)
*
* @return
*/
Node getUncle() {
if (parent == null || parent.parent == null) {
return null;
}
// 如果當(dāng)前節(jié)點(diǎn)的父親是屬于左孩子,那么爺爺?shù)挠液⒆泳褪鞘迨? if (parent == parent.parent.left) {
return parent.parent.right;
} else {
return parent.parent.left;
}
}
/**
* 獲取當(dāng)前節(jié)點(diǎn)的兄弟
*
* @return
*/
Node getBrother() {
if (parent == null) {
return null;
}
// 當(dāng)前節(jié)點(diǎn)是屬于左孩子,那么父親的右孩子就是兄弟
if (this.isLeftChild()) {
return parent.right;
} else {
return parent.left;
}
}
}
}
判斷節(jié)點(diǎn)顏色:
/**
* 判斷節(jié)點(diǎn)是否是紅色
*
* @param node
* @return
*/
boolean isRed(Node node) {
return node != null && node.color == Color.RED;
}
/**
* 判斷節(jié)點(diǎn)是否是黑色
* @param node
* @return
*/
boolean isBlack(Node node) {
return !isRed(node);
//return node == null || node.color == Color.BLACK;
}
3.2 左右旋代碼
這里的思路與AVL樹的相同,只是多了parent屬性要維護(hù),所以對每一個移動了的節(jié)點(diǎn),都需要更新parent屬性。(關(guān)于AVL樹旋轉(zhuǎn)的詳細(xì)邏輯可以看我上一篇文章)
右旋:
/**
* 右旋:
* 1.旋轉(zhuǎn)本身的邏輯:失衡節(jié)點(diǎn)左孩子上位 ,處理失衡節(jié)點(diǎn)的后事
* 2.處理 待處理后事節(jié)點(diǎn),上位節(jié)點(diǎn),失衡節(jié)點(diǎn) 的父親
* 3.處理旋轉(zhuǎn)節(jié)點(diǎn)父親的孩子
* @param node 失衡的節(jié)點(diǎn)
*/
private void rightRotate(Node node) {
// 失衡的節(jié)點(diǎn) node
// 失衡節(jié)點(diǎn)的左孩子:要上位的節(jié)點(diǎn)
Node upNode = node.left;
// 待處理的上位節(jié)點(diǎn)的后代:上位節(jié)點(diǎn)的右孩子
Node toChangeParent = upNode.right;
// 1.正常的右旋處理:
// 1.1上位
upNode.right = node;
// 1.2處理后事
node.left = toChangeParent;
// 2.更新相關(guān)移動節(jié)點(diǎn)的父親
// 2.1 更新上位節(jié)點(diǎn)后代的父親 為 要失衡的節(jié)點(diǎn)
if (toChangeParent != null) {
toChangeParent.parent = node;
}
// 2.2 更新上位節(jié)點(diǎn)的父親:為 失衡節(jié)點(diǎn)的父親
upNode.parent = node.parent;
// 2.3 更新失衡節(jié)點(diǎn)的父親
node.parent = upNode;
// 3.處理上位節(jié)點(diǎn)父親的孩子(因?yàn)槭请p向的,步驟二只處理了節(jié)點(diǎn)的父親,針對父親還要更新父親的孩子)
// 判斷旋轉(zhuǎn)節(jié)點(diǎn)原本是屬于左還是還是右孩子
if (node.parent == null) {
// 只需要把上位節(jié)點(diǎn)更新為根節(jié)點(diǎn)
root = upNode;
}
// 旋轉(zhuǎn)節(jié)點(diǎn)原本是屬于左孩子
else if (node.parent.left == node) {
node.parent.left = upNode;
}
// 旋轉(zhuǎn)節(jié)點(diǎn)原本是屬于右孩子
else {
node.parent.right = upNode;
}
}
左旋:
/**
* 左旋:
* 1.旋轉(zhuǎn)本身的邏輯:失衡節(jié)點(diǎn)右孩子上位 ,處理失衡節(jié)點(diǎn)的后事
* 2.處理 待處理后事節(jié)點(diǎn),上位節(jié)點(diǎn),失衡節(jié)點(diǎn) 的父親
* 3.處理失衡節(jié)點(diǎn)父親的孩子
* @param node 失衡的節(jié)點(diǎn)
*/
private void leftRotate(Node node) {
// 失衡節(jié)點(diǎn)node
// 上位節(jié)點(diǎn):失衡節(jié)點(diǎn)的右孩子
Node upNode = node.right;
// 待處理后事的節(jié)點(diǎn):上位節(jié)點(diǎn)的左孩子
Node toChangeParent = upNode.left;
// 1.正常的左旋處理
// 1.1 節(jié)點(diǎn)上位
upNode.left = node;
// 1.2 處理后事: 后代toChangeParent父親更換為原本的失衡節(jié)點(diǎn)
node.right = toChangeParent;
// 2. 更新相關(guān)移動節(jié)點(diǎn)的父親
// 2.1 上位節(jié)點(diǎn)后代toChangeParent的父親 更新為:失衡的節(jié)點(diǎn)
if (toChangeParent != null) {
toChangeParent.parent = node;
}
// 2.2 更新上位節(jié)點(diǎn)的父親 為 失衡節(jié)點(diǎn)的父親
upNode.parent = node.parent;
// 2.3 更新失衡節(jié)點(diǎn)的父親 為 上位節(jié)點(diǎn)
node.parent = upNode;
// 3.處理失衡節(jié)點(diǎn)的父親的孩子
// 如果失衡節(jié)點(diǎn)原本是根節(jié)點(diǎn)
if (node.parent == null) {
root = upNode;
}
// 失衡節(jié)點(diǎn)原本是屬于左孩子
else if (node.parent.left == node) {
node.parent.left = upNode;
} else {
node.parent.right = upNode;
}
}
3.3 插入
假設(shè)插入的節(jié)點(diǎn)為紅色,一共包含以下四種情況:
1.插入節(jié)點(diǎn)為根節(jié)點(diǎn)
將根節(jié)點(diǎn)變黑
2.插入節(jié)點(diǎn)的父親為黑色
樹的紅黑性質(zhì)不變,無需調(diào)整
3.插入節(jié)點(diǎn)的父親和叔叔都為紅色
此種情況只需要變色:
- 父親變?yōu)楹谏ù藭r違反了黑色平衡),為了保證黑色平衡,連帶的叔叔也變?yōu)楹谏?/li>
- 此時當(dāng)前路徑多了一個黑色,其他路徑?jīng)]有多的黑色,所以需要在當(dāng)前路徑中再減少黑色:爺爺變成紅色。
- 爺爺變成紅色,可能又會接著觸發(fā)紅紅相鄰,因此對將爺爺進(jìn)行遞歸調(diào)整,最后根節(jié)點(diǎn)需要變黑
在下圖的情況下插入1:
樹的變色過程:
第一輪:對于插入節(jié)點(diǎn)來說:父親和叔叔變黑,爺爺變紅;
第二輪:對于爺爺來說:爺爺?shù)母赣H和爺爺?shù)氖迨遄兒冢瑺敔數(shù)臓敔斪兗t;(與第一步相同,直接遞歸)
最后爺爺?shù)臓敔斒歉?jié)點(diǎn),所以變黑
總結(jié):父親和叔叔變黑,爺爺變紅,遞歸變化直到根節(jié)點(diǎn),最后根節(jié)點(diǎn)變?yōu)楹谏?/p>
4.插入節(jié)點(diǎn)的父親為紅色,叔叔為黑色
此種情況需要變色+旋轉(zhuǎn):
-
①父親為左孩子,插入節(jié)點(diǎn)也是左孩子,此時即 LL 不平衡
- 讓父親變黑,為了保證這顆子樹黑色不變,將爺爺變成紅,但叔叔子樹少了一個黑色
-
爺爺右旋,補(bǔ)齊一個黑色給叔叔,父親旋轉(zhuǎn)上去取代爺爺,由于它是黑色,不會再次觸發(fā)紅紅相鄰
image.png
-
②父親為左孩子,插入節(jié)點(diǎn)是右孩子,此時即 LR 不平衡
-
父親左旋,變成 LL 情況,按 1. 來后續(xù)處理
image.png
-
-
③父親為右孩子,插入節(jié)點(diǎn)也是右孩子,此時即 RR 不平衡
- 讓父親變黑,為了保證這顆子樹黑色不變,將爺爺變成紅,但叔叔子樹少了一個黑色
-
爺爺左旋,補(bǔ)齊一個黑色給叔叔,父親旋轉(zhuǎn)上去取代爺爺,由于它是黑色,不會再次觸發(fā)紅紅相鄰
image.png
-
④父親為右孩子,插入節(jié)點(diǎn)是左孩子,此時即 RL 不平衡
-
父親右旋,變成 RR 情況,按 3. 來后續(xù)處理
image.png
-
插入方法:尋找位置插入:
/**
* 插入/更新節(jié)點(diǎn)
* @param key
* @param value
*/
public void put(int key,Object value) {
// 找到插入的位置
Node pointer = root;
// 插入位置的父親
Node parent = null;
while (pointer != null) {
parent = pointer;
if (key < pointer.key) {
// 往左找
pointer = pointer.left;
} else if (key > pointer.key) {
// 往右找
pointer = pointer.right;
} else {
// 找到了 直接更新值
pointer.value = value;
return;
}
}
// 沒有找到,當(dāng)前指針pointer指向的是要插入的位置
Node added = new Node(key, value);
// 樹為空,新增節(jié)點(diǎn)作為根節(jié)點(diǎn)
if (parent == null) {
root = added;
} else if (key < parent.key) {
// 新增節(jié)點(diǎn)作為左孩子
parent.left = added;
// 設(shè)置新增節(jié)點(diǎn)的父親
added.parent = parent;
} else {
// 新增節(jié)點(diǎn)作為右孩子
parent.right = added;
added.parent = parent;
}
// 插入結(jié)束后,不平衡的情況下需要對樹進(jìn)行旋轉(zhuǎn)和變色
fixRedRed(added);
}
在插入了節(jié)點(diǎn)之后,樹失衡,對樹進(jìn)行旋轉(zhuǎn)變色方法:
/**
* 當(dāng)出現(xiàn)兩個相鄰紅色節(jié)點(diǎn)的時候?qū)涞恼{(diào)整
* 1.插入的是根節(jié)點(diǎn)直接變黑
* 2.插入節(jié)點(diǎn)的父親就是黑色,無需做任何操作
* 3.插入節(jié)點(diǎn)的父親是紅色,本身也是紅色,觸發(fā)紅紅相連
* - 叔叔是紅色:父親和叔叔一起變紅,爺爺變黑;遞歸直到根節(jié)點(diǎn)
* - 叔叔是黑色:此種情況,因?yàn)樽兩珪r叔叔跟父親顏色不同,所以變色做不到平衡,需要旋轉(zhuǎn)
* - 父親是左孩子,插入節(jié)點(diǎn)也是左孩子 LL
* - 父親是左孩子,插入節(jié)點(diǎn)是右孩子 LR
* - 父親是右孩子,插入節(jié)點(diǎn)是右孩子 RR
* - 父親是右孩子,插入節(jié)點(diǎn)是左孩子 RL
*/
public void fixRedRed(Node node) {
// 1.插入的是根節(jié)點(diǎn)直接變黑
if (node == root) {
node.color = Color.BLACK;
return;
}
// 2.插入節(jié)點(diǎn)的父親就是黑色,無需做任何操作
else if (isBlack(node.parent)) {
return;
}
//3. 父親和叔叔都是紅色
// 代碼執(zhí)行到這里 父親一定是紅色
// 父親
Node parent = node.parent;
// 叔叔
Node uncle = node.getUncle();
// 爺爺
Node grandpa = parent.parent;
// 叔叔是紅色(此時父親是紅色):
if (isRed(uncle)) {
// 父親和叔叔變黑 (路徑多了黑,所以爺爺要變紅)
parent.color = Color.BLACK;
uncle.color = Color.BLACK;
// 爺爺變紅 (又可能觸發(fā)紅紅相連)
grandpa.color = Color.RED;
// 遞歸執(zhí)行
fixRedRed(grandpa);
}
// 叔叔是黑色(此時父親是紅色)叔叔和父親顏色不同,通過變色做不到平衡,需要旋轉(zhuǎn)
// 父親是左孩子,插入節(jié)點(diǎn)也是左孩子 LL不平衡
if (parent.isLeftChild() && node.isLeftChild()) {
// 1.變色:父親變黑,爺爺變紅
parent.color = Color.BLACK;
grandpa.color = Color.RED;
// 2.右旋:爺爺右旋
rightRotate(grandpa);
}
// 父親是左孩子,插入節(jié)點(diǎn)是右孩子 LR不平衡
else if (parent.isLeftChild() && !node.isLeftChild()) {
// 1.父親左旋 變成了LL
leftRotate(parent);
// LL:變色+旋轉(zhuǎn)
// 變色:父親變黑(在父親左旋之后,此時父親是插入的節(jié)點(diǎn)),爺爺變紅
node.color = Color.BLACK;
grandpa.color = Color.RED;
rightRotate(grandpa);
}
// 父親是右孩子,插入節(jié)點(diǎn)也是右孩子 RR不平衡
else if (!parent.isLeftChild() && !node.isLeftChild()) {
// 1.變色:父親變黑,爺爺變紅
parent.color = Color.BLACK;
grandpa.color = Color.RED;
// 2.爺爺左旋
leftRotate(grandpa);
}
// 父親是右孩子,插入節(jié)點(diǎn)是左孩子,RL不平衡
else {
// 父親右旋,變成RR場景 此時父親變成了node
rightRotate(parent);
// 1.變色:父親變黑,爺爺變紅
node.color = Color.BLACK;
grandpa.color = Color.RED;
// 2.旋轉(zhuǎn):爺爺左旋
leftRotate(grandpa);
}
}
平衡的過程就是變色+旋轉(zhuǎn)
3.4 刪除
1.刪除節(jié)點(diǎn)沒有孩子
- 刪除的節(jié)點(diǎn)就是根節(jié)點(diǎn):直接刪除
- 刪除的節(jié)點(diǎn)不是根節(jié)點(diǎn):直接把刪除節(jié)點(diǎn)的父親的孩子置空,刪除節(jié)點(diǎn)的父親也置空(有助于垃圾回收)
2.刪除節(jié)點(diǎn)有一個孩子
- 刪除的節(jié)點(diǎn)是根節(jié)點(diǎn):
- 這個孩子一定是紅色(如果是黑色的話就不平衡了)。
- 剩余節(jié)點(diǎn)和根節(jié)點(diǎn)的 key,value交換,再把根節(jié)點(diǎn)孩子設(shè)置為null,顏色保持黑色不變
- 這個孩子一定是紅色(如果是黑色的話就不平衡了)。
- 刪除的節(jié)點(diǎn)不是根節(jié)點(diǎn):讓刪除節(jié)點(diǎn)的父親指向刪剩下的節(jié)點(diǎn)(向下的指針),刪剩下的父親指向刪除節(jié)點(diǎn)的父親(向上的指針);本身刪除節(jié)點(diǎn)的左右孩子和父親置空(垃圾回收)
3.刪除節(jié)點(diǎn)有兩個孩子
想辦法把兩個孩子轉(zhuǎn)變成情況二的一個孩子:
交換刪除節(jié)點(diǎn)和后繼節(jié)點(diǎn)的 key,value,這樣就轉(zhuǎn)變成了情況二,遞歸刪除后繼節(jié)點(diǎn),直到該節(jié)點(diǎn)沒有孩子或只剩一個孩子。
失衡情況:
刪黑色節(jié)點(diǎn)需要考慮平衡,刪紅色不需要:
- 紅色是葉子節(jié)點(diǎn),不會失衡
- 紅色是非葉子節(jié)點(diǎn),他會有兩個孩子,針對兩個孩子的情況,都會被轉(zhuǎn)換為只有一個孩子的情況,通過交換,進(jìn)入上面的刪除情況二中。
失衡情況一:
刪的黑色節(jié)點(diǎn),剩下的是一個紅色節(jié)點(diǎn):
- 刪了黑色之后,把剩下的這個紅節(jié)點(diǎn)變黑(缺少的黑色通過紅孩子變黑補(bǔ)齊了)
失衡情況二:
刪的是黑色節(jié)點(diǎn),剩下的也是一個黑色節(jié)點(diǎn)(或者是null): 當(dāng)該節(jié)點(diǎn)刪除了這個黑色之后,整條路徑上對比其他路徑就少了一個黑色。
-
2.1 刪除節(jié)點(diǎn)的兄弟為紅色,此時兩個侄子一定是黑色(此種情況轉(zhuǎn)為后續(xù)兩種處理)
image.png
-
2.2 刪除節(jié)點(diǎn)的兄弟為黑色,侄子都是黑:
-
有紅色父親的時候:把兄弟變紅(平衡刪掉的黑色節(jié)點(diǎn)),把父親變黑(平衡該子樹與其他子樹對比少的黑色)
image.png -
沒有紅色父親的時候:以父親作為起點(diǎn),觸發(fā)黑黑的方法,把父親的兄弟變成紅色;
依次遞歸處理到根節(jié)點(diǎn),那么每一條路徑都把一個黑色變成了紅色,實(shí)現(xiàn)黑色平衡(當(dāng)然如果父親已經(jīng)是紅色了,可以直接變父親的顏色為黑色(就是上面一種情況)
image.png
-
-
2.3 刪除節(jié)點(diǎn)的兄弟為黑色,侄子至少有一個紅:
-
兄弟是左孩子,左侄子是紅色:LL
image.png
-
變色邏輯:
1.通過旋轉(zhuǎn)過來的3變成黑色補(bǔ)齊該路徑上少的黑色
2.針對上位節(jié)點(diǎn)2,變成原本父親的顏色,因?yàn)檫@條路徑上本身是平衡的,所以上來的要變成原本父親的顏色
3.針對紅色的侄子1,變成黑色,因?yàn)樵驹撀窂缴希粍h除節(jié)點(diǎn)的兄弟被當(dāng)成了父親,所以原本作為這個路徑的黑色兄弟就走了,少了一個黑色,所以讓紅色侄子變黑。
-
兄弟是左孩子,右侄子是紅色:LR
這里代碼層面可以做簡化,直接對比圖中1和6,最后侄子上位變成了父親的顏色,原本的父親變成黑色
image.png- 兄弟是右孩子,右侄子是紅色:RR(此種場景和LL類似)
- 兄弟是右孩子,左侄子是紅色:RL(此種場景和LR類似)
-
針對左右侄子都是紅色的場景,走LL或者RR都是行得通的
image.png
代碼:
/**
* 刪除節(jié)點(diǎn)
* @param deleted
*/
public void doRemove(Node deleted) {
// 刪除節(jié)點(diǎn)的后繼節(jié)點(diǎn)
Node replace = findReplace(deleted);
//分三種大情況:沒有孩子 一個孩子 兩個孩子
// 被刪除節(jié)點(diǎn)的父親
Node deletedParent = deleted.parent;
// 一、沒有孩子
if (replace == null) {
// 1.1 刪除的節(jié)點(diǎn)是根節(jié)點(diǎn)
if (deleted == root) {
root = null;
}
// 1.2 刪除的節(jié)點(diǎn)不是根節(jié)點(diǎn)
else {
// 變色(注意這里要先變色 再刪除)
if (isBlack(deleted)) { // 被刪除的是黑色 因?yàn)闆]有孩子(孩子為黑色),所以也屬于下面的刪除的是黑,剩下的孩子也是黑
// 旋轉(zhuǎn) + 變色
fixBlackBlack(deleted);
} else { // 被刪除的是紅色 不需要做處理
// do nothing
}
// 刪除操作
// 刪除節(jié)點(diǎn)屬于左孩子
if (deleted.isLeftChild()) {
deletedParent.left = null;
} else { // 刪除節(jié)點(diǎn)屬于右孩子
deletedParent.right = null;
}
deleted.parent = null;
}
return;
}
// 二、有一個孩子
if (deleted.left == null || deleted.right == null) {
// 2.1 刪除的節(jié)點(diǎn)是根節(jié)點(diǎn) 這個唯一的孩子一定是紅色:根節(jié)點(diǎn)和孩子互換,刪除孩子
if (deleted == root) {
// delete和replace交換
deleted.key = replace.key;
deleted.value = replace.value;
deleted.left = null;
deleted.right = null;
}
// 2.2 不是根節(jié)點(diǎn)
else {
// 刪除操作
// 看被刪除的節(jié)點(diǎn)是左孩子還是右孩子
if (deleted.isLeftChild()) {
deletedParent.left = replace;
} else {
deletedParent.right = replace;
}
replace.parent = deletedParent;
// 被刪除的節(jié)點(diǎn)孩子還父親置空
deleted.parent = null;
deleted.left = null;
deleted.right = null;
// 變色操作
// 刪除的是黑色 ,刪剩下的也是黑色
if (isBlack(deleted) && isBlack(replace)) {
// 旋轉(zhuǎn) + 變色
fixBlackBlack(replace);
} else { // 刪除的是黑色,剩下的是紅色:
// 少了一個黑色,把剩下的紅色變黑即可
replace.color = Color.BLACK;
}
}
return;
}
// 三、有兩個孩子
/**
* 這種情況轉(zhuǎn)為只有一個孩子的情況:
* 將要刪除的節(jié)點(diǎn)和后繼節(jié)點(diǎn)的鍵值交換,那么要刪除的節(jié)點(diǎn)就變成了后繼節(jié)點(diǎn),此時一直遞歸調(diào)用,直到只有一個孩子的情況,進(jìn)入情況二的分支
*/
// 交換key
int tempKey = deleted.key;
deleted.key = replace.key;
replace.key = tempKey;
// 交換value
Object tempVal = deleted.value;
deleted.value = replace.value;
replace.value = tempVal;
// 要刪的就變成了replace 遞歸直到進(jìn)入條件二
doRemove(replace);
}
被刪除的節(jié)點(diǎn)是黑色,刪剩下的也是黑色場景的旋轉(zhuǎn)和變色:
/**
* 被刪除的節(jié)點(diǎn)是黑色,刪剩下的也是黑色(整體這個路徑上少了一個黑色,失衡)
* - 情況一.被刪節(jié)點(diǎn)的兄弟是紅色,此時侄子一定是黑色:左旋+變色 轉(zhuǎn)為下面兩種情況
* - 情況二.被刪節(jié)點(diǎn)的兄弟是黑色,侄子(這倆的孩子)都是黑:
* - 情況三.被刪節(jié)點(diǎn)的兄弟是黑色,侄子(這倆的孩子)中至少一個紅:
* @param node
*/
private void fixBlackBlack(Node node) {
if (node == root) {
//此時處理結(jié)束
return;
}
// 節(jié)點(diǎn)的兄弟
Node brother = node.getBrother();
// 節(jié)點(diǎn)的父親
Node parent = node.parent;
// 情況一.被刪節(jié)點(diǎn)的兄弟是紅色,此時侄子一定是黑色:左旋+變色
if (isRed(brother)) {
if (node.isLeftChild()) {
// node屬于左節(jié)點(diǎn) 父親左旋
// 1.1 父親左旋
leftRotate(parent);
} else {
rightRotate(parent);
}
// 1.2 調(diào)整平衡
brother.color = Color.BLACK;
parent.color = Color.RED;
// 此時把兄弟變成了黑色了 就符合了情況二和情況三,再次調(diào)用方法進(jìn)行二和三情況的處理
fixBlackBlack(node);
return;
}
// 情況二:刪除節(jié)點(diǎn)的兄弟為黑色,侄子都是黑:
assert brother != null;
if (isBlack(brother.left) && isBlack(brother.right)) {
// 2.1兄弟變紅
brother.color = Color.RED;
// 如果此時父親是紅,則讓父親變黑就維持了平衡了
if (isRed(parent)) {
parent.color = Color.BLACK;
}
// 如果父親是黑,觸發(fā)黑黑調(diào)整:讓兄弟變紅;依次遞歸,讓每一條路徑都有一個兄弟變紅,就都少了一個黑色,實(shí)現(xiàn)黑色平衡
else {
fixBlackBlack(parent);
}
} else {
// 情況三:刪除的節(jié)點(diǎn)兄弟是黑色,侄子至少一個紅色(一個紅色或者兩個都是紅色)
/**
* 兄弟是左孩子,左侄子是紅色:LL :父親右旋+變色
* 兄弟是左孩子,右侄子是紅色:LR
* 兄弟是右孩子,右侄子是紅色:RR
* 兄弟是右孩子,左侄子是紅色:RL
*/
// LL: 兄弟是左孩子,左侄子是紅色
if (brother.isLeftChild() && isRed(brother.left)) {
// 父親右旋
rightRotate(parent);
/**
* 變色邏輯:
* 1.通過旋轉(zhuǎn)過來的節(jié)點(diǎn)變成黑色補(bǔ)齊該路徑上少的黑色
* 2.針對上位節(jié)點(diǎn),變成原本父親的顏色,因?yàn)檫@條路徑上本身是平衡的,所以上來的要變成原本父親的顏色
* 3.針對紅色的侄子,變成黑色,因?yàn)樵驹撀窂缴希粍h除節(jié)點(diǎn)的兄弟被當(dāng)成了父親,所以原本作為這個路徑的黑色兄弟就走了,少了一個黑色,所以讓紅色侄子變黑。
*/
// 代碼變色從后往前,因?yàn)閺那巴髸?dǎo)致還變色的顏色丟失了
// 侄子變黑
brother.left.color = Color.BLACK;
//上位節(jié)點(diǎn)(兄弟節(jié)點(diǎn))變成原來父親的顏色
brother.color = parent.color;
// 原本父親節(jié)點(diǎn)旋轉(zhuǎn)下去了 變成黑色
parent.color = Color.BLACK;
}
// LR:兄弟是左孩子,右侄子是紅色
else if (brother.isLeftChild() && isRed(brother.right)) {
/**
* 這里的代碼可以做簡化,旋轉(zhuǎn)之前和之后分別做一次變色即可
*/
// 侄子上位,變成父親的顏色(這里要先變色,因?yàn)橹蹲邮且氯サ模驼也坏接液⒆恿耍? brother.right.color = parent.color;
// 兄弟左旋
leftRotate(brother);
// 父親右旋
rightRotate(parent);
// 父親最后退位,顏色變黑,補(bǔ)齊刪除的黑色
parent.color = Color.BLACK;
}
// RR
else if (!brother.isLeftChild() && !isRed(brother.left)) {
// 父親左旋
leftRotate(parent);
// 侄子變黑
brother.right.color = Color.BLACK;
//上位節(jié)點(diǎn)(兄弟節(jié)點(diǎn))變成原來父親的顏色
brother.color = parent.color;
// 原本父親節(jié)點(diǎn)旋轉(zhuǎn)下去了 變成黑色
parent.color = Color.BLACK;
}
// RL
else {
// 侄子上位,變成父親的顏色(這里要先變色,因?yàn)橹蹲邮且氯サ模驼也坏接液⒆恿耍? brother.left.color = parent.color;
// 兄弟右旋
rightRotate(brother);
// 父親左旋
leftRotate(parent);
// 父親最后退位,顏色變黑,補(bǔ)齊刪除的黑色
parent.color = Color.BLACK;
}
}
}
四、總結(jié)
復(fù)雜度分析
操作 | 普通二叉搜索樹 | AVL樹 | 紅黑樹 |
---|---|---|---|
查詢 | 平均O(logn),最壞O(n) | O(logn) | O(logn) |
插入/更新 | 平均O(logn),最壞O(n) | O(logn) | O(logn) |
刪除 | 平均O(logn),最壞O(n) | O(logn) | O(logn) |
二叉搜索樹
二叉搜索樹的插入、刪除、查詢的時間復(fù)雜度與樹的高度相關(guān),因此在最壞情況下,時間復(fù)雜度為O(n),而且容易退化成鏈表,查找效率低,不平衡。
AVL樹
AVL樹是一種嚴(yán)格平衡的二叉搜索樹,其左右子樹的高度差不超過1。因此,它能夠在logn的平均時間內(nèi)完成插入、刪除、查詢操作,但是在維護(hù)平衡的過程中,需要頻繁地進(jìn)行旋轉(zhuǎn)操作,導(dǎo)致插入刪除效率較低。
紅黑樹
紅黑樹是一種近似平衡的二叉搜索樹,它在保持高度平衡的同時,又能夠保持較高的插入刪除效率。紅黑樹通過節(jié)點(diǎn)著色和旋轉(zhuǎn)操作來維護(hù)平衡。紅黑樹在維護(hù)平衡的過程中,能夠進(jìn)行較少的節(jié)點(diǎn)旋轉(zhuǎn)操作,因此插入刪除效率較高,并且查詢效率也較高。
綜上所述,紅黑樹具有較高的綜合性能,是一種廣泛應(yīng)用的數(shù)據(jù)結(jié)構(gòu)。
補(bǔ)充:這篇文章對旋轉(zhuǎn)部分沒有描述的很詳細(xì),在我上一篇AVL樹的實(shí)現(xiàn)里有很詳細(xì)的介紹旋轉(zhuǎn)的邏輯,可以參考。