从零开始的C++学习生活 13:红黑树全面解析

个人主页:Yupureki-CSDN博客
C++专栏:C++_Yupureki的博客-CSDN博客
目录
前言
1. 红黑树的概念
1.1 红黑树的规则
1.2 红黑树如何保证平衡?
2. 红黑树的实现
3.1 基本结构
3.2 插入操作详解
3.2.1 变色
情况1:叔叔节点存在且为红色 (变色)
情况2:叔叔节点不存在或为黑色(旋转+变色)
单旋情况:
双旋情况:
3.3 查找操作
3.4 红黑树的验证
上一篇:从零开始的C++学习生活 12:AVL树全面解析-CSDN博客
前言
前面我们学习了AVL树,一种高级的二叉搜索树,通过平衡因子控制树的高低差不超过2来保持树的平衡,可以大幅增加查找数据的效率
而今天,我们又有一种由AVL树而来的变种:红黑树。同样是高级的二叉搜索树,红黑树同样限制了树的最大高度来增加效率,但是方法有所差异
我将深入探讨红黑树的原理、特性及实现细节,帮助你全面理解这一重要数据结构的工作原理。

1. 红黑树的概念
红黑树是一种特殊的二叉搜索树,它在每个节点上增加了一个存储位来表示节点的颜色(红色或黑色)。通过对从根到叶子的任何路径上的节点颜色施加约束,红黑树确保没有任何一条路径会比其他路径长出两倍,因而保持了近似平衡的状态。

1.1 红黑树的规则
-
颜色规则:每个节点不是红色就是黑色
-
根节点规则:根节点必须是黑色的
-
红色节点规则:红色节点的两个子节点必须是黑色的(即不能有连续的红色节点)
-
黑色高度规则:从任意节点到其所有NULL节点的简单路径上,包含相同数量的黑色节点
总地来说,红黑树顾名思义,只有红和黑,没有蓝白树或者其他的。
当然这里红和黑只是一种标志,你要想变成其他的颜色其实也可以awa
回答我们所说的,通过上面的规则分析,可以知道不能由连续的红色节点,即父亲和孩子不能同时为红色,但兄弟就不用,毕竟都不是连续的。但是可以存在连续的黑色节点
要注意的是,所有路径上的黑色节点数量相同,这里的路径指的是从根节点到任意一个NULL节点,不是直观的只算有节点的路径。
这样能够保证红黑树的最小长度是全黑(N),最大长度是红和黑交叉出现(2*N),因此限制了红黑树的高度
1.2 红黑树如何保证平衡?
红黑树通过上述四条规则巧妙地维持了树的平衡:
-
根据规则4,从根到NULL节点的每条路径都有相同数量的黑色节点。设最短路径(全黑路径)的黑色节点数为bh
-
根据规则2和3,最长路径由黑红节点交替组成,其长度不超过2×bh
-
因此,最长路径不会超过最短路径的两倍
假设红黑树有N个节点,最短路径长度为h,则有:
2^h - 1 < N < 2^(2×h) - 1
由此可得 h ≈ logN,这意味着红黑树的插入、删除和查找操作的最坏情况时间复杂度都是O(logN)。
与AVL树相比,红黑树对平衡的控制相对宽松,这使得在插入相同数量节点时,红黑树所需的旋转操作更少,整体性能更加稳定。
2. 红黑树的实现
3.1 基本结构
红黑树有红色和颜色,因此我们利用枚举常量
// 枚举值表示颜色
enum Colour {RED,BLACK
};
红黑树的节点和AVL树的节点相似,只不过多了颜色
// 红黑树节点
template<class K, class V>
struct RBTreeNode {std::pair<K, V> _kv;RBTreeNode<K, V>* _left;RBTreeNode<K, V>* _right;RBTreeNode<K, V>* _parent;Colour _col;RBTreeNode(const std::pair<K, V>& kv): _kv(kv), _left(nullptr), _right(nullptr), _parent(nullptr), _col(RED){}
};
红黑树本体也和AVL树极其相似
template<class K, class V>
class RBTree {typedef RBTreeNode<K, V> Node;
private:Node* _root = nullptr;// 其他成员函数...
};
3.2 插入操作详解
红黑树的插入我们默认按照二叉搜索树的规则
如果是非空树,插入的一定是红色节点,因为红黑树保证每条路径上的黑色节点相同。如果插入黑色节点,那么会破坏相等这一条件
如果是空树,插入的节点作为根节点,颜色设为黑色
之后要检查并修复红黑树性质:如果父节点是黑色,插入完成;如果父节点是红色,需要根据叔叔节点的颜色进行不同的处理
3.2.1 变色

