红黑树的基本性质
红黑树与普通的二叉搜索树不同,它在每个节点上附加了一个额外的属性——颜色,该颜色可以是红色或黑色。通过引入这些颜色,红黑树能够维持以下 5 个基本性质,以确保树的平衡性:
- 每个节点是红色或黑色。
- 根节点是黑色。
- 所有叶子节点(NIL 节点)是黑色。
- 如果一个节点是红色的,那么它的两个子节点都是黑色的(即,红色节点不能有红色子节点)。
- 从任一节点到其每个叶子节点的所有路径上都包含相同数量的黑色节点
红黑树节点结构
template<class K>
struct RBtree_Node
{
RBtree_Node<K>* _parent;
RBtree_Node<K>* _right;
RBtree_Node<K>* _left;
color _color;
K _value;
RBtree_Node(const K& value)
:_parent(nullptr)
,_right(nullptr)
,left(nullptr)
,_color(RED)
,_value(value)
{}
};
红黑树与AVL树不同使用 颜色作为判断旋转或者变色的条件,也就是 红色节点的子节点不能为红色 ,每一条路径上黑色节点个数相同(这个大家刚开始看可能不理解怎么保持,每一条路径上黑色节点个数相同,没事,往下看),我们可以使用 枚举类型将我们需要的颜色封装起来,这里的颜色就是一个标识符,我们使用符号,或者是任意数字都是可以的
enum color
{
RED,
BACK
};
红黑树的旋转变色
变色
uncle节点存在并为红色
这里的cur节点一定是新插入的节点,这里的cur无论是作为parent的左节点还是右节点都是无所谓的, 因为并不存在旋转问题,我们只需要遵循好红黑树的性质规则,更新节点颜色即可,同时更新parent节点和cur节点,因为pparent节点颜色改变了,我们就可以把pparent节点红色节点作为新插入节点进行上层树的更新,保持红黑树性质
if (uncle && uncle->_colour == RED)
{
//更换颜色
parent->_colour = uncle->_colour = BLACK;
grandfather->_colour = RED;
//更新节点
cur = grandfather;
parent = cur->_parent;
}
旋转变色(单方向的节点)
uncle存在但为黑色
cur可以作为插入节点,也可能是因为插入而导致自己从黑色变成的红色,他的子节点一定为黑色(我们可以参考上面变色的情况),所以并不违反所有路径上黑色节点数量相等的情况,这也就是黑色节点怎么往下更新变多,同时路径上黑色节点数量相等的原因!
uncle不存在
因为没有uncle节点,所以cur节点一定是新插入的 (如果不是新插入的,其子节点一定存在黑黑色节点,但是右树并没有额外的黑色节点,这样每一条路径上的黑色节点并不相同),类似于AVL树旋转一下
旋转变色(变折的节点)
uncle存在但为黑色
只是将变折的节点方向改成单向的,没有其他特色,甚至方向反转一下就是右左情况的
uncle不存在
我在这里列举不同情况的两个方向的反折,都是一样的
具体代码
循环条件及变色
while (parent && parent->_colour == RED)
{
// g
// p u
//c
Node* grandfather = parent->_parent;
if (parent == grandfather->_left)
{
Node* uncle = grandfather->_right;
if (uncle && uncle->_colour == RED)
{
//更换颜色
parent->_colour = uncle->_colour = BLACK;
grandfather->_colour = RED;
cur = grandfather;
parent = cur->_parent;
}
else
{
//单旋
// g
// p
// c
//双旋
// g
// p
// c
if (cur == parent->_left)
{
RotateR(grandfather);
grandfather->_colour = RED;
parent->_colour = BLACK;
}
else
{
RotateL(parent);
RotateR(grandfather);
grandfather->_colour = RED;
cur->_colour = BLACK;
}
break;
}
}
else
{
Node* uncle = grandfather->_left;
if (uncle && uncle->_colour == RED)
{
//更换颜色
parent->_colour = uncle->_colour = BLACK;
grandfather->_colour = RED;
cur = grandfather;
parent = cur->_parent;
}
else
{
//单旋
// g
// p
// c
//双旋
// g
// p
// c
if (cur == parent->_right)
{
RotateL(grandfather);
grandfather->_colour = RED;
parent->_colour = BLACK;
}
else
{
RotateR(parent);
RotateL(grandfather);
grandfather->_colour = RED;
cur->_colour = BLACK;
}
break;
}
}
}
终止代码解释
while (parent && parent->_colour == RED)
为什么如果parent是红色节点就要继续呢,或者旋转变色后就可以退出?
在变色情况下 cur还是红色,他的parent如果还是红色,不满足性质4 需要继续变色,或者旋转变色,同理在旋转变色情况下,cur变成了黑色 ,这时候cur的parent无论是什么颜色都不会影响,同时,每一条路径上的黑色节点没有变换,符合所有性质
左右旋转函数代码
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
parent->_right = subRL;
if (subRL) subRL->_parent = parent;
subR->_left = parent;
Node* Parent = parent->_parent;
parent->_parent = subR;
subR->_parent = Parent;
if (Parent == nullptr)
{
_root = subR;
}
else
{
if (Parent->_left == parent)
{
Parent->_left = subR;
}
else
{
Parent->_right = subR;
}
}
}
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
if (subLR) subLR->_parent = parent;
subL->_right = parent;
Node* Parent = parent->_parent;
parent->_parent = subL;
subL->_parent = Parent;
if (Parent == nullptr)
{
_root = subL;
}
else
{
if (Parent->_left == parent)
{
Parent->_left = subL;
}
else
{
Parent->_right = subL;
}
}
}
红黑树与AVL树的区别
红黑树和AVL树都是自平衡二叉搜索树,它们的主要目的是通过保持树的平衡来优化查找、插入和删除操作的时间复杂度。
为什么需要自平衡搜索树呢?
如果我们一直在二叉树中输入较大的值,我们会发现,树慢慢成为了链表,远远没有达到我们想要的搜索时间复杂度O(logn)
红黑树与AVL树的区别
AVL树严格的控制树左右高度差小于2,也就是严格保持搜索树处于接近于满二叉树的情况,使时间复杂度极限接近于O(logn),但是这样会导致插入时,树的旋转开销较大,但是红黑树,保持树的黑色节点相同,在保持树的高度差的同时,减少了旋转的情况,
可以总结为:
- 红黑树适合频繁插入和删除的场景,因其在操作过程中旋转次数少,性能较为稳定。
- AVL树则更适合查找频繁、插入删除较少的场景,提供了更好的查找性能,但在频繁更新时旋转次数较多,性能较低。
C++库函数中 map set也是使用红黑树实现的 ,我们也开始将自己写的红黑树封装出 自己的map set
ps:具体代码可见:study/c++_study/RBtree/RBtree/RBtree.h at main · zjsnh/study (github.com)