红黑树原理及实现
1 二叉搜索树
二叉搜索树BST能够很好地实现数据的快速插入、删除与检索。对于BST的一个节点,其中存在一个键值数据key,设某个节点的key=K,则其左子树中任一节点的key都小于k,右子树中任一节点的key都大于等于K。
2 红黑树
2.1 红黑树性质
- 每个结点是红的或者黑的
- 根结点是黑的
- 每个叶子结点是黑的
- 如果一个结点是红的,则它的两个儿子都是黑的(父子不能同时为红色)
- 对每个结点,从该结点到其子孙结点的所有路径上的包含相同数目的黑结点
红黑树的这五个性质直接决定了其树结构具有平衡性,特别是性质5,个人认为是其能够平衡的本质。而后面会涉及到的左旋、右旋、变色等方式则是用于巧妙地维持红黑树的这种性质,从而间接达到平衡的目的!
2.2 红黑树为什么常用
- 以key-value的形式进行数据查找,速度快
- 采用中序遍历,复杂度O(logn)
2.3 应用举例
- Linux进程调度 CFS
- Nginx Timer事件管理
- Epoll事件块的管理
- 内存管理
2.4 如何实现红黑树
2.4.1 基于二叉搜索树BST
问题: BST有可能出现趋向于退化为链表,导致性能下降。因此在BST的基础上引入红黑树解决这个问题。
2.4.2 定义红黑树的结构
红黑树结构中除了左右子节点外,还需要父节点和颜色属性。
typedef enum
{
RBT_RED = 0,
RBT_BLACK = 1,
} RBT_COLOR;
typedef struct
{
RBT_NODE* parent;
RBT_NODE* left;
RBT_NODE* right;
RBT_COLOR color; // RBT_COLOR
int key;
} RBT_NODE;
typedef struct
{
RBT_NODE* root;
RBT_NODE* nil;
} RBT_TREE;
// --------------------------//
typedef struct
{
RBT_NODE node;
// node data
void* value;
} NODE;
typedef struct
{
RBT_TREE root;
// Tree attribute
} TREE;
定义了一个nil节点,所有的叶子节点都指向nil节点(哨兵),并且nil为黑色,从而使性质3得到满足;同时使根节点的父节点也指向nil。
2.4.3 左旋和右旋
红黑树通过左旋、右旋来维持其平衡的性质,避免像BST那样出现性能退化。当某一边的子树出现不平衡时,通过节点的旋转来恢复平衡。
(图片为转载)
- 左旋: 将父节点x向左边旋转,变成y的子节点,y变成父节点;即每左旋一次,x会下沉一次,y得到提升;
- 右旋: 将父节点y向右边旋转,变成x的子节点,x变成父节点;即没右旋一次,y会下沉一次,x得到提升;
左旋、右旋的实质是操作爷、父、子、孙节点之间共6个指针实现节点位置的改变。写完左旋函数后,将该函数复制一遍,将其中的所有left跟right对换,将所有y跟x对换,就实现右旋了。
static int _leftRotate(RBT_TREE* t, RBT_NODE* x)
{
if(t->nil == x->right)
return -1;
RBT_NODE* p = x->parent;
RBT_NODE* y = x->right;
RBT_NODE* z = y->left;
if(p == t->nil)
t->root = y;
else if(x == p->left)
p->left = y;
else
p->right = y;
y->parent = p;
y->left = x;
x->parent = y;
x->right = z;
if(z != t->nil)
z->parent = x;
return 0;
}
static int _rightRotate(RBT_TREE* t, RBT_NODE* y)
{
if(t->nil == y->left)
return -1;
RBT_NODE* p = y->parent;
RBT_NODE* x = y->left;
RBT_NODE* z = x->right;
if(p == t->nil)
t->root = x;
if(y == p->right)
p->right = x;
else
p->left = x;
x->parent = p;
x->right = y;
y->parent = x;
y->left = z;
if(z != t->nil)
z->parent = y;
return 0;
}
此外,旋转是二叉树本身就可以实现的性质,与红黑树的颜色暂时还没有关系。
2.4.4 如何插入一个节点
- 插入节点的基本过程跟BST非常相似,都是先遍历到待插入位置的叶子节点,插入节点后再进行平衡性调整。
- 当插入新一个节点时,首选将其颜色设为红色更好,因为不会改变 “黑高”,使之满足性质5。
- 何时调整节点颜色:只有在性质4无法满足时,也就是说插入位置的父节点已经是红色。设插入节点为X,此时需要分几种情况来进行调整。
-
情况1:父节点P是黑色
此时,直接插入红色节点X即可,不影响红黑树的性质。
-
情况2:父节点P与叔节点U都是红色
这种情况下,光把P改为黑色是不行的,因为子树G将无法满足性质5,可以将P和U都改为黑色,并且把G改为红色。这时就变成了情况1,已满足性质。
-
情况3:叔节点U是黑色(与叔节点为nil是一样的),且父节点P为祖父节点G的左孩子,X插入P的左节点
注意,图中讨论的是插入节点后向上调整平衡性的情形(如情况2调整后的G节点),此时X下面存在子树。这时我们的调整思路是用P把U挤下去,但又不是直接进行替换,而是通过P与G换色后再将G右旋的方式。按照这个规律调整后的树依然能够满足红黑树性质,并且从结果直观地来看确实变平衡了。这么巧妙的方法鬼知道前人是怎么想到的…
-
情况4:叔节点U是黑色,且父节点P为祖父节点G的左孩子,X插入P的右节点
这时候将P左旋,就可以将树变成跟情况3一模一样的形态,然后按情况3的方法处理即可。
-
情况5:叔节点U是黑色,且父节点P为祖父节点G的右孩子,X插入P的右节点
这就与情况3是镜像关系了,按情况3的镜像过程处理即可。
-
情况6:叔节点U是黑色,且父节点P为祖父节点G的右孩子,X插入P的左节点
这就与情况4是镜像关系了。这时候将P右旋,就可以将树变成跟情况5一模一样的形态,然后按情况5的方法处理即可。
-
总得来说,由于情况5、6与情况3、4之间镜像关系,可以将其合并。因此一般认为可以将红黑树的插入分成前4种情况。
对于情况2,由于G节点被改为红色,因此还需要继续向上处理确保G的变化也不影响树结构满足红黑树性质,因此这个过程实际上就是由子节点向上遍历的过程。
代码实现:
/* 插入后对节点进行调整 */
/* 插入后对节点进行调整 */
static int _adjustAfterInsert(RBT_TREE* t, RBT_NODE* node)
{
while(node->parent->color == RBT_RED)
{
if(node->parent == node->parent->parent->left) // 父节点是祖父节点的左子树
{
RBT_NODE* uncle = node->parent->parent->right;
if(uncle->color == RBT_RED) // 叔节点是红色,情况2
{
uncle->color = node->parent->color = RBT_BLACK;
node->parent->parent->color = RBT_RED;
node = node->parent->parent;
}
else // 叔节点是黑色
{
if(node == node->parent->right) // 情况4
{
node = node->parent;
_leftRotate(t, node);
}
// 情况3
node->parent->color = RBT_BLACK;
node->parent->parent->color = RBT_RED;
_rightRotate(t, node->parent->parent);
}
}
else
{
RBT_NODE* uncle = node->parent->parent->left;
if(uncle->color == RBT_RED) // 叔节点是红色
{
uncle->color = node->parent->color = RBT_BLACK;
node->parent->parent->color = RBT_RED;
node = node->parent->parent;
}
else // 叔节点是黑色
{
if(node == node->parent->left)
{
node = node->parent;
_rightRotate(t, node);
}
node->parent->color = RBT_BLACK;
node->parent->parent->color = RBT_RED;
_leftRotate(t, node->parent->parent);
}
}
}
t->root->color = RBT_BLACK;
}
RBT_NODE* RBT_insertNode(RBT_TREE* t, RBT_NODE* target)
{
RBT_NODE* node = t->root;
RBT_NODE* prev = node;
// 遍历找到插入点
while (node != t->nil)
{
prev = node;
if(target->key < node->key)
node = node->left;
else if(target->key > node->key)
node = node ->right;
else
{
// 已存在的key暂时按丢弃处理
printf("# ERROR: repeat key, abanded.\n");
return node;
}
}
// 插入新节点
target->parent = prev;
if(prev == t->nil)
t->root = target; // 传入的是空树,返回根节点
if(target->key < prev->key)
prev->left = target;
else
prev->right = target;
// 插入节点参数初始化
target->left = t->nil;
target->right = t->nil;
target->color = RBT_RED; // 默认插入红色
// adjust - 红黑树性质恢复
_adjustAfterInsert(t, target);
return target;
}
2.4.5 如何删除一个节点
1)删除节点的基础
对于红黑树,删除节点可分为两步:1)按照BST的删除策略找到对应节点并将其删除;2)该节点被删除后,显然红黑树的性质有可能不被满足,因此需要进行平衡性调整。
首先对于BST节点的删除,此处不做赘述,但需要先特别说明的是:如果待删除的节点同时存在左、右子节点,则其删除过程可以转换为该节点后继节点的删除。
最终待删节点X可以有三种情况:
对于红黑树,最终待删节点与nil之间要么没有节点(如3),要么只剩下一个节点且X一定是黑色,子节点一定是红色(如1和2)。可以试着分析一下(如下图设待删节点为X举了2个例子),你将会发现不符合这个事实的情况无法满足红黑树的性质。这是由红黑树的性质直接就决定了,也是红黑树能够平衡的本质原因。
对于(1)、(2)这两种情况,可以将子节点修改为黑色后直接替换待删节点即可,子树的黑高没有改变,相当于删除红色节点,最终只有情况(1)需要单独处理。
在此基础上进行平衡性调整的策略比插入节点还要复杂,倒不是说特别抽象,而是在于需要划分的情况更多了。
2)删除节点的简单情况(无需调整)
- 情况1:最终待删节点X为红色或者为根节点,可以直接删除,不会对红黑树性质有任何影响;
这是显而易见的,此处不再赘述。
3)删除节点的复杂情况(需要调整)
对于删除节点为黑色的情况,可以想象我们所需要做的是对于该节点所在子树进行变换以弥补丢失的黑高,如果在当前子树中黑高就能够恢复,则不需要对树的其他部分进行调整。
我们先基于这个原则来处理,并且先考虑被删的节点是其父节点右孩子的情况,左孩子的情况按相反方式处理即可。
-
情况2:删除节点X为黑色,其父节点P是红色,兄弟节点B一定存在且一定为黑色,侄子节点LN和RN必然要么为红色,要么为nil。侄子节点的分布存在4种可能情况,图中X所在位置就是该节点被删除前所在位置。
-
情况(2.1):X位置被删除后由nil替代,P子树右侧的黑高减少了1。通过将父节点P涂黑,兄弟节点B涂红,即可修复黑高。
-
情况(2.2):此时,将P和LN涂黑,将B涂红,再将P右旋,可恢复黑高。
-
情况(2.3):此时将B设为红色,将RN设为黑色,对B左旋后再对P右旋就变成了情况(2.2)。
-
情况(2.4):P和LN涂黑,B涂红,对P右旋即可。
-
-
情况(3):删除节点X为黑色,其父节点P是黑色,兄弟节点B一定存在,B为红色时,则侄子节点都为黑色。
X位置被删除之前,该P子树的黑高为2,为恢复其黑高,可以将B设为黑色,RN设为红色,然后对P右旋。
-
情况4:删除节点X为黑色,其父节点P是黑色,兄弟节点B一定存在,B为黑色,则侄子节点LN和RN必然要么为红色,要么为nil。侄子节点的分布也存在4种可能情况。
-
情况(4.1)和情况(4.2)将LN设为黑色,就进入了与情况(3)非常相似的过程!再对P右旋就实现了黑高恢复为2。
-
情况(4.3):与情况(2.3)的处理一模一样,将B设为红色,将RN设为黑色,对B右旋就变成了情况(4.2)。
-
情况(4.4)比较特殊,因为像这种子树全为黑节点的情况下,少了一个黑节点后,P子树无法靠自身来恢复原来的黑高。此时必须向上遍历,直到能够获取到红色节点并将其转换成黑色节点来修复黑高。如果一直到达了根节点都没有找到红色节点,则调整过程将整棵树的黑高都减少1,从而满足性质5。
-
为了方便描述,我们将某个子树用黑色三角形简略表示,三角形内部的数字代表子树的黑高。
4)总结一下删除时需要调整的情况
根据条件和步骤对情况进行合并,最终得到简化后的情况分类如下表。
就上表可将红黑树删除时需要调整的最终情况分类总结如下:
前提:删除节点X是其父节点P的右孩子(左孩子的情况为镜像,左右互换即可)
- 兄弟节点B为红色,对应1
- 兄弟节点B为黑色,且左右侄子都为黑色(或nil),对应2
- 兄弟节点B为黑色,且左侄子为黑色(或nil),右侄子为红色,对应3
- 兄弟节点B为黑色,且左侄子为红色,对应4
根据这张表就可以写出删除后节点调整的代码。
5)代码实现
static void _adjustAfterDelete(RBT_TREE* t, RBT_NODE* succesor)
{
RBT_NODE* bro;
RBT_NODE* parent;
while(succesor->color == RBT_BLACK && t->root != succesor)
{
parent = succesor->parent;
if(succesor == parent->right)
{
bro = parent->left;
if(bro->color == RBT_RED) // 情况1
{
bro->color = RBT_BLACK;
bro->right->color = RBT_RED;
_rightRotate(t, parent);
break; // OVER
}
else
{
if(bro->left->color == RBT_BLACK && bro->right->color == RBT_BLACK) // 情况2
{
bro->color = RBT_RED;
if(parent->color == RBT_RED) // 遇到父节点为红
{
parent->color = RBT_BLACK;
break; // OVER
}
// need to continue, don't break;
succesor = parent;
}
else if(bro->left->color == RBT_BLACK) // 情况3
{
bro->color = parent->color;
bro->right->color = RBT_BLACK;
_leftRotate(t, bro);
_rightRotate(t, parent);
break; // OVER
}
else // 情况4
{
bro->color = parent->color;
parent->color = RBT_BLACK;
bro->left->color = RBT_BLACK;
_rightRotate(t, parent);
break; // OVER
}
}
}
else
{
bro = parent->right;
if(bro->color == RBT_RED) // 情况1
{
bro->color = RBT_BLACK;
bro->left->color = RBT_RED;
_leftRotate(t, parent);
break; // OVER
}
else
{
if(bro->right->color == RBT_BLACK && bro->left->color == RBT_BLACK) // 情况2
{
bro->color = RBT_RED;
if(parent->color == RBT_RED) // 遇到父节点为红
{
parent->color = RBT_BLACK;
break; // OVER
}
// need to continue, don't break;
succesor = parent;
}
else if(bro->right->color == RBT_BLACK) // 情况3
{
bro->color = parent->color;
bro->left->color = RBT_BLACK;
_rightRotate(t, bro);
_leftRotate(t, parent);
break; // OVER
}
else // 情况4
{
bro->color = parent->color;
parent->color = RBT_BLACK;
bro->right->color = RBT_BLACK;
_leftRotate(t, parent);
break; // OVER
}
}
}
}
}
RBT_NODE* RBT_deleteNode(RBT_TREE* t, RBT_NODE* target)
{
RBT_NODE* succesor = target;
RBT_NODE* parent;
if(target == t->nil)
return t->nil;
/* 左右子树不为nil,则找到后继节点并与当前节点替换 */
if(succesor->left != t->nil || succesor->right != t->nil)
{
succesor = _getSuccessor(t, succesor); // 寻找后继节点
_swapNode(t, target, succesor); // 交换将两个节点
}
if(target->left != t->nil)
succesor = target->left; // 待删节点为情况(1)
else if(target->right != t->nil)
succesor = target->right; // 待删节点为情况(2)
else
succesor = t->nil; // 待删节点为情况(3)
parent = target->parent;
succesor->parent = parent;
if(parent == t->nil)
t->root = succesor; // 删除根节点的情况
else if(target == parent->left)
parent->left = succesor;
else
parent->right = succesor;
// 至此target已从树中脱离,其位置被succesor替代
succesor->color = RBT_BLACK; // 对于情况(1)和(2),删除后,替补的后继节点一定要涂成黑色
/* 只有待删节点为情况(3)且删除的节点颜色为黑时需要调整 */
if(succesor == t->nil && target->color == RBT_BLACK)
_adjustAfterDelete(t, succesor);
return target;
}
5)遍历红黑树并在控制台比较直观地打印出来
static void _traversal(RBT_TREE* t, RBT_NODE* node, int lvl)
{
int i;
if(node == t->nil)
{
for(i = 0; i < lvl; i++)
printf("\t");
printf("nil\n");
return ;
}
_traversal(t, node->left, lvl + 1);
for(i = 0; i < lvl; i++)
printf("\t");
printf("%02d:%c\n", node->key, (node->color == RBT_BLACK)?'B':'R');
_traversal(t, node->right, lvl + 1);
}
void RBT_traversal(RBT_TREE* t)
{
if(t == NULL)
return;
_traversal(t, t->root, 0);
}
2.5 红黑树的应用案例
案例一、服务器端高并发IO的keep alilve方案,满足以下几个需求
- 每个IO都是自己的时间戳
- 每个IO收到自己的beat后,重置自己的定时器
- 若IO定时没有收到beat,则执行IO的回调函数,并重置定时器
- 若再次没有收到beat,销毁IO,注销定时器。
(未完待续)