目录
4.3.2 parent为红,uncle为黑且插入结点是右孩子,父节点是左孩子
4.3.3 叔叔为黑节点,父亲节点为爷爷节点右孩子,新插入节点为父节点左孩子
4.3.4 叔叔为黑节点,父亲节点为爷爷节点左孩子,新插入节点为父节点左孩子
4.3.5 父亲节点为爷爷节点右孩子,新插入节点为父节点右孩子
case1:node为黑,兄弟节点(sibling )为红色
case2: node为黑,sibling 为黑色,且 sibling 的左右子树均为黑色
Case 3:node为黑,sibling 为黑色,且 sibling 的左子树为红色,右子树为黑色
case 4:node为黑,sibling 为黑色,且 sibling 的左子树为任意色,右子树为红色
case1:node为黑,兄弟节点(sibling )为红色
Case 2:node为黑,sibling 为黑色,且 sibling 的左右子树均为黑色
case 3:node为黑,sibling 为黑色,且 sibling 的左子树为黑色,右子树为红色
case4:node为黑,sibling为黑,sibling的左子树为红,右子树为任意色
一、为什么需要红黑树这种数据结构?
我们知道ALV树是一种严格按照定义来实现的平衡二叉查找树,所以它查找的效率非常稳定,为O(log n),由于其严格按照左右子树高度差不大于1的规则,插入和删除操作中需要大量且复杂的操作来保持ALV树的平衡(左旋和右旋),因此ALV树适用于大量查询,少量插入和删除的场景中。
那么假设现在假设有这样一种场景:大量查询,大量插入和删除,现在使用ALV树就不太合适了,因为ALV树大量的插入和删除会非常耗时间,那么我们是否可以降低ALV树对平衡性的要求从而达到快速的插入和删除呢?
答案肯定是有的,红黑树这种数据结构就应运而生了(因为ALV树是高度平衡的,所以查找起来肯定比红黑树快,但是红黑树在插入和删除方面的性能就远远不是ALV树所能比的了)
二、红黑树特性简介
红黑树是一种特殊的二叉查找树,每个结点都要储存位表示结点的颜色,或红或黑。
红黑树的特性:
1)每个节点不是红色就是黑色。
2)根节点一定为黑色。
3)如果节点为红色,其子节点一定为黑色。
1.红黑树的任何一条路径上不存在连续的两个红色节点。
4)叶子节点一定为黑色(此处的叶子节点指无值节点 NIL);
5)从根节点到叶节点的每条路径,必须包含相同数目的黑色节点。
1.红黑树的最长路径长度最多为最短路径长度的两倍,最长路径黑红节点交替,最短路径节点全黑。
示意图:(红黑树首先是一颗搜索树,所以左子树点小于根结点,右子树大于根节点,等于根结点的按照自己的需要放左边和右边都是可以的)(红黑树不仅是一颗搜索树还是一颗非严格的平衡树)。
三、红黑树的基本操作旋转
红黑树的基本操作是添加和删除,在对红黑树进行添加和删除之后,都会用到旋转方法,为什么呢?道理很简单,因为添加或者删除红黑树中的结点之后,红黑树就发生了变化,可能不满足上面的5条性质了,这个时候就需要通过旋转操作来保证它依旧是一棵红黑树,旋转分为左旋和右旋
(旋转操作仅仅只是用来调节结点的位置的,就是为了满足红黑树的性质5)
3.1 左旋
左旋是将X的右子树绕X逆时针旋转,使得X的右子树成为X的父亲,同时修改相关结点的引用,旋转之后,要求二叉查找树的属性依然满足
实如下:
/// <summary>
/// 左旋
/// </summary>
/// <param name="node"></param>
/// <exception cref="NotImplementedException"></exception>
private void LeftRotate(RBTreeNode<T> node)
{
var parent = FindParentNode(node); //旋转节点的父节点
var rightNode = node.right; //节点的右孩子
var rightLeftNode = rightNode.left;//节点右孩子节点的左孩子
SetParent(node, rightNode);//node.parent=rightNode
SetParent(rightNode, parent);
SetParent(rightLeftNode, node);
if (parent == null)//若node为根节点
{
_root = rightNode;
}
else
{
if (parent.right == node) //若节点为父节点的右孩子,则
parent.right = rightNode;
else
parent.left = rightNode;
}
node.right = rightLeftNode;
rightNode.left = node;
}
3.2 右旋
右旋是将X的左子树绕X顺时针旋转,使得X的左子树成为X的父亲,同时注意修改相关结点的引用,旋转之后要求仍然满足搜索树的属性。如下图
实现:
/// <summary>
/// 右旋
/// </summary>
/// <param name="node"></param>
private void RightRotate(RBTreeNode<T> node)
{
var parent = FindParentNode(node); //旋转节点的父节点
var leftNode = node?.left;//旋转节点的左孩子
var rightLeftNode = leftNode.right;//旋转节点左孩子的右节点
SetParent(leftNode, parent);
SetParent(node, leftNode);
SetParent(rightLeftNode, node);
if (parent == null)//若node为根节点
{
_root = leftNode;
}
else
{
if (node == parent.right)//若节点为父节点的右孩子,则
parent.right = leftNode;
else
parent.left = leftNode;
}
node.left = rightLeftNode;
leftNode.right = node;
}
四、红黑树插入
添加操作过程:首先将红黑树当作一颗查找树一样将结点插入,然后将结点着为红色,最后通过旋转和重新着色的方法使之重新成为红黑树。
为什么新节点要设置成红色呢?
1)node 默认为黑色的话,那么 node 插入后一定会增加某条路径上的黑色节点数量,因此每一次的插入都需要进行修正
2)node 默认为红色的话,只有当 parent 为红色时,才会违反红黑树的规则,才需要进行修正。
新增一个红色的节点违背红黑树的哪些特性
1)每个节点不是红色就是黑色。
2)根节点一定为黑色。
3)如果节点为红色,其子节点一定为黑色。
1.红黑树的任何一条路径上不存在连续的两个红色节点。
4)叶子节点一定为黑色(此处的叶子节点指无值节点 NIL);
5)从根节点到叶节点的每条路径,必须包含相同数目的黑色节点。
1).显然没有违背
2).根据查找树的特定,插入操作不好改变根结点,所以也没有违背
3).有可能违背。(新插入的节点的父节点可能也是红色或插入为根节点)
4).插入的肯定不是空叶子结点,所以也没有违背
5).插入结点涂成红色就是为了不违背第5条性质
现在我们来分析一下新增的节点(红色),插入可能面临的情况
4.1 parent为黑插入的节点为根节点
将新插入的红色结点变成黑色结点,满足根结点为黑色结点的要求!
4.2 parent为黑
这个时候不需要进行任何调整操作,此时的树仍然是一颗标准的红黑树。
4.3 插入需要修正情况(parent节点为红)
设祖父节点为 grand(G),父节点为 parent(P),叔叔节点为 uncle(U),插入的节点为 node(N)。
注:如果 uncle 为黑色,那么 uncle 一定为 NIL 节点,因为 parent 为红色,所以有两个黑色子树,而 node 既然能插入到 parent 下方,说明 parent 最多存在一个有值子树,而只有一个子树的话,很明显两条子树的路径是不平衡的,所以 parent 的两个黑色子树一定均为 NIL 节点。因此,uncle 如果为黑色,那么一定为 NIL 节点。
修正的条件为父节点为红
//3.1 父亲节点为红色
while ((parentNode == FindParentNode(curNode)) && IsRed(parentNode))
{
//.............
}
4.3.1 parent为红,uncle为红
将 uncle 和 parent 置黑,grand 置红,然后把 grand 当作插入节点,再进行一次修正。
这样相当于将 grand 的黑色下沉到了 parent 和 uncle,所以各路径的黑色节点数量保持不变,不会影响树的平衡。
但是由于 grand 置红了,可能导致 grand 与 grand’s parent 产生冲突,所以要把 grand 当作插入节点,再进行一次修正(重复3.3 操作,看满足哪种条件,执行对应的操作)
if (IsRed(uncle))
{
SetBlack(parent);
SetBlack(uncle);
SetRed(gParent);
node = gParent;
continue;
}
当 grand 刚好为 root 时,root 为红色是违反规则的,所以在全部修正结束后,需要将 root 置黑,以保证不违反红黑树的规则。
//将根节点置黑
SetBlack(_root);
示例: 示例1为从插入节点到根节点,都符合父亲为红,叔叔为红的情况。
示例2在向上遍历过程中,出现父节点为红,叔叔为黑的情况。可能也会出现其他情况,只需要继续向上遍历,找到对应的逻辑处理即可。
4.3.2 parent为红,uncle为黑且插入结点是右孩子,父节点是左孩子
将 parent 左旋,然后把 node 当作 parent,parent 当作 node。(图中的交换是交换引用的对象,不是交换值)
这样的目的是转成4.3.4(父节点为爷爷节点的左孩子,插入节点为父节点的左孩子)
// 4.3.4 父亲节点为爷爷节点左孩子,新插入节点为父节点右孩子
if ((parentNode == gParentNode.left && curNode == parentNode.right))
{
LeftRotate(parentNode);
var tempNode = parentNode;
parentNode = curNode;
curNode = tempNode;
//curNode = parentNode;
}
示例如下:
4.3.3 叔叔为黑节点,父亲节点为爷爷节点右孩子,新插入节点为父节点左孩子
将 parent 右旋,然后把 node 当作 parent,parent 当作 node。(图中的交换是交换引用的对象,不是交换值)
这样的目的是转成4.3.5(父节点为爷爷节点的右孩子,插入节点为父节点的右孩子)
// 4.3.3 父亲节点为爷爷节点右孩子,新插入节点为父节点左孩子
if ((parentNode == gParentNode.right && curNode == parentNode.left))// 4.3.3 父亲节点为爷爷节点右孩子,新插入节点为父节点左孩子
{
RightRotate(parentNode);
var tempNode = parentNode;
parentNode = curNode;
curNode = tempNode;
//curNode = parentNode;
}
示例:
4.3.4 叔叔为黑节点,父亲节点为爷爷节点左孩子,新插入节点为父节点左孩子
将 parent 置黑,grand 置红,然后将 grand 右旋。
grand 右旋后,parent 就到了原先 grand 的位置,为了黑色节点的数量保持不变,便将 parent 置黑,grand 置红,这样左分支和右分支的黑色节点数量不变,不会影响树的平衡。
//4.3.4 父亲节点为爷爷节点左孩子,新插入节点为父节点左孩子
if (parentNode == gParentNode.left && curNode == parentNode.left)
{
var tempColor = gParentNode.color;
SetColor(gParentNode, parentNode.color);
SetColor(parentNode, tempColor);
RightRotate(gParentNode);
}
示例:
4.3.5 父亲节点为爷爷节点右孩子,新插入节点为父节点右孩子
将 parent 置黑,grand 置红,然后将 grand 左旋。
grand 左旋后,parent 就到了原先 grand 的位置,为了黑色节点的数量保持不变,便将 parent 置黑,grand 置红,这样左分支和右分支的黑色节点数量不变,不会影响树的平衡。
//4.3.5 父亲节点为爷爷节点右孩子,新插入节点为父节点右孩子
if (parentNode == gParentNode.right && curNode == parentNode.right)
{
var tempColor = gParentNode.color;
SetColor(gParentNode, parentNode.color);
SetColor(parentNode, tempColor);
LeftRotate(gParentNode);
}
示例:
总结:
插入情况:
4.1.插入的为根节点(节点变黑即可)
4.2.父亲节点为黑色(无需处理)
4.3.父亲节点为红色
4.3.1 叔叔节点为红色
1)父亲、叔叔节点置黑
2)爷爷节点置红
3)爷爷节点作为新插入的节点
4)重复4.3操作
4.3.2 叔叔为黑节点,父亲节点为爷爷节点左孩子,新插入节点为父节点右孩子
1)对父节点做左旋
2)左旋后情况和4.3.4一样,参考4.3.4即可
4.3.3 叔叔为黑节点,父亲节点为爷爷节点右孩子,新插入节点为父节点左孩子
1)对父节点做右旋
2)右旋后情况和4.3.5一样,参考4.3.5即可
4.3.4 叔叔为黑节点,父亲节点为爷爷节点左孩子,新插入节点为父节点左孩子
1)父亲节点和爷爷节点颜色互换
2)针对爷爷节点进行一次右旋即可
4.3.5 叔叔为黑节点,父亲节点为爷爷节点右孩子,新插入节点为父节点右孩子
1)父亲节点和爷爷节点颜色互换
2)针对爷爷节点进行一次左旋即
现在我们来讨论一下,为什么插入的情况只有上面这些:
1.爷爷结点为红色结点的情况下,父亲结点只能为黑色(红黑树的性质3),处理操作:上面情况2
2.爷爷结点为黑色的情况下,父亲结点和叔叔结点:可以为红色,也可以为黑色
2.1 父亲结点为黑,叔叔结点为黑:处理操作:上面情况4.2
2.2 父亲结点为黑,叔叔结点为红:处理操作:上面情况4.2
2.3 父亲结点为红,叔叔结点为红:处理操作:上面情况4.3.1
(上面3种情况都是不用考虑左右的)
2.4 父亲结点为红,叔叔结点为黑:
2.4.1 父亲结点为左孩子,叔叔结点为左孩子:处理操作:上面情况4.3.4
2.4.2 父亲结点为右孩子,叔叔结点为右孩子:处理操作:上面情况4.3.5
2.4.3 父亲结点为左孩子,插入结点为右孩子:处理操作:上面情况4.3.2
2.4.4 父亲结点为右孩子,插入结点为左孩子:处理操作:上面情况4.3.3
总结:可以发现我们没有遗漏任何情况,所有可能面临的情况我们都处理了
五、红黑树的删除
先说一个删除结点的过程原理:首先将红黑树当作一个二叉查找树,将该结点从二叉查找树种删除,然后通过一些列重新着色操作等一系列措施来修正该树,使之重新成为一颗红黑树
设父节点为 parent(P),兄弟节点为 sibling(S),待删除的节点为 delete(D),替代节点为 node(N)。
关于二叉搜索树的删除操作,根据 delete 的子树情况分为三种情况:
- 没有任何子树,那么 node 就是 NIL 节点,可以直接删除 delete;
- 存在一个子树,那么 node 就是这个子树,删除 delete,用 node 替代 delete;
- 存在两个子树,那么找到 delete 在中序遍历下的后继节点 successor,修改 delete 的值为 successor 的值,然后把 successor 当作 delete 再执行一次删除操作。
由此可知,最终被删除的节点一定不同时存在左右子树。
(注:之后的内容中的 delete 均指最终被删除的节点。)
示例:
接下来在看删除后的修正操作:
- 当 delete 为红色时,删除后不会对红黑树的平衡产生影响。
- 因为红色节点的子树都是黑色节点,如果 delete 只存在一个子树的话,会导致子树的两条路径不平衡,所以此时 delete 的子树一定都是 NIL 节点。
- 当 delete 为黑色时,该节点所在路径的黑色节点数量会减少,因此会导致不平衡,需要进行修正。
//修正
if (IsBlack(delNode))
RemoveFixedUp(childOfdelNode, parentOfdelNode);
在删除黑色 delete 后,根据 node 的颜色进行修正:
- node 为红色时,直接置黑就可以补充被删除的黑色;
- node 为黑色时,则需要分情况讨论。
另外,关于 sibling 的说明:
sibling 一定不为 NIL 节点,因为 delete 为黑色且不为 NIL 节点,所以 delete 及之后的路径至少包含两个黑色节点(包括 delete 自身和 NIL 叶子节点),如果 sibling 为 NIL 节点的话,那么 sibling 及之后的路径只有 sibling 一个黑色节点,这样的话 parent 的左右子树路径就是不平衡的,这在一颗符合规则的红黑树中是不存在的,所以 sibling 一定不为 NIL 节点。
进行修正的条件:
private void RemoveFixedUp(Node node, Node parent) { while (Node.IsBlack(node) && node != Root) { // ... } }
(注:
- 以下是 node 为 parent 的左子树的情况,当 node 为 parent 的右子树时,执行镜像操作即可。
- 下面都是在delete节点被删除后形成新的红黑树结构(需要调整的结构)
5.1 node 为 parent 的左子树
case1:node为黑,兄弟节点(sibling )为红色
因为 sibling 为红色,所以 parent 一定为黑色,所以将 parent 置红,sibling 置黑,再将 parent 左旋。
这样相当于将右路的红色节点转到了左路,不会影响树的平衡。
修正后,黑色的 sibling.left 成了新的 sibling,这样就将 Case 1 转换为了 Case 2 或 Case 3 或 Case 4。
//case1:兄弟节点为红色(注:兄弟节点为红色,则父节点必然为黑色,不然不满足红黑树特性)
if (IsRed(sibling))
{
//将 parent 置红,sibling 置黑,再将 parent 左旋。这样相当于将右路的红色节点转到了左路,不会影响树的平衡。
//修正后,黑色的 sibling.left 成了新的 sibling,这样就将 Case 1 转换为了 Case 2 或 Case 3 或 Case 4。
/*
* 1.操作:将 parent 置红,sibling 置黑,再将 parent 左旋。这样相当于将右路的红色节点转到了左路,不会影响树的平衡。
*
* 2.为什么这样做?
* 修正后,黑色的 sibling.left 成了新的 sibling,这样就将 Case 1 转换为了 Case 2 或 Case 3 或 Case 4。
* */
parent.color = RED;
sibling.color = BLACK;
LeftRotate(parent);
sibling = parent.right;
}
示例:
case2: node为黑,sibling 为黑色,且 sibling 的左右子树均为黑色
将 sibling 置红,然后把 parent 当作 node,再进行一次修正。(粉色表示任意颜色均可)
因为删除节点后,左路少了一个黑,而右路的 sibling 及其左右子树均为黑色,所以可以直接将 sibling 置红,这样右路也少了一个黑,左右路就平衡了。
与此同时,经过 parent 这个节点的所有路径就整体少了一个黑,所以接下来就需要处理 parent 路径和其他路径的平衡,也就是把 parent 当作 node,再进行一次修正。
//case2:兄弟节点(sibling)为黑且兄弟节点(sibling)左右子树为黑
if (IsBlack(sibling.left) && IsBlack(sibling.right))
{
/*
* 1.操作:将 sibling 置红,然后把 parent 当作 node,再进行一次修正。
*
* 2.为什么这样做?
* 因为删除节点后,左路少了一个黑,而右路的 sibling 及其左右子树均为黑色,所以可以直接将 sibling 置红,这样右路也少了一个黑,左右路就平衡了。
* 与此同时,经过 parent 这个节点的所有路径就整体少了一个黑,所以接下来就需要处理 parent 路径和其他路径的平衡,也就是把 parent 当作 node,再进行一次修正。
*
* 注:sibling 被置红,但是 parent 的颜色是不确定的,如果 parent 是红色就会导致连续出现红色节点的情况,而且 node = parent 后进行的新一次修正,
* 是不符合修正条件的,会直接结束,所以在修正循环结束后,需要手动将 node(也就是 parent)置黑。
* */
SetRed(sibling);
node = parent;
parent = node.parent;
continue;
}
sibling 被置红,但是 parent 的颜色是不确定的,如果 parent 是红色就会导致连续出现红色节点的情况,而且 node = parent 后进行的新一次修正,是不符合修正条件的,会直接结束,所以在修正循环结束后,需要手动将 node(也就是 parent)置黑。
//case2 中若是parent红色,则出现sibling和parent都是红色节点,则违反红黑树特性,同时会退出while循环,再次手动置黑
if (node != null)
SetBlack(node);
示例:
Case 3:node为黑,sibling 为黑色,且 sibling 的左子树为红色,右子树为黑色
将 sibling 置红,sibling.left 置黑,然后将 sibling 右旋。
这样相当于把 sibling 左路的红色转到 sibling 的右路,不会影响树的平衡。
修正后,黑色的 sibling.left 就成了新的 sibling,而新的 sibling.right 为红色,这样就将 Case 3 转换为了 Case 4。
//case3:兄弟节点(sibling)为黑且sibling 的左子树为红色,右子树为黑色
if(IsRed(sibling.left)&&IsBlack(sibling.right))
{
/*
* 1.操作:将 sibling 置红,sibling.left 置黑,然后将 sibling 右旋。
*
* 2.为什么这样做?
* 这样相当于把 sibling 左路的红色转到 sibling 的右路,不会影响树的平衡。
* 修正后,黑色的 sibling.left 就成了新的 sibling,而新的 sibling.right 为红色,这样就将 Case 3 转换为了 Case 4。
* */
SetRed(sibling);
SetBlack(sibling.left);
RightRotate(sibling);
sibling=parent.right;
}
示例:
case 4:node为黑,sibling 为黑色,且 sibling 的左子树为任意色,右子树为红色
将 sibling 置为 parent 的颜色,然后将 parent 和 sibling.right 置黑,再将 parent 左旋。
左旋后,sibling 就到了原先 parent 的位置,为了黑色节点数量保持不变,所以将 sibling 置为 parent 的颜色。但 sibling 换位并变色会导致右路少了一个黑色节点,所以将 sibling.right 置黑以保持平衡。最后将 parent 置黑,这样左路因删除节点而缺少的一个黑色节点,由 parent 进行了补充,整棵红黑树就平衡了。
SetColor(sibling,parent.color);
SetBlack(parent);
SetBlack(sibling.right);
LeftRotate(parent);
break;
示例:
5.2 node 为parent的右子树
case1:node为黑,兄弟节点(sibling )为红色
因为 sibling 为红色,所以 parent 一定为黑色,所以将 parent 置红,sibling 置黑,再将 parent 右旋。
这样相当于将左路的红色节点转到了右路,不会影响树的平衡。
修正后,黑色的 sibling.right成了新的 sibling,这样就将 Case 1 转换为了 Case 2 或 Case 3 或 Case 4。
//兄弟节点
var sibling=parent.left;
//case1: 兄弟节点为红色
if (IsRed(sibling))
{
SetBlack(sibling);
SetRed(parent);
RightRotate(parent);
sibling=parent.left;
}
Case 2:node为黑,sibling 为黑色,且 sibling 的左右子树均为黑色
将 sibling 置红,然后把 parent 当作 node,再进行一次修正。(粉色表示任意颜色均可)
因为删除节点后,左路少了一个黑,而右路的 sibling 及其左右子树均为黑色,所以可以直接将 sibling 置红,这样右路也少了一个黑,左右路就平衡了。
与此同时,经过 parent 这个节点的所有路径就整体少了一个黑,所以接下来就需要处理 parent 路径和其他路径的平衡,也就是把 parent 当作 node,再进行一次修正。
//case2:sibling为黑且左右子树为黑
if(IsBlack(sibling.left) && IsBlack(sibling.right))
{
SetRed(sibling);
node=parent;
parent = node.parent;
continue;
}
sibling 被置红,但是 parent 的颜色是不确定的,如果 parent 是红色就会导致连续出现红色节点的情况,而且 node = parent 后进行的新一次修正,是不符合修正条件的,会直接结束,所以在修正循环结束后,需要手动将 node(也就是 parent)置黑。
//case2 中若是parent红色,则出现sibling和parent都是红色节点,则违反红黑树特性,同时会退出while循环,再次手动置黑
if (node != null)
SetBlack(node);
case 3:node为黑,sibling 为黑色,且 sibling 的左子树为黑色,右子树为红色
这样相当于把 sibling 右路的红色转到 sibling 的左路,不会影响树的平衡。
修正后,黑色的 sibling.right就成了新的 sibling,而新的 sibling.left 为红色,这样就将 Case 3 转换为了 Case 4。
//case3: sibling为黑,sibling的左子树为黑,右子树为红
if (IsBlack(sibling.left) && IsRed(sibling.right))
{
SetRed(sibling);
SetBlack(sibling.right);
RightRotate(sibling);
sibling=parent.left;
}
case4:node为黑,sibling为黑,sibling的左子树为红,右子树为任意色
将 sibling 置为 parent 的颜色,然后将 parent 和 sibling.left 置黑,再将 parent 右旋。
右旋后,sibling 就到了原先 parent 的位置,为了黑色节点数量保持不变,所以将 sibling 置为 parent 的颜色。但 sibling 换位并变色会导致左路少了一个黑色节点,所以将 sibling.left 置黑以保持平衡。最后将 parent 置黑,这样右路因删除节点而缺少的一个黑色节点,由 parent 进行了补充,整棵红黑树就平衡了。
//case4:sibling为黑,sibling的左子树为红,右子树为任意色
SetColor(sibling, parent.color);
SetBlack(sibling.left);
SetBlack(parent);
RightRotate(parent);
break ;
六、 红黑树的实现(c#)
6.1 红黑树节点定义
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ConsoleApp3.tree
{
/// <summary>
/// 节点类
/// </summary>
/// <typeparam name="T"></typeparam>
internal class RBTreeNode<T> : IComparable<RBTreeNode<T>> where T : IComparable
{
/// <summary>
/// 节点颜色:黑-true/ 红-false
/// </summary>
public bool color;
/// <summary>
/// 节点的值
/// </summary>
public T nodeKey;
/// <summary>
/// 左孩子
/// </summary>
public RBTreeNode<T> left;
/// <summary>
/// 右孩子
/// </summary>
public RBTreeNode<T> right;
/// <summary>
/// 父节点
/// </summary>
public RBTreeNode<T> parent;
public int CompareTo(RBTreeNode<T>? other)
{
if (other == null)
return 1;
return this.nodeKey.CompareTo(other.nodeKey);
}
}
}
6.2 红黑树实现类(包含插入和删除)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq;
namespace ConsoleApp3.tree
{
/// <summary>
/// 红黑树
/// 特性:
/// 1.红黑树是有颜色的,节点分为黑色和红色
/// 2.根节点是黑色
/// 3.节点为红色,则左右孩子节点为黑色
/// 4.叶子节点(空节点)为黑色
/// 5.任意一个节点到该节点的每个叶子节点的所有拘谨上包含相同的黑节点
/// </summary>
/// <typeparam name="T"></typeparam>
internal class RBTree<T> where T : IComparable
{
/// <summary>
/// 根节点
/// </summary>
private RBTreeNode<T> _root;
private static readonly bool RED = false;
private static readonly bool BLACK = true;
public RBTreeNode<T> Root => _root;
/// <summary>
/// 获取父节点
/// </summary>
/// <param name="node"></param>
/// <returns></returns>
private RBTreeNode<T> FindParentNode(RBTreeNode<T> node) => node?.parent;
private RBTreeNode<T> FindUncleNode(RBTreeNode<T> node)
{
if (node == null || node.parent == null)
return null;
var gParent = FindParentNode(node).parent;
var uncleNode = gParent?.left;
if (node.parent == gParent?.left)
uncleNode = gParent?.right;
return uncleNode;
}
/// <summary>
/// 获取节点的颜色
/// </summary>
/// <param name="node"></param>
/// <returns></returns>
private bool GetColor(RBTreeNode<T> node) => node.color;
/// <summary>
/// 判断节点是否为红色
/// </summary>
/// <param name="node"></param>
/// <returns></returns>
private bool IsRed(RBTreeNode<T> node) => node?.color == RED;
/// <summary>
/// 判断节点是否为黑色
/// </summary>
/// <param name="node"></param>
/// <returns></returns>
private bool IsBlack(RBTreeNode<T> node)
{
if (node == null)//节点为空,则为NIL节点,默认为黑色
return true;
return node.color == BLACK;
}
/// <summary>
/// 设置节点为红色
/// </summary>
/// <param name="node"></param>
private void SetRed(RBTreeNode<T> node)
{
if (node == null)
return;
node.color = RED;
}
/// <summary>
/// 设置节点为黑色
/// </summary>
/// <param name="node"></param>
private void SetBlack(RBTreeNode<T> node)
{
if (node == null)
return;
node.color = BLACK;
}
private void SetColor(RBTreeNode<T> node, bool color)
{
if (node == null)
return;
node.color = color;
}
/// <summary>
/// 设置节点的父节点
/// </summary>
/// <param name="node"></param>
/// <param name="parent"></param>
private void SetParent(RBTreeNode<T> node, RBTreeNode<T> parent)
{
if (node == null)
return;
node.parent = parent;
}
/// <summary>
/// 插入节点
/// </summary>
/// <param name="node"></param>
/// <returns></returns>
public bool Insert(RBTreeNode<T> root, RBTreeNode<T> node)
{
//根节点为空,节点设置为根节点并返回
if (_root == null)
{
_root = node;
_root.color = BLACK;
return true;
}
if (root.nodeKey.CompareTo(node.nodeKey) > 0)//小于根节点,则查找左孩子
{
//左孩子不存在,则插入
if (root.left == null)
{
root.left = node;
node.parent = root;
node.color = RED;
//检查插入是否满足红黑树要求,不满足,则调整
InsertFix(node);
//InsertFixedUp(node);//另一种实现,可用
return true;
}
//继续查找左子树
Insert(root.left, node);
}
else if (root.nodeKey.CompareTo(node.nodeKey) < 0)// 大于根节点,则查找右孩子
{
//左孩子不存在,则插入
if (root.right == null)
{
root.right = node;
node.parent = root;
node.color = RED;
//检查插入是否满足红黑树要求,不满足,则调整
InsertFix(node);
//InsertFixedUp(node); //另一种实现,可用
return true;
}
//继续查找右子树
Insert(root.right, node);
}
else { return false; }
return true;
}
/// <summary>
/// 修正红黑树
/// 插入情况:
/// 4.1.插入的为根节点(节点变黑即可)
/// 4.2.父亲节点为黑色(无需处理)
/// 4.3.父亲节点为红色
/// 4.3.1 叔叔节点为红色
/// 1)父亲、叔叔节点置黑
/// 2)爷爷节点置红
/// 3)爷爷节点作为新插入的节点
/// 4)重复4.3操作
/// 4.3.2 叔叔为黑节点,父亲节点为爷爷节点左孩子,新插入节点为父节点右孩子
/// 1)对父节点做左旋
/// 2)左旋后情况和4.3.4一样,参考4.3.4即可
/// 4.3.3 叔叔为黑节点,父亲节点为爷爷节点右孩子,新插入节点为父节点左孩子
/// 1)对父节点做右旋
/// 2)右旋后情况和4.3.5一样,参考4.3.5即可
/// 4.3.4 叔叔为黑节点,父亲节点为爷爷节点左孩子,新插入节点为父节点左孩子
/// 1)父亲节点和爷爷节点颜色互换
/// 2)针对爷爷节点进行一次右旋即可
/// 4.3.5 叔叔为黑节点,父亲节点为爷爷节点右孩子,新插入节点为父节点右孩子
/// 1)父亲节点和爷爷节点颜色互换
/// 2)针对爷爷节点进行一次左旋即
///
/// </summary>
/// <param name="node"></param>
/// <exception cref="NotImplementedException"></exception>
private void InsertFix(RBTreeNode<T> curNode)
{
//1.插入的为根节点(节点变黑即可)
if (_root == curNode)
{
SetBlack(curNode);
return;
}
//2.父亲节点为黑色(无需处理)]
var parentNode = FindParentNode(curNode);
if (parentNode != null && IsBlack(parentNode))
return;
//3.1 父亲节点为红色
while ((parentNode == FindParentNode(curNode)) && IsRed(parentNode))
{
//爷爷节点(父亲为红色,必存在爷爷节点)
var gParentNode = FindParentNode(parentNode);
var uncleNode = FindUncleNode(curNode);
//4.3.1 叔叔节点为红色
if (uncleNode != null && IsRed(uncleNode))//叔叔节点为红色
{
SetBlack(parentNode);
SetBlack(uncleNode);
SetRed(gParentNode);
curNode = gParentNode;
parentNode = FindParentNode(curNode);
continue;
}
//以下叔叔为黑节点
//4.3.2 叔叔为黑节点,父亲节点为爷爷节点左孩子,新插入节点为父节点右孩子
if ((parentNode == gParentNode.left && curNode == parentNode.right))
{
LeftRotate(parentNode);
var tempNode = parentNode;
parentNode = curNode;
curNode = tempNode;
//curNode = parentNode;
}
// 4.3.3 父亲节点为爷爷节点右孩子,新插入节点为父节点左孩子
if ((parentNode == gParentNode.right && curNode == parentNode.left))// 4.3.3 父亲节点为爷爷节点右孩子,新插入节点为父节点左孩子
{
RightRotate(parentNode);
var tempNode = parentNode;
parentNode = curNode;
curNode = tempNode;
//curNode = parentNode;
}
//4.3.4 父亲节点为爷爷节点左孩子,新插入节点为父节点左孩子
if (parentNode == gParentNode.left && curNode == parentNode.left)
{
var tempColor = gParentNode.color;
SetColor(gParentNode, parentNode.color);
SetColor(parentNode, tempColor);
RightRotate(gParentNode);
}
//4.3.5 父亲节点为爷爷节点右孩子,新插入节点为父节点右孩子
if (parentNode == gParentNode.right && curNode == parentNode.right)
{
var tempColor = gParentNode.color;
SetColor(gParentNode, parentNode.color);
SetColor(parentNode, tempColor);
LeftRotate(gParentNode);
}
}
//设置根节点为黑
SetBlack(_root);
}
/// <summary>
/// 移除key
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
public bool Remove(T key)
{
var node = FindNode(_root, key);
if (node == null)
return false;
Remove(node);
return true;
}
/// <summary>
/// 删除节点
/// 1.节点无孩子
/// 1.1 删除节点为红色
/// 1)直接删除
/// 1.2 删除节点为黑色,兄弟节点无孩子(兄弟节点肯定为黑色,不然不满足低5条性质)
///
/// 2.节点存在一个孩子
/// 3.节点存在两个孩子
///
///
///
/// 1、删除操作中真正被删除的必定是只有一个红色孩子或没有孩子的结点。
/// 2、如果真正的删除点是一个红色结点,那么它必定是一个叶子结点。
///
/// </summary>
/// <param name="node">待删除的节点</param>
public void Remove(RBTreeNode<T> delNode)
{
//若删除节点不存在孩子
if (delNode.left == null && delNode.right == null)
{
//删除节点的孩子,此时为空
var child = delNode.left ?? delNode.right;
var parent = delNode.parent;
//父节点为空,则删除节点为根节点且只有根节点,直接置为空即可
if (delNode.parent == null)
_root = child;
else if (parent.left == delNode)
parent.left = child;
else
parent.right = child;
if (IsBlack(delNode))
RemoveFixedUp(child, parent);
}
else if (delNode.left != null)//被删除节点存在左孩子
{
var childOfdelNode = delNode.left;
var parentOfdelNode = delNode.parent;
childOfdelNode.parent = parentOfdelNode;
//被删除的为根节点
if (parentOfdelNode == null)
_root = childOfdelNode;
else if (parentOfdelNode.left == delNode)//若删除节点为父节点的左子树,则将父节点的左子树指向替代节点(childOfdelNode),反之,则父节点的右子树指向替代节点
parentOfdelNode.left = childOfdelNode;
else
parentOfdelNode.right = childOfdelNode;
//修正
if (IsBlack(delNode))
RemoveFixedUp(childOfdelNode, parentOfdelNode);
}
else if (delNode.right != null)//被删除节点存在右孩子
{
var childOfdelNode = delNode.right;
var parentOfdelNode = delNode.parent;
childOfdelNode.parent = parentOfdelNode;
//被删除的为根节点
if (parentOfdelNode == null)
_root = childOfdelNode;
else if (parentOfdelNode.left == delNode)//若删除节点为父节点的左子树,则将父节点的左子树指向替代节点(childOfdelNode),反之,则父节点的右子树指向替代节点
parentOfdelNode.left = childOfdelNode;
else
parentOfdelNode.right = childOfdelNode;
//修正
if (IsBlack(delNode))
RemoveFixedUp(childOfdelNode, parentOfdelNode);
}
else//存在左右孩子
{
var replaceNode = delNode.right;
while (replaceNode.left != null)
{
replaceNode = replaceNode.left;
}
//将替代节点复制到删除节点
delNode.nodeKey = replaceNode.nodeKey;
//删除替代节点
Remove(replaceNode);
}
}
/// <summary>
/// 查找节点
/// </summary>
/// <param name="Node">根节点</param>
/// <param name="key">key</param>
/// <returns></returns>
private RBTreeNode<T> FindNode(RBTreeNode<T> root, T key)
{
if (root == null)
root = _root;
if (root.nodeKey.CompareTo(key) == 0)
{
return root;
}
else if (root.nodeKey.CompareTo(key) > 0)
{
if (root.right == null)
return null;
FindNode(root.right, key);
}
else
{
if (root.left == null)
return null;
FindNode(root.left, key);
}
return null;
}
/// <summary>
/// 修正红黑树使其满足红黑树性质
/// </summary>
/// <param name="node">被删除节点的替代节点</param>
/// <param name="parent">被删除节点的父节点</param>
private void RemoveFixedUp(RBTreeNode<T> node, RBTreeNode<T> parent)
{
//节点不是根节点且节点为黑色(空节点(NIL)默认也为黑色)
while (node != _root && IsBlack(node))
{
//节点为父节点的左子树
if (node == parent.left)//节点为父节点左子树
{
//兄弟节点
var sibling = parent.right;
//case1:兄弟节点为红色(注:兄弟节点为红色,则父节点必然为黑色,不然不满足红黑树特性)
if (IsRed(sibling))
{
//将 parent 置红,sibling 置黑,再将 parent 左旋。这样相当于将右路的红色节点转到了左路,不会影响树的平衡。
//修正后,黑色的 sibling.left 成了新的 sibling,这样就将 Case 1 转换为了 Case 2 或 Case 3 或 Case 4。
/*
* 1.操作:将 parent 置红,sibling 置黑,再将 parent 左旋。这样相当于将右路的红色节点转到了左路,不会影响树的平衡。
*
* 2.为什么这样做?
* 修正后,黑色的 sibling.left 成了新的 sibling,这样就将 Case 1 转换为了 Case 2 或 Case 3 或 Case 4。
* */
parent.color = RED;
sibling.color = BLACK;
LeftRotate(parent);
sibling = parent.right;
}
//case2:兄弟节点(sibling)为黑且兄弟节点(sibling)左右子树为黑
if (IsBlack(sibling.left) && IsBlack(sibling.right))
{
/*
* 1.操作:将 sibling 置红,然后把 parent 当作 node,再进行一次修正。
*
* 2.为什么这样做?
* 因为删除节点后,左路少了一个黑,而右路的 sibling 及其左右子树均为黑色,所以可以直接将 sibling 置红,这样右路也少了一个黑,左右路就平衡了。
* 与此同时,经过 parent 这个节点的所有路径就整体少了一个黑,所以接下来就需要处理 parent 路径和其他路径的平衡,也就是把 parent 当作 node,再进行一次修正。
*
* 注:sibling 被置红,但是 parent 的颜色是不确定的,如果 parent 是红色就会导致连续出现红色节点的情况,而且 node = parent 后进行的新一次修正,
* 是不符合修正条件的,会直接结束,所以在修正循环结束后,需要手动将 node(也就是 parent)置黑。
* */
SetRed(sibling);
node = parent;
parent = node.parent;
continue;
}
//case3:兄弟节点(sibling)为黑且sibling 的左子树为红色,右子树为黑色
if(IsRed(sibling.left)&&IsBlack(sibling.right))
{
/*
* 1.操作:将 sibling 置红,sibling.left 置黑,然后将 sibling 右旋。
*
* 2.为什么这样做?
* 这样相当于把 sibling 左路的红色转到 sibling 的右路,不会影响树的平衡。
* 修正后,黑色的 sibling.left 就成了新的 sibling,而新的 sibling.right 为红色,这样就将 Case 3 转换为了 Case 4。
* */
SetRed(sibling);
SetBlack(sibling.left);
RightRotate(sibling);
sibling=parent.right;
}
//sibling 为黑色,且 sibling 的左子树为任意色,右子树为红色
/*
* 1.操作:将 sibling 置为 parent 的颜色,然后将 parent 和 sibling.right 置黑,再将 parent 左旋。
*
* 2.为什么这样做?
* 左旋后,sibling 就到了原先 parent 的位置,为了黑色节点数量保持不变,所以将 sibling 置为 parent 的颜色。
* 但 sibling 换位并变色会导致右路少了一个黑色节点,所以将 sibling.right 置黑以保持平衡。最后将 parent 置黑,
* 这样左路因删除节点而缺少的一个黑色节点,由 parent 进行了补充,整棵红黑树就平衡了。
* */
SetColor(sibling,parent.color);
SetBlack(parent);
SetBlack(sibling.right);
LeftRotate(parent);
break;
}
//节点为父节点的右子树
else
{
//节点为父节点的右子树
//兄弟节点
var sibling=parent.left;
//case1: 兄弟节点为红色
if (IsRed(sibling))
{
SetBlack(sibling);
SetRed(parent);
RightRotate(parent);
sibling=parent.left;
}
//case2:sibling为黑且左右子树为黑
if(IsBlack(sibling.left) && IsBlack(sibling.right))
{
SetRed(sibling);
node=parent;
parent = node.parent;
continue;
}
//case3: sibling为黑,sibling的左子树为黑,右子树为红
if (IsBlack(sibling.left) && IsRed(sibling.right))
{
SetRed(sibling);
SetBlack(sibling.right);
RightRotate(sibling);
sibling=parent.left;
}
//case4:sibling为黑,sibling的左子树为红,右子树为任意色
SetColor(sibling, parent.color);
SetBlack(sibling.left);
SetBlack(parent);
RightRotate(parent);
break ;
}
}
//case2 中若是parent红色,则出现sibling和parent都是红色节点,则违反红黑树特性,同时会退出while循环,再次手动置黑
if (node != null)
SetBlack(node);
}
/// <summary>
/// 节点插入,分为左右子树处理(插入的另外一种实现)
/// </summary>
/// <param name="node"></param>
private void InsertFixedUp(RBTreeNode<T> node)
{
//根节点,置黑即可
if (node == _root)
{
SetBlack(_root);
return;
}
//父节点为黑色,不需要处理
if (IsBlack(node.parent))
return;
while(IsRed(node.parent))
{
var parent = node.parent;
var gParent=parent.parent;
//父节点为爷爷节点的左孩子
if (parent == gParent.left)
{
var uncle = gParent.right;
//case1: uncle为红色
if (IsRed(uncle))
{
SetBlack(parent);
SetBlack(uncle);
SetRed(gParent);
node = gParent;
continue;
}
//case2: uncle为黑色且节点为父节点的右孩子
if (node == parent.right)
{
LeftRotate(parent);
var tmp = parent;
parent = node;
node = tmp;
}
//case2: uncle为黑色且node节点为父节点的左孩子
SetBlack(parent);
SetRed(gParent);
RightRotate(gParent);
}
else//右子树
{
var uncle = gParent.left;
//case1: uncle为红色
if (IsRed(uncle))
{
SetBlack(parent);
SetBlack(uncle);
SetRed(gParent);
node = gParent;
continue;
}
//case2: uncle为黑色且节点为父节点的左孩子
if (node == parent.left)
{
RightRotate(parent);
var tmp = parent;
parent = node;
node = tmp;
}
//case2: uncle为黑色且node节点为父节点的右孩子
SetBlack(parent);
SetRed(gParent);
LeftRotate(gParent);
}
}
//将根节点置黑
SetBlack(_root);
}
/// <summary>
/// 左旋
/// </summary>
/// <param name="node"></param>
/// <exception cref="NotImplementedException"></exception>
private void LeftRotate(RBTreeNode<T> node)
{
var parent = FindParentNode(node); //旋转节点的父节点
var rightNode = node.right; //节点的右孩子
var rightLeftNode = rightNode.left;//节点右孩子节点的左孩子
SetParent(node, rightNode);//node.parent=rightNode
SetParent(rightNode, parent);
SetParent(rightLeftNode, node);
if (parent == null)//若node为根节点
{
_root = rightNode;
}
else
{
if (parent.right == node) //若节点为父节点的右孩子,则
parent.right = rightNode;
else
parent.left = rightNode;
}
node.right = rightLeftNode;
rightNode.left = node;
}
/// <summary>
/// 右旋
/// </summary>
/// <param name="node"></param>
private void RightRotate(RBTreeNode<T> node)
{
var parent = FindParentNode(node); //旋转节点的父节点
var leftNode = node?.left;//旋转节点的左孩子
var rightLeftNode = leftNode.right;//旋转节点左孩子的右节点
SetParent(leftNode, parent);
SetParent(node, leftNode);
SetParent(rightLeftNode, node);
if (parent == null)//若node为根节点
{
_root = leftNode;
}
else
{
if (node == parent.right)//若节点为父节点的右孩子,则
parent.right = leftNode;
else
parent.left = leftNode;
}
node.left = rightLeftNode;
leftNode.right = node;
}
}
}
6.3 测试类
通过VerifyRedBlackTree函数,测试插入或删除后是否符合红黑树特性。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ConsoleApp3.tree
{
internal class RBTreeCheck
{
public bool VerifyRedBlackTree(RBTreeNode<int> root)
{
// Check if the tree is empty
if (root == null)
return true;
// Check the root node's color
if (root.color != NodeColor.Black)
{
Console.WriteLine("Error: Root node is not black.");
return false;
}
// Verify properties recursively
return VerifyRedBlackTreeProperties(root) && VerifyBlackBalance(root);
}
private bool VerifyRedBlackTreeProperties(RBTreeNode<int> node)
{
// Base case: leaf nodes are always black
if (node == null)
return true;
// Check for consecutive red nodes
if (IsRed(node) && (IsRed(node.left) || IsRed(node.right)))
{
Console.WriteLine("Error: Red node has red child.");
return false;
}
// Recursively check left and right subtrees
return VerifyRedBlackTreeProperties(node.left) && VerifyRedBlackTreeProperties(node.right);
}
private bool VerifyBlackBalance(RBTreeNode<int> node)
{
// Base case: leaf nodes contribute 1 to the black height
if (node == null)
return true;
int leftBlackHeight = GetBlackHeight(node.left);
int rightBlackHeight = GetBlackHeight(node.right);
// Check if black heights are balanced
if (leftBlackHeight != rightBlackHeight)
{
Console.WriteLine("Error: Black heights are not balanced.");
return false;
}
// Recursively check left and right subtrees
return VerifyBlackBalance(node.left) && VerifyBlackBalance(node.right);
}
private int GetBlackHeight(RBTreeNode<int> node)
{
// Base case: leaf nodes are black
if (node == null)
return 1;
int leftHeight = GetBlackHeight(node.left);
int rightHeight = GetBlackHeight(node.right);
// Only count black height of black nodes
int height = (node.color == NodeColor.Black) ? 1 : 0;
return height + Math.Max(leftHeight, rightHeight);
}
private bool IsRed(RBTreeNode<int> node)
{
return node != null && node.color == NodeColor.Red;
}
}
public class NodeColor
{
public static bool Red = false;
public static bool Black = true;
}
}
6.4 测试
随机生成100个数字,生成红黑树,调用验证函数判断是否满足红黑树的特性。生成红黑树后,在随机删除一个数字,再调用验证函数判断是否满足红黑树的特性。
// 创建一个 Random 对象
using ConsoleApp3.tree;
var random = new Random();
// 数组用来存储随机整数
var randomNumbers = new int[100];
var tree = new RBTree<int>();
var list = new List<int>();
// 生成随机整数并存储在数组中
for (int i = 0; i < 100; i++)
{
var num = random.Next(1000);
var node = new RBTreeNode<int>();
node.nodeKey = num;
tree.Insert(tree.Root, node);
list.Add(num);
}
var check1 = new RBTreeCheck();
var resul1t = check1.VerifyRedBlackTree(tree.Root);
WriteLine($" 插入后是否满足红黑树 : {resul1t}");
var rd = new Random();
var index = rd.Next(list.Count);
var model = list[index];
tree.Remove(model);
var check2 = new RBTreeCheck();
var resul1t2 = check1.VerifyRedBlackTree(tree.Root);
WriteLine($" 删除后是否满足红黑树: {resul1t2}");
结果如下:插入或删除后都是符合红黑树特性。
参考:
1.目前最详细的红黑树原理分析(大量图片+过程推导!!!) - 西*风 - 博客园 (cnblogs.com)