【AVL树与红黑树】:告别“瘸腿”树,体验平衡的艺术
前言:在上一篇“【二叉搜索树】:程序的“决策树”,排序数据的基石”中,我们了解到普通二叉搜索树在最坏情况下会退化成链表,时间复杂度恶化到O(N)。为了解决这个问题,计算机科学家们提出了自平衡二叉搜索树,其中最具代表性的就是AVL树和红黑树。下面我们将深入探索这两种经典平衡树的原理与实现,看看它们是如何通过巧妙的平衡策略,让查找效率始终稳定在O(logN)的。学的时候记得别问是怎么想出来的
目录
一、AVL树的概念
二、AVL树的插入
1. AVL树的结构
2. 插入过程
3. 更新平衡因子
(1)更新方法
(2)停止更新条件
4. 旋转
(1)左单旋
(2)右单旋
(3)左右双旋
(4)右左双旋
(5)旋转函数
三、AVL树的其他知识
1. AVL树的查找
2. AVL树平衡检测
3. AVL树的删除
4. 完整代码
四、红黑树的概念
1. 红黑树介绍
2. 红⿊树的规则✨✨✨
3. 红黑树的效率
五、红黑树的插入
1. 红黑树的结构
2. 红黑树的插入过程
3. 具体情况的分解
(1)u存在且为红
(2)u不存在或者u为⿊
4. 核心问题解决
六、红黑树的其他知识
1. 红黑树的查找
2. 红黑树的平衡检测
3. 红黑树的删除
4. 完整代码
一、AVL树的概念
AVL树是最早发明的自平衡二叉搜索树,由前苏联科学家Adelson-Velsky和Landis在1962年提出。它要求:
左右子树的高度差绝对值不超过1
左右子树也都是AVL树
为了量化平衡程度,AVL树引入了平衡因子的概念:平衡因子 = 右子树高度 - 左子树高度,每个节点的平衡因子只能是-1、0、1。

