目录
前言
红黑树在实际中应用更广,本文将深入探讨这一数据结构的特点,同时通过详细的步骤分析红黑树的插入操作。
1. 概念及性质
红黑树是一种平衡二叉搜索树。在红黑树中,每个节点都存储有一个额外的位,用来表示节点的颜色,这个颜色或者是红色,或者是黑色。红黑树通过这些颜色约束来保证树的大致平衡,从而确保树的高度大约是 log(N),其中 N 是树中节点的数量。以下是红黑树的一些关键特性:
- 节点颜色:每个节点要么是红色,要么是黑色。
- 根节点:根节点是黑色的。
- 红色规则:如果一个节点是红色的,则它的子节点必须是黑色的(也就是说,不能有两个连续的红色节点)。
- 黑色高度:从任意节点到其每个叶子节点的所有路径上包含相同数量的黑色节点。
- 叶子节点:所有的叶子节点(NIL节点,树尾端的虚拟节点)都是黑色的。
通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路
径会比其他路径长出俩倍,即最长路径的结点个数不超过最短路径结点个数的两倍。
如果最短路径结点颜色全是黑色,根据性质4,最长路径也拥有相同个数的黑色结点,会出现红黑交替的情况,才会达到最长路径结点个数刚好等于最短路径节点个数。
2. 红黑树实现
2.1 数据结构设计
红黑树的结点需要存储键值对元素,结点的颜色,还有结点的左孩子结点,右孩子结点和父亲结点。红黑树结点的默认构造函数需要pair<K,V>类型值进行初始化。
红黑树的成员变量使用红黑树节点类型定义一个根节点。
enum Colour
{
RED,
BLACK
};
template<class K, class V>
struct RBTreeNode
{
//全缺省构造函数
RBTreeNode(const pair<K, V>& kv = pair<K, V>())
:_kv(kv)
, _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
{}
pair<K, V> _kv; //键值对
RBTreeNode<K, V>* _left; //左孩子
RBTreeNode<K, V>* _right; //右孩子
RBTreeNode<K, V>* _parent; //父亲结点
Colour _col; //颜色
};
//红黑树类成员变量的定义
template<class K, class V>
class RBTree
{
typedef RBTreeNode<K, V> Node;
public:
//...
private:
Node* _root = nullptr;
};
2.2 构造与析构函数
下面的拷贝构造函数和析构函数,传入根节点,调用了子函数,
template<class K, class V>
class RBTree
{
typedef RBTreeNode<K, V> Node;
public:
//强制生成默认构造函数
RBTree() = default;
RBTree(const RBTree<K, V>& t)
{
_root = Copy(t._root);
}
RBTree<K, V>& operator=(const RBTree<K, V> t)
{
swap(_root, t._root);
return *this;
}
~RBTree()
{
Destroy(_root);
_root = nullptr;
}
//...
private:
//前序拷贝结点
Node* Copy(Node* root)
{
if (root == nullptr)
return nullptr;
Node* newRoot = new Node(root->_kv);
newRoot->_left = Copy(root->_left);
newRoot->_right = Copy(root->_right);
}
//后序释放结点
void Destroy(Node* root)
{
if (root == nullptr)
return;
Destroy(root->_left);
Destroy(root->_right);
delete root;
}
private:
Node* _root = nullptr;
};
2.3 红黑树的插入
红黑树是平衡二叉搜索树,在二叉搜索树上面有平衡的限制。因此,可以分为两步:
- 先按照key值查找结点的插入位置,进行插入操作。
- 给结点上色,按照红黑树的规则判断是否平衡。如果破坏规则,需要做出相应的改变,可能是改变其他节点的颜色,或者是进行旋转。
第一步:
- 如果根节点为空,说明没有节点,直接开辟结点,根节点颜色设置为黑色。如果不为空,就要通过比较寻找结点所处位置,并开辟新结点。
此时,还要思考新结点颜色设置为什么颜色。
- 性质4是需要保证每条路径黑色节点个数相同。如果新结点颜色设置为黑色,就会破坏性质4。规则遭到破坏,就要做出改变。如果修改结点颜色,那么所有路径结点的颜色都需要转变。但是,红色节点的孩子结点颜色必须是黑色,并且一般红黑树节点颜色都是红黑交替,难以修改。如果不修改结点颜色,就要进行旋转,改变结点位置,也比较麻烦。
- 如果新结点设置为红色,新结点的父亲结点颜色是黑色,就不需要做出什么改变。如果新结点的父亲结点颜色是红色,会破坏性质3,不能出现连续的红色节点。所以,需要变色或者进行旋转。
- 相比较下,插入红色节点有概率不用进行调整,插入黑色节点很麻烦。因此,插入新结点一般设置为红色。
bool Insert(const pair<K, V>& kv)
{
if (_root == nullptr)
{
_root = new Node(kv);
_root->_col = BLACK;
return true;
}
//二叉树搜索
Node* parent = nullptr;
Node* cur = _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 = new Node(kv);
cur->_col = RED;
if (parent->_kv.first < kv.first)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
cur->_parent = parent;
//破坏规则,进行处理
//...
return true;
}
第二步:
插入新结点设置为红色,如果父亲结点也是红色,需要进行调整,分成三种情况。其中新增节点记作cur,它的父亲结点记作parent,祖父结点记作grandfather,叔叔结点与父亲结点有相同的父亲结点,叔叔节点记作uncle。下面使用英文单词的缩写。
第一种情况:插入到abcd任意一个位置,不过会区分左右子树,一般以左子树为例,右子树操作类似。cur为红色,parent为红色,uncle也为红色,grandfather为黑色。操作方法如下:
- parent和uncle结点颜色改为黑色,grandfather改为红色。此时,这棵树所有路径黑色结点不变,也不会出现连续的红色结点。
修改完颜色之后,还需要考虑grandfather结点是否为跟结点。
- 如果grandfather为根节点,根据性质2,根节点颜色必须是黑色,将grandfather结点修改为黑色,全部路径的黑色节点都加一,不会造成路径黑色节点个数不同。
- 如果grandfather不是根节点,说明它还有父亲结点,需要判断其父亲结点的颜色。如果其父亲结点颜色是黑色,就不用再往上更新颜色了。如果还是红色,需要继续更新。
第二种情况:cur在最左边或者最右边,下面我们讨论最左边的情况。cur为红色,parent为红色,grandfather为黑色,uncle不存在或者为黑色。
- 我们要进行左单旋,下图有左单旋的步骤,如果对二叉搜索树旋转有问题,可以参考我上篇关于AVL树的文章--->http://t.csdnimg.cn/DtYF0
我们分析一下uncle的两种情况:
- 如果uncle不存在,那么cur一定是新增节点。因为如果cur不是新增结点,那么cur或者a其中之一是黑色节点,不满足每条路径黑色节点数量相同的性质。
- 如果uncle存在,那么cur不是新增节点。因为uncle结点这条路径至少有两个黑色结点,如果cur是新增节点,那么每条路径的黑色节点数量不同。所以这种情况是由第一种情况转变而来的,是cur的子树中有插入新结点,通过改变颜色,影响到祖先节点,
第三种情况:我们先讨论cur在祖父节点左子树的情况,右子树情况类似。cur为红色,parent为红色,uncle不存在或者为黑色。
- 这种情况是parent是grandfather结点的左孩子,cur是parent的右孩子,这种不是一边高的情况,需要用到双旋。先对parent进行左单旋,再对grandfather进行右单旋。最后,cur颜色修改为黑色,grandfather颜色修改为红色
uncle不存在或者uncle为黑色情况的分析上面有,参照上面的分析。
下面是插入函数的完整代码。我们上面讨论的都是parent是grandfather的左孩子的情况,右孩子的情况相当于左孩子的镜像。
class RBTree
{
public:
bool Insert(const pair<K, V>& kv)
{
if (_root == nullptr)
{
_root = new Node(kv);
_root->_col = BLACK;
return true;
}
Node* parent = nullptr;
Node* cur = _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 = new Node(kv);
cur->_col = RED;
if (parent->_kv.first < kv.first)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
cur->_parent = parent;
//插入节点完毕,如果父亲结点存在且为红色
//说明需要进行修改操作。
while (parent && parent->_col == RED)
{
Node* grandfather = parent->_parent;
//parent是父亲的左孩子的情况
if (parent == grandfather->_left)
{
Node* uncle = grandfather->_right;
if (uncle && uncle->_col == RED)
{
//uncle存在且为红色 -->变色再继续往上处理
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//parent是父亲结点的右孩子的情况
{
// g
// u p
// c
Node* uncle = grandfather->_left;
if (uncle && uncle->_col == RED)
{
//uncle存在且为红色 -->变色再继续往上处理
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;
}
}
}
//强行转换,针对parent是根的情况,
_root->_col = BLACK;
return true;
}
//...
private:
void RotateL(Node* parent)
{
_rotateNum++;
Node* subR = parent->_right;
Node* subRL = subR->_left;
parent->_right = subRL;
if (subRL)
subRL->_parent = parent;
//提前记录parent的父亲结点
Node* parentParent = parent->_parent;
subR->_left = parent;
parent->_parent = subR;
if (parentParent == nullptr)
{
_root = subR;
subR->_parent = nullptr;
}
else
{
if (parent == parentParent->_left)
parentParent->_left = subR;
else
parentParent->_right = subR;
subR->_parent = parentParent;
}
}
void RotateR(Node* parent)
{
_rotateNum++;
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
if (subLR)
subLR->_parent = parent;
Node* parentParent = parent->_parent;
subL->_right = parent;
parent->_parent = subL;
if (parentParent == nullptr)
{
_root = subL;
subL->_parent = nullptr;
}
else
{
if (parent == parentParent->_left)
parentParent->_left = subL;
else
parentParent->_right = subL;
subL->_parent = parentParent;
}
}
//...
}
2.4 红黑树的验证
我们写一个IsBalance函数来验证一颗二叉搜索树是不是红黑树。
- 先判断根节点是否为空,空树也是红黑树。再判断根节点的颜色,根节点颜色是黑色。
- 在定义一个整型变量,它用来记录一条路径黑色节点的个数,我们找最左边这条路径。
- 我们再定义一个Check子函数,用来判断每条路径的黑色节点数量是否相同,查看是否有连续的红色节点。
class RBTree
{
public:
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, 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);
}
}
2.5 其他接口函数
- Height函数用来计算红黑树的高度。
- Size函数用来计算红黑树结点的个数。
- InOrder函数中序遍历红黑树,会得到一个有序序列。
class RBTree
{
public:
int Height()
{
return _Height(_root);
}
int Size()
{
return _Size(_root);
}
void InOrder()
{
_InOrder(_root);
cout << endl;
}
private:
int _Size(Node* root)
{
if (nullptr == root)
return 0;
return _Size(root->_left) + _Size(root->_right) + 1;
}
int _Height(Node* root)
{
if (root == nullptr)
return 0;
int leftHeight = _Height(root->_left);
int rightHeight = _Height(root->_right);
return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}
void _InOrder(Node* root)
{
if (root == nullptr)
return;
_InOrder(root->_left);
cout << root->_kv.first << ":" << root->_kv.second << " ";
_InOrder(root->_right);
}
//...
}
2.6 测试函数
写一个测试函数,测试红黑树插入逻辑是否正确,顺便测试红黑树的性能。改变N,就可以改变插入元素的数量。
void TestRBTree()
{
const int N = 10000000;
vector<int> v;
v.reserve(N);
srand(time(0));
for (size_t i = 0; i < N; i++)
{
//v.push_back(i); //有序值
v.push_back(rand() + i); //随机值
}
size_t begin1 = clock();
RBTree<int, int> t1;
for (auto& e : v)
{
t1.Insert({ e, e });
}
size_t end1 = clock();
cout << "IsBalanceTree:" << t1.IsBalance() << endl;
cout << "RBTreeInsert:" << end1 - begin1 << endl;
cout << "RBTreeHeight:" << t1.Height() << endl;
cout << "RBTreeSize:" << t1.Size() << endl;
size_t begin2 = clock();
//查找确定的值
//for (auto& e : v)
//{
// t1.Find(e);
//}
//查找随机值
for (size_t i = 0; i < N; i++)
{
t1.Find(rand() + i);
}
size_t end2 = clock();
cout << "RBFind:" << end2 - begin2 << endl;
}
当N等于一百万时,插入随机值,查找随机值。插入花费时间137ms,查找花费时间12ms。
如果查找有序值,查找时间小于1ms。
当N等于一千万,插入随机值,查找随机值。运行结果如下,插入花费1.3s左右,查找花费时间119ms。
查找确定值时,查找时间小于1ms。
当N等于一千万,插入有序确定值,查找随机值。明显插入时间缩短。
3. 红黑树和AVL树的比较
插入和删除操作:
-
AVL树:在插入或删除节点后,AVL树可能会破坏平衡条件,需要通过旋转来重新平衡。AVL树可能需要进行最多两次旋转(单旋转或双旋转)来恢复平衡。
-
红黑树:插入和删除操作后,红黑树可能需要通过重新着色和旋转(一次或两次旋转加上重新着色)来维持红黑树的性质。
性能:
-
AVL树:由于AVL树更严格平衡,其查找操作比红黑树更快,但插入和删除操作可能更慢,因为可能需要更多的旋转来维持平衡。
-
红黑树:红黑树在插入、删除操作上通常比AVL树更快,因为它不需要那么严格的平衡,旋转次数较少。查找操作虽然略慢于AVL树,但在实际应用中这种差异通常不大。
总结:
- AVL树提供了更严格的平衡,确保了更好的最坏情况性能,但可能导致插入和删除操作更耗时。
- 红黑树提供了较为宽松的平衡条件,通常在插入、删除和查找操作中提供了较好的平均性能。
所以在实际应用中,红黑树更多。
总结
红黑树的插入操作虽然相对复杂,但其核心思想是通过调整树的结构来保持红黑树的五大性质。在插入过程中,我们主要涉及到颜色变换和树的旋转。通过这些操作,我们能够确保红黑树的平衡,从而保证其查找、插入和删除操作的时间复杂度为O(log n)。虽然删除操作更为繁琐,但掌握了插入操作的精髓,删除操作的理解就会相对简单,所以本文暂不介绍删除操作。
创作不易,希望这篇文章能给你带来启发和帮助,如果喜欢这篇文章,请留下你的三连,你的支持的我最大的动力!!!