AVL树、红黑树
AVL树和红黑树的出现,是为了解决二叉搜索树的节点不均匀而导致的效率降低的问题,将二叉树的子树高度控制平衡。他们的底层解决方法都是通过旋转来调整节点(与其说是旋转,不如说是按压节点),他们的不同就是依赖逻辑不同。
一、AVL树
特性
要平衡左右子树的高度的话,就得记录左右子树的高度差:因此AVL树引入了平衡因子(balance factor)的概念。
平衡因子:右子树的高度减去左子树的高度。
那么我们想要让左右子树高度平衡,就是要控制平衡因子的绝对值尽量的小。我们能控制的最小范围是多少呢?
能不能就一直是0,让左右子树高度一直相等?当然是不行!
最简单的就比如当在根节点下插入一个新节点,根节点的平衡因子必然不是0。
所以要控制的范围是[ -1, 1],也就是正常的平衡因子只能是-1、0、1。
因此将增删查改的效率控制在了O(logn) 
结构性质
- 增加了平衡因子的属性;
- 节点的key、value包装在了pair容器中;
- 节点的key大小规则都满足二叉搜索树的规则。
- 增加了父节点的维护。
c++
template<class K, class V>
struct AVLTreeNode {
pair<K, V> _kv;
AVLTreeNode<K, V>* _left;
AVLTreeNode<K, V>* _right;
AVLTreeNode<K, V>* _parent;
int _bf; // 平衡因子(balance factor)
AVLTreeNode(const pair<K, V>& kv)
:_kv(kv)
, _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _bf(0)
{}
};
template<class K, class V>
class AVLTree {
typedef AVLTreeNode<K, V> Node;
public:
// ...成员函数
private:
Node* _root = nullptr;
};
插入节点
步骤:
- 寻找插入位置;
- 找到位置之后接入新节点;
- 向上调整节点的平衡因子,因为增加节点之后大概率也会影响其祖先节点,遇到不正常的平衡因子要进行旋转,最坏情况:调整到根节点。
步骤1、2
操作和二叉搜索树没有区别,先判空树,再对比key值,大了往右走,小了往左走,直到遇到空节点,将新节点接入,连接父子节点。
c++
bool Insert(const pair<K, V>& kv) {
if (_root == nullptr) {
_root = new Node(kv);
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;
// *****调整平衡*****
// .....
// *****************
return true;
}
步骤3 调整平衡
当插入新节点cur时,cur的_bf必然为0;
需要修改parent->_bf
parent的_bf的变化规则:
方法:
c++
while (parent) { // 因为需要向上调整祖先节点的_bf
// 改变平衡因子
if (cur == parent->_left) {
--(parent->_bf);
}
else {
++(parent->_bf);
}
// ...后续分析_bf
}
那么parent->_bf变化之后的结果有哪些呢?
分析结果
分三种大类情况:
1.最棒情况:插入后parent->_bf == 0
说明插入前parent->_bf等于-1或1,也就是本来就有一个节点

那么此时就不需要再向上调整祖先节点的_bf了,因为此时插入的节点并没有改变parent这棵树的高度。所以可以break跳出向上调整的循环了。
c++
while (parent) { // 因为需要向上调整祖先节点的_bf
// 改变平衡因子
if (cur == parent->_left) {
--(parent->_bf);
}
else {
++(parent->_bf);
}
// ...后续分析_bf
if (parent->_bf == 0) {
break;
}
// else if (....)
}
2.比较好的情况:插入后parent->_bf == 1或者-1
说明插入前一定parent->_bf == 0(也就是parent本来没有节点),如果是-2或2,那这棵树本来就是一颗坏树了


此时就是改变了parent这棵树的高度,会影响祖先节点的_bf,所以要向上调整,(也就是cur、parent向上一层,再次进入循环)
c++
while (parent) { // 因为需要向上调整祖先节点的_bf
// 改变平衡因子
if (cur == parent->_left) {
--(parent->_bf);
}
else {
++(parent->_bf);
}
// 分析_bf
if (parent->_bf == 0) {
break;
}
else if (parent->_bf == 1 || parent->_bf == -1) {
// 继续向上更新
cur = parent;
parent = parent->_parent;
}
// else if(...)
}
3.麻烦的情况:插入后parent->_bf == 2或者-2
(此时就超出我们能接受的平衡范围-1、0、1了,此时要进行旋转处理)。
说明插入前parent->_bf等于-1或1(高度差已经是极限了),且cur插入到已经高出一截的那个parent的子节点的下面了。
c++
while (parent) { // 因为需要向上调整祖先节点的_bf
// 改变平衡因子
if (cur == parent->_left) {
--(parent->_bf);
}
else {
++(parent->_bf);
}
// 分析_bf
if (parent->_bf == 0) {
break;
}
else if (parent->_bf == 1 || parent->_bf == -1) {
// 继续向上更新
cur = parent;
parent = parent->_parent;
}
else if (parent->_bf == 2 || parent->_bf == -2) {
// 旋转处理
// if (...)
// ......
}
else {
// 插入之前就没处理好
assert(false);
}
}
不对呀?那cur和parent不就是爷孙关系了吗?当然没这么简单!此时的情况其实是从情况2向上调整到这的,parent最终停留在了6节点初:

当然,插入的情况并没有这么简单,从插入前就的不平衡分析,情况三又可以分为四种小情况:
(1)极端左[右单旋];(2)极端右[左单旋];(3)左多中高[左右双旋];(4)右多中高[右左双旋]。
h是代表这棵局部子树的高度,可以是任何值。

(1)极端左(parent->_____bf == -2 && cur->_bf == -1):
例如:(只是实例说明一下h可以等于任何值,后几种小情况不会再举这么多例子)
当h=0时,也就是没节点,

当h=1时

当h=2时

(所以当h变多时,只是情况2的循环次数多,最后遇到了这四种(极端左;极端右;左多中高;右多中高)小情况。)
话说回来极端左,该怎么右单旋转成平衡呢?
1.先赋值一些用得到的变量

2.parent与subLR整体下压(改变parent、subL、subLR的父子关系),再改变subL和parent的_bf

3.不要忘了,若parent原本还有他的父节点和其他兄弟的话,还要将parentParent与subL相关联
例如:
4.最后修改parent和subL的平衡因子
c++
else if (parent->_bf == 2 || parent->_bf == -2) {
// 旋转处理
if (parent->_bf == -2 && cur->_bf == -1) {
// 右单旋
RotateR(parent);
}
// else if (...)
break;
}
void RotateR(Node* parent) {
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;
}
parent->_bf = subL->_bf = 0;
}
(2)极端右(parent->_____bf == 2 && cur->_bf == 1):
与极端左大致相同,不用多余谈论
c++
else if (parent->_bf == 2 || parent->_bf == -2) {
// 旋转处理
if (parent->_bf == -2 && cur->_bf == -1) {
// 右单旋
RotateR(parent);
}
else if (parent->_bf == 2 && cur->_bf == 1) {
// 左单旋
RotateL(parent);
}
// else if (...)
break;
}
void RotateL(Node* parent) {
Node* subR = parent->_right;
Node* subRL = subR->_left;
parent->_right = subRL;
if (subRL) {
subRL->_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;
}
parent->_bf = subR->_bf = 0;
}
(3)左多中高(parent->_____bf == -2 && cur->_bf == 1):
都是需要进行左右双旋,先左旋后右旋(先左压,后右压)
复用的左单旋,右单旋,所以如果parent之上还有祖先需要连接,复用的单旋函数就帮我们解决了

