红黑树的概念
红黑树,是一种二叉搜索树,但是在每个结点上会多出一个存储位来表示结点的颜色,可以是红色(RED)或者黑色(BLACK),通过任何一条根到叶子的路径上各个结点着色方式的限制,确保红黑树没有一条路径会比其他路径长出两倍,因此是接近平衡的。
红黑树的性质
1.每个结点不是红色就是黑色
2.根结点是黑色的
3.如果一个结点是红色的,那么它的两个孩子结点一定是黑色的
4.对于每个结点,从该结点到其后代叶结点的简单路径上,均包含相同数目的黑色结点
5.每个叶子结点都是黑色的(这里的叶子结点指的是空结点)
问题来了:为什么满足上面的性质,红黑树就能保证最长路径中结点个数不会超过最短路径结点个数的两倍?
首先,根据上述特征,根结点是黑色的,黑色结点可以连续,红色结点不可以连续,假设有两条路径,都含有3个黑色结点,那么最长的一条可以穿插3个红色结点,最短的一条全是黑色结点
所以由此可得,最长路径的个数不会超过最短路径结点的两倍。
红黑树的结点的定义
首先,为了增加代码的可读性与后续操作的方便,这里采用枚举来定义结构体到颜色。
而在定义结点的时候,我们可以采用与AVL树相同的方法,为了后续方便旋转操作,结点里面含有相互链接的三叉链。
那么,为什么在构造函数里,颜色会初始化为红色呢?
按照红黑树的性质,假设:
1.初始化为黑色,那么原本符合要求的树,插入一个黑色结点后,一定会有一条路径多出一个黑色结点,那么此时红黑树的性质将会被破坏(每条路径的黑色结点数量相等),必须调整。
2.初始化为红色,原本符合要求的树,在插入一个红色节点后,每条路径的黑色结点数量不改变,也就是没有破坏性质(每条路径红色结点数量不作要求),但是,如果连续有两个红色结点,那么也会破坏性质。所以,插入红色结点可能会破坏。
综上所述,插入黑色一定会破坏性质,插入红色可能会破坏性质。那么按照期望值,我们就会选择插入红色结点。
//用枚举定义结点颜色
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(RED)//初始化为红色
{}
};
红黑树的插入操作
在红黑树中,插入步骤分别有两步:
1.搜索可插入位置并插入结点。
2.插入完成后,判断是否破坏红黑树性质,是则进行处理。
在第一步中,与二叉搜索树的搜索插入逻辑相同。但是少了平衡因子的修改。
在第二步中,我们可以知道,插入的结点默认是红色的,那么此时就需要判断情况
1.插入在根结点,那么按照红黑树的性质(根结点一定为黑色),我们需要将结点变为黑色。
2.插入不是在根结点,就得开始判断父节点的颜色:
2.1如果父结点是黑色,那么此时并不会影响红黑树的性质。因为红色结点没有连续,黑色结点没有变化。
2.2如果父结点是红色,那么此时此时就会破坏红黑树的性质,红色结点出现连续了。在这种情况下,我们又需要进行情况处理,也就是对叔叔结点的情况进行判断:
情况一:叔叔结点存在且为红色
这种情况是最简单的情况,此时每条路径都只有一个黑色结点,那么我们怎么让每条路径的黑色结点数量不变且红色结点不连续呢?
只需要将_parent结点与uncle结点变为黑色,grandfather结点变为红色,此时就能解决。
情况二:叔叔结点存在且为黑色
首先我们要知道,这种情况的cur结点不是新插入的,是原本为黑色的结点变色而来的,假如cur是新插入的,那么插入之前,左右子树的黑色结点不同,已经不是红黑树的性质了,所以cur原本一定是存在且为黑色,然后通过往上调整变为红色得来的
下图是其中两种变化:
在情况二中,总共有四种变化:
parent在grandfather的左边,cur在parent左右两边的两种变化
parent在grandfather的右边,cur在parent左右两边的两种变化
开始判断:
1.假如为左左,也就是子 父 祖父 三个结点都在左边,那么我们直接进行右旋处理,然后令parent和grandfather变色即可。
2.假如为左右,父结点在祖父结点的左边,子结点在父节点右边,那么我们直接进行左右双旋,然后让祖父结点和子结点变色即可。
3.假如为右右,也就是子 父 祖父 三个结点都在右边,那么我们直接进行左旋处理,然后让parent和grandfather变色即可。
4.假如为右左,也就是父结点在祖父结点的右边,子结点在父结点的左边,那么我们直接进行右左双旋处理,然后让祖父结点和子结点变色即可
情况三:叔叔结点不存在
这种情况可以得出cur一定是新插入的,因为此时左右子树都没有黑色结点,只有根一个结点,假如cur原本存在,那么cur只能是黑色,那么此时左右子树的黑色结点不同,所以cur原本不存在,只能是新插入的。
那么此时的解决方案其实是和情况二一样进行旋转即可。
//插入函数
bool insert(const pair<K, T>& kv)
{
//按照二叉树搜索树插入
if (_root == nullptr)//根结点为空时new一个最初的根结点
{
_root = new Node(kv);
_root->_col = BLACK;//根结点一定为黑
return true;
}
Node* parent = nullptr;//这个为当前指针cur的父结点指针
Node* cur = _root;//当前指针指向根
while (cur)//当不为空,说明存在值,那么继续搜索可插入的地方
{
if (cur->_kv.first < kv.first)//key大于结点值,往右走
{
parent = cur;
cur = cur->_right;
}
else if (cur->_kv.first > kv.first)//key小于结点值,往左走
{
parent = cur;
cur = cur->_left;
}
else//相等,那么不插入,插入失败
{
return false;
}
}
cur = new Node(kv);//新增结点
cur->_col = RED;//默认红色
//插入
if (parent->_kv.first > kv.first)
{
parent->_left = cur;
cur->_parent = parent;
}
else
{
parent->_right = cur;
cur->_parent = parent;
}
//开始判断颜色
while (parent != nullptr && parent->_col == RED)
{
Node* grandfather = parent->_parent;
//如果父亲为红,那么违反红红规则,开始判断情况
if (parent != nullptr && parent == grandfather->_left)
{
Node* uncle = grandfather->_right;//记录叔叔结点
if (uncle != nullptr && uncle->_col == RED)//如果叔叔存在或为红色,情况一
{
//变色
parent->_col = uncle->_col = BLACK;//父亲和叔叔都变黑
grandfather->_col = RED;//爷爷变红
//将cur和parent往上移继续判断
cur = grandfather;
parent = cur->_parent;
}
else//叔叔不存在或者存在且为黑色,情况二和情况三结合
{
if (cur == parent->_left)
{
RotateR(grandfather);//右旋
parent->_col = BLACK;
grandfather->_col = RED;
}
else
{
RotateLR(grandfather); //左右双旋
grandfather->_col = RED;
cur->_col = BLACK;
}
break;//根结点为黑,不需要往上了
}
}
else//parent在grandfather的右边
{
Node* uncle = grandfather->_left;//记录叔叔结点
if (uncle != nullptr && uncle->_col == RED)//如果叔叔存在或者为红色,情况一
{
parent->_col = uncle->_col = BLACK;//父亲和叔叔都变黑
grandfather->_col = RED;//爷爷变红
//向上调整
cur = grandfather;
parent = grandfather->_parent;
}
else//叔叔不存在或者存在且为黑色,情况二和情况三结合
{
if (cur == parent->_left)//如果插入在parent的左边
{
RotateRL(grandfather);//右左双旋
cur->_col = BLACK;
grandfather->_col = RED;
}
else//如果插入在parent的右边
{
RotateL(grandfather);//左旋
grandfather->_col = RED;
parent->_col = BLACK;
}
break;//根结点为黑,不需要往上了
}
}
}
_root->_col = BLACK;//往上移动后无论cur是否为根结点,统一为改黑
return true;//插入成功
}
//左单旋
void RotateL(Node* parent)
{
//定义新指针,方便操作
Node* subR = parent->_right;
Node* subRL = subR->_left;
Node* pp = parent->_parent;//方便更改_root的操作
parent->_right = subRL;//让parent结点链接subRL
subR->_left = parent;//让subR的左子树链接parent
parent->_parent = subR;//由于parent的_parent由nullptr变成了subR,所以也需要重新链接
if (subRL)
//判断subRL是否为空,如果为空的话就不需要对subRL进行操作了,不然会出现对空指针进行解引用的问题
{
subRL->_parent = parent;//不为空,那么让subRL链接parent
}
if (pp == nullptr)//如果parent是整棵树的根结点
{
_root = subR;//subR变为根结点
subR->_parent = nullptr;//subR的_parent为空
}
else//如果parent不是整棵树的根结点,那么将新的parent重新链接上一个结点
{
if (pp->_left = parent)//如果parent是上一个结点的左子树,那么新的parent也是
{
pp->_left = subR;
}
else//如果parent是上一个结点的右子树,那么新的parent也是
{
pp->_right = subR;
}
subR->_parent = pp;//更新subR的父结点
}
//parent->_bf = subR->_bf = 0;//由于旋转后,整棵树的高度变回插入前的,那么此时parent和subR(cur)的因子都变回0
}
//右单旋
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subRR = subL->_right;
Node* pp = parent->_parent;
//建立subL和parent之间的关系
parent->_left = subRR;
subL->_right = parent;
//建立parent和subRR之间的关系
parent->_parent = subL;
if (subRR != nullptr)
{
subRR->_parent = parent;
}
//建立PP和subL之间的关系
if (pp == nullptr)
{
_root = subL;
subL->_parent = nullptr;
}
else
{
if (pp->_left == parent)
{
pp->_left = subL;
}
else
{
pp->_right = parent;
}
subL->_parent = pp;
}
//更新平衡因子
//subL->_bf = parent->_bf = 0;
}
//左右双旋
void RotateLR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
//int bf = subLR->_bf;
RotateL(parent->_left);
RotateR(parent);
//if (bf == 0)
//{
// //subLR自己就是新增
// subLR->_bf = 0;
// subL->_bf = 0;
// parent->_bf = 0;
//}
//else if (bf == -1)
//{
// //subLR的左子树新增
// subLR->_bf = 0;
// subL->_bf = 0;
// parent->_bf = 1;
//}
//else if (bf == 1)
//{
// //subLR的右子树新增
// subLR->_bf = 0;
// subL->_bf = -1;
// parent->_bf = 0;
//}
//else
//{
// assert(false);
//}
}
//右左双旋
void RotateRL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
//int bf = subRL->_bf;
RotateR(parent->_right);
RotateL(parent);
//if (bf == 0)
//{
// //subRL自己就是新增
// parent->_bf = subR->_bf = subRL->_bf = 0;
//}
//else if (bf == -1)
//{
// //subRL的左子树新增
// parent->_bf = 0;
// subRL->_bf = 0;
// subR->_bf = 1;
//}
//else if (bf == 1)
//{
// //subRL的右子树新增
// parent->_bf = -1;
// subRL->_bf = 0;
// subR->_bf = 0;
//}
//else
//{
// assert(false);
//}
}
红黑树的验证
第一:红黑树也是一种特殊的二叉搜索树,因此我们可以先获取二叉树的中序遍历序列,来判断该二叉树是否满足二叉搜索树的性质。
//中序遍历副函数
void Inorder()
{
_Inorder(_root);
}
//中序遍历主函数
void _Inorder(Node* root)
{
if (root == nullptr)
return;
_Inorder(root->_left);
cout << root->_kv.first << " ";
_Inorder(root->_right);
}
第二:判断根结点的情况,如果为空,那么返回true。如果不为空且红,那么返回false。
第三:判断是否有连续的红色结点
第四:判断每条路径的黑色结点是否相同
// blacknum是根结点到当前结点的黑色结点数量
bool check(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;
}
if (root->_col == BLACK)
{
++blacknum;
}
return check(root->_left,blacknum,count) && check(root->_right,blacknum,count);
}
bool isbalance()
{
//首先判断根结点
if (_root == nullptr)
{
return true;
}
if (_root->_col == RED)
{
return false;
}
//找最左路径作为黑色结点数目的参考值
Node* cur = _root;
int count = 0;
while (cur)
{
if (cur->_col == BLACK)
++count;
cur = cur->_left;
}
int blacknum = 0;
return check(_root,blacknum,count);
}
红黑树与AVL树的比较
红黑树和AVL树都是高效的平衡二叉树,增删差改的时间复杂度都是Olog(N),但是红黑树不追求绝对平衡,其只需保证最长路径不超过最短路径的两倍即可。相对而言,降低了插入和旋转的次数,所以在经常进行增删的结构中性能比AVL树更优,而且红黑树实现比较简单,所以实际运用中使用红黑树的更多。