红黑树的概念
AVL树是一个近似平衡的搜索二叉树,它的查找效率可以接近O(logN)
,AVL树的每个节点左右子树最大高度差不会超过一
,也就是说它十分接近满二叉树,而要构造这样的树结构,需要在插入/删除时进行多次调整,会有较多的性能损耗
所以就有了红黑树这样一个平衡条件没有AVL树那么严格
的结构,红黑树的插入/删除并不会十分频繁的破坏红黑树的条件,不需要频繁进行调整
红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近平衡
的
- 红黑树并没有AVL树那么的追求平衡,所以红黑树的查找效率并没有AVL树高
- 红黑树在经常进行增删的结构中性能比AVL树更优
红黑树的性质
- 根节点必须是
黑色
的 - 如果一个节点是
红色
的,那么这个节点的左右孩子必须是黑色
(也就是说不能有两个连续的红色节点) - 每条路径上的黑色节点个数是相等的
- 叶子节点(这里的叶子节点是指NIL空节点)都是黑色的
这四个性质保证了:红黑树最长路径中节点个数不会超过最短路径节点个数的两倍
红黑树的实现
红黑树的实现采用三叉链和颜色标记来实现
红黑树节点
采用KV结构存储键值对数据,节点中还包括节点颜色,parent指针,左子树指针,右子树指针,还有节点的构造函数
//枚举结构用来定义节点颜色
enum Colour {
RED,
BLACK
};
template <class K, class V>
struct RBTreeNode {
RBTreeNode<K, V>* _left;
RBTreeNode<K, V>* _right;
RBTreeNode<K, V>* _parent;
//节点颜色
Colour _Col;
//KV键值对
pair<K, V> _kv;
RBTreeNode(const pair<K, V>& kv)
:_left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _Col(RED)
, _kv(kv)
{}
};
在构造节点时,把节点的默认颜色设置为红色,因为插入的节点如果是黑色,就一定会破坏红黑树的性质
,而插入红色节点则有可能破坏性质,也有可能不破坏性质
,插入红色节点破坏的是
不能有两个连续的红色节点的性质,在进行调整时也要简单
红黑树的插入(重点)
红黑树是带有颜色平衡性质的二叉搜索树,红黑树的插入分为两个部分
- 按照二叉搜索树的插入方式插入新节点
- 对插入节点的
父节点
进行检测,如果插入节点的父节点是红色
,则对红黑树进行调整
插入新节点
按照二叉搜索树的插入方式
- 树为空,创建一个结点,直接链接到根结点
- 树不为空,按照二叉搜索树的性质找到插入的位置,再创建结点插入链接
检测父节点的颜色并调整
插入新节点后,新节点是红色的,需要对插入节点的parent节点进行检测
- 如果插入节点的parent节点是
黑色
的,那么插入一个红色节点,并没有对红黑树的性质造成破坏,那么插入成功
- 如果插入节点的parent节点是
红色
的,那么就违反了不能有两个连续的红色节点
的性质,需要对红黑树进行调整
具体的调整还得看parent的兄弟节点,也就是uncle节点,具体的调整方法分为两种情况
cur为当前节点,p为父节点,g为祖父节点,u为叔叔节点
以下情况是cur的p节点为g节点的左节点
情况一:插入结点的叔叔存在,且
叔叔的颜色是红色
此时出现了两个红色节点连续的情况,可以把p节点变成黑色,为了让每个路径上的黑色节点数量一样,还需要把u节点变成黑色,g节点变为红色
-
这时如果g节点是根节点,那么只需要把g再次变为黑色,就完成了插入
-
要是g节点不是根节点,那么需要让g节点作为cur节点,再次向上判断,如果g节点的父亲结点依然是红色,那么需要把g节点看做新增节点cur,再次对上面的节点进行变色处理
不管cur是p节点的左节点还是右节点
,只要u节点是红色,都是这种处理方式
情况二:插入节点的
叔叔节点存在且为黑
,或者叔叔节点不存在
情况二还分为两种小情况
- cur是parent的
左子树
,也就是说,cur,p,g三个节点处于一条直线
上图是叔叔节点存在且为黑
上图是叔叔节点不存在
这两种情况的操作是一样的,所以可以合并为一种情况
- 先对g节点进行右单旋,降低这个树的高度
- 然后对p和g节点进行变色处理
此时p节点变为黑色,不管现在p节点是不是树的根,只要旋转完成,就会插入完成
因为现在p节点的父亲节点不管是什么颜色,都符合红黑树的性质
- cur是parent的右子树,也就是说,cur,p,g三个节点
不处于一条直线
上图是叔叔节点存在且为黑
上图是叔叔节点不存在
这两种情况的操作是一样的,所以可以合并为一种情况
- 对p节点进行左单旋,把树调整成上一种情况(祖孙三代在一条直线上)
- 按照上一种情况的处理方法,右单旋降低这个树的高度
- 然后对cur和g节点进行变色处理
当cur的p节点为g节点的右节点时,处理方式和上面情况都是镜像的
实现代码
pair<Node*, bool> Insert(const pair<K, V>& kv) {
//先按照二叉搜索树的插入方式把节点插入到红黑树
//根节点为空
if (_root == nullptr) {
_root = new Node(kv);
//根节点必须是黑色
_root->_Col = BLACK;
return make_pair(_root, true);
}
//根节点不为空
Node* parent = nullptr;
Node* cur = _root;
while (cur) {
if (cur->_kv.first > kv.first) {
parent = cur;
cur = cur->_left;
}
else if (cur->_kv.first < kv.first) {
parent = cur;
cur = cur->_right;
}
else {
//节点已经存在,插入失败
return make_pair(cur, false);
}
}
//找到要插入的节点位置
cur = new Node(kv);
//记录插入节点的位置
Node* newNode = cur;
//新节点的链接关系
if (parent->_kv.first > kv.first) {
parent->_left = cur;
cur->_parent = parent;
}
else {
parent->_right = cur;
cur->_parent = parent;
}
//插入新节点成功
//检测红黑树的性质是否被破坏
//如果cur的parent存在且为红,说明树的性质已经被破坏
while (parent && parent->_Col == RED) {
//这时就要看cur的uncle
Node* grandParent = parent->_parent;
if (parent == grandParent->_left) {
Node* uncle = grandParent->_right;
//情况1 cur的uncle存在且为红色
//变色处理
if (uncle && uncle->_Col == RED) {
parent->_Col = BLACK;
uncle->_Col = BLACK;
grandParent->_Col = RED;
cur = grandParent;
parent = cur->_parent;
}
//情况2 cur的uncle存在且为黑
//说明cur不是新增节点,cur是情况1迭代上来的
//情况3 cur的uncle不存在 可以和情况2合并
//进行右单旋 变色
else{
//祖孙三代在一条直线
if (parent->_left == cur) {
//右单旋
_RotateR(grandParent);
//变色
parent->_Col = BLACK;
grandParent->_Col = RED;
}
else { //parent->_right == cur
//左单旋
_RotateL(parent);
//右单旋
_RotateR(grandParent);
//变色
grandParent->_Col = RED;
cur->_Col = BLACK;
}
break;
//旋转完后,调整完成,不用再向上调整
}
}
//parent == grandParent->_right
else {
Node* uncle = grandParent->_left;
//uncle为红 变色
if (uncle && uncle->_Col == RED) {
uncle->_Col = BLACK;
parent->_Col = BLACK;
grandParent->_Col = RED;
//继续向上检测
cur = grandParent;
parent = cur->_parent;
}
//uncle为黑或者uncle不存在
else {
//祖孙三代在一条直线
if (parent->_right == cur) {
//左单旋
_RotateL(grandParent);
//变色
grandParent->_Col = RED;
parent->_Col = BLACK;
}
//祖孙三代不在一条直线
else {
_RotateR(parent);
_RotateL(grandParent);
//变色
grandParent->_Col = RED;
cur->_Col = BLACK;
}
//旋转完结束
break;
}
}
}
//插入成功
//把红黑树的根节点变成黑色
_root->_Col = BLACK;
return make_pair(newNode, true);
}
//左单旋
void _RotateL(Node* parent) {
Node* subR = parent->_right;
Node* subRL = subR->_left;
Node* grandParent = parent->_parent;
parent->_right = subRL;
if (subRL) {
subRL->_parent = parent;
}
subR->_left = parent;
parent->_parent = subR;
if (parent == _root) {
_root = subR;
subR->_parent = nullptr;
}
else {
if (grandParent->_left == parent) {
grandParent->_left = subR;
}
else {
grandParent->_right = subR;
}
subR->_parent = grandParent;
}
}
//右单旋
void _RotateR(Node* parent) {
Node* subL = parent->_left;
Node* subLR = subL->_right;
Node* grandParent = parent->_parent;
parent->_left = subLR;
//subLR有可能为空
if (subLR) {
subLR->_parent = parent;
}
subL->_right = parent;
parent->_parent = subL;
//看要旋转的点是子树还是根
if (parent == _root) {
_root = subL;
subL->_parent = nullptr;
}
else {
if (grandParent->_left == parent) {
grandParent->_left = subL;
}
else {
grandParent->_right = subL;
}
subL->_parent = grandParent;
}
}
红黑树的查找
红黑树的查找按照二叉搜索树的查找方式查找即可
- 若树为空,查找失败,返回
nullptr
- key值小于当前节点,就去当前节点的左子树查找
- key值大于当前节点,就去当前节点的右子树查找
- key值等于当前节点,查找成功,返回对应节点指针
Node* Find(const K& key) {
if (_root == nullptr)
return nullptr;
Node* cur = _root;
while (cur) {
if (cur->_kv.first > key) {
cur = cur->_left;
}
else if (cur->_kv.first < key) {
cur = cur->_right;
}
else {
//找到了
return cur;
}
}
//没找到
return nullptr;
}
红黑树的验证
可以用中序遍历是否有序的方式来验证红黑树的搜索性质
void InOrder() {
_InOrder(_root);
}
void _InOrder(Node* root) {
if (root == nullptr)
return;
_InOrder(root->_left);
cout << root->_kv.first << " : " << root->_kv.second << endl;
_InOrder(root->_right);
}
除了验证红黑树的搜索性质,还需要验证红黑树的三个特性
- 根是否为黑色
- 是否有两个连续的红色节点
- 每条路径上的黑色节点个数是否相等
bool IsRBTree() {
if (_root == nullptr)
return true;
if (_root->_Col == RED) {
cout << "根节点为红色" << endl;
return false;
}
//计算一个路径黑色节点的标准值
Node* cur = _root;
int BlackNum = 0;
while (cur) {
if (cur->_Col == BLACK)
BlackNum++;
cur = cur->_left;
}
int count = 0;
return _IsRBTree(_root, BlackNum, count);
}
//子函数用来递归
bool _IsRBTree(Node* root, int BlackNum, int count) {
//找到了路径结束
if (root == nullptr) {
if (count != BlackNum) {
//有一条路径黑色节点数量和最左路径不相等
cout << "有一条路径黑色节点数量和最左路径不相等" << endl;
return false;
}
return true;
}
//检测红色节点的父节点是否是红色节点
if (root->_Col == RED && root->_parent->_Col == RED) {
cout << "有两个连续的红色节点" << endl;
return false;
}
//如果节点是黑色 ++count
if (root->_Col == BLACK)
count++;
return _IsRBTree(root->_left, BlackNum, count)
&& _IsRBTree(root->_right, BlackNum, count);
}
红黑树的删除
红黑树的删除参考
红黑树和AVL树的比较
红黑树保证了最大的路径差不超过二倍,所以其在最坏情况下,查找效率
是O(2logN)
AVL树保证了高度平衡,其最大的高度差不超过2,所以他的查找效率
一直都是保持在O(logN)左右
可以看出AVL树的查找效率几乎是红黑树的2倍,但是由于是O(logN)数量级,其实在计算机看来,他们俩的查找效率几乎可以忽略不计
但是AVL树的高度平衡是因为其通过大量的旋转来完成
的,所以对于经常发生删除和插入的结构
,红黑树的效率会更优
,并且红黑树的实现比起AVL更加容易且易于控制,所以实际中使用红黑树更多
图片转自
高级数据结构与算法 | 红黑树(Red-Black Tree)