假设我们插入的节点之前是红色节点,那么就会出现红红的情况,因此我们需要进行变色处理
这里我们把新插入的节点37作为孩子,40作为父亲,45作为祖父,48作为叔叔
情况1:叔叔节点存在且为红色 (变色)
在这里叔叔存在且为红色,那么我们就把parent和uncle变为黑色,grandparent变为红色
因为grandparent为红色之后,可能grandparent的parent也为红色,例如下图,因此我们还需向上检查,把child赋值为grandparent

情况2:叔叔节点不存在或为黑色(旋转+变色)
单旋情况:
对于上面的右图,叔叔虽然存在但是为黑色,那么我们还需要进行旋转处理
这里我们专门把child,parent,grandparent拆分出来(uncle无需关心,同时把偷偷把child变到左边去,child在右边是双旋情况)

我们对grandparent进行右单旋,同时把parent变为黑色,grandparent变为黑色
到了这种情况,uncle要么不存在要么为黑色,我们不用管uncle到底存不存在,也不用进行任何操作,继续保持grandparent指向uncle即可,别问,问就是巧妙awa,可以细品
我们再把child赋值为parent,由于parent已经为黑色了,不管parent和parent是红色还是黑色,都不会冲突,因此可以直接退出
双旋情况:
对于child在parent的右边情况,和AVL树类似,我们需要先对parent进行左单旋,保证三个节点在一条直线上,才能对grandparent进行右单旋

插入过程完成代码:
bool Insert(const K& key, const V& value)
{if (_root == nullptr){_root = new Node({key,value});_root->_col = BLACK;return true;}Node* newnode = new Node({ key,value });newnode->_col = RED;Node* cur = _root;Node* child = cur;while (cur)//按照二叉搜索树的规则插入{if (key < cur->_kv.first){child = cur;cur = cur->_left;}else if (key > cur->_kv.first){child = cur;cur = cur->_right;}else{find(key)->_kv.second++;return true;}}if (key < child->_kv.first){child->_left = newnode;newnode->_parent = child;}else{child->_right = newnode;newnode->_parent = child;}child = newnode;Node* parent = child->_parent;while (child->_col == RED && parent && parent->_col == RED)
//当child为红色时并且parent为黑色时继续循环{parent = child->_parent;Node* pparent = parent->_parent;Node* uncle = nullptr;if (pparent == nullptr)break;else{if (pparent->_left == parent)uncle = pparent->_right;elseuncle = pparent->_left;if (uncle && uncle->_col == RED)//叔叔存在并且为红色直接变色{uncle->_col = BLACK;parent->_col = BLACK;pparent->_col = RED;child = pparent;parent = child->_parent;}else//叔叔不存在或者为黑色,需要旋转和变色{if (pparent->_left == parent){if (parent->_right == child){RotateL(parent);RotateR(pparent);child->_col = BLACK;pparent->_col = RED;}else{RotateR(pparent);parent->_col = BLACK;pparent->_col = RED;child = parent;parent = child->_parent;}}else{if (parent->_left == child){RotateR(parent);RotateL(pparent);child->_col = BLACK;pparent->_col = RED;}else{RotateL(pparent);parent->_col = BLACK;pparent->_col = RED;child = parent;parent = child->_parent;}}}}}_root->_col = BLACK;//在变色处理时可能会对根节点进行变动,根节点需要保持黑色return true;
}
3.3 查找操作
红黑树的查找操作与普通二叉搜索树完全相同,时间复杂度为O(logN)。
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;
}
3.4 红黑树的验证
验证红黑树是否满足所有规则是确保实现正确的关键。验证方法包括:
-
检查根节点是否为黑色
-
检查是否存在连续的红色节点
-
检查所有路径的黑色节点数量是否相同
我们利用递归来实现
规则1很好判断
规则2我们对每个节点和其父节点检查即可
规则3我们可以定义一个专门统计一条路径上黑色节点数量的变量,一直传给下一个节点,随后判断左右两条路是否相等即可
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);
}
