文章目录
- 红黑树
- 前言
- 1. 红黑树的概念及性质
- 1.1 红黑树的概念
- 1.2 红黑树的性质
- 2. 红黑树的结构
- 2.1 红黑树节点的定义
- 2.2 红黑树的结构
- 3. 红黑树的操作
- 3.1 红黑树的查找
- 3.2 红黑树的插入
- 处理红黑树颜色的过程(重点)
- 情况1: 只变色
- 情况2: 变色 + 单旋
- 情况3: 变色 + 双旋
- 处理颜色的代码
- 3.3 红黑树的验证
- 4. 红黑树完整代码
- 5. 红黑树与AVL树的比较
红黑树
前言
前面学习了AVL树, 它是一棵绝对平衡的二叉搜索树,查找的效率很高,结构完美严格,但是一旦对AVL树做插入操作就可能会引发旋转,使性能降低。所以提出了另一种实现平衡二叉搜索树的思路,即红黑树。
1. 红黑树的概念及性质
1.1 红黑树的概念
红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出两倍,因而是接近平衡的。
1.2 红黑树的性质
- 每个结点不是红色就是黑色
- 根节点是黑色的
- 如果一个节点是红色的,则它的两个孩子结点是黑色的 — 不能出现连续的红色节点。
- 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点 — 每条路径上都有相同数量的黑色节点
- 每个叶子结点都是黑色的(此处的叶子结点指的是空结点)
满足上面的性质,最短路径是全是黑色节点,如下图的红黑树,最左路径即最短路径有两个黑色节点,那么要满足每条路径上都有相同数量的黑色节点,所以其他路径上黑色节点与最短路径黑色节点数量相同,再添加一些红色节点,最长路径是一红一黑的相间路径,下图中其最长路径中节点个数是最短路径节点个数的两倍。由此可得: 红黑树最长路径中节点个数不会超过最短路径节点个数的两倍。
最短路径: 全是黑色节点
最长路径: 一个黑色节点一个红色节点,红黑相间
由此也引入两种情况:
- 最好情况: 左右平衡,全部是黑色节点或每条路径都是一黑一红相间,此时接近满二叉树,假设全部黑色节点有N个,红色节点数量不会超过黑色节点数量,此时整棵树的节点数量保持在[N, 2N]之间,平均下来路径高度为logN
- 最坏情况: 左右极其不平衡,左子树全黑,且右子树一黑一红。那么此时最长路径为2* logN
最好情况与最坏情况对于计算机来说相差并不大,比如有10亿个节点,AVL树最多查找30次左右,红黑树最多查找60次左右,两者虽然相差二倍,但在往树上插入节点时,AVL可能会引发大量的旋转使性能降低。比如上面形状的那棵树,AVL树时一定旋转,红黑树不一定旋转。
2. 红黑树的结构
2.1 红黑树节点的定义
相比于AVL树,红黑树不用定义平衡因子,但是想要表示节点的颜色,所以可以定义一个枚举类型表示节点颜色,还是向AVL树一样定义成三叉链的结构,方便后续找父节点来修改颜色及旋转。
//节点的颜色
enum Colour
{
RED,
BLACK,
};
template<class K, class V>
struct RBTreeNode //三叉链
{
RBTreeNode<K, V>* _left;
RBTreeNode<K, V>* _right;
RBTreeNode<K, V>* _parent;
pair<K, V> _kv;
Colour _col;
RBTreeNode(const pair<K, V>& kv)
:_left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _kv(kv)
, _col(RED) //默认颜色给红色
{}
};
2.2 红黑树的结构
//节点的颜色
enum Colour
{
RED,
BLACK,
};
template<class K, class V>
struct RBTreeNode //三叉链
{
RBTreeNode<K, V>* _left;
RBTreeNode<K, V>* _right;
RBTreeNode<K, V>* _parent;
pair<K, V> _kv;
Colour _col;
RBTreeNode(const pair<K, V>& kv)
:_left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _kv(kv)
, _col(RED) //默认颜色给红色,假设是黑色,插入此节点会使这条路径黑节点数量变,其他路径也要跟着变
{}
};
template<class K, class V>
class RBTree
{
typedef RBTreeNode<K, V> Node;
public:
//一系列操作
private:
Node* _root = nullptr;
};
当然还有另一种红黑树的实现方式:
红黑树的实现中增加一个头结点, 因为跟节点必须为黑色,为了与根节点进行区分,将头结点给成黑色,并且让头结点的 pParent 域指向红黑树的根节点,pLeft域指向红黑树中最小的节点,_pRight域指向红黑树中最大的节点。
3. 红黑树的操作
3.1 红黑树的查找
与搜索二叉树查找思路完全类似,就是注意这里是用key值来查找
Node *Find(const K &key)
{
Node *cur = _root;
while (cur)
{
if (cur->_kv.first < key)
{
cur = cur->_right;
}
else if (cur->_kv.first > key)
{
cur = cur->_left;
}
else
{
return cur;
}
}
return nullptr;
}
3.2 红黑树的插入
红黑树的删除比较复杂,就不实现了
插入整体的大框架与AVL树相似,还是先找到要插入的位置再去链接的过程,重点是后续处理红黑树颜色的过程
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);
if (parent->_kv.first > kv.first)
{
parent->_left = cur;
}
else
{
parent->_right = cur;
}
cur->_parent = parent;
//接下来需要处理红黑树的颜色
//...
}
处理红黑树颜色的过程(重点)
此问题的核心: 寻找叔叔节点, 总体上分为3种情况:
情况1: 只变色
情况2: 变色 + 单旋
情况3: 变色 + 双旋
处理颜色的代码
这里颜色处理要结合之前AVL树中的旋转,同时需要将旋转函数中平衡因子的更新去掉
while (parent && parent->_col == RED)
{
Node *grandfather = parent->_parent;
// 找叔叔 => 看父亲在祖父的哪边
if (grandfather->_left == parent)
{
Node *uncle = grandfather->_right;
// 3种情况
// 情况1: u存在且为红, 变色处理, 并继续向上处理
// 变色: p,u变黑, g变红
if (uncle && uncle->_col == RED)
{
parent->_col = BLACK;
uncle->_col = BLACK;
grandfather->_col = RED;
// 继续向上调整
cur = grandfather;
parent = cur->_parent;
}
else // 情况2+3: u不存在/u存在且为黑, 旋转 + 变色
{
// g
// p u
// c
if (cur == parent->_left)
{
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 //(grandfather->_right == parent)
{
Node *uncle = grandfather->_left;
// 3种情况
// 情况1: u存在且为红, 变色处理, 并继续向上处理
// 变色: p,u变黑, g变红
if (uncle && uncle->_col == RED)
{
parent->_col = BLACK;
uncle->_col = BLACK;
grandfather->_col = RED;
// 继续向上调整
cur = grandfather;
parent = cur->_parent;
}
else // 情况2+3: u不存在/u存在且为黑, 旋转 + 变色
{
// g
// u p
// c
if (cur == parent->_right)
{
RotateL(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
else
{
// g
// u p
// c
RotateR(parent);
RotateL(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break;
}
}
}
_root->_col = BLACK; //根节点是黑色的
3.3 红黑树的验证
红黑树的验证分为两步:
-
验证其为二叉搜索树
- 若中序遍历可得到一个有序序列,就说明为二叉搜索树
-
验证其是否满足红黑树的性质
重点实现验证满足红黑树的性质:
- 检查根节点
- DFS检查黑色节点数量,同时用一个基准值来记录最左路径的黑色节点数量,各个路径与之比较黑色节点数量
- 检查不能存在连续的红色节点(反向检查)
bool IsBalance() // 重点检查规则
{
// 先检查根节点
if (_root && _root->_col == RED)
{
cout << "根节点颜色是红色" << endl;
return false;
}
int benchmark = 0; // 基准值
Node *cur = _root;
while (cur) // 走最左路径
{
if (cur->_col == BLACK)
++benchmark;
cur = cur->_left;
}
// 连续红色节点
return _Check(_root, 0, benchmark);
}
bool _Check(Node *root, int blackNum, int benchmark) // 基准值
{
if (root == nullptr) // 空树也是红黑树
{
if (benchmark != blackNum)
{
cout << "某条路径黑色节点的数量不相等" << endl;
return false;
}
return true;
}
// DFS检查黑色节点数量
if (root->_col == BLACK)
{
++blackNum;
}
// 反向检查 ---> 红色节点不能连续
if (root->_col == RED && root->_parent && root->_parent->_col == RED)
{
cout << "存在连续的红色节点" << endl;
return false;
}
return _Check(root->_left, blackNum, benchmark) && _Check(root->_right, blackNum, benchmark);
}
4. 红黑树完整代码
#include<iostream>
#include<utility>
#include<assert.h>
#include<stdlib.h>
using namespace std;
//节点的颜色
enum Colour
{
RED,
BLACK,
};
template<class K, class V>
struct RBTreeNode //三叉链
{
RBTreeNode<K, V>* _left;
RBTreeNode<K, V>* _right;
RBTreeNode<K, V>* _parent;
pair<K, V> _kv;
Colour _col;
RBTreeNode(const pair<K, V>& kv)
:_left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _kv(kv)
, _col(RED) //默认颜色给红色
{}
};
template<class K, class V>
class RBTree
{
typedef RBTreeNode<K, V> Node;
public:
~RBTree()
{
_Destroy(_root);
_root = nullptr;
}
Node* Find(const K& key)
{
Node* cur = _root;
while (cur)
{
if (cur->_kv.first < key)
{
cur = cur->_right;
}
else if (cur->_kv.first >key)
{
cur = cur->_left;
}
else
{
return cur;
}
}
return nullptr;
}
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);
if (parent->_kv.first > kv.first)
{
parent->_left = cur;
}
else
{
parent->_right = cur;
}
cur->_parent = parent;
//处理红黑树颜色
while (parent && parent->_col==RED)
{
Node* grandfather = parent->_parent;
//找叔叔 => 看父亲在祖父的哪边
if (grandfather->_left == parent)
{
Node* uncle = grandfather->_right;
//3种情况
//情况1: u存在且为红, 变色处理, 并继续向上处理
//变色: p,u变黑, g变红
if (uncle && uncle->_col == RED)
{
parent->_col = BLACK;
uncle->_col = BLACK;
grandfather->_col = RED;
//继续向上调整
cur=grandfather;
parent = cur->_parent;
}
else //情况2+3: u不存在/u存在且为黑, 旋转 + 变色
{
// g
// p u
// c
if(cur==parent->_left)
{
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 //(grandfather->_right == parent)
{
Node* uncle = grandfather->_left;
//3种情况
//情况1: u存在且为红, 变色处理, 并继续向上处理
//变色: p,u变黑, g变红
if (uncle && uncle->_col == RED)
{
parent->_col = BLACK;
uncle->_col = BLACK;
grandfather->_col = RED;
//继续向上调整
cur = grandfather;
parent = cur->_parent;
}
else //情况2+3: u不存在/u存在且为黑, 旋转 + 变色
{
// g
// u p
// c
if (cur == parent->_right)
{
RotateL(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
else
{
// g
// u p
// c
RotateR(parent);
RotateL(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break;
}
}
}
_root->_col = BLACK; //根节点是黑色的
return true;
}
void Inorder()
{
_Inorder(_root);
}
int Height()
{
return _Height(_root);
}
bool IsBalance() //重点检查规则
{
//先检查根节点
if (_root && _root->_col == RED)
{
cout << "根节点颜色是红色" << endl;
return false;
}
int benchmark = 0; //基准值
Node* cur = _root;
while(cur) //走最左路径
{
if (cur->_col == BLACK)
++benchmark;
cur = cur->_left;
}
//连续红色节点
return _Check(_root, 0,benchmark);
}
private:
//左单旋
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
parent->_right = subRL;
if (subRL)
subRL->_parent = parent;
Node* ppnode = parent->_parent;
subR->_left = parent;
parent->_parent = subR;
if (ppnode == nullptr) //parent本身就是根
{
_root = subR;
_root->_parent = nullptr;
}
else //parent只是一棵子树
{
if (ppnode->_left == parent) //判断原来的节点是左右哪一棵子树
{
ppnode->_left = subR;
}
else
{
ppnode->_right = subR;
}
subR->_parent = ppnode;
}
}
//右单旋
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
if (subLR)
subLR->_parent = parent;
Node* ppnode = parent->_parent;
subL->_right = parent;
parent->_parent = subL;
if (ppnode == nullptr)
{
_root = subL;
_root->_parent = nullptr;
}
else
{
if (ppnode->_left == parent)
{
ppnode->_left = subL;
}
else
{
ppnode->_right = subL;
}
subL->_parent = ppnode;
}
}
int _Height(Node* root)
{
if (root == nullptr)
return 0;
int leftH = _Height(root->_left);
int rightH = _Height(root->_right);
return leftH > rightH ? leftH + 1 : rightH + 1;
}
bool _Check(Node* root, int blackNum, int benchmark) //基准值
{
if (root == nullptr) //空树也是红黑树
{
if (benchmark != blackNum)
{
cout << "某条路径黑色节点的数量不相等" << endl;
return false;
}
return true;
}
//DFS检查黑色节点数量
if (root->_col == BLACK)
{
++blackNum;
}
//反向检查 ---> 红色节点不能连续
if (root->_col == RED
&& root->_parent
&& root->_parent->_col == RED)
{
cout << "存在连续的红色节点" << endl;
return false;
}
return _Check(root->_left, blackNum, benchmark)
&& _Check(root->_right, blackNum, benchmark);
}
void _Destroy(Node* root)
{
if (root == nullptr)
{
return;
}
_Destroy(root->_left);
_Destroy(root->_right);
delete root;
}
void _Inorder(Node* root)
{
if (root == nullptr)
return;
_Inorder(root->_left);
cout << root->_kv.first << " ";
_Inorder(root->_right);
}
Node* _root = nullptr;
};
5. 红黑树与AVL树的比较
红黑树和AVL树都是高效的平衡二叉树,增删改查的时间复杂度都是O( l o g 2 N log_2 N log2N),红黑树不追求绝对平衡,其只需保证最长路径不超过最短路径的2倍,相对而言,降低了插入和旋转的次数,所以在经常进行增删的结构中性能比AVL树更优,而且红黑树实现比较简单,所以实际运用中红黑树更多。