目录:
- 红黑树的概念
- 红黑树的性质
- 红黑树节点的定义
- 红黑树结构
- 红黑树的插入操作
- 红黑树的验证
- 红黑树的代码实现
- 红黑树的删除
- 红黑树与AVL树的比较
- 红黑树的应用
- 总结
红黑树的概念
红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近平衡的。
红黑树的性质
- 每个结点不是红色就是黑色
- 根节点是黑色的
- 如果一个节点是红色的,则它的两个孩子结点是黑色的
- 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点
- 每个叶子结点都是黑色的(此处的叶子结点指的是空结点)
6.新加入到红黑树的节点为红色节点
7.从根节点到叶子节点的最长路径不大于最短路径的2倍
有了上面的几个性质作为限制,即可避免二叉搜索树退化成单支的情况。但是,仅仅避免这种情况还不够,这里还要考虑某个节点到其每个叶子节点路径长度的问题。如果某些路径长度过长,那么,在对这些路径上进行增删查改操作时,效率也会大大降低。这个时候性质4和性质5的作用就比较明显了,有了这两个性质作为约束,即可保证任意节点到其每个叶子节点路径最长不会超过最短路径的2倍。
从图中观察我们可以看到最短的路径必然是由两个黑色节点构成,最长路径是一黑一红相间构成,性质34限定了不能出现两个连续的红色节点,性质4又限定了对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点 。
假设我们从根节点B出发,那么最短路径就是:
B->A,长度为2。
假设我们从B出发,最长路径有4条,分别是:
B->C->D->E
B->C->D>G
B->C->E->H
B->C=>E->I
长度为4。
这里最长的路径正好为最短路径的2倍,不会超过最短路径的2倍。
红黑树节点的定义
#pragma once
enum Colour
{
RED,
BLACK,
};
template<class K,class V>
struct RBTreeNode
{
pair<K, V> _KV;//大写
RBTreeNode<K, V>* _left;
RBTreeNode<K, V>* _right;
RBTreeNode<K, V>* _parent;
Colour _col;
RBTreeNode(const pair<K,V>& KV)
:_KV(KV)
,_let(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_col(RED)//给一个默认红色
{}//构造函数初始值列表
};
红黑树结构
为了后续实现关联式容器简单,红黑树的实现中增加一个头结点,因为跟节点必须为黑色,为了与根节点进行区分,将头结点给成黑色,并且让头结点的 pParent 域指向红黑树的根节点,_pLeft域指向红黑树中最小的节点,_pRight域指向红黑树中最大的节点,如下图:
红黑树的插入操作
红黑树是在二叉搜索树的基础上加上其平衡限制条件,因此红黑树的插入可分为两步:
- 按照二叉搜索的树规则插入新节点
template<class ValueType>
class RBTree
{
//……
bool Insert(const ValueType& data)
{
PNode& pRoot = GetRoot();
if (nullptr == pRoot)
{
pRoot = new Node(data, BLACK);
// 根的双亲为头节点
pRoot->_pParent = _pHead;
_pHead->_pParent = pRoot;
}
else
{
// 1. 按照二叉搜索的树方式插入新节点
// 2. 检测新节点插入后,红黑树的性质是否造到破坏,
// 若满足直接退出,否则对红黑树进行旋转着色处理
}
// 根节点的颜色可能被修改,将其改回黑色
pRoot->_color = BLACK;
_pHead->_pLeft = LeftMost();
_pHead->_pRight = RightMost();
return true;
}
private:
PNode& GetRoot(){ return _pHead->_pParent;}
// 获取红黑树中最小节点,即最左侧节点
PNode LeftMost();
// 获取红黑树中最大节点,即最右侧节点
PNode RightMost();
private:
PNode _pHead;
};
- 检测新节点插入后,红黑树的性质是否造到破坏
因为新节点的默认颜色是红色,因此:如果其双亲节点的颜色是黑色,没有违反红黑树任何性质,则不需要调整;但当新插入节点的双亲节点颜色为红色时,就违反了性质3不能有连在一起的红色节点,此时需要对红黑树分情况来讨论:
约定:cur为当前节点,p为父节点,g为祖父节点,u为叔叔节点
情况一: cur为红,p为红,g为黑,u存在且为红
图一:
图二:
cur和p均为红色,违反了性质3不能有连续的红色节点,此时,就要将p,u改成黑色,g改为红色,然后把g当成cur,让g充当新增节点cur继续向上调整,因为这棵树有可能是一棵子树并且g的双亲也可能是红色。对于上图实际上是一个抽象图,因为a,b,c,d,e可以是任意的红黑树子树,情况会随着这几个抽象图树节点的增加而变化的非常多,我们只需要牢记图一这种抽象图即可。
情况二: cur为红,p为红,g为黑,u不存在/u存在且为黑
p为g的左孩子,cur为p的左孩子,则进行右单旋转;相反,
p为g的右孩子,cur为p的右孩子,则进行左单旋转
p、g变色–p变黑,g变红
u存在且为黑一定是第一种情况变过来的,u不存在则cur一定新插入节点,情况一有可能再变情况一,有可能情况一变成情况二,那么情况二有可能是插入后引发的,有可能是情况一变过来的,这时候就需要做旋转加变色处理。
情况三: cur为红,p为红,g为黑,u不存在/u存在且为黑
图中p为g的左孩子,cur为p的右孩子,这棵树好像是一条折线,这时候就需要进行双旋加变色处理了,先以p为轴点进行左单旋,旋转一次之后变成了情况二,再以cur为轴点进行右单旋。
如果p为g的右孩子,cur为p的左孩子,同理我们先对p为轴点进行右单旋,再对cur为轴点进行左单旋。
总结:我们情况三实际以分二种情况解决,左单旋转和右单旋转,然后再变色处理,关键点我们还是要看叔叔u,比如u存不存在?u存在是红色还是黑色?所以,红黑树大致还是分这三种情况。
红黑树的验证
红黑树的检测分为两步:
- 检测其是否满足二叉搜索树(中序遍历是否为有序序列)
- 检测其是否满足红黑树的性质
bool Check(Node* root, int blackNum, const int ref)
{
if (root == nullptr)
{
//cout << blackNum << endl;
if (blackNum != ref)
{
cout << "违反规则:本条路径的黑色节点的数量跟最左路径不相等" << endl;
return false;
}
return true;
}
if (root->_col == RED && root->_parent->_col == RED)
{
cout << "违反规则:出现连续红色节点" << endl;
return false;
}
if (root->_col == BLACK)
{
++blackNum;
}
return Check(root->_left, blackNum, ref)
&& Check(root->_right, blackNum, ref);
}
bool IsBalance()
{
if (_root == nullptr)
{
return true;
}
if (_root->_col != BLACK)
{
return false;
}
int ref = 0;
Node* left = _root;
while (left)
{
if (left->_col == BLACK)
{
++ref;
}
left = left->_left;
}
return Check(_root, 0, ref);
}
void TestRBTree1()
{
srand(time(0));
const size_t N = 100000;
RBTree<int, int> t;
for (size_t i = 0; i < N; ++i)
{
size_t x = rand();
t.Insert(make_pair(x, x));
//cout << t.IsBalance() << endl;
}
//t.Inorder();
cout << t.IsBalance() << endl;
}
红黑树的代码实现
这是我RBTree.h的代码:
#pragma once
enum Colour
{
RED,
BLACK,
};
template<class K, class V>
struct RBTreeNode
{
pair<K, V> _kv;
RBTreeNode<K, V>* _left;
RBTreeNode<K, V>* _right;
RBTreeNode<K, V>* _parent;
Colour _col;
RBTreeNode(const pair<K, V>& kv)
:_kv(kv)
, _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _col(RED)
{}
};
template<class K, class V>
class RBTree
{
typedef RBTreeNode<K, V> Node;
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;
cur->_parent = parent;
}
else
{
parent->_left = cur;
cur->_parent = parent;
}
while (parent && parent->_col == RED)
{
Node* grandfater = parent->_parent;
if (parent == grandfater->_left)
{
Node* uncle = grandfater->_right;
// 情况一 uncle存在且为红
if (uncle && uncle->_col == RED)
{
parent->_col = uncle->_col = BLACK;
grandfater->_col = RED;
cur = grandfater;
parent = cur->_parent;
}
else
{
if (cur == parent->_left)//cur是父亲的左
{
// 情况二
RotateR(grandfater);
parent->_col = BLACK;
grandfater->_col = RED;
}
else//cur是父亲的右
{
// 情况三
RotateL(parent);
RotateR(grandfater);
cur->_col = BLACK;
grandfater->_col = RED;
}
break;
}
}
else //(parent == grandfater->_right)
{
Node* uncle = grandfater->_left;
if (uncle && uncle->_col == RED)
{
parent->_col = uncle->_col = BLACK;
grandfater->_col = RED;
cur = grandfater;
parent = cur->_parent;
}
else
{
if (cur==parent->_right)//cur是父亲的右
{
RotateL(grandfater);
parent->_col = BLACK;
grandfater->_col = RED;
}
else//cur是父亲的左
{
RotateR(parent);
RotateL(grandfater);
cur->_col = BLACK;
grandfater->_col = RED;
}
break;
}
}
}
_root->_col = BLACK;//根节点一定要变黑
return true;
}
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)
{
_root = subR;
_root->_parent = nullptr;
}
else
{
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 (_root == parent)
if (ppNode == nullptr)
{
_root = subL;
_root->_parent = nullptr;
}
else
{
if (ppNode->_left == parent)
{
ppNode->_left = subL;
}
else
{
ppNode->_right = subL;
}
subL->_parent = ppNode;
}
}
void Inorder()
{
_Inorder(_root);
}
void _Inorder(Node* root)
{
if (root == nullptr)
return;
_Inorder(root->_left);
cout << root->_kv.first << ":" << root->_kv.second << endl;
_Inorder(root->_right);
}
bool Check(Node* root, int blackNum, const int ref)
{
if (root == nullptr)
{
//cout << blackNum << endl;
if (blackNum != ref)
{
cout << "违反规则:本条路径的黑色节点的数量跟最左路径不相等" << endl;
return false;
}
return true;
}
if (root->_col == RED && root->_parent->_col == RED)
{
cout << "违反规则:出现连续红色节点" << endl;
return false;
}
if (root->_col == BLACK)
{
++blackNum;
}
return Check(root->_left, blackNum, ref)
&& Check(root->_right, blackNum, ref);
}
bool IsBalance()
{
if (_root == nullptr)
{
return true;
}
if (_root->_col != BLACK)
{
return false;
}
int ref = 0;
Node* left = _root;
while (left)
{
if (left->_col == BLACK)
{
++ref;
}
left = left->_left;
}
return Check(_root, 0, ref);
}
private:
Node* _root = nullptr;
};
void TestRBTree1()
{
srand(time(0));
const size_t N = 100000;
RBTree<int, int> t;
for (size_t i = 0; i < N; ++i)
{
size_t x = rand();
t.Insert(make_pair(x, x));
//cout << t.IsBalance() << endl;
}
//t.Inorder();
cout << t.IsBalance() << endl;
}
这是我Test.cpp的代码:
#include<iostream>
#include <set>
#include <map>
#include <string>
using namespace std;
#include"RBTree.h"
int main()
{
TestRBTree1();
return 0 ;
}
和前面我们AVL树验证一样,随机插入100000个数,返回的bool是1说明我们写的红黑树是没有问题的,都说红黑树难,确实有点复杂特别是旋转的时候我们要熟悉旋转的过程,相比于AVL,我反倒觉得AVL树要复杂一点,因为AVL树还要考虑平衡因子的更新,如果我们有了AVL树的基础,那么画图理解红黑树就不难了。
红黑树的删除
相较于插入操作,红黑树的删除操作则要更为复杂一些。删除操作首先要确定待删除节点有几个孩子,如果有两个孩子,不能直接删除该节点。而是要先找到该节点的前驱(该节点左子树中最大的节点)或者后继(该节点右子树中最小的节点),然后将前驱或者后继的值复制到要删除的节点中,最后再将前驱或后继删除。由于前驱和后继至多只有一个孩子节点,这样我们就把原来要删除的节点有两个孩子的问题转化为只有一个孩子节点的问题,问题被简化了一些。我们并不关心最终被删除的节点是否是我们开始想要删除的那个节点,只要节点里的值最终被删除就行了,至于树结构如何变化,这个并不重要。红黑树删除操作的复杂度在于删除节点的颜色,当删除的节点是红色时,直接拿其孩子节点补空位即可。因为删除红色节点,性质4(从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点)仍能够被满足。当删除的节点是黑色时,那么所有经过该节点的路径上的黑节点数量少了一个,破坏了性质4。如果该节点的孩子为红色,直接拿孩子节点替换被删除的节点,并将孩子节点染成黑色,即可恢复性质5。但如果孩子节点为黑色,处理起来就要复杂的多。删除的过程比较复杂,有兴趣的伙伴可参考:《算法导论》或者《STL源码剖析》。
红黑树与AVL树的比较
红黑树和AVL树都是高效的平衡二叉树,增删改查的时间复杂度都是O( l o g 2 N log_2 N log2N),红黑树不追求绝对平衡,其只需保证最长路径不超过最短路径的2倍,相对而言,降低了插入和旋转的次数,所以在经常进行增删的结构中性能比AVL树更优,而且红黑树实现比较简单,所以实际运用中红黑树更多
红黑树的应用
- C++ STL库 – map/set、mutil_map/mutil_set
-
- Java 库
- linux内核
- 其他一些库
总结
红黑树是一种重要的二叉树,应用广泛,但在很多数据结构相关的书本中出现的次数并不多。很多书中要么不说,要么就一笔带过,并不会进行详细的分析,这可能是因为红黑树比较复杂的缘故,特别是红黑树的删除,网上不是流传着有一张图: