【探寻C++之旅】第十三章:红黑树
请君浏览
- 前言
- 1. 红黑树的概念
- 1.2 红黑树的规则
- 1.3 红黑树如何确保最长路径不超过最短路径的两倍?
- 1.4 红黑树的效率
- 2. 红黑树的实现
- 2.1 红黑树的结构
- 2.2 红黑树的插入
- 情况1:变色
- 情况2:单旋+变色
- 情况2:双旋+变色
- 代码演示
- 2.3 红黑树的查找
- 3. 红黑树的验证
- 4. 红黑树与AVL树
- 尾声
前言
今天,我们继续踏入追寻C++的冒险历程。之前我们讲解了最早的自平衡二叉搜索树——AVL树,那么本章我们将讲解另一类自平衡二叉搜索树——红黑树。下面让我们一起来进入红黑树树的学习。
1. 红黑树的概念
前面我们讲解了一种自平衡二叉搜索树——AVL树,它可以使自己每一个节点的左右高度差严格保证在1之间,由于它更严格平衡,树高度较低,接近于log₂n,所以它的旋转次数很多,实现相对复杂。除了AVL树外还有另一种自平衡二叉搜索树,也是今天我们要讲解的主角——红黑树。红黑树是一种自平衡的二叉搜索树,其由来可以追溯到1972年由Russell J. R. R. Anderson 和 Robert Sedgewick 提出的研究。与AVL树引入平衡因子来控制平衡不同,红黑树通过引入**节点的颜色(红色和黑色)**来帮助维护二叉搜索树树的平衡性。
红黑树的每个节点增加⼀个存储位来表⽰节点的颜⾊,可以是红⾊或者⿊⾊。 通过对任何⼀条从根到叶⼦的路径上各个节点的颜⾊进⾏约束,红⿊树确保没有⼀条路径会⽐其他路径⻓出2倍,因⽽是接近平衡的。
1.2 红黑树的规则
那么红黑树是如何做到确保没有⼀条路径会⽐其他路径⻓出2倍的呢?这得益于红黑树特有的规则:
- 每一个节点不是红色就是黑色。
- 根节点是黑色的。
- 任意一条路径不会有连续的红色节点,也就是说一个节点如果是红色的,那么它的孩子只能是黑色的。
- 对于任意⼀个节点,从该节点到其所有叶子节点的简单路径上,必须含有数量相同的⿊⾊节点。
这些性质保证了红黑树的高度不会超过log₂n,从而确保了良好的性能。
下面让我们来看一看一些具体的红黑树:
观察上面的红黑树我们可以其每一个节点都符合上述的规则,同时也没有任何路径比其他路径长出两倍,这就是红黑树。
说明:
《算法导论》等书籍上补充了⼀条每个叶⼦节点(NIL)都是⿊⾊的规则。他这⾥所指的叶⼦节点不是传统的意义上的叶⼦节点,⽽是我们说的空节点,有些书籍上也把NIL叫做外部节点。NIL是为了⽅便准确的标识出所有路径,《算法导论》在后续讲解实现的细节中也忽略了NIL节点,所以我们知道⼀下这个概念即可
1.3 红黑树如何确保最长路径不超过最短路径的两倍?
由规则4可知,从根到叶子节点的每条路径都有相同数量的⿊⾊节点,所以极端场景下,最短路径就就是全是⿊⾊节点的路径,假设最短路径⻓度为bh(black height)。
由规则2和规则3可知,任意⼀条路径不会有连续的红⾊节点,所以极端场景下,最⻓的路径就是⼀⿊⼀红间隔组成,那么最⻓路径的⻓度为2*bh。
如下图所示:
综合红⿊树的4点规则⽽⾔,理论上的全⿊最短路径和⼀⿊⼀红的最⻓路径并不是在每棵红⿊树都存在的。假设任意⼀条从根到叶子节点路径的⻓度为x,那么bh RED, BLACK }; // 这⾥我们默认按key/value结构实现 template // 这⾥更新控制平衡也要加⼊parent指针 pair} }; template typedef RBTreeNode if (_root == nullptr) { _root = new Node(kv); _root-_col = BLACK; return true; } Node* cur = _root; Node* parent = nullptr; while (cur) { if (cur-_kv.first _right; } else if (cur->_kv.first > kv.first) { parent = cur; cur = parent->_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; while (parent && parent->_col == RED) { Node* grandfather = parent->_parent; if (parent == grandfather->_left) { Node* uncle = grandfather->_right; if (uncle && uncle->_col == RED) { // u存在且为红 -> 变⾊再继续往上处理 parent->_col = BLACK; uncle->_col = BLACK; grandfather->_col = RED; cur = grandfather; parent = cur->_parent; } else { // u存在且为⿊或不存在 -> 单旋+变⾊ // g // p u // c if (cur == parent->_left) { RotateR(grandfather); parent->_col = BLACK; grandfather->_col = RED; } // 双旋+变色 // g // p u // c else { RoteteL(parent); RoteteR(grandfather); cur->_col = BLACK; grandfather->_col = RED; } break; } } else { Node* uncle = grandfather->_left; if (uncle && uncle->_col == RED) { //u存在且为红 -> 变⾊再继续往上处理 parent->_col = BLACK; uncle->_col = BLACK; grandfather->_col = RED; cur = grandfather; parent = cur->_parent; } else { // u存在且为⿊或不存在 -> 单旋+变⾊ // g // u p // c if (cur == parent->_right) { RotateL(grandfather); parent->_col = BLACK; grandfather->_col = RED; } // 双旋+变色 // g // u p // c else { RoteteR(parent); RoteteL(grandfather); cur->_col = BLACK; grandfather->_col = RED; } break; } } } _root->_col = BLACK; return true; }
2.3 红黑树的查找
按⼆叉搜索树逻辑实现即可,搜索效率为 O(logN)
Node* Find(const K& key) { Node* cur = _root; while (cur) { if (cur->_kv.first _right; } else if (cur->_kv.first > key) { cur = cur->_left; } else { return cur; } } return nullptr; }
红黑树的删除操作相对复杂,因为它需要在删除节点后维护红黑树的特性。与插入一样都需要维护节点的颜色并且还要维护树的平衡,因此这里就不再赘述,感兴趣的可以去了解一下。这里只是带大家认识一下红黑树以及体会一下其维护平衡的一些做法。
3. 红黑树的验证
那么我们如何去判断一棵树是否是红黑树呢?或者说在插入或者删除后如何检测该树是否还是红黑树。
如果只是获取最⻓路径和最短路径,检查最⻓路径不超过最短路径的2倍是不可⾏的,因为就算满⾜这个条件,红⿊树也可能颜⾊不满⾜规则,当前暂时没出问题,后续继续插⼊还是会出问题的。
所以我们还是去检查4点规则,满⾜这4点规则,⼀定能保证最⻓路径不超过最短路径的2倍。
-
规则1枚举颜⾊类型,天然实现保证了颜⾊不是⿊⾊就是红⾊。
-
规则2直接检查根即可。
-
规则3前序遍历检查,遇到红⾊结点查孩⼦不太⽅便,因为孩⼦有两个,且不⼀定存在,反过来检查⽗亲的颜⾊就⽅便多了。
-
规则4前序遍历,遍历过程中⽤形参记录跟到当前结点的blackNum(⿊⾊结点数量),前序遍历遇到⿊⾊结点就++blackNum,⾛到空就计算出了⼀条路径的⿊⾊结点数量。再以任意⼀条路径⿊⾊结点数量作为参考值,依次⽐较即可。
下面让我们来看一看具体的代码实现:
bool Check(Node* root, int blackNum, const int refNum) { if (root == nullptr) { // 前序遍历⾛到空时,意味着⼀条路径⾛完了 //cout cout cout 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); }
4. 红黑树与AVL树
红黑树和AVL树都是自平衡二叉搜索树,那它们之间有什么区别呢?
特性 AVL树 红黑树 平衡条件 每个节点的左右子树高度差最多为1 每条从根到叶子的路径上,黑色节点数相同,且不允许两个连续的红色节点 树的高度 更严格平衡,树高度较低,接近于log₂n 平衡条件较松,树高度较AVL树略高,接近于2*log₂n 旋转次数 插入和删除时旋转次数较多 插入和删除时旋转次数较少 实现复杂度 实现相对复杂,需要维护节点的高度或平衡因子 实现较复杂,需要维护节点的颜色属性 查找效率 查找效率较高,因为树高度更低 查找效率稍低,但仍为对数时间复杂度 删除操作性能 删除较复杂,可能引起多次旋转 删除相对简单,旋转次数较少 红黑树的优点:
- 插入和删除效率较高:旋转次数较少,写操作性能较好,适合写操作较频繁的场景。
- 实现灵活:由于平衡条件较松,实现和维护相对灵活。
- 广泛应用:许多库和语言(如Linux内核、Java的TreeMap等)使用红黑树作为底层数据结构。
AVL树的优点:
- 查询性能优越:由于其严格的平衡条件,AVL树的高度最小,查询操作速度更快,适合读操作多于写操作的场景。
- 平衡维护严格:高度差保证在1以内,保证了高度的最小化。
尾声
若有纰漏或不足之处欢迎大家在评论区留言或者私信,同时也欢迎各位一起探讨学习。感谢您的观看!