根据旋转后,调整parent、subL和subLR的平衡因子的情况不同,又分三种子情况:
(左旋右旋,都可以看成是,将插入节点所在的那棵树的左右子树各分一边)
<1>当h=0时(subLR->_bf==0)

此时调整subL->_____bf = subLR->_____bf = parent->_bf = 0
<2>当h不等于0,且新插入的在子树的左边(subLR->_bf==-1)
(h的变化已经分析过,也就是新插入的节点cur通过一层层调整平衡因子,经过情况2的循环遇到了这种需要旋转的情况3)

此时调整subL->_____bf = 0;parent->_____bf = 1;subLR->_____bf = 0;
<3>当h不等于0,且新插入的在子树的右边(subLR->_bf==1)
(旋转都是一样的操作,不再赘述)
旋转后最后调整subL->_____bf = -1;parent->_____bf = 0;subLR->_____bf = 0;
c++
else if (parent->_bf == 2 || parent->_bf == -2) {
// 旋转处理
if (parent->_bf == -2 && cur->_bf == -1) {
// 右单旋
RotateR(parent);
}
else if (parent->_bf == 2 && cur->_bf == 1) {
// 左单旋
RotateL(parent);
}
else if (parent->_bf == -2 && cur->_bf == 1) {
// 左右双旋
RotateLR(parent);
}
// else if(...)
break;
}
void RotateLR(Node* parent) {
Node* subL = parent->_left;
Node* subLR = subL->_right;
// 获取subLR的平衡因子,后续判断双旋后的结果
int bf = subLR->_bf;
// 可以复用单旋成员函数
RotateL(parent->_left);
RotateR(parent);
// 通过之前获取到的bf,可以推断双旋后的状态,从调整其节点的平衡因子
if (bf == 0) {
// <1>
subL->_bf = subLR->_bf = parent->_bf = 0;
}
else if (bf == -1) {
// <2>
subL->_bf = 0;
subLR->_bf = 0;
parent->_bf = 1;
}
else if (bf == 1) {
// <3>
subL->_bf = -1;
parent->_bf = 0;
subLR->_bf = 0;
}
else {
assert(false);
}
}
(4)右多中高(parent->_____bf == 2 && cur->_bf == -1):
都是需要进行右左双旋,先右旋后左旋
与左多中高相同的逻辑
根据旋转后,调整parent、subL和subLR的平衡因子的情况不同,分三种子情况:
(左旋右旋,都可以看成是,将插入节点所在的那棵树的左右子树各分一边)
<1>当h=0时(subLR->_bf==0)
此时调整subR->_____bf = subRL->_____bf = parent->_bf = 0
<2>当h不等于0,且新插入的在子树的左边(subLR->_bf==-1)
此时调整subR->_____bf = -0;parent->_____bf = 0;subRL->_____bf = 0;
<3>当h不等于0,且新插入的在子树的右边(subLR->_bf==1)
此时调整subR->_____bf = 0;parent->_____bf = -1;subRL->_____bf = 0;
(由于旋转逻辑与左多中高相似,就不展示各种例子,只拿<3>举例)

