目录
前言:
一,红⿊树的概念
思考:红黑树如何确保最长路径不超过最短路径的2倍
红⿊树的效率:
二,红黑树与AVL树对比
三,红黑树的实现
红⿊树的结构
红黑树的插入
情况一:插入的是根节点
情况二:变色(u存在且颜色为红色时)
情况三:单旋+变色(u为null(不存在)或颜色为黑色时)
情况四:双旋+变色(u为null(不存在)或颜色为黑色时)
红黑树的插入代码实现
红⿊树的查找
红⿊树的验证
红黑树的删除
结语:
前言:
二叉搜索树可以快速完成查找,但在极端插入顺序下会退化成单链表,查询效率从 \(O(\log N)\) 退化至 (O(N))。为解决普通 BST 的失衡问题,人们设计出自平衡二叉树,红黑树就是其中应用极广的一种。
红黑树并非依靠严格平衡,而是通过节点颜色规则与旋转操作,约束最长路径不超过最短路径的两倍,把查找、插入、删除稳定维持在 (O(log N))。它不追求绝对平衡,减少了旋转开销,在工程中大量使用,C++ 的std::map、std::set底层就是红黑树。
本文将从红黑树的五条核心性质入手,拆解节点变色、左旋与右旋的底层逻辑,重点分析插入场景下的四种修复 Case,并提供可编译的 C++ 完整代码,一步步理解红黑树的自平衡机制。
AVL树:C++高阶数据结构——AVL树-CSDN博客
一,红⿊树的概念
红⿊树是⼀棵⼆叉搜索树,他的每个结点增加⼀个存储位来表⽰结点的颜⾊,可以是红⾊或者⿊⾊。 通过对任何⼀条从根到叶⼦的路径上各个结点的颜⾊进⾏约束,红⿊树确保没有⼀条路径会⽐其他路 径⻓出2倍,因⽽是接近平衡的。
每个结点不是红⾊就是⿊⾊。
根结点是⿊⾊的。
如果⼀个结点是红⾊的,则它的两个孩⼦结点必须是⿊⾊的,也就是说任意⼀条路径不会有连续的 红⾊结点。
对于任意⼀个结点,从该结点到其所有NULL结点的简单路径上,均包含相同数量的⿊⾊结点。
下面是一个红黑树:
它有9条路径,每条路径的黑色结点数量相同,为2,且没有红色结点与红色结点相连接的情况,黑色为根结点,满足红黑树的条件。
注意:路径是到null结点的路径,如下图:
所以有9条路径。
注:这里的NIL 哨兵(外部结点),不是存储业务数据的普通叶子。理论上依靠 NIL 统一路径边界;但实际编码不会实例化 NIL 对象,直接用空指针NULL来充当 NIL,这个看图更好理解。
这个NIL其实在日常时候不需要怎么管,把NIL哨兵理解成NULL节点就可以了,每条路径以NULL为结束(NIL只要有个概念就行)。
上面这些均为红黑树。
思考:红黑树如何确保最长路径不超过最短路径的2倍
首先,我们通过第五点(黑高相等)和第三点NIL全为黑色,我们在推导的时候可以不用考虑NIL节点对路径的影响,只需要考虑我们看得见的节点就可以了。
由规则 5(黑高相等)可知,从根到达 NIL 哨兵的每一条路径,黑色结点数量完全相同,这个数值记作bh,也就是树的黑高。这里的bh我们不包含NIL节点。
极端场景下最短的数据结点路径,整条路径全部由黑色结点构成。
根据规则 4,树中不允许出现连续红色结点。因此路径上红色结点最多只能与黑色结点交替出现,极端最长路径就是黑、红交替排布。由于黑色结点总数固定为bh(不包含NIL节点)交替排布(黑红黑红)下得到的最长路径就是2*bh(不包含NIL节点)。
附:包含NIL节点的情况下,最短路径是bh+1,最长路径就是2*bh+1
注:全黑的最短路径、一黑一红交替的最长路径,这两种极端情况并不会在每一棵红黑树中同时出现。设任意一条从根出发到 NIL 的路径中,数据结点的总个数为 h(不包含NIL节点),则满足不等式:bh<= h <=2*bh
红⿊树的效率:
假设N是红⿊树树中结点数量,h最短路径的⻓度
由此推出
也就是意味着红⿊树增删查改最坏也就是⾛最⻓路径
,那么时间复杂度还是O(logN)。
红⿊树的表达相对AVL树要抽象⼀些,AVL树通过⾼度差直观的控制了平衡。红⿊树通过4条规则的颜 ⾊约束,间接的实现了近似平衡,他们效率都是同⼀档次,但是相对⽽⾔,插⼊相同数量的结点,红 ⿊树的旋转次数是更少的,因为他对平衡的控制没那么严格。
二,红黑树与AVL树对比
平衡条件
AVL 树属于强平衡二叉搜索树,依靠平衡因子约束,保证左右子树高度差不超过 1;
红黑树是弱平衡二叉搜索树,不严格约束高度差,依靠四条颜色规则,限制最长路径不超过最短路径 2 倍。
性能差异查询:AVL 树树高更低,查询理论略快;但两者时间复杂度都是𝑂(𝑙𝑜𝑔𝑁)。
插入:AVL 插入最多 1 次旋转;红黑树插入最多 2 次旋转,很多场景仅变色无需旋转。
删除:这是两者最核心区别,AVL 删除最坏会触发𝑂(𝑙𝑜𝑔𝑁)次旋转;红黑树删除最多 3 次旋转,写操作代价更小。
空间与工程使用两者结点额外开销很小:AVL 需要保存平衡因子,红黑树只需要保存颜色标记。
AVL 更适合读多写极少的场景;红黑树读写综合表现更好,C++ STL 的 map、set 底层选用红黑树。
三,红黑树的实现
红⿊树的结构
#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: private: Node* _root = nullptr; };红黑树的插入
红黑树插入一个值的大概过程
1,插入一个值,先按照二叉搜索树的规则完成插入,插入完成后再校验是否满足红黑树规则。
2,如果是空树插入:新增节点作为根节点,根节点需要设置为黑色。 如果是非空树插入:新增节点默认是红色。倘若新增节点为黑色,会直接破坏「每条路径黑色节点数量相等」这规则 ,黑高的失衡很难修复,因此新节点必须为红色。
3,插入完成,如果存在父结点且父节点
p是黑色:没有违反任何红黑树规则,插入直接结束,无需修复。4,如果父节点p是红色:此时出现连续的红色节点,违反规则 。 由于p是红色,那么祖父节点g一定是黑色。 此时c(cur 当前节点) 红、p(parent 父节点) 红、g(grandfather 祖父节点) 黑,三者颜色固定。后续处理的关键取决于叔叔节点u(uncle,父节点的兄弟) 的颜色,根据u的不同情况分支处理。
c(cur):当前需要处理的节点(插入的点)。
p(parent):c 的父节点。
g(grandfather):c 的祖父节点。
u(uncle):g 的孩子,p 的兄弟,c 的叔叔节点。
红黑树的插入,当插入red结点时,parent是red结点,有四种情况,下面进行讲解。
情况一:插入的是根节点
这个情况非常的简单,开辟需要新空间插入进去,将color变为black就完成了。
情况二:变色(u存在且颜色为红色时)
情况二的操作流程:
parent与cur是red,与规则冲突了,所以看uncle是什么color,如果uncle的color是red,就进行下面的操作:
将父节点
parent、叔叔节点uncie修改为黑色;祖父节点grandfather修改为红色,然后将grandfather作为新的cur结点,继续向上迭代修复。
分析:因为parent和uncle都是红⾊,
grandfather是⿊⾊,把parent和uncle变⿊,左边⼦树路径各增加⼀个⿊⾊结点,grandfather再变红,相 当于保持grandfather所在⼦树的⿊⾊结点的数量不变,同时解决了cur和parent续红⾊结点的问题,需要继续往上更新 是因为,grandfather是红⾊,如果grandfather的⽗亲还是红⾊,那么就还需要继续处理;如果grandfather的⽗亲是⿊⾊,则处理结束 了;如果grandfather就是整棵树的根,再把grandfather变回⿊⾊。
跟AVL树类似,上图我们展⽰了⼀种具体情况,但是实际中需要这样处理的有很多种情况。
下图将以上类似的处理进⾏了抽象表达,d/e/f代表每条路径拥有hb个⿊⾊结点的⼦树,a/b代表每 条路径拥有hb-1个⿊⾊结点的根为红的⼦树,hb>=0。 (hb为black结点的高度)
hb=0的情况,如下:
hb=1的情况,如下:
hb=2的情况,如下:
这⾥组合 情况上百亿种,这些样例是帮助我们理解,不论情况多少种,多么复杂,处理⽅式⼀样的,变⾊再 继续往上处理即可,所以我们只需要看抽象图即可。
情况三:单旋+变色(u为null(不存在)或颜色为黑色时)
注意:
uncle为null或者color为black并不代表一定走情况三 单旋+变色,也可以走情况四。
当u不存在时:cur一定是本次新插入的红色结点。新插入的节点是红结点,不会影响原树的黑结点高度,只会产生双红情况。 如果cur不是新插入节点,且uncle不存在的,那么原树的黑结点高度就是不相等的,因为cur是进行变色向上迭代修复出来的,cur这个位置本来是黑色,那么会与规则相冲突,hb是不相同的,所以原本不是红黑树,所以cur是新插入的红色的结点。
当uncle存在且为黑色时:c绝对不是本次新插入节点。c原先就存在,本来是黑色;下层子树插入节点触发更新逻辑,向上更新到 c 节点,把 c 节点变为了红色。只有这样才能够满足黑高的规则。
情况三的操作流程:
单旋是与AVL树一样进行单旋的,单旋不会可以看看前面的AVL部分。
根据具体情况更新红黑树结点颜色(保证hb相等)——只记结论就是g变红,p变黑
c为红,p为红,g为⿊,u不存在或者u存在且为⿊,u不存在,则c⼀定是新增结点,u存在且为⿊,则 c⼀定不是新增,c之前是⿊⾊的,是在c的⼦树中插⼊,符合情况1,变⾊将c从⿊⾊变成红⾊,更新上 来的。
分析:p必须变⿊,才能解决,连续红⾊结点的问题,u不存在或者是⿊⾊的,这⾥单纯的变⾊⽆法解 决问题,需要旋转+变⾊。
这里的单旋一样有左单旋与右单旋。
情况四:双旋+变色(u为null(不存在)或颜色为黑色时)
虽然情况四与情况三的条件一样,但是有很明显的差别的,单旋像一条直线,二双旋像一个拐。
上图是AVL树的图,如果对这种拐进行单旋的话是无法完成的,所以需要双旋来解决。
情况四的处理流程:
双旋就是两个单旋,先对局部进行处理,将拐变成一个直线,如下图,来源于AVL树:
c为红,p为红,g为⿊,u不存在或者u存在且为⿊,u不存在,则c⼀定是新增结点,u存在且为⿊,则 c⼀定不是新增,c之前是⿊⾊的,是在c的⼦树中插⼊,符合情况1,变⾊将c从⿊⾊变成红⾊,更新上 来的。
分析:p必须变⿊,才能解决,连续红⾊结点的问题,u不存在或者是⿊⾊的,这⾥单纯的变⾊⽆法解 决问题,需要旋转+变⾊。
红黑树的插入代码实现
#pragma once #include<iostream> using namespace std; // 枚举值表⽰颜⾊ 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); private: Node* _root = nullptr; }; template<class K,class V> bool RBTree<K, V>::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; } else { parent->_left = cur; } while (parent && parent->_col == RED) { Node* grandfather = parent->_parent; if (parent == grandfather->_left) { 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) { // g // p u // c 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 { // 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 { RotateR(parent); RotateL(grandfather); cur->_col = BLACK; grandfather->_col = RED; } break; } } } _root->_col = BLACK; return true; }旋转没有在里面,可以去AVL树中寻找。
红⿊树的查找
红黑树的查找与前面学习的树是一样的,不过多赘叙了。
template<class K, class V> typename RBTree<K, V>::Node* RBTree<K, V>::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点规则,满⾜这4点规则,⼀定能保证最⻓路径不超过最短路径的2倍。
1.规则1枚举颜⾊类型,天然实现保证了颜⾊不是⿊⾊就是红⾊。
2.规则2直接检查根即可
3.规则3前序遍历检查,遇到红⾊结点查孩⼦不太⽅便,因为孩⼦有两个,且不⼀定存在,反过来检 查⽗亲的颜⾊就⽅便多了。
4.规则4前序遍历,遍历过程中⽤形参记录跟到当前结点的blackNum(⿊⾊结点数量),前序遍历遇到 ⿊⾊结点就++blackNum,⾛到空就计算出了⼀条路径的⿊⾊结点数量。再任意⼀条路径⿊⾊结点 数量作为参考值,依次⽐较即可。
template<class K,class V> bool RBTree<K, V>::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); } template<class K, class V> bool RBTree<K,V>::Check(Node* root, int blackNum, const int refNum) { if (root == nullptr) { // 前序遍历⾛到空时,意味着⼀条路径⾛完了 //cout << blackNum << endl; 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); }红黑树的删除
红黑树删除逻辑复杂度远高于插入,且面试一般只要求口述删除各个场景,很少要求完整手写实现,本文就不再实现删除部分。
结语:
希望可以给你提供帮助,谢谢观看!!!
0" />