【C++】深入理解红黑树:概念、性质和实现
一、红黑树的概念
红黑树是一种二叉搜索树,但它在每个节点上增加了一个存储位来表示节点的颜色,颜色只能是红色(Red)或黑色(Black)。通过对任何一条从根到叶子的路径上各个节点着色方式的限制,红黑树确保没有一条路径会比其他路径长出两倍(最长路径不超过最短路径的2倍),因而近似于平衡的二叉搜索树。
二、红黑树的性质
- 每个结点不是红色就是黑色
- 根节点是黑色的
- 如果一个节点是红色的,则它的两个孩子结点是黑色的(任何路径没有连续的红色节点)
- 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均 包含相同数目的黑色结点(每条路径上黑色节点的数量相等)
- 每个叶子结点都是黑色的(此处的叶子结点指的是空结点,即
NIL
节点)基于以上性质,红黑树能够保证:其最长路径中节点个数不会超过最短路径节点个数的两倍。
三、红黑树的实现
(一)红黑树节点的定义
首先定义红黑树节点的结构,包含左右子节点指针、父节点指针、数据以及颜色信息:
enum Color {RED,BLACK };template<class K, class V> struct RBTreeNode {RBTreeNode<K, V>* _left;RBTreeNode<K, V>* _right;RBTreeNode<K, V>* _parent;pair<K, V> _kv;Color _col;RBTreeNode(const pair<K, V>& kv):_left(nullptr), _right(nullptr), _parent(nullptr), _kv(kv), _col(RED){} };
(二)红黑树的插入
红黑树的插入是在二叉搜索树插入的基础上,通过一系列调整(变色、旋转)来维持红黑树的性质。下面结合图示和原理,详细拆解插入过程。
1、插入的前置知识
红黑树的新节点默认颜色为红色。这是因为:若新节点为黑色,会直接破坏 “每条路径黑色节点数相同” 的性质,需要大量调整;而红色节点若父节点为黑色,不会破坏任何性质,只有父节点也为红色时,才需要调整(避免连续红节点)。
2、插入的核心逻辑
红黑树是在二叉搜索树的基础上加上其平衡限制条件 ,因此插入分为两步:
a. 按二叉搜索树规则插入新节点:找到合适位置,插入红色新节点。
b. 调整红黑树性质:若插入后出现 “连续红节点”(父节点也为红色),则根据叔叔节点(
uncle
)的颜色,分情况调整。3、分情况调整(关键看uncle的颜色)
我们先来看看可能的插入情况:
因为新节点的默认颜色是红色,因此:如果其双亲节点的颜色是黑色,没有违反红黑树任何 性质,则不需要调整;但当新插入节点的双亲节点颜色为红色时,就违反了性质三不能有连在一起的红色节点,此时需要对红黑树分情况来讨论:
约定:
cur
为新插入节点,p
为父节点,g
为祖父节点,u
为叔叔节点(p
的兄弟节点)。
- 情况一:cur为红,p为红,g为黑,u存在且为红
调整方法:
a. 变色:
p
和u
变为黑色,g
变为红色b. 继续向上检查:将
g
视为新的cur
,重复调整逻辑(因为g
变红后,若其父亲也为红色,会再次出现连续红节点)。
- 情况二:cur为红,p为红,g为黑,u不存在/u存在且为黑
- 情况三:cur为红,p为红,g为黑,u不存在/u存在且为黑
4、最终收尾:根节点必须为黑色
无论上述哪种调整,最后都要检查根节点:若根节点因调整变为红色,需将其改回黑色(保证 “根节点为黑色” 的性质)。
5、插入操作的完整代码
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->_right = cur;}else{parent->_left = cur;}cur->_parent = parent;while (parent && parent->_col == RED){Node* grandfather = parent->_parent;if (parent == grandfather->_left){Node* uncle = grandfather->_right;// u存在且为红if (uncle && uncle->_col == RED){// 变色parent->_col = uncle->_col = BLACK;grandfather->_col = RED;// 继续向上处理cur = grandfather;parent = cur->_parent;}else // u不存在 或 存在且为黑{if (cur == parent->_left){// g// p// cRotateR(grandfather);parent->_col = BLACK;grandfather->_col = RED;}else{// g// p// cRotateL(parent);RotateR(grandfather);cur->_col = BLACK;grandfather->_col = RED;}break;}}else // parent == grandfather->_right{Node* uncle = grandfather->_left;// u存在且为红if (uncle && uncle->_col == RED){// 变色parent->_col = uncle->_col = BLACK;grandfather->_col = RED;// 继续向上处理cur = grandfather;parent = cur->_parent;}else{if (cur == parent->_right){// g// p// cRotateL(grandfather);grandfather->_col = RED;parent->_col = BLACK;}else{// g// p// cRotateR(parent);RotateL(grandfather);cur->_col = BLACK;grandfather->_col = RED;}break;}}}_root->_col = BLACK;return true; }
6、总结
红黑树插入的核心是根据 uncle 的颜色分情况调整:
uncle
为红:通过变色 + 向上递归解决。uncle
为黑或不存在:通过旋转 + 变色解决,旋转的目的是调整节点结构,让连续红节点的问题更易解决。所有调整的最终目的,都是为了维持红黑树的 5 条性质,确保树的近似平衡,从而保证插入、查询、删除的时间复杂度为 O(log n)。
四、红黑树的验证
写完插入操作后,我们怎么知道该树就已经是红黑树了呢?
红黑树的检测分为两步:
检测其是否满足二叉搜索树(中序遍历是否为有序序列)
检测其是否满足红黑树的性质
public:bool IsBalance(){return _IsBalance(_root);}void InOrder(){_InOrder(_root);cout << endl;} private:bool _IsBalance(Node* root){if (root == nullptr){return true;}if (root->_col != BLACK){return false;}//黑色节点数量的基准值int benchmark = 0;Node* cur = root;while (cur){if (cur->_col == BLACK){++benchmark;}cur = cur->_left;}return CheckColor(root, 0, benchmark);}bool CheckColor(Node* root, int blacknum, int benchmark){if (root == nullptr){if (blacknum != benchmark){return false;}return true;}if (root->_col == BLACK){++blacknum;}if (root->_col == RED && root->_parent && root->_parent->_col == RED){cout << root->_kv.first << "出现连续红色节点" << endl;}return CheckColor(root->_left, blacknum, benchmark)&& CheckColor(root->_right, blacknum, benchmark);}void _InOrder(Node* root){if (root == nullptr){return;}_InOrder(root->_left);cout << root->_kv.first << ":" << root->_kv.second << endl;_InOrder(root->_right);}