c++
else if (parent->_bf == 2 || parent->_bf == -2) {
// 旋转处理
if (parent->_bf == -2 && cur->_bf == -1) {
// 右单旋
RotateR(parent);
}
else if (parent->_bf == 2 && cur->_bf == 1) {
// 左单旋
RotateL(parent);
}
else if (parent->_bf == -2 && cur->_bf == 1) {
// 左右双旋
RotateLR(parent);
}
else if (parent->_bf == 2 && cur->_bf == -1) {
// 右左双旋
RotateRL(parent);
}
else {
assert(false); // 异常情况
}
break;
}
void RotateRL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf;
RotateR(parent->_right);
RotateL(parent);
if (bf == 0)
{
subR->_bf = 0;
subRL->_bf = 0;
parent->_bf = 0;
}
else if (bf == 1)
{
subR->_bf = 0;
subRL->_bf = 0;
parent->_bf = -1;
}
else if (bf == -1)
{
subR->_bf = 1;
subRL->_bf = 0;
parent->_bf = 0;
}
else
{
assert(false);
}
}
查找
十分的模板化
大了往右,小了往左
c++
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;
}
小结
重点在于理解平衡因子的调整和分析,以及旋转的逻辑。
AVL树其他的接口,比如删除,其逻辑也包含旋转之类,暂且不做分析
二、红黑树
特性
红黑树所保持的平衡是指:确保没有一条路径会比其他路径长出2倍
路径:指从该节点开始,直到nullptr的简单路径
规则
- 节点分为红色和黑色,一个节点不是红色就是黑色;
- 根节点是黑色;
- 父子节点不能同时为红色,但可以都是黑色;
- 对于任意一个节点,从这个结点开始的所有路径上,都包含相等数量的黑色节点
以下四种都是符合要求的红黑树

原理
红黑树是如何确保没有一条路径会比其他路径长出2倍的?
根据规则4,任何节点之下的路径,黑色节点数量都相等,那么再结合规则3
- 路径最短时,节点全是黑色,高度为h
- 路径最长时,节点黑红相间,高度为2h

效率
假设N是树中节点数量,h是最短路径长度,那么2h-1 <= N <= 22h-1,由此h约等于logN,那么最坏情况也就最长路径2 * logN
那么时间复杂度是O(logN).
结构
与AVL树相比,去掉了平衡因子_bf,增加了颜色_____col的属性
c++
// 通过枚举值表示颜色
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)
{}
};
template < class K, class V>
class RBTree {
typedef RBTreeNode<K, V> Node;
public:
// ......
private:
Node * _root = nullptr;
};
插入节点
与二叉搜索树同样的方法找到插入位置插入,只不过插入后要分析节点颜色是否符合红黑树的规则
- 当树原本是空树时,插入的一定就是黑色,因为树的根是要黑色的
- 当树原本不是空树,插入的一定就是红色,因为一旦新增节点是红色的话,规则4就会被破坏
那么此时可能会想:新节点都是红色的话,那黑色节点哪里来呢?
在插入节点之后的颜色分析中,会对不同的情况,做出不同操作。其中就包括变色以及旋转,红黑树的旋转方法和AVL树一样,只是不会再有平衡因子的操作了
c++
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->_left = cur;
}
else {
parent->_right = cur;
}
cur->_parent = parent;
// *****分析颜色*****
//
// *****************
// 最后直接将根变为黑色色
_root->_col = BLACK;
return true;
}
颜色分析
对颜色的分析,会用到四个变量:
1.新增节点_cur
2.新节点的父节点_parent
3.父节点的父节点_grandfather(以下描述为grand)
4.新节点的叔叔节点_uncle,也就是父节点的兄弟节点
第一层分析,对父节点分析
-
若父节点为黑色,则此时就已经符合红黑树的所有规则了,不需要再分析了。可以类比一下AVL树的第1大类情况(符合规则,跳出循环)

