閱讀本文你需具有知識點:html
libuv將紅黑樹的算法用在了signal
上,咱們先回顧一下signal是怎麼使用的:node
uv_signal_t signal_handle;
r = uv_signal_init(loop, &signal_handle);
CHECK(r, "uv_signal_init");
r = uv_signal_start(&signal_handle, signal_cb, SIGINT);
void signal_cb(uv_signal_t *handle, int signum) {
printf("signal_cb: recvd CTRL+C shutting down\n");
uv_stop(uv_default_loop()); //stops the event loop
}
複製代碼
當開發者每調用一次uv_signal_start
的時候,都會往生成的紅黑樹中插入一個節點,當調用uv_signal_stop
的時候,則會刪除一個節點,以下:git
static int uv__signal_start(uv_signal_t* handle,
uv_signal_cb signal_cb,
int signum,
int oneshot) {
... ...
RB_INSERT(uv__signal_tree_s, &uv__signal_tree, handle);
... ...
}
static void uv__signal_stop(uv_signal_t* handle) {
... ...
removed_handle = RB_REMOVE(uv__signal_tree_s, &uv__signal_tree, handle);
... ...
}
複製代碼
libuv使用宏的形式(源碼在tree.h),定義了一整套紅黑樹的完整實現,由於宏代碼看起來比較費勁,我隨意截了個圖,你們能夠瞄一眼:github
自己紅黑樹就是很難搞懂的概念,加上C語言,因此太不友好了。因而我根據C語言的實現,改寫成Js版本的,下面的講解都是基於js語言的,因此你們勿慌~算法
紅黑樹的基礎是二叉搜索樹,那麼二叉搜索樹很差嗎?非要再整這麼一個複雜的出來?二叉搜索樹本來已是個很好的數據結構,能夠快速地找到一個給定關鍵字的數據項,而且能夠快速地插入和刪除數據項。可是二叉搜索樹有個很麻煩的問題,若是樹中插入的是隨機數據,則執行效果很好,但若是插入的是有序或者逆序的數據,那麼二叉搜索樹的執行速度就變得很慢。由於當插入數值有序時,二叉樹就是非平衡的了,排在一條線上,其實就變成了一個鏈表……它的快速查找、插入和刪除指定數據項的能力就喪失了。好比下圖的bash
爲了能以較快的時間 O(logN) 來搜索一棵樹,須要保證樹老是平衡的(或者至少大部分是平衡的),這就是說對樹中的每一個節點在它左邊的後代數目和在它右邊的後代數目應該大體相等。紅-黑樹的就是這樣的一棵平衡樹,對一個要插入的數據項,插入過程當中要檢查會不會破壞樹的特徵,若是破壞了,程序就會進行糾正,根據須要改變樹的結構,從而保持樹的平衡。數據結構
Tips: 有的童鞋可能會提出問題:AVL和紅黑樹之間,爲啥紅黑樹用的場景更多?其實這是一個性能平衡的問題,當然AVL的節點查找比紅黑樹好,可是由於其嚴苛的條件,致使在每次插入和刪除的時候須要作的運算比紅黑樹多。因此犧牲一點查找時間,保證每次插入和刪除節點的時候性能也不會太差。函數
而後根據以上特徵有以下推論:oop
由此咱們獲得節點的數據結構:性能
function Node(data) {
this.leftNode = null
this.rightNode = null
this.parentNode = null
this.data = data
this.rbColor = RED // 初始化爲紅色的緣由下面有說到
}
複製代碼
紅黑樹的修正都是基於旋轉和變色的,由於咱們在搞懂插入一個節點以後會怎麼修正以前,咱們須要讀懂左旋和右旋。
下圖是一個左旋的動畫:
從圖中能夠看出,所謂左旋就是將要旋轉的節點往左邊挪動,也就是逆時針走向,完成的操做有如下三件事:
根據上面的介紹,用Js實現的代碼以下(代碼中的註釋以函數的頂部註釋爲準):
/*************對紅黑樹節點x進行左旋操做 ******************/
/*
* 左旋示意圖:對節點x進行左旋
* p p
* / /
* x y
* / \ / \
* lx y -----> x ry
* / \ / \
* ly ry lx ly
* 左旋作了三件事:
* 1. 將y的左子節點賦給x的右子節點,並將x賦給y左子節點的父節點(y左子節點非空時)
* 2. 將x的父節點p(非空時)賦給y的父節點,同時更新p的子節點爲y(左或右)
* 3. 將y的左子節點設爲x,將x的父節點設爲y
********************************************************/
rotateLeft(beRotatedNode) {
// x節點的右節點y
const rightNode = beRotatedNode.rightNode
// x節點的右節點變動掉,將y的左節點賦值過來
beRotatedNode.rightNode = rightNode.leftNode
// 若是y節點的左節點有值,那麼x將是y節點的左節點的父節點
if (rightNode.leftNode) {
rightNode.leftNode.parentNode = beRotatedNode
}
// 將x的父節點賦值給y節點的父節點
rightNode.parentNode = beRotatedNode.parentNode
// 若是x的父節點存在的話
if (beRotatedNode.parentNode) {
// 若是x節點以前是其父節點的左節點
if (beRotatedNode === beRotatedNode.parentNode.leftNode) {
beRotatedNode.parentNode.leftNode = rightNode
} else {
beRotatedNode.parentNode.rightNode = rightNode
}
} else {
// 若是x的父節點爲空,那麼說明是根節點
this.root = rightNode
}
// 3. 將y的左節點設爲x,將x的父節點設爲y
rightNode.leftNode = beRotatedNode
beRotatedNode.parentNode = rightNode
}
複製代碼
右旋的操做與左旋相反,仍是有動畫:
具體的就再也不贅述,實現的代碼以下:
/*************對紅黑樹節點y進行右旋操做 ******************/
/*
* 右旋示意圖:對節點y進行右旋
* p p
* / /
* y x
* / \ / \
* x ry -----> lx y
* / \ / \
* lx rx rx ry
* 右旋作了三件事:
* 1. 將x的右子節點賦給y的左子節點,並將y賦給x右子節點的父節點(x右子節點非空時)
* 2. 將y的父節點p(非空時)賦給x的父節點,同時更新p的子節點爲x(左或右)
* 3. 將x的右子節點設爲y,將y的父節點設爲x
*/
rotateRight(beRotatedNode) {
// y節點的左節點x
const leftNode = beRotatedNode.leftNode
// y節點的左節點變動掉,將x的右節點賦值過來
beRotatedNode.leftNode = leftNode.rightNode
// 若是x節點的右節點有值,那麼y將是x節點的右節點的父節點
if (leftNode.rightNode) {
leftNode.rightNode.parentNode = beRotatedNode
}
// 將y的父節點賦值給x節點的父節點
leftNode.parentNode = beRotatedNode.parentNode
// 若是y的父節點存在的話
if (beRotatedNode.parentNode) {
// 若是y節點以前是其父節點的左節點
if (beRotatedNode === beRotatedNode.parentNode.leftNode) {
beRotatedNode.parentNode.leftNode = leftNode
} else {
beRotatedNode.parentNode.rightNode = leftNode
}
} else {
// 若是x的父節點爲空,那麼說明是根節點
this.root = leftNode
}
// 3. 將x的左節點設爲y,將y的父節點設爲x
leftNode.rightNode = beRotatedNode
beRotatedNode.parentNode = leftNode
}
複製代碼
接着咱們進入正題,瞭解插入以前,咱們先約定好一些術語,這樣結合代碼閱讀的時候不至於懵。以下圖:
全部節點的一些稱呼都標註清楚了,後面的插入和刪除的操做都是基於這些稱呼的。
首先以二叉搜索樹的方式插入結點,並將其着爲紅色。若是着爲黑色,則會違背性質5,不便調整;若是着爲紅色,可能會違背性質2或性質4,能夠經過相對簡單的操做,使其恢復紅黑樹的性質。
將一個節點插入到紅黑樹中,須要執行哪些步驟呢?首先,實例化一個新節點,而後將紅黑樹看成一顆二叉查找樹,查找適合的位置將節點插入;最後,經過"旋轉和從新着色"等一系列操做來修正該樹,使之從新成爲一顆紅黑樹。
詳細描述以下:
實例化新節點,代碼以下:const node = new Node(data)
將紅黑樹看成一顆二叉查找樹,查找適合節點插入的位置。這一步查找的邏輯其實很簡單,總結以下:
實現代碼以下:
__insert(node) {
let root = this.root
let parent = null
let compareRes
// 2. 查找插入節點適合存放的位置,並與其父節點創建聯繫
while(root !== null) {
parent = root
compareRes = node.data - root.data
// 若是插入的節點比當前父節點大,那麼繼續尋找該父節點的右節點
if (compareRes > 0) {
root = root.rightNode
} else if (compareRes < 0) {
root = root.leftNode
} else {
return root
}
}
// 找到插入節點的父節點了,此時parent表示的就是插入節點的父節點
node.parentNode = parent
// 接着判斷是插入到該父節點的左邊仍是右邊
if (parent) {
if (compareRes < 0) {
parent.leftNode = node
} else {
parent.rightNode = node
}
} else {
this.root = node
}
// 3. 修整整個紅黑樹
this.__insert_color(this.root, node)
return null
}
複製代碼
經過一系列的旋轉或着色等操做,使之從新成爲一顆紅黑樹。
若是是第一次插入,因爲原樹爲空,因此只會違反紅黑樹的特徵2,因此只要把根節點塗黑便可;若是插入節點的父節點是黑色的,那不會違背紅黑樹的特徵,什麼也不須要作;
可是遇到以下三種狀況時(如下統一稱爲場景3),咱們就要開始變色和旋轉了:
插入節點的父節點和其叔叔節點均爲紅色的;
插入節點的父節點是紅色,叔叔節點是黑色,且插入節點是其父節點的右子節點;
插入節點的父節點是紅色,叔叔節點是黑色,且插入節點是其父節點的左子節點。
該場景下有這麼一個推論,須要記住:若是插入的父結點爲紅結點,那麼該父結點不可能爲根結點,因此插入結點老是存在祖父結點。
從紅黑樹性質4能夠,祖父結點確定爲黑結點,由於不能夠同時存在兩個相連的紅結點。那麼此時該插入子樹的紅黑層數的狀況是:黑紅紅。顯然最簡單的處理方式是把其改成:紅黑紅。
所以處理方式是:
將父節點和叔叔節點設置爲黑色
將祖父節點設置爲紅色
把祖父設置爲當前插入結點,繼續調整紅黑樹
複製代碼
單純從插入前來看,叔叔結點非紅即爲葉子結點(Nil)。由於若是叔叔結點爲黑結點,而父結點爲紅結點,那麼叔叔結點所在的子樹的黑色結點就比父結點所在子樹的多了,這不知足紅黑樹的性質5。後續情景一樣如此,再也不多作說明了。
前文說了,須要旋轉操做時,確定一邊子樹的結點多了或少了,須要租或借給另外一邊。插入顯然是多的狀況,那麼把多的結點租給另外一邊子樹就能夠了。
左邊兩個紅結點,右邊不存在,那麼一邊一個剛恰好,而且由於爲紅色,確定不會破壞樹的平衡。所以處理方式是:
將父節點設爲黑色
將祖父節點設爲紅色
對祖父節點進行右旋
複製代碼
這種情景顯然能夠經過旋轉將問題歸一化到3.2.1小節的狀況,所以處理方式以下:
對父節點進行左旋
把父節點設置爲插入結點,歸一化到3.2.1小節的狀況後繼續處理
複製代碼
該情景對應情景3.2,只是方向反轉,不作過多說明了,直接給出結論。
處理方式:
將父節點設爲黑色
將祖父節點設爲紅色
對祖父節點進行左旋
複製代碼
處理方式:
對父節點進行右旋
把父節點設置爲插入結點,歸一化到3.3.1小節的狀況後繼續處理
複製代碼
綜上所述,實現的代碼以下,代碼有註釋,一一對應上面的全部狀況:
__insert_color(root, node) {
let parent = node.parentNode
let gparent, uncle
// 第一種狀況和第二種狀況都不會進入這個while語句,因此都是很簡單的
// 只有第三種狀況纔會進入,也就是父節點存在而且父節點是紅色
while(parent !== null && parent.rbColor === RED) {
gparent = parent.parentNode
// 若是父節點是祖父節點的左節點
if (parent === gparent.leftNode) {
// 取叔叔節點,也就是父節點的兄弟節點
uncle = gparent.rightNode
// case3.1狀況:若是叔叔節點存在而且顏色是紅色
if (uncle && uncle.rbColor === RED) {
// 一、將叔叔節點、父節點、祖父節點分別置爲黑黑紅
uncle.rbColor = BLACK
parent.rbColor = BLACK
gparent.rbColor = RED
// 二、將祖父節點置爲當前節點
node = gparent
// 祖父節點變動後,父節點必然也得變動
parent = node.parentNode
continue
}
// case3.2.二、若是叔叔節點不存在或者叔叔節點是黑色,而且插入的節點是在父節點的右邊
// 由於該狀況最後會歸一化到case3.2.1的狀況,因此須要先執行
if (parent.rightNode === node) {
// 一、以父節點爲支點,進行左旋
this.rotateLeft(parent)
// 旋轉以後須要從新設置
uncle = parent
parent = node
node = uncle
}
// 上面一種狀況最後要歸一化到最後的這種狀況case3.2.1:叔叔節點不存在或者叔叔節點是黑色的,而且插入
// 的節點是在父節點的左邊
// 一、將父節點和祖父節點分別設置爲黑紅
parent.rbColor = BLACK
gparent.rbColor = RED
// 二、對祖父節點進行右旋
this.rotateRight(gparent)
// 更新父節點
parent = node.parentNode
} else {
// 父節點是祖父節點的右節點,狀況和上面徹底相反
// 取叔叔節點,也就是父節點的兄弟節點
uncle = gparent.leftNode
// case3.1狀況:若是叔叔節點存在而且顏色是紅色
if (uncle && uncle.rbColor === RED) {
// 一、將叔叔節點、父節點、祖父節點分別置爲黑黑紅
uncle.rbColor = BLACK
parent.rbColor = BLACK
gparent.rbColor = RED
// 二、將祖父節點置爲當前節點
node = gparent
// 祖父節點變動後,父節點必然也得變動
parent = node.parentNode
continue
}
// case3.3.二、若是叔叔節點不存在或者叔叔節點是黑色,而且插入的節點是在父節點的右邊
// 由於該狀況最後會歸一化到case3.3.1的狀況,因此須要先執行
if (parent.leftNode === node) {
// 一、以父節點爲支點,進行右旋
this.rotateRight(parent)
// 旋轉以後須要從新設置
uncle = parent
parent = node
node = uncle
}
// 上面一種狀況最後要歸一化到最後的這種狀況case3.3.1:叔叔節點不存在或者叔叔節點是黑色的,而且插入
// 的節點是在父節點的右邊
// 一、將父節點和祖父節點分別設置爲黑紅
parent.rbColor = BLACK
gparent.rbColor = RED
// 二、對祖父節點進行左旋
this.rotateLeft(gparent)
// 更新父節點
parent = node.parentNode
}
}
this.root.rbColor = BLACK
}
複製代碼
將紅黑樹內的某一個節點刪除。須要執行的操做依次是:首先,將紅黑樹看成一顆二叉查找樹,將該節點從二叉查找樹中刪除;而後,經過"旋轉和從新着色"等一系列來修正該樹,使之從新成爲一棵紅黑樹。詳細描述以下:
第一步:將紅黑樹看成一顆二叉查找樹,將節點刪除。 這和"刪除常規二叉查找樹中刪除節點的方法是同樣的"。分3種狀況:
① 被刪除節點沒有兒子,即爲葉節點。那麼,直接將該節點刪除就OK了。
② 被刪除節點只有一個兒子。那麼,直接刪除該節點,並用該節點的惟一子節點頂替它的位置。
③ 被刪除節點有兩個兒子。那麼,先找出它的後繼節點(也就是大於被刪除結點的最小結點);在這裏,後繼節點至關於替身,在將後繼節點的內容複製給"被刪除節點"以後,再將後繼節點刪除。這樣就巧妙的將問題轉換爲"刪除後繼節點"的狀況了,下面就考慮後繼節點。 在"被刪除節點"有兩個非空子節點的狀況下,它的後繼節點不多是雙子非空。既然"的後繼節點"不可能雙子都非空,就意味着"該節點的後繼節點"要麼沒有兒子,要麼只有一個兒子。若沒有兒子,則按"狀況① "進行處理;若只有一個兒子,則按"狀況② "進行處理。
這麼一說,因而咱們有了以下推論:刪除操做刪除的結點能夠看做刪除替代結點,而替代結點最後老是在樹末
示意圖以下:
第二步:經過"旋轉和從新着色"等一系列來修正該樹,使之從新成爲一棵紅黑樹。
由於"第一步"中刪除節點以後,可能會違背紅黑樹的特性。因此須要經過"旋轉和從新着色"來修正該樹,使之從新成爲一棵紅黑樹。
根據以上羅列的狀況,處理三種狀況。實現代碼以下:
__remove(node) {
let replaceNode = node
// 若是被刪除的節點左右節點都不爲空
if (node.leftNode && node.rightNode) {
// 先找出被刪除節點的後繼節點(大於刪除結點的最小結點)來當作替換節點
let replaceNode = node.rightNode
while(replaceNode.leftNode) {
replaceNode = replaceNode.leftNode
}
node.data = replaceNode.data
}
const parent = replaceNode.parentNode
const color = replaceNode.rbColor
// 這裏有着足夠的隱含信息,若是被刪除節點不是左右節點都有,那麼這裏的child是null或者左右節點之一,
// 若是被刪除節點左右節點皆有,那麼這裏的是替換節點,而替換節點的左節點確定是不存在的,只能是右節點或者null
const child = replaceNode.rightNode || replaceNode.leftNode
if (child) {
child.parentNode = parent
}
if (parent) {
if (parent.leftNode === replaceNode) {
parent.leftNode = child
} else {
parent.rightNode = child
}
} else {
this.root = child
}
if (color === BLACK) {
this.__remove_color(child, parent)
}
replaceNode = null
}
複製代碼
若是恰好刪除的節點是黑色,那麼開始咱們的紅黑樹重建
紅黑樹的重建,這個時候只有替換的節點是黑色的時候,才須要重建,所以忽略場景1(替換結點是紅色結點),直接說場景2:
處理方式:
將兄弟設爲黑色 將父節點設爲紅色 對父節點進行左旋,歸一化到場景2.1.2.3
該場景又根據兄弟節點的子節點的不一樣,分爲不一樣的場景
將兄弟節點的顏色設爲父節點的顏色 將父節點設爲黑色 將兄弟節點的右節點設爲黑色 對父節點進行左旋
將兄弟節點設爲紅色 將兄弟節點的左節點設爲黑色 對兄弟節點進行右旋,歸一化到場景2.1.2.1進行處理
將兄弟節點設爲紅色 把父節點做爲新的替換結點 從新進行刪除結點情景處理
將兄弟節點設爲黑色 將父節點設爲紅色 對父節點進行右旋,歸一化到場景2.2.2.3
將兄弟節點的顏色設爲父節點的顏色 將父節點設爲黑色 將兄弟節點的左節點設爲黑色 對父節點進行右旋
將兄弟設爲紅色 將兄弟節點的右節點設爲黑色 對兄弟節點進行左旋,歸一化到場景2.2.2.1
將兄弟節點設爲紅色 把父節點做爲新的替換結點 從新進行刪除結點情景處理
完整實現代碼以下:
// node表示待修正的節點,即後繼節點的子節點(由於後繼節點被刪除了)
__remove_color(node, parent) {
let brother
while((node === null || node.rbColor === BLACK) && node !== this.root) {
// 場景2.一、替換結點是其父結點的左子結點
if (node == parent.leftNode) {
brother = parent.rightNode // 取兄弟節點
// 場景2.1.一、替換結點的兄弟結點是紅結點
if (brother.rbColor === RED) {
brother.rbColor = BLACK
parent.rbColor = RED
this.rotateLeft(parent)
// 歸一化到場景2.1.2.3
brother = parent.rightNode
}
// 場景2.1.二、替換結點的兄弟結點是黑結點
// 場景2.1.2.三、替換結點的兄弟結點的子結點都爲黑結點
if ((brother.leftNode === null || brother.leftNode.rbColor === BLACK) && (brother.rightNode === null || brother.rightNode.rbColor === BLACK)) {
brother.rbColor = RED
node = parent
parent = node.parentNode
} else {
// 場景2.1.2.二、替換結點的兄弟結點的右子結點爲黑結點,左子結點爲紅結點
if (brother.rightNode === null || brother.rightNode.rbColor === BLACK) {
if (brother.leftNode) {
brother.leftNode.rbColor = BLACK
}
brother.rbColor = RED
// 右旋後歸一化到場景2.1.2.1
this.rotateRight(brother)
brother = parent.rightNode
}
// 場景2.1.2.一、替換結點的兄弟結點的右子結點是紅結點,左子結點任意顏色
brother.rbColor = parent.rbColor
parent.rbColor = BLACK
if (brother.rightNode) {
brother.rightNode.rbColor = BLACK
}
this.rotateLeft(parent)
node = this.root
break
}
} else {
// 場景2.二、替換結點是其父結點的右子結點
brother = parent.leftNode
// 場景2.2.一、替換結點的兄弟結點是紅結點
if (brother.rbColor === RED) {
brother.rbColor = BLACK
parent.rbColor = RED
// 右旋後會進入場景2.2.2.3
this.rotateRight(parent)
brother = parent.leftNode
}
// 場景2.2.二、替換結點的兄弟結點是黑結點
// 場景2.2.2.三、替換結點的兄弟結點的子結點都爲黑結點
if ((brother.leftNode === null || brother.leftNode.rbColor === BLACK) && (brother.rightNode === null || brother.rightNode.rbColor === BLACK)) {
brother.rbColor = RED
node = parent
parent = node.parentNode
} else {
// 場景2.2.2.二、替換結點的兄弟結點的左子結點爲黑結點,右子結點爲紅結點
if (brother.leftNode === null || brother.leftNode.rbColor === BLACK) {
if (brother.rightNode) {
brother.rightNode.rbColor = BLACK
}
brother.rbColor = RED
// 左旋後歸一化到場景2.2.2.1
this.rotateLeft(brother)
brother = parent.leftNode
}
// 場景2.2.2.一、替換結點的兄弟結點的左子結點是紅結點,右子結點任意顏色
brother.rbColor = parent.rbColor
parent.rbColor = BLACK
if (brother.leftNode) {
brother.leftNode.rbColor = BLACK
}
this.rotateRight(parent)
node = this.root
break
}
}
}
if (node) {
node.rbColor = BLACK
}
}
複製代碼