AVL树并不是必须要平衡因⼦,我们选择用平衡因子是为了⽅便我们去进⾏观察和控制树是否平衡, 就像⼀个⻛向标⼀样。
为什么高度差不是0?
理想的高度差为0确实更平衡,但实际中无法总是做到。比如只有2个或4个节点时,高度差1已经是最优平衡状态了。
二、AVL树的插入
1. AVL树的结构
template<class K, class V>
struct AVLTreeNode
{pair<K, V> _kv;AVLTreeNode<K, V>* _parent;AVLTreeNode<K, V>* _left;AVLTreeNode<K, V>* _right;int _bf; //平衡因子AVLTreeNode(const pair<K, V>& kv):_kv(kv),_parent(nullptr),_left(nullptr),_right(nullptr),{_bf = 0; //新增节点的平衡因子为0}
};
为什么要增加parent指针?
(1)旋转操作的需要
当进行左旋、右旋等平衡调整时,需要修改父节点对当前节点的引用
如果没有parent指针,就需要从根节点开始搜索父节点,效率极低
(2)更新平衡因子的需要
AVL树插入/删除后需要向上更新祖先节点的平衡因子
通过parent指针可以快速向上遍历整条路径:
cur → parent → grandparent → ...
2. 插入过程
- 插入一个值按二叉搜索树规则进行插入。
if (_root == nullptr)
{_root = new Node(kv);return true;
}Node* cur = _root;
Node* parent = nullptr;
while (cur)
{if (kv.first < cur->_kv.first){parent = cur;cur = cur->_left;}else if (kv.first > cur->_kv.first){parent = cur;cur = cur->_right;}else{return false;}
}cur = new Node(kv);
if (parent->_kv.first < kv.first)
{parent->_right = cur;
}
else
{parent->_left = cur;
}
cur->_parent = parent;
- 新增结点以后,只会影响祖先结点的高度,也就是可能会影响部分祖先结点的平衡因子,所以更新从新增结点到根结点路径上的平衡因子,实际中最坏情况下要更新到根,有些情况更新到中间就可以停止了,具体情况我们下面再详细分析。
- 更新平衡因子过程中没有出现问题,则插入结束。
- 更新平衡因子过程中出现不平衡,对不平衡子树旋转,旋转后本质调平衡的同时,本质降低了子树的高度,不会再影响上一层,所以插入结束。
3. 更新平衡因子
(1)更新方法
核心:只有⼦树⾼度变化才会影响当前结点平衡因⼦。
插入结点,会增加高度,所以新增结点在parent的右子树,parent的平衡因子++,新增结点在 parent的左子树,parent平衡因子--。之后再根据具体情况向上更新。
插入在根的左树,那它右树的平衡因子一定不会改变。
(2)停止更新条件
- 更新后parent的平衡因子等于0
更新中parent的平衡因子变化为-1→0或者1→0,说明更新前parent子树一边高一边低,新增的结点插入在低的那边,插入后parent所在的子树高度不变,不会影响parent的父亲结点的平衡因子,更新结束。(由亚健康变健康了,肯定不用管了)
- 更新后parent的平衡因子等于1或-1
更新中parent的平衡因子变化为0→1或者0→-1,说明更新前parent子树两边一样高,新增的插入结点后,parent所在的子树一边高一边低,parent所在的子树符合平衡要求,但是高度增加了1,会影响parent的父亲结点的平衡因子,所以要继续向上更新。(由健康变亚健康,他自己还没事,但会影响它的上边)
- 更新后parent的平衡因子等于2或-2
更新中parent的平衡因子变化为1→2或者-1→-2,说明更新前parent子树一边高一边低,新增的插入结点在高的那边,parent所在的子树高的那边更高了,破坏了平衡,parent所在的子树不符合平衡要求,需要旋转处理!(本来已经亚健康了,现在生病了,直接治病,好了就谁都不影响)
- 不断更新,更新到根,根的平衡因子是1或-1,停止
10所在的子树已经不平衡,需要旋转处理
不会影响上一层,更新结束
while (parent)
{if (cur == parent->_left) parent->_bf--;else parent->_bf++;if (parent->_bf == 0) break;else if (abs(parent->_bf) == 2){//旋转break;}else if (abs(parent->_bf) == 1){cur = parent;parent = parent->_parent;//平衡因子改变的那一端,新结点就相当于增加在那一边的子树上}
}
4. 旋转
旋转的原则:
parent的平衡因子能变到2,说明之前就一边高一边底了,像这种:


进一步分解来看就是
此时,10可能是整棵树的根,也可能是一个整棵树中局部的子树的根;a/b/c抽象为三棵高度为h的子树(h>=0),a/b/c均符合AVL树的要求。那由1变2,无非就是在画圈的四个地方加,正好对应我们四种情况(h=0就是一插入就不满足AVL树的要求了,而h>0就是向上调整的过程中有个节点不满足了)。
(1)左单旋
当在圈4的位置插入时,要进行左单旋
核心步骤:
因为10 < b子树的值 < 15,将b变成10的右子树,10变成15的左子树,15变成这棵树新的根,符合搜索树的规则,控制了平衡,同时这棵树的高度恢复到了插入之前的h+2,符合旋转原则。如果插入之前10是整棵树的一个局部子树,旋转后不会再影响上一层,我们可以看到平衡因子都是0了,插入结束了。
//主要是极端情况的处理,和新关系的链接(AVL的结点有三个指针哦)
void RotateL(Node* parent)
{Node* subR = parent->_right; //parent的右孩子(将成为新根)Node* subRL = subR->_left; //subR的左孩子(需要重新挂接)Node* grandparent = parent->_parent; //向上链接需要//链接新关系parent->_parent = subR;parent->_right = subRL;if (subRL) //防止原来h=0subRL->_parent = parent;subR->_left = parent;if (grandparent == nullptr) //如果parent原是整棵树的根{_root = subR;subR->_parent = nullptr;}else{ //链接关系if (parent == grandparent->_left)grandparent->_left = subR;else grandparent->_right = subR;subR->_parent = grandparent;}//重置平衡因子subR->_bf = 0;parent->_bf = 0;
}
(2)右单旋
当在圈1的位置插入时,要进行右单旋
核心步骤:
因为5 < b子树的值 < 10,将b变成10的左子树,10变成5的右子树,5变成这棵树新的根,符合搜索树的规则,控制了平衡,同时这棵树的高度恢复到了插入之前的h+2,符合旋转原则。如果插入之前10是整棵树的一个局部子树,旋转后不会再影响上一层,插入结束了。
//右单旋
void RotateR(Node* parent)
{Node* subL = parent->_left;Node* subLR = subL->_right;Node* grandparent = parent->_parent;parent->_parent = subL;parent->_left = subLR;if (subLR) subLR->_parent = parent;subL->_right = parent;if (grandparent == nullptr){_root = subL;subL->_parent = nullptr;}else{if (parent == grandparent->_left)grandparent->_left = subL;else grandparent->_right = subL;subL->_parent = grandparent;}subL->_bf = 0;parent->_bf = 0;
}
(3)左右双旋
如果在圈2位置插入,要进行左右双旋

左边高时,如果插入位置不是在a子树,而是插入在b子树,b子树高度从h变成h+1,引发旋转,右单旋无法解决问题,右单旋后,我们的树依旧不平衡。右单旋解决的纯粹的左边高,但是插入在b子树中,10为根的子树不再是单纯的左边高,对于10是左边高,但是对于5是右边高,需要用两次旋转才能解决,以5为旋转点进行一个左单旋,以10为旋转点进行一个右单旋,这棵树就平衡了。

所以光旋转很简单,复用上面的代码就好:
void RotateLR(Node* parent)
{Node* subL = parent->_left;Node* subLR = subL->_right;RotateL(parent->_left);RotateR(parent);
}
这块麻烦的是平衡因子的更新。参照上面的主图,我们将a/b/c子树抽象为高度h的AVL子树进行分析,另外我们需要把b子树的细节进一步展开为8和左子树高度为h-1的e和f子树,因为我们要对b的父亲5为旋转点进行左单旋,左单旋需要动b树中的左子树。b子树中新增结点的位置不同,平衡因子更新的细节也不同,通过观察8的平衡因子也不同,这里我们要分三个场景讨论。

这图你就看吧,看懂就懂了,AVL也就基本学完了,要是没看懂,那只能再看了😏😏。
- 场景1:当 h ≥ 1 时,新增结点插入在 e 子树,导致 e 子树高度从 h-1 变为 h,并依次更新结点 8 → 5 → 10 的平衡因子,最终引发旋转。此时结点 8 的平衡因子为 -1。旋转后,结点 8 和结点 5 的平衡因子变为 0,结点 10 的平衡因子变为 1。
- 场景2:当 h ≥ 1 时,新增结点插入在 f 子树,导致 f 子树高度从 h-1 变为 h,并依次更新结点 8 → 5 → 10 的平衡因子,引发旋转。此时结点 8 的平衡因子为 1。旋转后,结点 8 和结点 10 的平衡因子变为 0,结点 5 的平衡因子变为 -1。
- 场景3:当 h = 0 时,a、b、c 子树均为空树,此时 b 结点自身即为新增结点。依次更新结点 5 → 10 的平衡因子后引发旋转,结点 8 的平衡因子为 0。旋转后,结点 8、结点 10 和结点 5 的平衡因子均变为 0。
情况就这三种,我们写代码时,直接用8的平衡因子判断就行。
void RotateLR(Node* parent)
{Node* subL = parent->_left;Node* subLR = subL->_right;int bf = subLR->_bf;RotateL(parent->_left);RotateR(parent);if (bf == 0){subL->_bf = 0;subLR->_bf = 0;parent->_bf = 0;}else if (bf == 1){subL->_bf = -1;subLR->_bf = 0;parent->_bf = 0;}else if (bf == -1){subL->_bf = 0;subLR->_bf = 0;parent->_bf = 1;}else{assert(false);}
}
(4)右左双旋
如果在圈3位置插入,要进行右左双旋。和左右双旋原理一样,就是高的不纯粹,先右旋再左旋。直接上平衡因子更新的图片。

为了大家理解,我把subRL=0的旋转情况画一下

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);}
}
两次旋转的最终结果
- 左右双旋:
subLR变为根,根的左结点是subL,右结点是parent,左结点subL的左孩子不变,右孩子是subLR的左子树,右结点parent的右孩子不变,左孩子是subLR的右子树,这些子树可以为空。
- 右左双旋:
subRL变为根,根的左结点是parent,右结点是subR,左结点parent的左孩子不变,右孩子是subRL的左子树,右结点subR的右孩子不变,左孩子是subRL的右子树,这些子树可以为空。
每次看旋转太麻烦,遇到双旋,要记得这个结论
(5)旋转函数
说了这么多,那我们怎么表达圈1234的情况呢,我们可以发现parent的孩子在每种情况下都不一样,我们就用此来表示。比如,如果旋转结点的左孩子的平衡因子是-1,说明它右边低,一定对应圈1的情况。
//......
else if (abs(parent->_bf) == 2)
{_Revolvenode(parent);break;
}
//......void _Revolvenode(Node* parent)
{if (parent->_bf == -2 && parent->_left->_bf == -1) // 右单旋RotateR(parent);else if (parent->_bf == 2 && parent->_right->_bf == 1) // 左单旋RotateL(parent);else if (parent->_bf == -2 && parent->_left->_bf == 1) // 左右双旋RotateLR(parent);else if (parent->_bf == 2 && parent->_right->_bf == -1) // 右左双旋RotateRL(parent);elseassert(false);
}
注意点:
为什么上面已经确保平衡因子的绝对值为2了,而且parent-> _left/_right ->_bf每种情况对应值也不一样,而走进_Revolvenode()函数里还要再判断一次parent->_bf呢?可以不写前面的判断条件吗?答案就是为了防止空树!parent-> _left/_right是有可能为空的,那再访问_bf不就崩了。加上判空条件呢?那也不行,加判空总会落情况,或者继续崩,你们可以自己试试。所以只能这么写。
三、AVL树的其他知识
1. AVL树的查找
和二叉搜索树一样
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;
}
2. AVL树平衡检测
这可不能检查平衡因子,那直接检查它不是贼喊捉贼嘛,我们应该直接算两棵子树的高度,看他是否是那三个值,再和平衡因子对比。
bool _IsBalanceTree(Node* root)
{// 空树也是AVL树if (nullptr == root)return true;// 计算pRoot结点的平衡因子:即pRoot左右子树的高度差int leftHeight = _Height(root->_left);int rightHeight = _Height(root->_right);int diff = rightHeight - leftHeight;// 如果计算出的平衡因子与pRoot的平衡因子不相等,或者// pRoot平衡因子的绝对值超过1,则一定不是AVL树if (abs(diff) >= 2){cout << root->_kv.first << "高度差异常" << endl;return false;}if (root->_bf != diff){cout << root->_kv.first << "平衡因子异常" << endl;return false;}// pRoot的左和右如果都是AVL树,则该树一定是AVL树return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);
}
3. AVL树的删除
它比插入还要复杂,在这里就不讲了,有兴趣的小伙伴可以参考
《殷⼈昆 数据结构:⽤⾯向对象⽅法与C++语 ⾔描述》
4. 完整代码
#pragma once
#include<iostream>
#include <utility>
#include<math.h>
#include<assert.h>
using namespace std;template<class K, class V>
struct AVLTreeNode
{pair<K, V> _kv;AVLTreeNode<K, V>* _parent;AVLTreeNode<K, V>* _left;AVLTreeNode<K, V>* _right;int _bf; AVLTreeNode(const pair<K, V>& kv):_kv(kv),_parent(nullptr),_left(nullptr),_right(nullptr),{_bf = 0; }
};template<class K, class V>
class AVL
{typedef AVLTreeNode<K, V> Node;
public:bool insert(const pair<K, V>& kv){if (_root == nullptr){_root = new Node(kv);return true;}//1. 比较插入Node* cur = _root;Node* parent = nullptr;while (cur){if (kv.first < cur->_kv.first){parent = cur;cur = cur->_left;}else if (kv.first > cur->_kv.first){parent = cur;cur = cur->_right;}else{return false;}}cur = new Node(kv);if (parent->_kv.first < kv.first){parent->_right = cur;}else{parent->_left = cur;}cur->_parent = parent;//2. 调整平衡while (parent){if (cur == parent->_left) parent->_bf--;else parent->_bf++;if (parent->_bf == 0) break;else if (abs(parent->_bf) == 2){_Revolvenode(parent);break;}else if (abs(parent->_bf) == 1){cur = parent;parent = parent->_parent;}}}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;}void InOrder(){_InOrder(_root);cout << endl;}int Height(){return _Height(_root);}int Size(){return _Size(_root);}bool IsBalanceTree(){return _IsBalanceTree(_root);}private:Node* _root = nullptr;void _Revolvenode(Node* parent){if (parent->_bf == -2 && parent->_left->_bf == -1) // 右单旋RotateR(parent);else if (parent->_bf == 2 && parent->_right->_bf == 1) // 左单旋RotateL(parent);else if (parent->_bf == -2 && parent->_left->_bf == 1) // 左右双旋RotateLR(parent);else if (parent->_bf == 2 && parent->_right->_bf == -1) // 右左双旋RotateRL(parent);elseassert(false);}//右单旋void RotateR(Node* parent){Node* subL = parent->_left;Node* subLR = subL->_right;Node* grandparent = parent->_parent;parent->_parent = subL;parent->_left = subLR;if (subLR) subLR->_parent = parent;subL->_right = parent;if (grandparent == nullptr){_root = subL;subL->_parent = nullptr;}else{if (parent == grandparent->_left)grandparent->_left = subL;else grandparent->_right = subL;subL->_parent = grandparent;}subL->_bf = 0;parent->_bf = 0;}//左单旋void RotateL(Node* parent){Node* subR = parent->_right;Node* subRL = subR->_left;Node* grandparent = parent->_parent;parent->_parent = subR;parent->_right = subRL;if (subRL) subRL->_parent = parent;subR->_left = parent;if (grandparent == nullptr){_root = subR;subR->_parent = nullptr;}else{if (parent == grandparent->_left)grandparent->_left = subR;else grandparent->_right = subR;subR->_parent = grandparent;}subR->_bf = 0;parent->_bf = 0;}//左右双旋void RotateLR(Node* parent){Node* subL = parent->_left;Node* subLR = subL->_right;int bf = subLR->_bf;RotateL(parent->_left);RotateR(parent);if (bf == 0){subL->_bf = 0;subLR->_bf = 0;parent->_bf = 0;}else if (bf == 1){subL->_bf = -1;subLR->_bf = 0;parent->_bf = 0;}else if (bf == -1){subL->_bf = 0;subLR->_bf = 0;parent->_bf = 1;}else{assert(false);}}//右左双旋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);}}void _InOrder(Node* root){if (root == nullptr){return;}_InOrder(root->_left);cout << root->_kv.first << ":" << root->_kv.second << endl;_InOrder(root->_right);}int _Height(Node* root){if (root == nullptr)return 0;int leftHeight = _Height(root->_left);int rightHeight = _Height(root->_right);return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;}int _Size(Node* root){if (root == nullptr)return 0;return _Size(root->_left) + _Size(root->_right) + 1;}bool _IsBalanceTree(Node* root){// 空树也是AVL树if (nullptr == root)return true;int leftHeight = _Height(root->_left);int rightHeight = _Height(root->_right);int diff = rightHeight - leftHeight;if (abs(diff) >= 2){cout << root->_kv.first << "高度差异常" << endl;return false;}if (root->_bf != diff){cout << root->_kv.first << "平衡因子异常" << endl;return false;}// pRoot的左和右如果都是AVL树,则该树一定是AVL树return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);}};
四、红黑树的概念
1. 红黑树介绍
红黑树是一种自平衡的二叉搜索树,通过在结点上增加一个颜色标记(红色或黑色),并遵循一系列颜色规则,确保树的高度始终保持在对数级别,从而保证插入、删除、查找等操作的时间复杂度为 O(logN)。红黑树并不是严格平衡的,而是“近似平衡”,这使得它在插入和删除时旋转次数较少,性能优于严格的 AVL 树在某些场景下。
2. 红黑树的规则✨✨✨
-
每个结点是红色或黑色
-
根结点是黑色
-
红色结点的子结点必须是黑色(即不存在连续两个红色结点)
-
从任一结点到其所有空子结点(NIL)的路径上,黑色结点的数量必须相同
《算法导论》等书籍上补充了一条每个叶子结点(NIL)都是黑色的规则。他这里所指的叶子结点不是传统意义上的叶子结点,而是我们说的空结点,有些书籍上也把NIL叫做外部结点。NIL是为了方便准确的标识出所有路径,《算法导论》在后续讲解实现的细节中也忽略了NIL结点,所以我们知道一下这个概念即可。




红黑树如何保证平衡?
红黑树通过颜色约束来保证最长路径不超过最短路径的 2 倍:
最短路径:由规则4可知,最短路径就是全黑结点路径,长度为
bh(黑色高度)最长路径:由规则2、3可知,最长路径就是黑红交替路径,长度为
2 * bh综合红黑树的4点规则而言,理论上的全黑最短路径和一黑一红的最长路径并不是在每棵红黑树都存在的。假设任意一条从根到NULL结点路径的长度为x,那么 bh ≤ h ≤ 2*bh。
3. 红黑树的效率
假设 N 是红黑树中结点数量,h 是最短路径的长度,那么,由此推出
,也就是意味着红黑树增删查改最坏情况下需要走最长路径
,那么时间复杂度还是
。
红黑树的表达相对 AVL 树要抽象一些,AVL 树通过高度差直观地控制平衡。红黑树通过 4 条规则的颜色约束,间接实现了近似平衡。它们的效率属于同一档次,但相对而言,插入相同数量的结点,红黑树的旋转次数更少,因为其对平衡的控制没有那么严格。
五、红黑树的插入
(这一部分跟着我走你包能听懂的)
1. 红黑树的结构
//和AVL基本一样,只是用枚举定义了红黑两种颜色
enum Colour
{RED,BLACK
};template<class K, class V>
struct RBTreeNode
{pair<K, V> _kv;RBTreeNode<K, V>* _parent;RBTreeNode<K, V>* _left;RBTreeNode<K, V>* _right;Colour _col; //节点颜色RBTreeNode(const pair<K, V>& kv):_kv(kv),_parent(nullptr),_left(nullptr),_right(nullptr){ }
};
2. 红黑树的插入过程
(1)插入一个值按二叉搜索树规则进行插入,插入后我们只需要观察是否符合红黑树的4条规则。
(2)如果是空树插入,新增结点是黑色结点。如果是非空树插入,新增结点必须红色结点,因为非空树插入,新增黑色结点就破坏了规则4,规则4是很难维护的。
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->_left;}else if (cur->_kv.first < kv.first){parent = cur;cur = cur->_right;}else return false;
}cur = new Node(kv);
if (parent->_kv.first < kv.first)
{parent->_right = cur;
}
else
{parent->_left = cur;
}
cur->_parent = parent;
cur->_col = RED;
(3)非空树插入后,新增结点必须红色结点,如果父亲结点是黑色的,则没有违反任何规则,插入结束。
(4)如果父亲结点是红色的,则违反规则3,需要修改。
说明:下图中假设我们把新增结点标识为c (cur),c的父亲标识为p (parent),p的父亲标识为g (grandfather),p的兄弟标识为u (uncle)。
3. 具体情况的分解
首先,我们要搞清楚,如果结点变红色,那它一定不影响4,如果结点变黑色,那它一定不影响3。在修改之前,它本身就是一颗红黑树,所以,如果维护4,我们必须保证该支路黑色结点数量不变,这样才能只用修改我们涉及到的支路。其次,只有一种情况要修改,当我们插入结点记为红,它的父亲是红,它父亲的父亲是黑时。我们需要看叔叔的情况,从而分为3类。
(1)u存在且为红
做法:不管增加的结点,或者变红的结点在p的什么位置,将p和u变⿊,g变红。在把g当做新的c,继续往上更新。
分析:因为p和u都是红色,g是黑色,把p和u变黑,左边子树路径各增加一个黑色结点,g再变红,相当于保持g所在子树的黑色结点的数量不变,同时解决了c和p连续红色结点的问题,需要继续往上更新是因为,g是红色,如果g的父亲还是红色,那么就还需要继续处理;如果g的父亲是黑色,则处理结束了;如果g就是整棵树的根,再把g变回黑色。

