前言
红黑树是一种自平衡二叉查找树,它在1972年由鲁道夫·贝尔发明,是为了克服二叉查找树在最坏情况下的性能问题而设计的。红黑树能够在最坏情况下也保持较为高效的查找、插入和删除操作时间复杂度为O(log n),其中n是树中节点的数量。
一、红黑树的概念
1.红黑树的规则
- 每个结点不是红色就是黑色。
- 根节点是黑色的。
- 如果一个结点是红色的,则它的两个孩子结点必须是黑色的,也就是说任意一条路径不会有连续的红色结点
- 对于任意一个结点,从该结点到其所有NULL结点的简单路径上,均包含相同数量的黑色结点
- 所有叶子节点(外部节点,NIL节点,通常不画出)都是黑色(他这⾥所指的叶⼦结点不是传统的意义上的叶子结点,⽽是我们说的空结点,有些书籍上也把NIL叫做外部结点。NIL是为了方便准确的标识出所有路径)
这些性质保证了从根到叶子的最长路径不会超过最短路径的两倍,因此红黑树大致是平衡的。
2. 红黑树的效率
假设N是红⿊树树中结点数量,h是最短路径的⻓度,红⿊树增删查改最坏也就是⾛最⻓路径 ,时间复杂度还是O(logN)。
红⿊树的表达相对AVL树要抽象⼀些,AVL树通过⾼度差直观的控制了平衡。红⿊树通过4条规则的颜⾊约束,间接的实现了近似平衡,他们效率都是同⼀档次,但是相对⽽⾔,插⼊相同数量的结点,红⿊树的旋转次数是更少的,因为他对平衡的控制没那么严格。
二、红黑树的实现
1.红黑树的结构
// 枚举值表示颜色
enum Colour
{
RED,
BLACK
};
// 这里我们默认按key/value结构实现
template<class K, class V>
struct RBTreeNode
{
// 这里更新控制平衡也要加入parent指针
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)
{
}
};
template<class K, class V>
class RBTree
{
typedef RBTreeNode<K, V> Node;
public:
private:
Node* _root = nullptr;
};
2. 红黑树的插入
- 插⼊⼀个值按⼆叉搜索树规则进⾏插⼊,插⼊后我们只需要观察是否符合红⿊树的4条规则
- 如果是空树插⼊,新增结点是黑色结点。如果是非空树插⼊,新增结点必须红色结点,因为非空树插⼊黑色节点,新增黑色结点就破坏了规则4,规则4是很难维护的
- 非空树插⼊后,新增结点必须红色结点,如果⽗亲结点是黑色的,则没有违反任何规则,插⼊结束
- 非空树插⼊后,新增结点必须红色结点,如果父亲结点是红色的,则违反规则3。进⼀步分析,c是红色,p为红,g必为黑,这三个颜⾊都固定了,关键的变化看u的情况,需要根据u分为以下几种情况分别处理。
2.1 情况一:只需变色
- cur为红,p为红,g为黑,u存在且为红;则将p和u变⿊,g变红。在把g当做新的c,继续往上更新。
因为p和u都是红色,g是黑色,把p和u变⿊,左边子树路径各增加⼀个黑色结点,g再变红,相当于保持g所在子树的黑色结点的数量不变,同时解决了c和p连续红色结点的问题,需要继续往上更新是因为,g是红色,如果g的父亲还是红色,那么就还需要继续处理;如果g的⽗亲是黑色,则处理结束了;如果g就是整棵树的根,把g变为黑色。
情况1只变色,不旋转。所以⽆论c是p的左还是右,p是g的左还是右,都是上面的变色处理方式。
注:这里的树可能是一颗子树,也可能是一颗完整的树
g是完整的树:
g是一颗子树:
短一种g是红色,如果g的父亲还是红色,那么就还需要继续处理,g是一颗子树上面还有节点需要更新
2.2 情况二: 单旋 + 变色
- cur为红,p为红,g为黑,u不存在/u存在且为黑;u不存在,则c⼀定是新增结点,u存在且为⿊,则c⼀定不是新增的,c之前是黑色的,是在c的子树中插⼊,符合情况1,变色将c从黑色变成红色,更新上来的。
分析:p必须变黑,才能解决,连续红色结点的问题,u不存在或者是黑色的,这⾥单纯的变色无法解决问题,需要旋转+变色。
g
p u
c
如果p是g的左,c是p的左,那么以g为旋转点进行右单旋,再把p变⿊,g变红即可。p变成课这颗树新的根,这样子树黑色结点的数量不变,没有连续的红色结点了,且不需要往上更新,因为p的父亲是黑色还是红色或者空都不违反规则。
g
u p
c
如果p是g的右,c是p的右,那么以g为旋转点进行左单旋,再把p变黑,g变红即可。p变成课这颗树新的根,这样子树黑色结点的数量不变,没有连续的红色结点了,且不需要往上更新,因为p的父亲是黑色还是红色或者空都不违反规则
2. 3 情况三: 双旋 + 变色
g
p u
c
如果p是g的左,c是p的右,那么先以p为旋转点进⾏左单旋,再以g为旋转点进⾏右单旋,再把c变⿊,g变红即可。c变成课这颗树新的根,这样⼦树⿊⾊结点的数量不变,没有连续的红⾊结点了,且不需要往上更新,因为c的⽗亲是⿊⾊还是红⾊或者空都不违反规则。
g
u p
c
如果p是g的右,c是p的左,那么先以p为旋转点进⾏右单旋,再以g为旋转点进⾏左单旋,再把c变⿊,g变红即可。c变成课这颗树新的根,这样⼦树⿊⾊结点的数量不变,没有连续的红⾊结点了,且不需要往上更新,因为c的⽗亲是⿊⾊还是红⾊或者空都不违反规则
3. 代码实现
#pragma once
// 枚举值表示颜色
enum Colour
{
RED,
BLACK
};
// 这里我们默认按key/value结构实现
template<class K, class V>
struct RBTreeNode
{
// 这里更新控制平衡也要加入parent指针
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)
{
}
};
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(key);
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;
if (parent = Grandfather->_left)
{
// g
// p u
Node* Uncle = Grandfather->_right;
if (Uncle && Uncle->_col == RED)//叔叔存在且为红
{
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);
Grandfather->_col = RED;
cur->_col = BLACK;
}
break;
}
}
else //parent = Grandfather->_right
{
// g
//u p
Node* uncle = Grandfather->_left;
if(uncle && uncle->_col == RED)//叔叔存在且为红
{
parent->_col = uncle->_col = BLACK;
Grandfather->_col = RED;
// 继续往上处理
cur = Grandfather;
parent = cur->_parent;
}
else //叔叔不存在,或者存在且为黑
{
// 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;
}
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;
}
private:
Node* _root = nullptr;
};
总结
红黑树和AVL树都是高效的平衡二叉树,增删改查的时间复杂度都是O(log n),红黑树不追求绝对平衡,其只需保证最长路径不超过最短路径的2倍,相对而言,降低了插入和旋转的次数,所以在经常进行增删的结构中性能比AVL树更优,而且红黑树实现比较简单,所以实际运用中红黑树更多。
红黑树通过颜色规则和旋转操作,在动态数据集中维持近似平衡,适合需要高效插入、删除的场景。其核心在于插入后的颜色翻转与旋转,以及删除后的兄弟节点借调与结构调整。理解这些规则和场景,是掌握红黑树的关键。