文章目录
- 前言
- 一、红黑树介绍
- 1、红黑树的概念
- 2、红黑树的性质
- 二、实现红黑树
- 1、基本框架
- 2、插入
- 3、删除
- 4、查找
- 5、测试红黑树
- 6、红黑树代码
- 三、红黑树性能
- 四、AVL树和红黑树的差别
前言
红黑树是一种二叉搜索树,所以学习前需要学会基本的二叉搜索树,并且需要了解左右旋转操作。
一、红黑树介绍
1、红黑树的概念
红黑树是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或 Black。
通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路 径会比其他路径长出俩倍,因而是接近平衡的,从而获得较高的查找、插入和删除性能。
2、红黑树的性质
- 每个结点不是红色就是黑色
- 根节点是黑色的
- 如果一个节点是红色的,则它的两个孩子结点是黑色的
- 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均 包含相同数目的黑色结点
- 每个叶子结点都是黑色的(此处的叶子结点指的是空结点)
这些性质确保了红黑树从根到叶子的最长路径不会是最短路径的两倍长,从而保持树的相对平衡。
二、实现红黑树
1、基本框架
(1)红黑树节点
//状态
enum Color
{
BLACK, //黑色
RED //红色
};
//树节点
template<class K,class T>
struct RBTreeNode
{
RBTreeNode<K,T>* _left; //左
RBTreeNode<K, T>* _right; //右
RBTreeNode<K, T>* _parent; //父
Color _color; //状态表示
pair<K, T> _val; //数据
//构造函数 状态默认红色
RBTreeNode(const pair<K, T>& val = pair<K, T>()):_val(val),_left(nullptr),_right(nullptr),
_parent(nullptr),_color(RED)
{}
};
(2)红黑树类
template<class K,class T>
class RBTree
{
//重命名
typedef RBTreeNode<K, T> Node;
public:
RBTree() {};
~RBTree() {};
bool Insert(const pair<K, T>& val);
bool Erase(const K& key);
Node* Find(const K& key);
private:
// 右单旋
void RotateR(Node* parent)
{
//左节点
Node* L = parent->_left;
//左子树右边第一个节点
Node* Lr = L->_right;
//parent的父亲
Node* pparent = parent->_parent;
//连接过程
L->_right = parent;
parent->_parent = L;
//该节点可能为空
if (Lr)
{
Lr->_parent = parent;
}
parent->_left = Lr;
//更新L的父节点
L->_parent = pparent;
//是根的情况
if (pparent == nullptr)
{
_root = L;
}
else
{
if (parent == pparent->_left) pparent->_left = L;
else pparent->_right = L;
}
}
//左旋转
void RotateL(Node* parent)
{
//右边第一个节点
Node* R = parent->_right;
//右子树第一个左节点
Node* Rl = R->_left;
//父节点
Node* pparent = parent->_parent;
//连接过程
parent->_right = Rl;
if (Rl)
{
Rl->_parent = parent;
}
R->_left = parent;
//更新parent的父节点
parent->_parent = R;
//更新R的父节点
R->_parent = pparent;
//是根的情况
if (nullptr == pparent)
{
_root = R;
}
else
{
if (pparent->_left == parent) pparent->_left = R;
else pparent->_right = R;
}
}
Node* _root;
};
2、插入
红黑树的插入操作是一个复杂但高效的过程,它确保了树在插入新节点后仍然保持平衡。
(1)基本步骤:
1、找到插入位置:与二叉搜索树相同,首先通过比较节点值找到新节点应该插入的位置。
2、插入新节点:将新节点插入到找到的位置,并将其初始颜色设置为红色。这是因为将新节点设置为红色可以最小化对树平衡性的影响,同时满足红黑树的性质(对于破坏性质4来说,破坏性质3代价更小)。
3、调整树以保持平衡:插入红色节点后,可能会破坏红黑树的性质。为了恢复这些性质,需要进行一系列的旋转和重新着色操作。
(2)讨论插入节点后维持树的性质的情况
情况一:如果插入节点的p为黑色,不做处理也满足红黑树性质,结束。
情况二:p和u都是红色。
分析:通过性质可以推导出g为黑色
解决:将p和u变为黑色,将g变为红色。如果g是根节点,则将其变为黑色。否则,继续对g进行同样的调整。
情况三:p为红,u为黑或者为空
分析:
a.如果是u不存在,那么cur一定是新插入的节点,因为如果不是新插入节点,那么cur和p就一定会存在一个黑色节点,那么每条路径的黑色节点就不一致了。
b.如果u存在且为黑,那么cur不是新插入节点且一定是黑,因为p是红u为黑,那么说明p向下这条路径一定存在一个黑色节点,如果cur是新插入就会是红,就会导致原来的树就不满足性质4,如果cur原来是红色节点那么就不满足性质3了(为情况二转变而来)。
c.经过旋转后的树已经是红黑树了,结束。
子情况一:cur、p、g都在一边
解决:都在左边,g变红p变黑,对g使用右旋转(都在右边,g边红p变黑,对g使用左旋转)。
子情况二:cur和p,p和g不同边
解决:cur和p在右边,p和g在左边,cur变黑,g变红,对p使用左旋转,再对g使用右旋转(:cur和p在左边,p和g在右边,cur变黑,g变红,对p使用右旋转,再对g使用左旋转)。
3、删除
红黑树是一种自平衡的二叉搜索树,它通过修改节点颜色及执行特定的旋转操作来确保树在添加或删除节点后继续保持平衡。删除操作是红黑树中最复杂的操作之一,因为它需要在删除节点后恢复红黑树的性质。
(1)基本步骤:
1、查找节点: 首先,需要在树中找到需要删除的节点。如果找不到,则直接结束。
2、删除节点:如果找到的节点有两个子节点,通常的做法是用它的后继节点(即右子树中的最小节点)来替换它,并删除原后继节点。这保证了被删除的节点最多只有一个非空子节点。
3、修复树: 删除节点后,需要修复树以保持红黑树的特性: 每个节点是红色或黑色。 根节点是黑色。 每个叶子节点(NIL节点,空节点)是黑色。如果一个节点是红色的,则它的子节点必须是黑色的。 从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。
(2)讨论删除后恢复树的情况
情况一:cur是红色
解决:直接删除即可,不会影响红黑树的性质,结束。
情况二:cur为黑色,且cur存在一个子节点
分析:cur为黑色,且cur存在一个子节点,那么cur的子节点一定是红色。
解决:p连接cur的子节点,且cur子节点改为黑色,结束。
情况三:cur为黑色且不存在子节点
分析:此时想要不破坏性质就需要观察cur的兄弟节点b了。
子情况一:b为红色
解决:b在p的左侧,b变黑,p变红,再进行右旋转(b在p的右侧,b变黑,p变红,再进行左旋转),变化后仍然没有恢复红黑树的性质,需要再次对cur进行调整(此时可能会转化成其他情况),继续。
子情况二:b为黑,b的子节点都为空或者孩子节点都为黑
解决:将b改为红,如果p为红将p改为黑结束,如果p不为红,进行迭代,cur = p,继续。
子情况三:b为黑且存在一个红色子节点(剩余的一个子树任意)
假设:cur在p的左侧且b的右节点br为红
解决:将b变为p的颜色,p和br变为黑色,再对p左旋转,旋转完红黑树恢复,结束。
假设:cur在p的右侧且b的左节点bl为红
解决:将b变为p的颜色,p和bl变为黑色,再对p右旋,旋转完红黑树恢复,结束。
假设:cur在p的左侧且b的左节点bl为红
解决:将bl变为p的颜色,p和b变为黑色,先进行右旋转b再进行p左旋,旋转完红黑树恢复,结束。
假设:cur在p的右侧且b的右节点br为红
解决:将br变为p的颜色,p和b变为黑色,先进行对b左旋再进行p右旋,旋转完红黑树恢复,结束。
4、查找
红黑树查找与二叉搜索树一样。
步骤:
1、开始于根节点:查找操作从树的根节点开始。 比较节点值:将查找值与当前节点的值进行比较。
2、
如果查找值等于当前节点的值,则查找成功,返回该节点。
如果查找值小于当前节点的值,则移动到左子节点。
如果查找值大于当前节点的值,则移动到右子节点。3、重复步骤2:继续比较并移动,直到找到包含查找值的节点,或者遇到一个叶子节点(NIL节点),这意味着查找值不在树中。
4、查找结束:如果找到包含查找值的节点,则返回该节点;否则,返回NULL或者一个特殊的值表示查找失败。
5、测试红黑树
通过判断根节点是否为黑+每一条路径黑色节点数量是否一样+是否存在父子都为红。
测试代码:
bool IsBalance()
{
if (_root == nullptr)
return true;
if (_root->_color == RED)
{
return false;
}
// 参考值
int refNum = 0;
Node* cur = _root;
while (cur)
{
if (cur->_color == BLACK)
{
++refNum;
}
cur = cur->_left;
}
return Check(_root, 0, refNum);
}
bool Check(Node* root, int blackNum, const int refNum)
{
if (root == nullptr)
{
//cout << blackNum << endl;
if (refNum != blackNum)
{
cout << "存在黑色节点的数量不相等的路径" << endl;
return false;
}
return true;
}
//cout << root->_val.first << endl;
if (root->_color == RED && root->_parent->_color == RED)
{
cout << root->_val.first << "存在连续的红色节点" << endl;
return false;
}
if (root->_color == BLACK)
{
blackNum++;
}
return Check(root->_left, blackNum, refNum)
&& Check(root->_right, blackNum, refNum);
}
void test()
{
RBTree<int, int> rb;
vector<int> arr;
for (int i = 1; i <= 10000; i++)
{
arr.push_back(rand());
}
for (int i = 0; i < arr.size(); i++)
{
int e = arr[i];
rb.Insert({ e,e });
if (rb.IsBalance())
cout << "是红黑树" << endl;
else
assert(false);
}
for (int i = 0; i < arr.size(); i++)
{
rb.Erase(arr[i]);
if (rb.IsBalance())
cout << "是红黑树" << endl;
else
assert(false);
}
}
通过插入10000随机数观察是否报错。
在运行结束后仍然没有报错说明插入、删除没有问题。
6、红黑树代码
#pragma once
#include<iostream>
#include<vector>
#include<assert.h>
using namespace std;
//状态
enum Color
{
BLACK, //黑色
RED //红色
};
//树节点
template<class K,class T>
struct RBTreeNode
{
RBTreeNode<K,T>* _left; //左
RBTreeNode<K, T>* _right; //右
RBTreeNode<K, T>* _parent; //父
Color _color; //状态表示
pair<K, T> _val; //数据
//构造函数 状态默认红色
RBTreeNode(const pair<K, T>& val = pair<K, T>()):_val(val),_left(nullptr),_right(nullptr),
_parent(nullptr),_color(RED)
{}
};
template<class K,class T>
class RBTree
{
//重命名
typedef RBTreeNode<K, T> Node;
public:
RBTree() {};
~RBTree() {};
//插入
bool Insert(const pair<K, T>& val)
{
//找到放val的位置
Node* cur = _root;
//作为前驱指针,与val节点连接
Node* parent = nullptr;
while (cur)
{
//向左
if (cur->_val.first > val.first)
{
parent = cur;
cur = cur->_left;
}
//向右
else if (cur->_val.first < val.first)
{
parent = cur;
cur = cur->_right;
}
//存在相同的值
else
{
return false;
}
}
//插入新节点
cur = new Node(val);
//不存在根节点,作为根节点
if (parent == nullptr) _root = cur;
//连接在前驱指针左侧
else if (parent->_val.first > val.first)
{
cur->_parent = parent;
parent->_left = cur;
}
//连接在前驱指针右侧
else
{
cur->_parent = parent;
parent->_right = cur;
}
//父亲不为空且孩子和父亲都为红 --- 违规规则三
while (parent != nullptr &&parent->_color == RED && cur->_color == RED)
{
Node* grandfather = parent->_parent; //祖父
Node* uncle = nullptr; //叔叔
if (grandfather->_left == parent)
uncle = grandfather->_right;
else
uncle = grandfather->_left;
//情况一 uncle存在且为红 - 祖父变红父亲和叔叔变黑,向上更新
if (uncle != nullptr && uncle->_color == RED)
{
parent->_color = uncle->_color = BLACK;
grandfather->_color = RED;
}
//情况二 uncle不存在\存在
else
{
//子情况一,cur 、parent在一边
//都在左边 -- 使用右旋转
if (grandfather->_left == parent && parent->_left == cur)
{
RotateR(grandfather);
grandfather->_color = RED;
parent->_color = BLACK;
}
//都在右边 --- 使用左旋转
else if (grandfather->_right == parent && parent->_right == cur)
{
RotateL(grandfather);
grandfather->_color = RED;
parent->_color = BLACK;
}
//子情况二 cur与parent不再同一边
//cur在右边parent在左边 --对parent左旋转再对grandfather右旋转
else if (grandfather->_left == parent && parent->_right == cur)
{
RotateL(parent);
RotateR(grandfather);
cur->_color = BLACK;
grandfather->_color = RED;
}
//cur在左边parent在右边 --对parent右旋转再对grandfather左旋转
else if (grandfather->_right == parent && parent->_left == cur)
{
RotateR(parent);
RotateL(grandfather);
cur->_color = BLACK;
grandfather->_color = RED;
}
//经过旋转后树达到平衡
break;
}
//迭代
cur = grandfather;
parent = cur->_parent;
}
//根节点一定是黑色的
_root->_color = BLACK;
return true;
}
//删除
bool Erase(const K& key)
{
//从根节点开始搜索
Node* cur = _root;
//作为cur的前驱指针
Node* parent = nullptr;
//搜索查找
while (cur)
{
if (cur->_val.first > key)
{
parent = cur;
cur = cur->_left;
}
else if (cur->_val.first < key)
{
parent = cur;
cur = cur->_right;
}
else
break;
}
//找不到
if (cur == nullptr) return false;
//假设cur左右节点都存在,找右边最小值替换
if (cur->_left != nullptr && cur->_right != nullptr)
{
Node* tmp1 = cur->_right;
Node* tmp2 = nullptr;
while (tmp1->_left)
{
tmp2 = tmp1;
tmp1 = tmp1->_left;
}
cur->_val = tmp1->_val;
//tmp1左边没有节点,自己就是最小的节点
if (tmp2 == nullptr)
{
parent = cur;
cur = tmp1;
}
else
{
cur = tmp1;
parent = tmp2;
}
}
//如果是cur根节点
if (parent == nullptr)
{
if (cur->_left == nullptr)
{
_root = cur->_right;
if (_root != nullptr)
_root->_color = BLACK;
}
else
{
_root = cur->_left;
_root->_color = BLACK;
}
delete cur;
return true;
}
//标记要删除的节点
Node* deletecur = cur;
//删除节点为红色,直接删除结束
if (cur->_color == RED)
{
if (cur->_left == nullptr)
{
if (cur == parent->_left) parent->_left = cur->_right;
else parent->_right = cur->_right;
}
else
{
if (cur == parent->_right) parent->_right = cur->_left;
else parent->_right = cur->_left;
}
}
//删除节点为黑色
else
{
//存在左子树,变为黑色
if (cur->_left != nullptr)
{
cur->_left->_color = BLACK;
if (cur == parent->_right)
parent->_right = cur->_left;
else
parent->_left = cur->_left;
cur->_left->_parent = parent;
}
//存在右子树
else if (cur->_right != nullptr)
{
cur->_right->_color = BLACK;
if (cur == parent->_left)
parent->_left = cur->_right;
else
parent->_right = cur->_right;
cur->_right->_parent = parent;
}
//不存在孩子
else
{
while (parent != nullptr)
{
int sign = 0;
if (cur == parent->_left) sign = -1;
else sign = 1;
if (cur == deletecur)
{
if (sign == -1) parent->_left = nullptr;
else parent->_right = nullptr;
}
//兄弟节点
Node* brothers = nullptr;
if (sign == -1) brothers = parent->_right;
else brothers = parent->_left;
//兄弟节点为红色
if (brothers->_color == RED)
{
//变色
brothers->_color = BLACK;
parent->_color = RED;
//在右边-》左转
if (brothers == parent->_right)
{
RotateL(parent);
}
//在左边-》右转
else
{
RotateR(parent);
}
//重新更新brothers
if (sign == -1) brothers = parent->_right;
else brothers = parent->_left;
}
//兄弟节点的孩子节点为空或者孩子节点为黑
if ((brothers->_left == nullptr || brothers->_left->_color == BLACK) &&
(brothers->_right == nullptr || brothers->_right->_color == BLACK))
{
brothers->_color = RED;
//父亲为红色时直接结束
if (parent->_color == RED)
{
parent->_color = BLACK;
break;
}
//更新继续
cur = parent;
parent = parent->_parent;
}
//兄弟节点不为空有一个孩子为红或者都为红
else
{
//cur作为parend的左孩子
if (sign == -1)
{
//作为brothers右孩子--左旋
if (brothers->_right && brothers->_right->_color == RED)
{
brothers->_color = parent->_color;
parent->_color = brothers->_right->_color = BLACK;
RotateL(parent);
}
//作为brothers左孩子,先右旋再左旋
else if (brothers->_left && brothers->_left->_color== RED)
{
brothers->_left->_color = parent->_color;
brothers->_color = parent->_color = BLACK;
RotateR(brothers);
RotateL(parent);
}
}
else
{
//作为brothers左孩子--右旋
if (brothers->_left&&brothers->_left->_color == RED)
{
brothers->_color = parent->_color;
parent->_color = brothers->_left->_color = BLACK;
RotateR(parent);
}
//作为brothers右孩子--先左旋再右旋
else if (brothers->_right && brothers->_right->_color == RED)
{
brothers->_right->_color = parent->_color;
brothers->_color = parent->_color = BLACK;
RotateL(brothers);
RotateR(parent);
}
}
//再旋转完后性质恢复,结束
break;
}
}
}
}
delete deletecur;
return true;
}
//查找
Node* Find(const K& key)
{
//从根节点开始
Node* cur = _root;
while (cur)
{
//大了就去左子树中搜索
if (cur->_val.first > key)
{
cur = cur->_left;
}
//小了就去右子树中搜索
else if (cur->_val.first < key)
{
cur = cur->_right;
}
else
{
//找到返回当前节点
return cur;
}
}
return nullptr;
}
bool IsBalance()
{
if (_root == nullptr)
return true;
if (_root->_color == RED)
{
return false;
}
// 参考值
int refNum = 0;
Node* cur = _root;
while (cur)
{
if (cur->_color == BLACK)
{
++refNum;
}
cur = cur->_left;
}
return Check(_root, 0, refNum);
}
private:
// 右单旋
void RotateR(Node* parent)
{
//左节点
Node* L = parent->_left;
//左子树右边第一个节点
Node* Lr = L->_right;
//parent的父亲
Node* pparent = parent->_parent;
//连接过程
L->_right = parent;
parent->_parent = L;
//该节点可能为空
if (Lr)
{
Lr->_parent = parent;
}
parent->_left = Lr;
//更新L的父节点
L->_parent = pparent;
//是根的情况
if (pparent == nullptr)
{
_root = L;
}
else
{
if (parent == pparent->_left) pparent->_left = L;
else pparent->_right = L;
}
}
//左旋转
void RotateL(Node* parent)
{
//右边第一个节点
Node* R = parent->_right;
//右子树第一个左节点
Node* Rl = R->_left;
//父节点
Node* pparent = parent->_parent;
//连接过程
parent->_right = Rl;
if (Rl)
{
Rl->_parent = parent;
}
R->_left = parent;
//更新parent的父节点
parent->_parent = R;
//更新R的父节点
R->_parent = pparent;
//是根的情况
if (nullptr == pparent)
{
_root = R;
}
else
{
if (pparent->_left == parent) pparent->_left = R;
else pparent->_right = R;
}
}
bool Check(Node* root, int blackNum, const int refNum)
{
if (root == nullptr)
{
//cout << blackNum << endl;
if (refNum != blackNum)
{
cout << "存在黑色节点的数量不相等的路径" << endl;
return false;
}
return true;
}
//cout << root->_val.first << endl;
if (root->_color == RED && root->_parent->_color == RED)
{
cout << root->_val.first << "存在连续的红色节点" << endl;
return false;
}
if (root->_color == BLACK)
{
blackNum++;
}
return Check(root->_left, blackNum, refNum)
&& Check(root->_right, blackNum, refNum);
}
Node* _root;
};
三、红黑树性能
- 时间复杂度 查找、插入和删除操作:红黑树通过特定的旋转和重新着色操作来保持树的平衡,从而确保这些操作的时间复杂度保持在O(log n),其中n是树中元素的数目。这使得红黑树在大数据集上仍然能够保持高效的性能。
- 平衡性 平衡因子:与AVL树不同,红黑树并不直接维护每个节点的平衡因子(左子树高度减去右子树高度)。相反,它通过维护一系列颜色相关的性质来间接保持树的平衡。这些性质包括节点的颜色(红色或黑色)、根节点是黑色、所有叶子节点是黑色、红色节点的两个子节点都是黑色等。
路径长度限制:红黑树的这些性质确保了从根到任意叶子节点的最长路径不会超过最短路径的两倍。这种平衡性保证了在最坏情况下,查找、插入和删除操作的时间复杂度仍然是O(log
n)。- 插入和删除操作的性能 插入操作:在插入新节点时,红黑树会首先将其标记为红色,并通过一系列的旋转和重新着色操作来恢复树的平衡。由于红黑树的自平衡策略相对宽松,因此在插入操作中通常比AVL树具有更好的性能。
删除操作:删除操作可能稍微复杂一些,因为它涉及到节点的删除以及后续可能的旋转和重新着色操作来恢复树的平衡。然而,与插入操作类似,删除操作的时间复杂度也保持在O(log
n)。
四、AVL树和红黑树的差别
- 调整平衡的实现机制 AVL树:AVL树通过维护每个节点的平衡因子(左子树高度减去右子树高度)来实现平衡。在插入或删除节点后,如果某个节点的平衡因子绝对值大于1,则需要通过旋转操作来恢复平衡。AVL树的平衡条件是严格的,即所有节点的左右子树高度差的绝对值不超过1。
红黑树:红黑树则通过节点的颜色(红色或黑色)和一系列旋转操作来保持树的平衡。红黑树的平衡条件相对宽松,它要求从根节点到叶子节点的所有路径上黑色节点的数量相同,从而间接保证了树的平衡性。在插入或删除节点后,红黑树通过重新着色和旋转操作来恢复平衡,且任何不平衡都可以在三次旋转之内解决。- 性能差异 插入和删除操作:由于AVL树需要保持严格的平衡性,因此在插入或删除节点时可能需要进行更多的旋转操作,这在一定程度上降低了其性能。相比之下,红黑树通过非严格的平衡条件换取了更少的旋转次数,从而在插入和删除操作上表现出更高的效率。
查找操作:在查找操作上,AVL树和红黑树的性能相当。它们都保持了二叉查找树的性质,可以在对数时间内完成查找操作。然而,由于AVL树的高度通常比红黑树更低(在相同节点数的情况下),因此从理论上讲,AVL树的查找效率可能会略高于红黑树,但这种差异在实际应用中往往可以忽略不计。- 内存占用 AVL树需要存储每个节点的平衡因子,这增加了额外的内存开销。 红黑树则通过节点的颜色属性来间接维护平衡性,无需存储额外的平衡因子信息,因此在内存占用上更为节省。
- 适用场景 AVL树:适用于查找操作频繁且对内存占用要求不高的场景。由于AVL树保持了严格的平衡性,因此在查找性能上具有一定优势。然而,其插入和删除操作的性能相对较低,不适合数据变动频繁的场景。
红黑树:适用于插入和删除操作频繁且对查找性能要求不是极端严格的场景。红黑树通过牺牲一定的平衡性来换取更高的插入和删除效率,因此在这些场景下表现出更好的整体性能。此外,红黑树的内存占用更低,也使其在一些内存受限的环境中更具优势。
综上所述,AVL树和红黑树在调整平衡的实现机制、性能差异、内存占用和适用场景等方面存在显著的区别。在选择使用哪种数据结构时,需要根据具体的应用场景和需求进行综合考虑。