if (uncle && uncle->_col == RED)
{parent->_col = uncle->_col = BLACK;grandparent->_col = RED;cur = grandparent;parent = cur->_parent;
}
(2)u不存在或者u为黑
一共就四种情况(图中只表示相对位置,没画的不一定是空树):

分析:p必须变黑,才能解决连续红色结点的问题。u不存在或者是黑色的,这里单纯的变色无法解决问题,需要旋转+变色。
做法:
- 如果p是g的左,c是p的左(1号图),那么以g为旋转点进行右单旋,再把p变黑,g变红即可。p变成这颗树新的根,这样子树黑色结点的数量不变,没有连续的红色结点了,且不需要往上更新,因为p的父亲是黑色还是红色或者空都不违反规则。

- 如果p是g的左,c是p的右(2号图),那么先以p为旋转点进行左单旋,再以g为旋转点进行右单旋,再把c变黑,g变红即可,c变成这颗树新的根。

//grandparent->_left == parent
if (!uncle && uncle->_col == BLACK)
{if (parent->_left == cur){RotateR(grandparent);grandparent->_col = RED;parent->_col = BLACK;cur = parent;}else{RotateL(parent);RotateR(grandparent);cur->_col = BLACK;grandparent->_col = RED;}
}
-
如果p是g的右,c是p的左(3号图),那么先以p为旋转点进行右单旋,再以g为旋转点进行左单旋,再把c变黑,g变红即可,c变成这颗树新的根。
-
如果p是g的右,c是p的右(4号图),那么以g为旋转点进行左单旋,再把p变黑,g变红即可,p变成这颗树新的根。
//grandparent->_right == parent
if (!uncle && uncle->_col == BLACK)
{if (parent->_right == cur){RotateL(grandparent);grandparent->_col = RED;parent->_col = BLACK;cur = parent;parent = cur->_parent;}else{RotateR(parent);RotateL(grandparent);cur->_col = BLACK;grandparent->_col = RED;parent = cur->_parent;}
}
4. 核心问题解决
(1)为什么第一种情况不用区分cur的位置?
因为情况一的修复手段只有变色,不改变树的结构。无论cur是左孩子还是右孩子,颜色冲突都发生在parent层级,通过将parent和uncle变黑、grandfather变红,即可在保持黑高不变的前提下解决局部红色冲突。cur的具体位置不影响这个变色操作的效果。
(2)为什么当uncle不存在时,cur⼀定是新增结点?
因为如果cur不是新增结点,那么在插入之前,从grandfather到uncle(nullptr)的路径黑高为1(只有grandfather一个黑节点),而从grandfather到cur的路径由于包含parent(红)和cur(非新增,则必为黑),黑高至少为2,这已经违反了规则4。因此,在合法的红黑树中,uncle不存在时,cur不可能是原有节点,只能是本次新增的红色节点。相反,当uncle存在为黑时,如果cur是新增结点,那这条路原来少一个黑,所以cur为红是后序更新的结果。
(3)怎么保证我修改上面,但不影响我已经修改的下面
情况一(变色):修复后,
grandfather子树的黑高保持不变。虽然可能让grandfather变红导致新的红色冲突,但我们将冲突向上“传递”了。问题规模缩小了(从整棵树到grandfather的子树),最终会在根结点或一个黑色父结点处终止。情况二(旋转+变色):修复后,
grandfather子树的黑高保持不变,并且消除了所有红色冲突。旋转后将一个红色结点(原parent或cur)变为子树的根(黑色),彻底解决了局部冲突,无需向上传递。
其实就是每一次调整都保证黑色结点的数量不变,在祖父倍儿的那里修改,它的黑高会影响他的子孙,要多都多,要少都少,所以当子孙的黑高确定,祖父倍儿怎么修改也不影响子孙。
六、红黑树的其他知识
1. 红黑树的查找
和二叉搜索树一样
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;
}
2. 红黑树的平衡检测
平衡检测的本质就是逐一验证这4条规则是否被满足。
规则1因为枚举天然满足,无需检测。
检查根结点(规则2):
如果根结点存在且为红色,直接返回不平衡。
检查红色结点规则(规则3):
我们不能让一个红色结点去检查它的孩子(因为孩子可能不存在,或者有两个,代码复杂)。一个更聪明的方法是:从一个结点出发,检查它的父结点。如果当前结点是红色的,那么它的父结点必须是黑色的。写代码时,在遍历树的过程中(前序、中序、后序皆可),对每个非空的红色结点进行此检查。
检查路径规则(规则4):
- 选取一个参考值:任意选择一条从根到NIL的路径(比如最左路径),计算其黑色结点数量
refBlackNum。 - 遍历所有路径:使用深度优先搜索(DFS)遍历整棵树,每遇到一个黑色结点,当前路径的计数器
currentBlackNum就加一。 - 在叶子处验证:当走到
nullptr(即NIL结点)时,说明一条路径走到了尽头。此时将currentBlackNum与refBlackNum比较。如果不相等,则规则被违反。 - 递归传递计数器:在递归过程中,需要将当前累计的
currentBlackNum值传递下去。
bool Check(Node* root, int blackNum, const int refNum)
{if (root == nullptr){if (refNum != blackNum){cout << "存在黑色结点的数量不相等的路径" << endl;return false;}return true;}if (root->_col == RED && root->_parent->_col == RED){cout << root->_kv.first << "存在连续的红色结点" << endl;return false;}if (root->_col == BLACK){blackNum++;}return Check(root->_left, blackNum, refNum)&& Check(root->_right, blackNum, refNum);
}bool _IsBalance()
{if (_root == nullptr)return true;if (_root->_col == RED)return false;// 参考值 int refNum = 0;Node* cur = _root;while (cur){if (cur->_col == BLACK){++refNum;}cur = cur->_left;}return Check(_root, 0, refNum);
}
3. 红黑树的删除
红黑树的删除本章节不做讲解,有兴趣的同学可参考:《算法导论》或者《STL源码剖析》中讲解。
4. 完整代码
#include<iostream>
#include <utility>
#include<math.h>
#include<assert.h>
using namespace std;enum Colour
{RED,BLACK
};template<class K, class V>
struct RBTreeNode
{pair<K, V> _kv;RBTreeNode<K, V>* _parent;RBTreeNode<K, V>* _left;RBTreeNode<K, V>* _right;Colour _col; //节点颜色RBTreeNode(const pair<K, V>& kv):_kv(kv),_parent(nullptr),_left(nullptr),_right(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->_left;}else if (cur->_kv.first < kv.first){parent = cur;cur = cur->_right;}else return false;}cur = new Node(kv);if (parent->_kv.first < kv.first){parent->_right = cur;}else{parent->_left = cur;}cur->_parent = parent;cur->_col = RED;while (parent && cur->_col == RED){if (cur->_parent && cur->_parent->_col == BLACK) break;Node* grandparent = parent->_parent;//parent不为根节点,grandparent一定存在if (grandparent->_left == parent){Node* uncle = grandparent->_right;{if (uncle && uncle->_col == RED){parent->_col = uncle->_col = BLACK;grandparent->_col = RED;cur = grandparent;parent = cur->_parent;}else{if (parent->_left == cur){RotateR(grandparent);grandparent->_col = RED;parent->_col = BLACK;cur = parent;}else{RotateL(parent);RotateR(grandparent);cur->_col = BLACK;grandparent->_col = RED;}}}}// g// u pelse if (grandparent->_right == parent){Node* uncle = grandparent->_left;if (uncle && uncle->_col == RED){parent->_col = uncle->_col = BLACK;grandparent->_col = RED;cur = grandparent;parent = cur->_parent;}else{if (parent->_right == cur){RotateL(grandparent);grandparent->_col = RED;parent->_col = BLACK;cur = parent;parent = cur->_parent;}else{RotateR(parent);RotateL(grandparent);cur->_col = BLACK;grandparent->_col = RED;parent = cur->_parent;}}}else assert(false);}_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;}void InOrder(){_InOrder(_root);cout << endl;}int Height(){return _Height(_root);}int Size(){return _Size(_root);}bool IsBalance(){return _IsBalance();}private:Node* _root = nullptr;//右单旋void RotateR(Node* parent){Node* subL = parent->_left;Node* subLR = subL->_right;Node* grandparent = parent->_parent;parent->_parent = subL;parent->_left = subLR;if (subLR) subLR->_parent = parent;subL->_right = parent;if (grandparent == nullptr){_root = subL;subL->_parent = nullptr;}else{if (parent == grandparent->_left)grandparent->_left = subL;else grandparent->_right = subL;subL->_parent = grandparent;}}//左单旋void RotateL(Node* parent){Node* subR = parent->_right;Node* subRL = subR->_left;Node* grandparent = parent->_parent;parent->_parent = subR;parent->_right = subRL;if (subRL) subRL->_parent = parent;subR->_left = parent;if (grandparent == nullptr){_root = subR;subR->_parent = nullptr;}else{if (parent == grandparent->_left)grandparent->_left = subR;else grandparent->_right = subR;subR->_parent = grandparent;}}void _InOrder(Node* root){if (root == nullptr){return;}_InOrder(root->_left);cout << root->_kv.first << ":" << root->_kv.second << endl;_InOrder(root->_right);}int _Height(Node* root){if (root == nullptr)return 0;int leftHeight = _Height(root->_left);int rightHeight = _Height(root->_right);return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;}int _Size(Node* root){if (root == nullptr)return 0;int a = _Size(root->_left);int b = _Size(root->_right);return a + b + 1;}bool Check(Node* root, int blackNum, const int refNum){if (root == nullptr){if (refNum != blackNum){cout << "存在黑色结点的数量不相等的路径" << endl;return false;}return true;}if (root->_col == RED && root->_parent->_col == RED){cout << root->_kv.first << "存在连续的红色结点" << endl;return false;}if (root->_col == BLACK){blackNum++;}return Check(root->_left, blackNum, refNum)&& Check(root->_right, blackNum, refNum);}bool _IsBalance(){if (_root == nullptr)return true;if (_root->_col == RED)return false;// 参考值 int refNum = 0;Node* cur = _root;while (cur){if (cur->_col == BLACK){++refNum;}cur = cur->_left;}return Check(_root, 0, refNum);}};
本文源码在此
后记:学完其实我们就可以深切感受到这些发明的伟大,查找100万次的时间还是毫秒级别,这些天才真的太厉害了,这部分有些难,但是是笔试面试的高频考点,需要熟练掌握!如果有帮助,大家点个赞支持一下吧!



