在介绍红黑树之前,我们先来复习二叉搜索树和AVLTree。
1.二叉搜索树 -> 存储数据的同时方便进行高效搜索(之前的数据结构的搜索基本都是暴力搜索)
二叉搜索树的问题:极端情况下(比如有序的方式进行插入),二叉搜索树就会退化成单链形
式,效率变为o(N),效率低下。
2、AVL树 -> 在二叉树搜索树基础之上加了一个条件:左右子树高度差不超过1,并且左右子树
也满足次条件(也就说所有子树都满足)增删查改搜素效率非常高:0(logN)。
举个例子:如果内存足够的情况下,将所有中国人的信息放到树中,查找一个人的信息最多只需
要31次,通过这个例子可以感受到AVL树的效率,如果是暴力搜素需要14亿次。
3、红黑树 -> 本质上也是一个搜索二叉树,下面具体学习他的性质和他更优的地方。
一、红黑树的概念
1.红黑树的定义
搜索二叉树,节点中加了颜色,不是红色就是黑色,树中最长的路径不超过最短的路径的2倍。
AVL树是严格的平衡二叉搜索树;而红黑树是近似的平衡二叉搜索树。
2.红黑树的性质(5点)
(1)每个结点不是红色就是黑色
(2)根节点是黑色的
(3)如果一个节点是红色的,则它的两个孩子结点是黑色的
(4)对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点
(5)每个叶子结点都是黑色的(此处的叶子结点指的是空结点)
简单总结一下:红黑树的根是黑的,没有连续的红节点,每条路径都有相同数量的黑节点。
树中最短的路径:全黑的路径;树中最长的路径:一黑一红的路径 -> 所以最多是2倍。
满足上面的性质,红黑树就能保证:其最长路径中节点个数不会超过最短路径节点个数的两倍。
3.红黑树节点的实现
在节点的定义中,要将节点的默认颜色给成红色的,这样便于完成后续的操作。
enum Colour
{
BLACK,
RED
};
template<class K, class V>
class RBTreeNode
{
public:
RBTreeNode(const pair<K, V>& kv, Colour colour = RED)
:_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_kv(kv)
,_colour(colour)
{}
public:
RBTreeNode<K, V>* _left;
RBTreeNode<K, V>* _right;
RBTreeNode<K, V>* _parent;
pair<K, V> _kv;
Colour _colour;
};
二、红黑树的操作
(一)红黑树的插入
红黑树的插入分为两个大步骤:1.按照二叉搜索树插入 2.做出对应的调节,使得其变为红黑树
1.按照二叉搜索树插入:这一步到现在对大家来说已经非常熟练了,所以直接展示这部分的代码
//按照搜索二叉树的规则进行插入
//空树插入
if(_root == nullptr)
{
_root = new Node(kv, BLACK);//保证根节点是黑色的
return true;
}
//非空树插入
//找到要插入的位置
Node* cur = _root;
Node* parent = nullptr;
while(cur)
{
if(kv.first < cur->_kv.first)
{
parent = cur;
cur = cur->_left;
}
else if(kv.first > cur->_kv.first)
{
parent = cur;
cur = cur->_right;
}
else
return false;
}
//新建节点
cur = new Node(kv);
//连接
if(kv.first < parent->_kv.first)
{
parent->_left = cur;
cur->_parent = parent;
}
else
{
parent->_right = cur;
cur->_parent = parent;
}
2.做出对应的调节,使得其变为红黑树:这一步又分成三种情况,下面逐一讲解
(1)第一种情况:空树的插入 ==> 插入节点做根,并且把他变黑
(2)第二种情况:cur为红,parent为黑 ==> 直接插入就可以
(3)第三种情况:cur为红,parent为红,这种情况可以推断出grandfather一定存在且为黑。
遇到这种情况,关键看uncle的情况,根据uncle的情况,下面又分出了三种情况
①uncle存在且为红,这种情况所对应的操作:
将parent和uncle变为黑,grandfather变为红,然后继续向上调整grandfather
还要注意⚠️:这种情况g、p、u这几个节点在左边还是右边都是这么处理
②uncle不存在/uncle存在且为黑,这种情况所对应的操作:旋转 + 变换
1)如果grandfather、parent、cur是一条直线
a)parent为grandfather的左孩子,cur为parent的左孩子,则对grandfather进行右单旋
b)parent为grandfather的右孩子,cur为parent的右孩子,则对grandfather进行左单旋
变色:parent、grandfather变色 -> parent变黑,grandfather变红
2)如果grandfather、parent、cur是一条折线
a)parent为grandfather的左孩子,cur为parent的右孩子 ==>
对parent左单旋,在对grandfather右单旋
b)parent为grandfather的右孩子,cur为parent的左孩子 ==>
对parent右单旋,在对grandfather左单旋
变色:cur、grandfather变色 -> cur变黑,grandfather变红
上图只展示了一次旋转,因为在进行了一次旋转后,就转换成了g、p、cur在一条直线的情况。
下面展示完整的插入代码
bool Insert(const pair<K, V>& kv)
{
//1.按照搜索二叉树的规则进行插入
if(_root == nullptr)
{
_root = new Node(kv, BLACK);//保证根节点是黑色的
return true;
}
//找到要插入的位置
Node* cur = _root;
Node* parent = nullptr;
while(cur)
{
if(kv.first < cur->_kv.first)
{
parent = cur;
cur = cur->_left;
}
else if(kv.first > cur->_kv.first)
{
parent = cur;
cur = cur->_right;
}
else
return false;
}
cur = new Node(kv);
//连接
if(kv.first < parent->_kv.first)
{
parent->_left = cur;
cur->_parent = parent;
}
else
{
parent->_right = cur;
cur->_parent = parent;
}
//做出对应的调节,使得其变为红黑树
while(parent && parent->_colour == RED)
{
//红黑树的条件关键看叔叔
Node* grandfather = parent->_parent;
//parent是grandfather的左孩子
if(grandfather->_left == parent)
{
Node* uncle = grandfather->_right;//叔叔就是右孩子
//遇到情况三的①:cur为红,parent为红,grandfather为黑,uncle存在且为红
if(uncle && uncle->_colour == RED)
{
//将parent和uncle变为黑,grandfather变为红
parent->_colour = uncle->_colour = BLACK;
grandfather->_colour = RED;
//继续往上处理
cur = grandfather;
parent = cur->_parent;
}
//uncle为黑或者不存在
else
{
//情况三中折线的情况:对parent左单旋,在对grandfather右单旋
//下面只完成了对parent左单旋,因为这样就将折线转换成了直线
if(cur == parent->_right)
{
RotateL(parent);
swap(parent, cur);
}
//完成对grandfather右单旋:
//有可能直接是情况三中直线的情况
//也有可能是由情况三中折线的情况 变换过来的
RotateR(grandfather);
grandfather->_colour = RED;
parent->_colour = BLACK;
}
}
//parent是grandfather的右孩子
else
{
Node* uncle = grandfather->_left;//叔叔就是左孩子
//uncle存在且为红
if(uncle && uncle->_colour == RED)
{
parent->_colour = uncle->_colour = BLACK;
grandfather->_colour = RED;
//向上调整grandfather
cur = grandfather;
parent = cur->_parent;
}
//uncle为黑或者不存在
else
{
//完成折线的parent右单旋
if(cur == parent->_left)
{
RotateR(parent);
swap(parent, cur);
}
//完成直线的grandfather左单旋
RotateL(grandfather);
grandfather->_colour = RED;
parent->_colour = BLACK;
}
}
}
//强制将根改成黑色,保持性质
_root->_colour = BLACK;
return true;
}
//旋转处理
//1.左单旋
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
Node* ppNode = parent->_parent;//指向parent节点的父亲
//1.subR的左边放在parent的右边
//!!因为是用三叉链表示的树,所以不仅需要连接儿子,还需要连接父亲!!
parent->_right = subRL;//连儿子
if(subRL)//连父亲
subRL->_parent = parent;
//2.parent变成subR的左边
//同样 !!因为是用三叉链表示的树,所以不仅需要连接儿子,还需要连接父亲!!
subR->_left = parent;
parent->_parent = subR;
//找subR的父亲
//(1)原来parent是这棵树的根,现在subR成了这棵树的根
if(_root == parent)
{
_root = subR;
subR->_parent = nullptr;
}
//(2)parent为根的树只是整棵树的子树
else
{
//连孩子
//parent是一个左子树
if(ppNode->_left == parent)
ppNode->_left = subR;
//parent是一个右子树
else
ppNode->_right = subR;
//连父亲
subR->_parent = ppNode;
}
}
//2.右单旋
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
Node* ppNode = parent->_parent;//指向parent节点的父亲
//1.subL的右边放到parent的左边
parent->_left = subLR;
if(subLR)
subLR->_parent = parent;
//2.parent变成subL的右边
subL->_right = parent;
parent->_parent = subL;
//找subL的父亲
//(1)原来parent是这棵树的根,现在subL成了这棵树的根
if(_root == parent)
{
_root = subL;
subL->_parent = nullptr;
}
//(2)parent为根的树只是整棵树的子树
else
{
//连孩子
//parent是一个左子树
if(ppNode->_left == parent)
ppNode->_left = subL;
//parent是一个右子树
else
ppNode->_right = subL;
//连父亲
subL->_parent = ppNode;
}
}
(二)红黑树的删除
删除在这里不详细介绍了,给大家推荐一篇优秀的博客,是关于红黑树删除操作的。
https://www.cnblogs.com/fornever/archive/2011/12/02/2270692.html
(三)红黑树的查改
因为红黑树本质上还是一个搜索二叉树,所以他们的查改都一样,这里就不详细介绍了。
(四)红黑树的验证
红黑树的验证分成两步:1.验证中序遍历有序 2.验证红黑树的五条性质
由于这里不是重点,所以我们不详细分析,直接上验证红黑树的五条性质的代码
//验证五条性质
bool _IsValidRBTree(Node* root, size_t k, const size_t blackCount)
{
//走到null之后,判断k和black是否相等
if (nullptr == root)
{
if (k != blackCount)
{
cout << "违反性质四:每条路径中黑色节点的个数必须相同" << endl;
return false;
}
return true;
}
// 统计黑色节点的个数
if (BLACK == root->_colour)
k++;
// 检测当前节点与其双亲是否都为红色
Node* parent = root->_parent;
if (parent && RED == parent->_colour && RED == root->_colour)
{
cout << "违反性质三:没有连在一起的红色节点" << endl;
return false;
}
return _IsValidRBTree(root->_left, k, blackCount) &&
_IsValidRBTree(root->_right, k, blackCount);
}
bool IsValidRBTree()
{
Node* root = _root;
// 空树也是红黑树
if (nullptr == root)
return true;
// 检测根节点是否满足情况
if (BLACK != root->_colour)
{
cout << "违反红黑树性质二:根节点必须为黑色" << endl;
return false;
}
// 获取任意一条路径中黑色节点的个数
size_t blackCount = 0;
Node* cur = root;
while (cur)
{
if (BLACK == cur->_colour)
blackCount++;
cur = cur->_left;
}
// 检测是否满足红黑树的性质,k用来记录路径中黑色节点的个数
size_t k = 0;
return _IsValidRBTree(root, k, blackCount);
}
三、红黑树的性能
红黑树增删查改时间复杂度是:o(logN)
最短路径是:o(logN) 最长路径是:2*o(logN)
也就是说理论上而言,红黑树的效率比AVL树略差。但是现在呢,硬件的运算速度非常快,他们
之间己经基本没有差异了。因为常规数据集中logN足够小,与2*logN之间的差异不大。
为什么AVLTree和红黑树之间的性能基本差了2倍,但是我们认为基本上是一样的呢?
因为现在的硬件足够快:比如10亿个数查找,AVLTree最多查找30次;红黑树最多查找60次。30
次的查找和60次的查找对于现在的硬件基本是一样的。
为什么实际中红黑树得到了更广泛的应用,而不是AVL树?
第一点是因为插入刪除同样节点红黑树比AVL树旋转更少,AVLTree更严格的平衡其实是通过多
旋转达到的,更多次的旋转导致了效率的损耗。第二点是因为红黑树在实现上比AVL树更容易控
制。所以实际中红黑树得到了更广泛的应用。