数据结构真是有意思,当我看到二叉搜索树的中序遍历是有序时,我以为二叉搜索树已经无敌了,直到出现了二叉搜索树退化为单链表的极端情况,随后出现了,AVL树,通过大量的旋转将树调平衡,由于其对高度差的要求太高,使得删除和插入会增加许多的旋转,这是不小的消耗,随后有人整出了红黑树,早就听闻红黑树的大名了,一开始我还以为有点难,当我实现了部分功能后回头看其实还挺有意思的。
先来看看红黑树的五条规则
1 插入红色节点(默认是红色,后面会解释)时,若父节点为黑色节点,此时插入结束,因为此时对于这棵树而言,还是符合下面的规则的,若父节点为红色节点,此时就违反了规则4。
2 每个节点非黑即红
3 头节点必须是黑色节点(如果不是黑色,可以学了后面变色试想一下,只有根节点时,插入子节点时该如何变色,这就得再独立出一种情况,还不如直接定为黑色)。
4 如果一个节点是红色节点,其两个孩子节点是黑色的,此时父节点必须是黑色的,当然如果当前节点是黑色的,其父节点是黑色还是红色都可以,孩子节点也一样。(也就是说不能出现连续的红色节点)
5 对于所有节点来说,其后代的节点到该节点的路径上的黑色节点数相同。
6 每个叶子节点都是黑色的(叶子节点指的是最后的空节点,当然这条规则可以不理会,黑色节点数又不包括这个空节点,而且当我们插入红色节点,判断父节点是否是黑色,这个时候也与空节点无关,都被覆盖了,看啥嘛)
注意:规则1是我自行添加的,为了让大家先知道insert结束的最简易情况,因为后面文字不少,免得这句话被遗漏了。
先来看看树上一个节点内部保存了什么,目前树是存k-v键值对的,后续会再改造这个红黑树适配出map和set。
template
struct RBTreeNode
{
RBTreeNode(const pair kv)//默认构造
:_kv(kv)
,_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
{
;
}
pair _kv; 存数据
三个指针实现的是三叉链结构
RBTreeNode*_left;
RBTreeNode* _right;
RBTreeNode* _parent;
Color _color=Red; 默认节点颜色为红
};
默认节点为什么是红色,这是个值得思考的问题,大家还记不记得其中一个规则,每个支路上的黑色节点数目是一样的,如果你插入一个黑色节点,其它支路都会受影响,都要想办法增加一个黑色节点,那是一件多么麻烦的事,而插入一个红色节点,父节点如果是黑色节点,那没有什么变化,如果父节点是红色节点,那最多违反不能出现连续红色节点这一条,后面变色我们再好好体会这里插入红色节点的影响是比较小的。
还有就是如何标记一个节点是红色还是黑色呢,我想很多人肯定想问为什么不用0,1,怎么说呢,我觉得我们写代码应该突出直观,简洁这些特点,如果用0,1表示是不直观的,别人可能还要去看看注释,才知道0,1表示什么,如果没写注释,会更麻烦,至于用宏,例如Red表示1,Black表示0,我试了试,可行性是有的,不过枚举可以一次性定义多个常量,这一点还是比宏好的,免得太多宏定义的代码,看得眼花缭乱,下面来看看枚举常量的代码
enum Color
{
Red,
Black
};
定义枚举常量语法: enum+枚举类型名(Color),{}内定义多个常量(例如Red,Black),Color就成为了一个数据类型,只能保存Red,Black这些常量,这些常量默认从零开始计数,所以Color本质是个int。
一路走来,实现了不少容器,想必此时我们应该都大致能理解节点类和树类为什么要分别封装了吧,从节约空间和描述对象出发,描述树的信息对于节点来说是多余的,分离开来是最好的选择。
类RETree只要管理一个_root指针即可。
template
class RBTree
{
typedef RBTreeNode Node;
public:
Node* _root=nullptr;
};
红黑树本质也还是一颗二叉搜索树,要先按照二叉搜索树规则找到插入位置
bool Insert(const pair& kv)
{
if (_root == nullptr)
{
_root = new Node(kv);
_root->_color = Black;
return true;
}
Node* cur = _root;
Node* parent = _root;
while (cur) 按照搜索二叉树规则插入节点
{
if (cur->_kv.first < kv.first)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_kv.first > kv.first)
{
parent = cur;
cur = cur->_left;
}
else
{
return false;
}
}
当cur等于nullptr时,
插入节点
cur = new Node(kv);
if (parent->_kv.first > kv.first) 小于parent的key,插入到左边
{
parent->_left = cur;
}
else 大于parent的key,插入到右边
{
parent->_right = cur;
}
cur->_parent = parent;
变色代码在下面
}
到这还得先看看图,由于根节点必定是黑色的,假设其子节点均存在,那就都是红色节点,此时我们就可以得出一颗一般情况下的红黑树。当parent是gparent节点的左节点时,因为要找parent节点的兄弟节点,所以判断parent是gparent节点的左还是右节点是变色代码的大前提。而如何变色要看下面的三种情况。
此时只要向上变色即可,也就是把parent和uncle节点变为黑色,然后将gparent节点变为红色,但是这里还有个问题,如果gparent是根节点,那我们就要把gparent再弄成黑色,因为根节点必须是黑色。
如果gparent是根节点,如下图,把parent和uncle变成黑色,gparent变成红色后解决了两个问题,第一个是parent和cur的连续红节点的问题,第二个就是让gparent的后代节点到gparent的黑色节点数不变(也就是规则5),此时对于gparent所在的树来说,红黑树的规则大都符合了,但有一点,如果gparent是根节点,此时要将其变为黑色。(规则3),而且由于gparent是根节点,此时将其变为黑色,才没有违反规则5。
如果gparent不是根节点,如下图,因为我们把gparent节点从黑色变为了红色,对于其双亲节点来说,可能也会出现连续红色节点的情况,所以要将gparent给cur, 然后向上讨论。
对应处理代码如下
当cur的父节点为空或者颜色为黑时,调整结束
while (parent && parent->_color == Red)
{
Node* gparent = parent->_parent;
if (parent==gparent->_left)
{
Node* uncle = gparent->_right;
if (uncle && uncle->_color==Red)//当uncle节点存在并且颜色为红时
{
uncle->_color = Black;
parent->_color =Black;
gparent->_color = Red;
//继续向上调整
cur = gparent;
parent = cur->_parent;
}
else uncle节点不存在或者存在节点颜色为黑色
{
}
}
else parent==gparent->_right,下面代码是和上面代码对称的
为什么遇到parent为黑色那就结束了?树原先是合规的,插入节点后你一顿操作使得各个节点的后代节点到空节点的路径上黑色节点数相同。(如下图),此时我们整颗树中只要没有连续红色节点那树就合规了,cur所在树被我们变色处理了,所以没有连续红色节点,而去掉cur所在树,剩下的树也没有连续红色节点(因为这棵树在插入节点前是合规的,而且变色中也未涉及那些节点),那只要cur和parent不是连续的红色节点,那整颗树也就合规了。红黑树比AVL树复杂的就是它不是一种情况处理完就结束了,而是直到遇到parent为黑色,才处理完。
如下图
此时uncle节点不存在,这个时候就不能直接把parent变黑,gparent变红,注意:路径是到空节点的,所以你看着只有一条路径,实际上却有四条路径,如果仅仅是把parent变黑,gparent变红,然后因为gparent是根节点,又把它变黑,此时你再看看根节点到空节点的四条路径上黑色节点数一样吗?
还有个隐含细节要清楚,那就是cur节点一定是新增节点,如果不是,那parent和cur一定有一个原来是黑色节点(虽然这个黑色节点一般来看应该cur节点),那插入节点前,gparent到cur上就有两个黑色节点,而gparent到4号空节点的路径上又只有一个黑色节点,此时说明没插入节点前就已经出问题了,由插入节点前该树还是一颗红黑树得出,那cur一定只能是新增节点。
可是这个图有没有一种很熟悉的感觉,有点像AVL树里的右单旋情况,没错这时候就是要用旋转,以gparent为旋转点进行右单旋。将parent变黑,gparent变红。
而且也会出现双旋,也就是当cur为parent的右节点时。(旋转代码会贴在后面,和AVL树中的操作是一样的,只是不用改平衡因子)
这种情况是变色中出现的,不是说插入节点cur就发现uncle节点是黑色的了,因为parent节点是红色的(这样才会触发变色条件),uncle如果是黑色,那此时parent和uncle的路径上黑色节点就不一样了,也就是说cur节点一定不是新增节点。
向上变色得下图:还是一样,变色处理不了就要考虑旋转。
parent是grandfather的左节点,cur是parent的左节点,进行右单旋。
什么时候是双旋呢?
上述情况中cur是parent的右孩子时,就不能用单旋了,必须先对parent进行右单旋,才可以对grandfather进行左单旋。总结一下就是,只要uncle节点不存在或者存在为黑色节点,这个时候就是用旋转, 当grandfarther,parent,cur在同一侧,单旋,呈现折线,双旋。
完整处理代码如下,我们会发现情况2和情况3都是在一个else内部进行处理的。
当cur的父节点为空或者颜色为黑时,调整结束
while (parent && parent->_color == Red)
{
Node* gparent = parent->_parent;
if (parent==gparent->_left)
{
Node* uncle = gparent->_right;
if (uncle && uncle->_color==Red)//当uncle节点存在并且颜色为红时
{
uncle->_color = Black;
parent->_color =Black;
gparent->_color = Red;
//继续向上调整
cur = gparent;
parent = cur->_parent;
}
else uncle节点不存在/或者uncle节点存在为黑色节点
{
if (cur == parent->_left)//右单旋
{
RotateR(gparent);
parent->_color = Black;
gparent->_color = Red;
}
if (cur == parent->_right)//双旋
{
RotateL(parent);
RotateR(gparent);
cur->_color = Black;
gparent->_color = Red;
}
break;
}
}
else
而说了这么久,我们都是一个大前提,if(parent==gparent->-left)讨论的,
当parent是gparent的右节点时,处理逻辑是一样的,都是分情况处理变色。
{
Node* uncle = gparent->_left;
if (uncle && uncle->_color == Red)//当uncle节点存在并且颜色为红时
{
uncle->_color = Black;
parent->_color = Black;
gparent->_color = Red;
//继续向上调整
cur = gparent;
parent = cur->_parent;
}
else
{
if (cur == parent->_right)//左单旋
{
RotateL(gparent);
parent->_color = Black;
gparent->_color = Red;
}
if (cur == parent->_left)
{
RotateR(parent);
RotateL(gparent);
cur->_color = Black;
gparent->_color = Red;
}
break;
}
}
}
由于根一定是黑色的,防止上面有修改,统一在外面再变黑
_root->_color = Black;
return true;
void RotateR(Node* parent)
{
Node* cur = parent->_left;
Node* Curright = cur->_right;
//旋转
Node* pparent = parent->_parent;//记录该树parent节点的父节点
cur->_right = parent;
parent->_left = Curright;
parent->_parent = cur;
cur->_parent = pparent;
if (Curright)//cur的右子树不为空时
{
Curright->_parent = parent;
}
if (parent == _root)//改变根
{
_root = cur;
}
else//和上一层链接
{
if (pparent->_left == parent)
pparent->_left = cur;
else
pparent->_right = cur;
}
}
void RotateL(Node* parent)//左旋转
{
Node* cur = parent->_right;
Node* Curleft = cur->_left;
//旋转
Node* pparent = parent->_parent;
cur->_left = parent;
parent->_right = Curleft;
parent->_parent = cur;
cur->_parent = pparent;
if (Curleft)//cur的左子树不为空时
{
Curleft->_parent = parent;
}
if (parent == _root)//改变根
{
_root = cur;
}
else//和上一层链接
{
if (pparent->_left == parent)
pparent->_left = cur;
else
pparent->_right = cur;
}
}
每次到这总是忐忑的,我们有时候总想一次性写完代码,一次性运行通过,结果总是一堆错误,写这些实现的时候要一个功能一个功能写,然后一个个测试,运行无错误不能算通过,最后还得用大量测试用例测试通过才行,下面就来看看测试部分的代码。
bool CheckColor(Node* root,int& BlackBase,int Blacknum)
{
if (root == nullptr)
{
if (BlackBase != Blacknum) 当该支路走到空,比较两支路黑色节点数,
只要不相等就不是红黑树
return false;
return true;
}
Node* parent = root->_parent;
if (parent && root->_color == Red) 当前节点为红色节点
{
if (parent->_color == Red) 父节点颜色也为红色,出现了连续红节点,违反规则
return false;
}
if (root->_color == Black) 计算支路的黑节点数
Blacknum++;
去左子树检查,去右子树检查
return CheckColor(root->_left, BlackBase, Blacknum) &&
CheckColor(root->_right, BlackBase, Blacknum);
}
bool IsRBTree()
{
if (_root == nullptr) 树为空,是红黑树
return true;
if (_root->_color == Red) 根节点颜色不是黑色
return false;
Node* cur = _root;
int BlackBase = 0;
while (cur)
{
if (cur->_color == Black) 计算一条支路的黑节点数
BlackBase++;
cur = cur->_left;
}
return CheckColor(_root,BlackBase,0);
}
3 随机数测试代码
void test2()
{
RBTree r1;
srand(time(0));
vector v;
v.reserve(100000);
for (int i = 0; i < 100000; i++)
{
v.push_back(rand());
}
for (auto e : v)
{
r1.Insert(make_pair(e, e)); 插入十万个随机数
}
cout << "->" << r1.IsRBTree() << endl; 最后统一检查,不用插入一次检查一次
}
光是介绍插入就已经写了不少字数了,删除就不写了,如果面试考我手撕插入,估计天黑了。