學習紅黑樹,用js擼了一個
紅黑樹是一個效率很高且穩定的資料結構,插入和刪除操作的時間複雜度都是logn。
紅黑樹的性質:
- 每一個節點或者著紅色,或者著黑色
- 根是黑色
- 如果一個節點是紅色的,那麼它的子節點必須是黑色
- 從一個節點到一個Null節點(樹葉)的每一條簡單路徑必須包含相同數目的黑色節點
插入操作以後再補~
刪除操作(自頂向下的實現方式)
刪除操作是紅黑樹最難的部分,通常有兩種實現方式:自頂向下和自底向上。《演算法導論》裡使用的是自底向上的實現方式,對我而言相當晦澀,又看了幾篇類似的實現方式,需要羅列出相當多的情形,實現不易。《資料結構與演算法 -- C語言實現》裡使用的是自頂向下的實現方式,但只討論了大致邏輯,並未給出具體實現,最後在這篇文章裡找到了完整的實現。自頂向下的方式確實簡單易懂,且非常巧妙,我也是用這種方式來實現紅黑樹的刪除操作
在此之前,先複習一下前面列出的紅黑樹的性質。刪除操作之所以複雜,是因為如果需要刪除的節點是黑色的,那麼直接刪除它會破壞性質4。因此,我們需要保證刪除該節點之後,能有一種方式修復刪除後被破壞的部分。自底向上實現的思路是:先刪除節點,然後通過旋轉、變色等手段恢復破壞了紅黑樹性質的部分,而自頂向下實現的思路是:在查詢需要刪除的節點的路徑上,保證每個節點都是紅色的(如果不是就要通過變換讓它變成紅色,且不破壞樹的性質),如果它是要刪除的節點,就可以安心地刪除它。就思路而言,顯然自底向上的方式更易理解,好像也更容易實現,但當你去處理修復過程時會發現有相當多的情況需要考慮。而自頂向下的方式看似笨拙,卻能夠通過巧妙的變換簡化變換的過程
總的來說,就是我們要讓當前訪問的節點X變紅,然後再去考慮它是不是需要刪除的節點
- 啟動條件
刪除是一個遞迴函式
,在進入遞迴之前需要確保當前當前結構符合啟動條件。這裡的結構指以X
為中心的部分樹結構,可能包含P
, S
,GP
, XL
,XR
,SL
,SR
。啟動條件如下:
即:X和它的兄弟節點S為黑色,父親節點P為紅色。實現insert
時我們做了一個特殊處理,構造了一個假的根節點,值為負無窮,顏色為黑色,因此所有的真實節點都在它的右子樹上。它的左節點是一個null節點(黑色),右節點是真正的根節點(必然是黑色)。而自頂向下刪除的第一步,就是把根節點塗成紅色,這樣就天然滿足了啟動條件
-
若X有兩個黑色的節點。注意,當X為葉子節點時也適用,因為葉子節點的子節點為Null節點,是黑色
這時還需要考察S的子節點的情況才能決定如何變換,因此
2
還需要分幾種子情形2.1 符合
2
的條件,同時S也有兩個黑色的子節點這種情況是最簡單的,讓X變紅只需要變換P,X,S的顏色即可。變換方法如下圖
2.2 符合
2
的條件,且S有一個紅色的左子節點,一個黑色的右子節點這種情況下,X變紅後,左邊就少了一個黑色的節點,只能從P的右子樹借一個過來。SL這個紅色的節點可以利用起來,讓它移到P現在的位置。顯然這需要一個雙旋轉(SL位於整個旋轉路徑的內側),變換如下:
2.3 符合
2
的條件,且S有一個黑色的左子節點,一個紅色的右子節點與
2.2
類似,我們要利用SR這個紅色節點,因為SR在整個旋轉路徑的外側,所以使用一個單旋轉在
2
的這三種情況完成變換之後,X已經是紅色且保證了紅黑樹的性質,接下來就可以判斷X是否為需要刪除的節點了。這一步我們也標記一下,叫它D-1好了D-1
如果X不是要刪除的節點,那麼下降一層,即讓
P = X
,X = XL或者XR
,S = X的另一個子節點
,然後進入下一個刪除迴圈(即1
)。因為X是紅色,它的子節點必然為黑色,所以符合啟動條件如果X正式需要刪除的節點,需要分兩種情況
- X恰好是葉子節點,這種情況直接刪除X即可,不會對紅黑樹的性質有任何影響
- X為非葉子節點,如果直接刪除X,它的子節點應該如何與它的父節點對接是個很複雜的操作,所以我們採用二叉查詢樹的節點刪除方法:找到該節點的後繼或前驅(如果後繼不存在,再使用它的前驅節點),用它的值代替X的值,然後再去刪除它的後繼或前驅,反覆這個過程直到我們需要刪除的節點是葉子節點
ok,
2
這類大的情況就處理好了,下面需要考慮X有至少一個紅色節點的情況 -
X至少有一個紅色節點。這種情況需要改變一下思路,因為X有至少一個紅色節點,如果X不是要刪除的節點,就沒必要再把X變紅了,如果直接下降一層,X有很大概率直接落到紅色的節點上,這能節省很多時間
所以對於
3
這種情況,先判斷X是否為要刪除的節點,這就分成兩種情況3.1 X不是要刪除的節點,那麼下降一層。這時X可能落到紅色節點上,也可能落到黑色節點上,兩種情況都需要考慮
3.1.1 X是紅色節點,那麼X已經符合D-1的刪除條件,可以直接進入D-1
3.1.2 X是黑色節點,這時需要作一次節點變換。為了更直觀,這裡分成兩個步驟,第一步下降一層,並讓X落到黑色節點上,第二步才是變換
此時X還是黑色,並不滿足進入D-1的條件,但是仔細看X節點的上下結構,P、SR、X構成的子樹正好滿足
1
的啟動條件。所以下一步是進入1
的刪除迴圈自此,3.1的所有情況已經處理好了,下面我們來看X是需要刪除的節點這種情況
3.2 X是需要刪除的節點。因為X有紅色的子節點,所以它不可能是葉子節點,也就是說即使把它變紅也不能直接刪除。在這種情況下,我們在D-1的基礎上稍作修改:找到X的後繼或前驅,用它的值代替X的值,之後下降一層,再一次進入到
3
的邏輯
這就是自頂向下刪除需要考慮的所有情形了。我畫了一個流程圖,梳理了刪除的邏輯
按照這個流程圖來寫程式碼,會非常清晰~
原始碼
/**
* RedBlackTreeNode.js
* 紅黑樹的節點實現,比普通的二叉樹節點多了color屬性
*/
class RedBlackTreeNode {
constructor(data, color, lchild, rchild) {
// validate color
if (!Color[color]) {
throw new Error(`color can only be RED or BLACK`);
}
this.color = color;
this.data = data;
this.lchild = lchild;
this.rchild = rchild;
}
}
const Color = {
"RED": "RED",
"BLACK": "BLACK"
};
module.exports = {
RedBlackTreeNode,
Color,
};
複製程式碼
/**
* @file 紅黑樹實現
* @author YDSS
*
* Created on Sun May 27 2018
*/
const { RedBlackTreeNode, Color } = require("./RedBlackTreeNode");
class RedBlackTree {
constructor(arr) {
this._initialize();
this.create(arr);
}
_initialize() {
// init NullNode
this.NullNode = new RedBlackTreeNode(
Number.NEGATIVE_INFINITY,
Color.BLACK,
null,
null
);
this.NullNode.lchild = this.NullNode;
this.NullNode.rchild = this.NullNode;
// extra attr for recognizing the NullNode
this.NullNode.type = "null";
// init header
this.header = new RedBlackTreeNode(
Number.NEGATIVE_INFINITY,
Color.BLACK,
this.NullNode,
this.NullNode
);
// init nodes to store parent, grandparent and grandgrandparent
this.X = null;
this.P = null;
this.GP = null;
this.GGP = null;
// X's sister
this.S = null;
}
create(arr) {
arr.forEach(item => {
this.header = this.insert(item);
});
}
find(val) {
return this._find(val, this.header);
}
_find(val, T) {
if (!T) {
return null;
}
if (val === T.data) {
return T;
}
if (val > T.data) {
return this._find(val, T.rchild);
}
if (val < T.data) {
return this._find(val, T.lchild);
}
}
insert(data) {
this.X = this.P = this.GP = this.GGP = this.header;
this.NullNode.data = data;
while (data !== this.X.data) {
this.GGP = this.GP;
this.GP = this.P;
this.P = this.X;
if (data < this.X.data) {
this.X = this.X.lchild;
} else {
this.X = this.X.rchild;
}
if (
this.X.lchild.color === Color.RED &&
this.X.rchild.color === Color.RED
)
this._handleReorient(data);
}
// duplicate
if (this.X !== this.NullNode) {
return this.NullNode;
}
this.X = new RedBlackTreeNode(
data,
Color.RED,
this.NullNode,
this.NullNode
);
if (data < this.P.data) {
this.P.lchild = this.X;
} else {
this.P.rchild = this.X;
}
this._handleReorient(data);
return this.header;
}
_handleReorient(data) {
this.X.color = Color.RED;
this.X.lchild.color = Color.BLACK;
this.X.rchild.color = Color.BLACK;
if (this.P.color === Color.RED) {
this.GP.color = Color.RED;
if (data < this.GP.data !== data < this.P.data)
this.P = this._rotate(data, this.GP);
this.X = this._rotate(data, this.GGP);
this.X.color = Color.BLACK;
}
this.header.rchild.color = Color.BLACK;
}
/**
* single rotate
*
* @param {*} data
* @param {RedBlackTreeNode} Parent Parent Node of the subtree will rotate
*/
_rotate(data, Parent) {
if (data < Parent.data) {
return (Parent.lchild =
data < Parent.lchild.data
? this._singleRotateWithLeft(Parent.lchild)
: this._singleRotateWithRight(Parent.lchild));
} else {
return (Parent.rchild =
data > Parent.rchild.data
? this._singleRotateWithRight(Parent.rchild)
: this._singleRotateWithLeft(Parent.rchild));
}
}
_singleRotateWithLeft(T) {
let root = T.lchild;
T.lchild = root.rchild;
root.rchild = T;
return root;
}
_singleRotateWithRight(T) {
let root = T.rchild;
T.rchild = root.lchild;
root.lchild = T;
return root;
}
/**
* find precursor node of this node
* if this node doesn't have the left subtree, return null
*
* @param {*} data data of current node
* @return {BinaryTreeNode|Null}
*/
findPrecursor(node) {
// let node = this.find(data);
// if (!node) {
// throw new Error(`node with data(${data}) is not in the tree`);
// }
if (!node.lchild) {
return null;
}
let pre = node.lchild;
let tmp;
while (!this._isNilNode(tmp = pre.lchild)) {
pre = tmp;
}
return pre;
}
/**
* find successor node of this node
* if this node doesn't have the right subtree, return null
*
* @param {BinaryTreeNode} current node
* @return {BinaryTreeNode|Null}
*/
findSuccessor(node) {
// let node = this.find(data);
// if (!node) {
// throw new Error(`node with data(${data}) is not in the tree`);
// }
if (!node.rchild) {
return null;
}
let suc = node.rchild;
let tmp;
while (!this._isNilNode(tmp = suc.lchild)) {
suc = tmp;
}
return suc;
}
/**
* delete node by means of top to down
*
* @param {*} val
*/
delete(val) {
// prepare for deleting
this.header.color = Color.RED;
this.GP = null;
this.P = this.header;
this.X = this.header.rchild;
this.S = this.header.lchild;
this._delete(val);
}
_delete(val) {
if (
this.X.lchild.color === Color.BLACK &&
this.X.rchild.color === Color.BLACK
) {
// S has two black children
if (
this.S.lchild.color === Color.BLACK &&
this.S.rchild.color === Color.BLACK
) {
this._handleRotateSisterWithTwoBlackChildren();
// judge if X.data is what we are looking for
this._handleDeleteXWhenXhasTwoBlackChildren(val);
}
// S has at last one red children
else {
// single rotate when S with it's red child in a line,
// reference to avl rotate
// 2.3
if (
this.S.data > this.P.data ===
(this.S.rchild.color === Color.RED)
) {
this._rotate(this.S.data, this.GP);
// change color
this.P.color = Color.BLACK;
this.X.color = Color.RED;
this.S.color = Color.RED;
this.S.lchild.color = Color.BLACK;
this.S.rchild.color = Color.BLACK;
// judge if X.data is what we are looking for
this._handleDeleteXWhenXhasTwoBlackChildren(val);
// double rotate when S with it's red child in a z-shape line
// 2.2
} else {
let firstData =
this.S.data < this.P.data
? this.S.rchild.data
: this.S.lchild.data;
this._rotate(firstData, this.P);
this._rotate(this.S.data, this.GP);
// change color
this.P.color = Color.BLACK;
this.X.color = Color.RED;
// judge if X.data is what we are looking for
this._handleDeleteXWhenXhasTwoBlackChildren(val);
}
}
} else {
this._handleDeleteXWhenXhasAtLastOneRedChild(val);
}
}
// 2.1
_handleRotateSisterWithTwoBlackChildren() {
this.P.color = Color.BLACK;
this.X.color = Color.RED;
this.S.color = Color.RED;
}
_handleDeleteXWhenXhasTwoBlackChildren(val) {
if (this.X.data === val) {
if (this._hasChild(this.X)) {
val = this._replaceWithSuccessorOrPrecursor(val);
this._descend(val);
this._delete(val);
} else {
// delete X when it's a leaf
this._deleteLeafNode(val, this.P);
}
} else {
this._descend(val);
this._delete(val);
}
}
_handleDeleteXWhenXhasAtLastOneRedChild(val) {
if (this.X.data === val) {
val = this._replaceWithSuccessorOrPrecursor(val);
this._descend(val);
} else {
this._descend(val);
}
// X is red, enter the phase of judging X's data
if (this.X.color === Color.RED) {
this._handleDeleteXWhenXhasTwoBlackChildren(val);
} else {
this._handleRotateWhenXIsBlackAndSisterIsRed();
this._delete(val);
}
}
// 3.1.2
_handleRotateWhenXIsBlackAndSisterIsRed() {
let curGP = this._rotate(this.S.data, this.GP);
// change color
this.S.color = Color.BLACK;
this.P.color = Color.RED;
// fix pointer of S and GP
this.S = this.X.data > this.P.data ? this.P.lchild : this.P.rchild;
this.GP = curGP;
}
_deleteLeafNode(val, parent) {
if (parent.rchild.data === val) {
parent.rchild = this.NullNode;
} else {
parent.lchild = this.NullNode;
}
}
_hasChild(node) {
return !this._isNilNode(node.lchild) || !this._isNilNode(node.rchild);
}
_isNilNode(node) {
return node === this.NullNode;
}
/**
* replace X with it's successor,
* if it has no successor, instead of it's precursor
* @param {*} val the delete data
*
* @return {*} updated delete data
*/
_replaceWithSuccessorOrPrecursor(val) {
let child = this.findSuccessor(this.X);
if (!child) {
child = this.findPrecursor(this.X);
}
this.X.data = child.data;
return child.data;
}
/**
* descend one floor
*
* @param {*} val the val of node will be deleted
*/
_descend(val) {
this.GP = this.P;
this.P = this.X;
if (val < this.X.data) {
this.S = this.X.rchild;
this.X = this.X.lchild;
} else if (val > this.X.data) {
this.S = this.X.lchild;
this.X = this.X.rchild;
}
// val === this.X.data when X's successor or precursor
// is it's child, in this situation it's wrong to choise
// where X to go down by comparing val, cause X.data is equal
// with new delete value
else {
if (val === this.X.lchild) {
this.S = this.X.rchild;
this.X = this.X.lchild;
}
else {
this.S = this.X.lchild;
this.X = this.X.rchild;
}
}
}
}
module.exports = RedBlackTree;
複製程式碼