-
若父节点为红色,则此时违反了规则3,父子不能同时为红色,需要进入第二层分析:

在进行第二层分析之前,还要先分清楚父节点和叔节点是爷节点的左还是右
父左叔右的第二层分析
(父节点已经是红色了)
- uncle节点为红。只变色,再向上分析。(可以类比AVL树的第2类情况)

c++
while (parent && parent->_col == RED) { // 可层层向上调整
Node* grandfather = parent->_parent;
if (grandfather->_left == parent) { // 确定parent与uncle的左右
Node* uncle = grandfather->_right;
if (uncle && uncle->_col == RED) { // 叔叔为红色
// 只需变色
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
// 继续向上
cur = grandfather;
parent = cur->_parent;
}
else {
// ......
}
}
else {
// 父在右
}
}
- uncle节点为黑或不存在。要进行旋转+变色,再向上分析。但还需要进入第三层分析

父左叔右的第三层分析
此时已经确定:父在左,叔在右,父为红,叔为黑或空
第三层要分cur插入的位置是在parent的左还是右
- cur为左孩子(可以类比AVL树的极端左)。右单旋+变色
grand变红,parent变黑

c++
while (parent && parent->_col == RED) { // 可层层向上调整
Node* grandfather = parent->_parent;
if (grandfather->_left == parent) { // 确定parent与uncle的左右
Node* uncle = grandfather->_right;
if (uncle && uncle->_col == RED) { // 叔叔为红色
// 只需变色
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
// 继续向上
cur = grandfather;
parent = cur->_parent;
}
else { // 叔叔不存在或叔叔为黑
if (cur == parent->_left) { // cur为左(极端左)
// 右单旋 + 变色
RotateR(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
else {
// ......
}
// 父亲已经是黑,跳出循环
break;
}
}
else {
// 父在右
}
}
void RotateR(Node* parent) {
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;
}
}
- cur为右孩子(可以类比AVL树的左多中高)。左右双旋+变色
cur变黑,grand变红

c++
while (parent && parent->_col == RED) { // 可层层向上调整
Node* grandfather = parent->_parent;
if (grandfather->_left == parent) { // 确定parent与uncle的左右
Node* uncle = grandfather->_right;
if (uncle && uncle->_col == RED) { // 叔叔为红色
// 只需变色
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
// 继续向上
cur = grandfather;
parent = cur->_parent;
}
else { // 叔叔不存在或叔叔为黑
if (cur == parent->_left) { // cur为左(极端左)
// 右单旋 + 变色
RotateR(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
else { // cur为右(左多中高)
// 双旋 + 变色
RotateL(parent);
RotateR(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
// 父亲已经是黑,跳出循环
break;
}
}
else {
// 父在右
}
}
void RotateL(Node* parent) {
Node* subR = parent->_right;
Node* subRL = subR->_left;
parent->_right = subRL;
if (subRL) {
subRL->_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;
}
}
父右叔左的第二层分析
-
uncle节点为红。只变色,再向上分析。(可以类比AVL树的第2类情况)
-
uncle节点为黑或不存在。要进行旋转+变色,再向上分析。但还需要进入第三层分析
父右叔左的第三层分析
- cur为右孩子(可以类比AVL树的极端右)。左单旋+变色
grand变红,parent变黑
- cur为左孩子(可以类比AVL树的右多中高)。右左双旋+变色
cur变黑,grand变红
(大致逻辑与父左叔右类似,不再举例说明)
c++
else { // 父右叔左
Node* uncle = grandfather->_left;
if (uncle && uncle->_col == RED) { // 叔叔为红色
// 只需变色
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
// 继续向上
cur = grandfather;
parent = cur->_parent;
}
else { // 叔叔不存在或叔叔为黑
if (cur == parent->_right) { // cur为右(极端右)
// 左单旋 + 变色
RotateL(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
else { // cur为左(右多中高)
// 双旋 + 变色
RotateR(parent);
RotateL(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
// 父亲已经是黑,跳出循环
break;
}
}
查找
模板化
c++
Node* Find(const K& key) {
Node* cur = _root;
while (cur) {
if (cur->_kv.first > key) {
cur = cur->_left;
}
else if (cur->_kv.first < key) {
cur = cur->_right;
}
else {
return cur;
}
}
return false;
}
小结
旋转逻辑与AVL树相同,之不过将平衡的控制权从平衡节点换成了颜色
着重理解旋转加换色的控制魅力
总体
AVL树和红黑树都是基于二叉搜索树衍生出的平衡树,通过一系列的限制规则,努力将增删查改的时间复杂度控制在logN。
这些精妙的设计,每每思考,都让人感叹前人的智慧。