学习红黑树之前你应该保证你学过AVL树,也就是平衡二叉搜索树
数据结构 AVL树
AVL树是一棵高度平衡的二叉搜索树,其要求每个结点的高度差不能大于1,这样子就保证了其查询的时间复杂度为log2(N),不会出现单支树而导致时间复杂度退化到线性时间。但是AVL树的插入和删除性能非常低下,只要稍微不平衡,都需要进行旋转操作,实现起来也相对困难。所以如果当你需要查询效率高且有序时你可以有优先选择AVL树,但是如果你所需的数据结构是频繁的插入与删除,且需要高效查询且有序时,你可以优先考虑红黑树(RBTree)
红黑树与AVL树的比较:AVL树的查询效率虽然优于红黑树,但是这种差距不大,在cpu的快速调度下,这种差距可以忽略不计。而红黑树的插入与删除的性能要高于AVL树,AVL树插入一个节点或者删除一个节点都有可能需要旋转,且有可能追溯至根节点;红黑树是只有特定情况下才旋转,且很少会追溯至根节点。所以说红黑树是AVL树的一种更优的迭代版
红黑树是一棵二叉搜索树,每个结点都增加了存储颜色的标志,这个颜色只有红色RED或者黑色BLACK。通过任意一条从跟结点到叶子结点的路径上颜色的限制,从而确保红黑树的最长路径不会超过最短路径的两倍,因此就可以达到近似平衡的效果
由这三条性质就能知道为什么红黑树可以保证最长路径不会超过最短路径的两倍了。如下图
最短路径的路径上的结点全为黑色结点,最长路径则是黑红相间,而要保证黑色结点数目相同且不能出现连续的两个红结点。从而确定了最长路径不会超过最短路径的两倍
结点信息和AVL树类似,有5个属性,分别是父节点_parent
,左孩子_left
,右孩子_right
,结点的值_val
,颜色信息_color
。其中节点都是该结点的指针,结点的值我们可以使用泛型T来实现,颜色我们可以通过bool值或者枚举来实现,这里为了提高可读性,我们使用枚举来实现。其构造函数参数就为pair,设置为缺省参数。
enum COLOR
{
BLACK,
RED
};
template <class T>
struct RBTreeNode
{
RBTreeNode<T>* _parent; //父节点
RBTreeNode<T>* _left; //左孩子
RBTreeNode<T>* _right; //右孩子
T _val; //键值对
COLOR _color; //颜色
RBTreeNode(const T& val = T())
:_parent(nullptr)
, _left(nullptr)
, _right(nullptr)
, _val(val)
, _color(RED)
{
}
};
这里颜色为什么默认为红色呢?如果默认为黑色,那么就可以插入无论父亲是什么颜色,由于红黑树的性质,需要确保每条路径上的黑色结点个数相同,所以你此时插入了一个黑色结点,其他的路径上的黑色结点肯定会比插入新结点的这条路径少1个,调整起来就会非常繁琐。而当插入的结点为红色时,如果父节点的颜色为黑色,那么就直接插入;如果父节点为红色,那么就在该路径上进行颜色调整或者旋转操作。所以插入的结点默认为红色,调整的成本就远低于黑色
只有一个成员根节点_root
template <class T>
class RBTree
{
public:
typedef RBTreeNode<T> Node;
RBTree()
:_root(nullptr)
{
}
private:
Node* _root;
};
红黑树的结点插入是红黑树的重点也是难点,插入结点和AVL的插入类似,只是最后颜色修改和调整会不一样
大致的步骤分为三步
前两步实现简单,和AVL树类似,这里着重讲第三步颜色修改以及结构调整。当父节点为黑色时,此时没有存在连续的两个红色结点,所以就不需要颜色修改以及结构调整;而当父节点为红色时,就会出现连续的红色结点,此时就有必要修改结点颜色或者调整结构。是修改颜色还是调整结构又分为三种情况
结点约定:cur为当前节点、parent为父节点、gfather为祖父结点、uncle为叔结点
第一种情况:爸叔通红就变色----cur为红色,parent结点为红色,uncle结点存在且为红色
调整方法:需要对该路径上的子树进行调整,parent结点和uncle结点都修改为黑色,祖父结点修改为红色,此时以祖父结点为根的子树就不存在连续的两个红色结点。但是由于祖父结点颜色也被修改了,此次需要向上追溯,判断是否还存在连续的两个红色结点。令cur = gfather结点向上追溯调整
第二种情况:爸红叔黑就旋转,爸红没叔也要旋,哪边黑往哪边转----cur为红色,parent结点为红色,uncle结点不存在或者为黑色。parent在gfather的左边,cur在parent的左边;或者parent在gfather的右边,cur在parent的右边
parent在gfather的左边,cur在parent的左边:以gfather结点为轴进行右旋,parent结点修改为黑色,gfather结点修改为红色
uncle结点不存在
uncle结点存在且为黑色
parent在gfather的右边,cur在parent的右边:以gfather结点为轴进行左旋,parent结点修改为黑色,gfather结点修改为红色
uncle结点不存在
uncle结点存在且为黑
第三种情况:爸红叔黑就旋转,爸红没叔也要旋,哪边黑往哪边转----cur为红色,parent结点为红色,uncle结点不存在或者为黑色。parent在gfather的左边,cur在parent的右边;或者parent在gfather的右边,cur在parent的左边
parent在gfather的左边,cur在parent的右边:先以parent结点为轴进行左旋,交换cur和parent结点,此时变成第二种情况,再对gfather结点进行右单旋,并修改parent颜色为黑色gfather颜色为红色
uncle结点不存在
uncle结点存在且为黑
parent在gfather的右边,cur在parent的左边:先以parent结点为轴进行右旋,交换cur和parent结点,此时变成第二种情况,再对gfather结点进行左单旋,并修改parent颜色为黑色gfather颜色为红色
uncle结点不存在
uncle结点存在且为黑
总结成顺口溜:根结点为黑,新结点为红,只能黑连黑,不能红连红;爸叔通红就变色,没叔叔黑就旋转,哪边黑往哪边转
代码:
bool insert(const T& val)
{
if (_root == nullptr)
{
_root = new Node(val);
//根节点为黑色
_root->_color = BLACK;
return true;
}
Node* cur = _root;
Node* parent = nullptr;
//1.寻找到要插入的结点的位置
while (cur)
{
parent = cur;
if (cur->_val == val)//key值相同插入失败
return false;
else if (cur->_val > val)
cur = cur->_left;
else
cur = cur->_right;
}
//2.创建节点
cur = new Node(val);
if (parent->_val > cur->_val)
parent->_left = cur;
else
parent->_right = cur;
cur->_parent = parent;
//3.颜色的修改或者结构的调整
while (cur != _root && cur->_parent->_color == RED)//不为根且存在连续红色,则需要调整
{
parent = cur->_parent;
Node* gfather = parent->_parent;
if (gfather->_left == parent)
{
Node* uncle = gfather->_right;
//情况1.uncle存在且为红
if (uncle && uncle->_color == RED)
{
parent->_color = uncle->_color = BLACK;
gfather->_color = RED;
//向上追溯
cur = gfather;
}
else
{
if (parent->_right == cur)//情况3
{
RotateL(parent);
swap(cur, parent);
}
//情况2.uncle不存在或者uncle为黑
RotateR(gfather);
parent->_color = BLACK;
gfather->_color = RED;
break;
}
}
else
{
Node* uncle = gfather->_left;
if (uncle && uncle->_color == RED)
{
parent->_color = uncle->_color = BLACK;
gfather->_color = RED;
//向上追溯
cur = gfather;
}
else
{
if (parent->_left == cur)
{
RotateR(parent);
swap(cur, parent);
}
RotateL(gfather);
parent->_color = BLACK;
gfather->_color = RED;
break;
}
}
}
//根节点为黑色
_root->_color = BLACK;
return true;
}
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
parent->_right = subRL;
if (subRL)
subRL->_parent = parent;
if (parent == _root)
{
_root = subR;
subR->_parent = nullptr;
}
else
{
Node* gfather = parent->_parent;
if (gfather->_left == parent)
gfather->_left = subR;
else
gfather->_right = subR;
subR->_parent = gfather;
}
subR->_left = parent;
parent->_parent = subR;
}
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
if (subLR)
subLR->_parent = parent;
if (parent == _root)
{
_root = subL;
subL->_parent = nullptr;
}
else
{
Node* gfather = parent->_parent;
if (gfather->_left == parent)
gfather->_left = subL;
else
gfather->_right = subL;
subL->_parent = gfather;
}
subL->_right = parent;
parent->_parent = subL;
}
验证一棵树是否为红黑树,必须要满足以下条件
我们先针对第一个条件写出中序遍历来验证是否有序输出
void inoder()
{
_inoder(_root);
cout << endl;
}
void _inoder(Node* root)
{
if (root)
{
_inoder(root->_left);
cout << root->_val << " ";
_inoder(root->_right);
}
}
测试给出100个随机数
满足第一个条件:中序遍历有序
剩下的三个条件都是红黑树的性质,我们可以都写在一个函数中,并递归的判断子树是否也满足红黑树的性质条件
在判断每条路径上的黑色结点是否相同时,我们可以选择一条路径上的黑色结点作为基准值,然后将该值作为该树的每条路径的黑色结点数,只要有一条路径与其值不同,则表示该树不是红黑树。代码简单,注释很明确
bool isRBTree()
{
if (_root == nullptr) //空树也属于红黑树
return true;
if (_root->_color == RED) //不满足条件二:根节点必须为黑色
return false;
int bCount = 0; //某条路径上的黑色结点数
Node* cur = _root;
while (cur)
{
if (cur->_color == BLACK)
++bCount;
cur = cur->_left;
}
int pathCount = 0;
return _isRBTree(_root, bCount, pathCount);//遍历判断每条路径上的黑色结点个数是否相同
}
bool _isRBTree(Node* root, const int bCount, int pathCount)
{
if (root == nullptr)//路径走完,判断黑色结点个数
{
if (pathCount == bCount)
return true;
else
return false;//不满足条件三: 每一条路径上的黑色结点个数相同
}
if (root->_color == BLACK)//遇到黑色结点就++
++pathCount;
Node* parent = root->_parent;
if (parent && parent->_color == RED && root->_color == RED)
return false; //不满足条件四:不能存在连续红色的节点
return _isRBTree(root->_left, bCount, pathCount) //继续遍历左子树和右子树
&& _isRBTree(root->_right, bCount, pathCount);
}