目录
点击跳转至上一篇文章: 【c++】:avl树的深度解析及其实现
点击跳转至文章:【c++】:二叉树进阶 — 搜索二叉树
💡前言
上一篇文章介绍了什么是avl树和avl树的实现,avl树也有它的缺点:就是太过追求绝对平衡,比如在插入时要维护其绝对平衡,旋转次数太多,在删除时甚至有可能要一直旋转到根位置,使之性能低下。
本篇文章介绍的红黑树也是一种平衡树,是通过改变节点颜色以及旋转操作,使之接近平衡。
红黑树比avl树的用途更加广泛,在一些方面效率甚至要优于avl树,并且 map/set 的底层封装用的也是红黑树。
一,红黑树的概念
红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是red或black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出两倍,因而是接近平衡的。
二,红黑树的性质
(1) 每个结点不是红色就是黑色 ;
(2) 根节点是黑色的 ;
(3) 如果一个节点是红色的,则它的两个孩子结点是黑色的(重点);
(4) 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点(重点) ;
(5) 每个叶子结点都是黑色的(此处的叶子结点指的是空结点,这条性质可有可无,平时不关注)。
三,红黑树节点的定义
红黑树的节点结构与avl树的大致相同,只是avl树中有节点的颜色,没有平衡因子。
//枚举颜色
enum colour
{
red,
black
};
template <class k, class v>
struct rbtreenode
{
rbtreenode<k, v>* _left;
rbtreenode<k, v>* _right;
rbtreenode<k, v>* _parent;
pair<k, v> _kv;
colour _col;
rbtreenode(const pair<k, v>& kv)
:_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_kv(kv)
,_col(black)
{}
};
四,红黑树的插入操作
首先我们要思考一个问题,插入节点时,到底是插入红色节点还是黑色节点?为什么?
答案:插入红色节点。
因为我们知道红黑树最重要的两条性质是第(3)(4)条,通过维护这两条规则使之成为红黑树,而当新插入一个节点时,必定会破坏两条规则之一。
假设插入节点为黑色节点,则所有路径的黑色节点数量均不相同,如何让它们相同将是一个巨大的难题,而插入红色节点(此时一定是作为孩子节点),就破坏规则(3),但是只要根据其父亲和叔叔节点进行适当的变色就可以继续恢复规则(3)。
显而易见,规则(3)(4)就好比"慈父严母",非要选择得罪其中一人,那当然是"慈父"了。
红黑树是在二叉搜索树的基础上加上其平衡限制条件,因此红黑树的插入可分为两步:
4.1 第一步
按照二叉搜索的树规则插入新节点:
bool insert(const pair<k, v>& kv)
{
if (_root == nullptr)
{
_root = new node(kv);
return true;
}
node* cur = _root;
node* parent = nullptr;
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 false;
}
cur = new node(kv);
//新增节点,颜色为红色
cur->_col = red;
//链接时要判断链接在parent的左还是右
if (parent->_kv.first > kv.first)
parent->_left = cur;
else
parent->_right = cur;
cur->_parent = parent;
//检测新节点插入后,红黑树的性质是否造到破坏
//……
}
4.2 第二步
检测新节点插入后,红黑树的性质是否造到破坏:
因为新节点的默认颜色是红色,因此:如果其双亲节点的颜色是黑色,没有违反红黑树任何性质,则不需要调整;但当新插入节点的双亲节点颜色为红色时,就违反了性质三不能有连在一起的红色节点,此时需要对红黑树分情况来讨论:
约定:
(1) cur 为当前节点,p为父节点,g为祖父节点,u为叔叔节点
(2) 下面的抽象图中,a/b/c/d/e 表示具有 n 个节点的红黑树,n >=0。
(一) 情况一: cur为红,p为红,g为黑,u存在且为红:
解决方式:将p,u改为黑,g改为红,然后把g当成cur,继续向上调整。
(二) 情况二: cur为红,p为红,g为黑,u不存在/u存在且为黑
下面我们根据 u 的情况用具象图分别解释单旋与双旋操作:
(1) u 不存在,a/b/c/d/e都是空,cur 是新增:
p为g的左孩子,cur为p的左孩子,则进行右单旋转,再 p 变黑,g 变红:
相反,p为g的右孩子,cur为p的右孩子,则进行左单旋转,再 p 变黑,g 变红:
p为g的左孩子,cur为p的右孩子,先针对p做左单旋转,再针对 g 做右单旋,cur 变黑,g 变红:
(2) u 存在且为黑
单旋情况:
双旋情况:
4.3 插入操作的完整代码
bool insert(const pair<k, v>& kv)
{
if (_root == nullptr)
{
_root = new node(kv);
return true;
}
node* cur = _root;
node* parent = nullptr;
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 false;
}
cur = new node(kv);
//新增节点,颜色为红色
cur->_col = red;
//链接时要判断链接在parent的左还是右
if (parent->_kv.first > kv.first)
parent->_left = cur;
else
parent->_right = cur;
cur->_parent = parent;
while (parent && parent->_col == red)
{
node* grandfather = parent->_parent;
// g
// p u
if (parent == grandfather->_left)
{
//找到u的位置
node* uncle = grandfather->_right;
if (uncle && uncle->_col == red)
{
//u存在且为红,把p/u变黑,g变红,继续向上调整
parent->_col = uncle->_col = black;
grandfather->_col = red;
cur = grandfather;
parent = cur->_parent;
}
else
{
//u不存在或存在且为黑
if (cur == parent->_left)
{
// g
// p u
//c
//单旋
rotater(grandfather);
parent->_col = black;
grandfather->_col = red;
}
else
{
// g
// p u
// c
//双旋
rotatel(parent);
rotater(grandfather);
cur->_col = black;
grandfather->_col = red;
}
break;
}
}
else
{
// g
// u p
node* uncle = grandfather->_left;
//u存在且为红,把p/u变黑,g变红,继续向上调整
if (uncle && uncle->_col == red)
{
parent->_col = uncle->_col = black;
grandfather->_col = red;
cur = grandfather;
parent = cur->_parent;
}
else
{
//u不存在或存在且为黑
if (cur == parent->_right)
{
// g
// u p
// c
//单旋
rotatel(grandfather);
parent->_col = black;
grandfather->_col = red;
}
else
{
// g
// u p
// c
//双旋
rotater(parent);
rotatel(grandfather);
cur->_col = black;
grandfather->_col = red;
}
break;
}
}
}
_root->_col = black;
return true;
}
五,红黑树的验证
红黑树的检测分为两步:
(1) 检测其是否满足二叉搜索树(中序遍历是否为有序序列)
(2) 检测其是否满足红黑树的性质
这里重点检测的是性质(3)与性质(4)。
检测性质(3):只要判断当前节点与其父亲节点是否为连续的红色节点;
检测性质(4):先计算出任意一条路径上的黑色节点作为参考值,再用这个参考值与其他路径上的黑色节点数量比较。
private:
//中序遍历
void inorder()
{
_inorder(_root);
cout << endl;
}
//判断是否平衡
bool isbalance()
{
if (_root == nullptr)
return true;
//检查根节点
if (_root->_col == red)
return false;
// 参考值
int refnum = 0;
node* cur = _root;
while (cur)
{
if (cur->_col == black)
++refnum;
cur = cur->_left;
}
return check(_root, 0, refnum);
}
private:
bool check(node* root, int blacknum, const int refnum)
{
//每条路径走到空后与参考值进行比较
if (root == nullptr)
{
//cout << blacknum << endl;
if (refnum != blacknum)
{
cout << "存在黑色节点的数量不相等的路径" << endl;
return false;
}
return true;
}
//检查是否存在连续的红色节点
if (root->_col == red && root->_parent->_col == red)
{
cout << root->_kv.first << "存在连续的红色节点" << endl;
return false;
}
//blacknum表示:根节点到当前节点的路径上黑色节点的数量
if (root->_col == black)
blacknum++;
return check(root->_left, blacknum, refnum)
&& check(root->_right, blacknum, refnum);
}
六,实现红黑树的完整代码
rbtree.h
#pragma once
#include <iostream>
#include <assert.h>
using namespace std;
//枚举颜色
enum colour
{
red,
black
};
template <class k, class v>
struct rbtreenode
{
rbtreenode<k, v>* _left;
rbtreenode<k, v>* _right;
rbtreenode<k, v>* _parent;
pair<k, v> _kv;
colour _col;
rbtreenode(const pair<k, v>& kv)
:_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_kv(kv)
,_col(black)
{}
};
template <class k, class v>
class rbtree
{
typedef rbtreenode<k, v> node;
public:
node* find(const pair<k, v>& kv)
{
node* cur = _root;
while (cur)
{
if (cur->_kv.first > kv.first)
cur = cur->_left;
else if (cur->_kv.first < kv.first)
cur = cur->_right;
else
return cur;
}
return nullptr;
}
bool insert(const pair<k, v>& kv)
{
if (_root == nullptr)
{
_root = new node(kv);
return true;
}
node* cur = _root;
node* parent = nullptr;
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 false;
}
cur = new node(kv);
//新增节点,颜色为红色
cur->_col = red;
//链接时要判断链接在parent的左还是右
if (parent->_kv.first > kv.first)
parent->_left = cur;
else
parent->_right = cur;
cur->_parent = parent;
while (parent && parent->_col == red)
{
node* grandfather = parent->_parent;
// g
// p u
if (parent == grandfather->_left)
{
//找到u的位置
node* uncle = grandfather->_right;
if (uncle && uncle->_col == red)
{
//u存在且为红,把p/u变黑,g变红,继续向上调整
parent->_col = uncle->_col = black;
grandfather->_col = red;
cur = grandfather;
parent = cur->_parent;
}
else
{
//u不存在或存在且为黑
if (cur == parent->_left)
{
// g
// p u
//c
//单旋
rotater(grandfather);
parent->_col = black;
grandfather->_col = red;
}
else
{
// g
// p u
// c
//双旋
rotatel(parent);
rotater(grandfather);
cur->_col = black;
grandfather->_col = red;
}
break;
}
}
else
{
// g
// u p
node* uncle = grandfather->_left;
//u存在且为红,把p/u变黑,g变红,继续向上调整
if (uncle && uncle->_col == red)
{
parent->_col = uncle->_col = black;
grandfather->_col = red;
cur = grandfather;
parent = cur->_parent;
}
else
{
//u不存在或存在且为黑
if (cur == parent->_right)
{
// g
// u p
// c
//单旋
rotatel(grandfather);
parent->_col = black;
grandfather->_col = red;
}
else
{
// g
// u p
// c
//双旋
rotater(parent);
rotatel(grandfather);
cur->_col = black;
grandfather->_col = red;
}
break;
}
}
}
_root->_col = black;
return true;
}
//中序遍历
void inorder()
{
_inorder(_root);
cout << endl;
}
//判断是否平衡
bool isbalance()
{
if (_root == nullptr)
return true;
//检查根节点
if (_root->_col == red)
return false;
// 参考值
int refnum = 0;
node* cur = _root;
while (cur)
{
if (cur->_col == black)
++refnum;
cur = cur->_left;
}
return check(_root, 0, refnum);
}
private:
//每个节点的位置记录一个值blacknum
//blacknum表示:根节点到当前节点的路径上黑色节点的数量
bool check(node* root, int blacknum, const int refnum)
{
//每条路径走到空后与参考值进行比较
if (root == nullptr)
{
//cout << blacknum << endl;
if (refnum != blacknum)
{
cout << "存在黑色节点的数量不相等的路径" << endl;
return false;
}
return true;
}
//检查是否存在连续的红色节点
if (root->_col == red && root->_parent->_col == red)
{
cout << root->_kv.first << "存在连续的红色节点" << endl;
return false;
}
if (root->_col == black)
blacknum++;
return check(root->_left, blacknum, refnum)
&& check(root->_right, blacknum, refnum);
}
//右单旋
void rotater(node* parent)
{
node* subl = parent->_left;
node* sublr = subl->_right;
if (sublr)
sublr->_parent = parent;
parent->_left = sublr;
subl->_right = parent;
//改变之前记录
node* ppnode = parent->_parent;
parent->_parent = subl;
//parent为根
if (parent == _root)
{
_root = subl;
_root->_parent = nullptr;
}
else
{
//parent为一颗子树
if (ppnode->_left == parent)
ppnode->_left = subl;
else
ppnode->_right = subl;
subl->_parent = ppnode;
}
}
//左单旋
void rotatel(node* parent)
{
node* subr = parent->_right;
node* subrl = subr->_left;
if (subrl)
subrl->_parent = parent;
parent->_right = subrl;
subr->_left = parent;
node* ppnode = parent->_parent;
parent->_parent = subr;
//parent为根
if (parent == _root)
{
_root = subr;
_root->_parent = nullptr;
}
else
{
//parent为一颗子树
if (ppnode->_left == parent)
ppnode->_left = subr;
else
ppnode->_right = subr;
subr->_parent = ppnode;
}
}
void _inorder(node* root)
{
if (root == nullptr)
return;
_inorder(root->_left);
cout << root->_kv.first << ":" << root->_kv.second << endl;
_inorder(root->_right);
}
private:
node* _root = nullptr;
};
//测试代码
void test1()
{
//int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
int a[] = { 16,3,7,11,9,26,18,14,15 };
rbtree<int, int> t;
for (auto e : a)
t.insert({ e ,e });
t.inorder();
cout << t.isbalance() << endl;
}
test.cpp
#include "rbtree.h"
int main()
{
test1();
return 0;
}
运行结果如下:
中序遍历是有序的,说明是搜索二叉树,返回1,说明满足红黑树的性质,是平衡树。
五,红黑树与avl树的比较
红黑树和avl树都是高效的平衡二叉树,增删改查的时间复杂度都是o( l o g 2 n log_2 n log2n),红黑树不追求绝对平衡,其只需保证最长路径不超过最短路径的2倍,相对而言,降低了插入和旋转的次数,所以在经常进行增删的结构中性能比avl树更优,而且红黑树实现比较简单,所以实际运用中红黑树更多。
发表评论