阅读本文你需具有知识点: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
}
}
复制代码