红黑树(Red-Black Tree),为什么需要红黑树?红黑树的核心特性(5 条规则)?黑色高度?平衡修复?红黑树旋转变色的具体操作案例?
红黑树(Red-Black Tree)是一种自平衡二叉搜索树(Self-Balancing Binary Search Tree),它通过一套特定的规则(颜色标记 + 旋转操作)维持树的平衡性,确保插入、删除、查找等操作的时间复杂度始终保持在O(log n)(n 为节点数)。与 AVL 树相比,红黑树的平衡条件更宽松,旋转操作更少,在实际应用中(如 C++ 的std::map、Java 的TreeMap)更为广泛。
为什么需要红黑树?
普通的二叉查找树在最坏情况下(例如,插入的元素已经有序)会退化成一个链表,导致查找、插入和删除操作的时间复杂度退化为 O(n)。而红黑树通过上述规则保证了树的高度始终保持在 O(log n) 的水平,从而使得查找、插入和删除操作的最坏情况时间复杂度均为 O(log n)。
红黑树的核心特性(5 条规则)
红黑树的每个节点都有一个 “颜色” 属性(红色或黑色),且必须满足以下 5 条规则。
(1)节点颜色
每个节点要么是红色,要么是黑色。
(2)根节点
根节点必须是黑色。
(3)叶子节点
所有叶子节点(NIL 节点,即空节点)都是黑色。(注:实际实现中,叶子节点通常被简化为一个共享的 NIL 节点,不存储数据)。
(4)红色节点的子节点
如果一个节点是红色,那么它的两个子节点必须是黑色(即不允许两个红色节点直接相连)。
(5)路径黑色性(黑色高度平衡)
从任意节点到其所有叶子节点的路径中,包含的黑色节点数量相同(称为 “黑色高度”)。
红黑树节点的定义
(1)颜色属性
使用RED和BLACK两种状态,新节点默认设为红色(减少插入时对黑色高度的影响)。
(2)指针设计
①左右子节点指针用于维持二叉搜索树的结构。
②父节点指针是红黑树特有的(普通 BST 可省略),用于在旋转和平衡调整时快速定位祖先节点。
(3)叶子节点处理
实际实现中,通常将null子节点视为 “哨兵节点”(NIL 节点),统一设为黑色,简化边界条件判断。
哨兵节点可以是一个全局共享的实例,所有空指针都指向它。
// 定义节点颜色的枚举
enum Color {
RED, BLACK
}
// 红黑树节点类
class RedBlackTreeNode {
int value; // 节点存储的值(可以是任意可比较类型)
Color color; // 节点颜色(红/黑)
RedBlackTreeNode left; // 左子节点引用
RedBlackTreeNode right; // 右子节点引用
RedBlackTreeNode parent; // 父节点引用(便于旋转和调整)
// 构造方法
public RedBlackTreeNode(int value) {
this.value = value;
this.color = Color.RED; // 新节点默认红色(插入操作的约定)
this.left = null;
this.right = null;
this.parent = null;
}
}
红黑树为何能保持平衡?
上面这 5 条规则共同保证了红黑树的 “近似平衡”。
最长路径(红黑交替)的长度不会超过最短路径(全黑)的 2 倍。因此,树的高度被限制在 2log(n+1) 以内,确保了所有操作的对数级时间复杂度。
关键概念:黑色高度
在红黑树中,黑色高度(Black Height) 是一个核心概念,用于衡量从某节点到其所有叶子节点的路径中黑色节点的数量,是维持树平衡的关键指标之一。
黑色高度的定义?
(1)对于任意节点,其黑色高度是指从该节点出发,到达其任意一个叶子节点(NIL 节点)的路径中,黑色节点的总数(包含该节点自身)。
(2)叶子节点(NIL 节点,空节点)被定义为黑色,其黑色高度为 1(自身)。
(3)红色节点的黑色高度与其父节点、子节点的黑色高度相关,但自身不增加黑色高度的计数。
红黑树规则明确规定:从任意节点到其所有叶子节点的路径中,包含的黑色节点数量必须相同。
意味着:
(1)任意节点的左子树和右子树的黑色高度必须相等(否则从该节点到左右叶子的黑色节点数会不同)。
(2)根节点的黑色高度决定了整棵树的黑色高度(从根到所有叶子的黑色节点数相同)。
插入操作与平衡修复
插入新节点时,默认将其标记为红色(减少对黑色高度的影响),然后检查是否违反红黑树规则。若违反,通过以下两种操作修复:
(1)旋转操作(与 AVL 树类似)
①左旋转
将右子树的根节点提升为父节点,原父节点变为其左子树。
②右旋转
将左子树的根节点提升为父节点,原父节点变为其右子树。
旋转不改变二叉搜索树的性质(左子树值 < 父节点值 < 右子树值),仅调整结构。
(2)变色操作
改变节点的颜色(红→黑或黑→红),用于调整红色节点的分布,避免违反规则。
①新节点默认染为 红色(若染为黑色,会直接破坏规则 5:黑色高度平衡,修复更复杂)。
②插入后可能违反规则 2(根节点必须是黑色)(根为红)或规则 4(不允许两个红色节点直接相连)(连续红节点),需分情况处理:
(1)若父节点是黑色:无冲突,无需调整。
(2)若父节点是红色(必存在黑色祖父节点,因规则 4:不允许两个红色节点直接相连)
若 “叔叔节点”(父节点的兄弟)是红色:通过重新着色(父、叔变黑,祖父变红)修复。
若 “叔叔节点” 是黑色:通过旋转(左旋 / 右旋)+ 着色修复。
(3)删除操作的调整
删除操作更复杂,因可能删除黑色节点,破坏规则 5(黑色高度平衡)。核心是处理 “双黑节点”(删除黑色节点后,其子节点继承 “额外黑色”),通过旋转和着色消除双黑状态。
红黑树 vs AVL 树
特性 | 红黑树 | AVL 树 |
---|---|---|
平衡条件 | 颜色规则 + 黑色高度相等 | 平衡因子绝对值≤1(严格平衡) |
旋转频率 | 低(插入最多 2 次,删除最多 3 次) | 高(插入 / 删除可能多次旋转) |
空间开销 | 存储颜色(1 bit) | 存储平衡因子(通常 1 byte) |
适用场景 | 频繁插入 / 删除(如映射、缓存) | 频繁查找(如数据库索引) |
红黑树与AVL树都是高效的平衡二叉树,增删改查时间复杂度都是O(log_{2}^{N}),红黑树不追求绝对平衡,只需保证最长路径不超过最短路径的两倍即可,相对而言,降低了插入和旋转的次数,所以经常在增删改查的结构中性别比AVL树更好,且红黑树的实现相对来说更简单,实际运用中也更多。
红黑树应用场景
红黑树因高效的插入删除性能,被广泛应用于:
(1)C++ 的 std::map、std::set
(2)Java 的 TreeMap、TreeSet
(3)Linux 内核的进程调度、虚拟内存管理
(4)数据库索引等需要动态维护有序数据的场景
红黑树通过颜色规则和少量旋转,在保证二叉搜索树特性的同时,实现了近似平衡,是兼顾效率与实现复杂度的优秀数据结构。
红黑树旋转变色的具体操作案例
下面从空树开始,逐步演示红黑树的插入、旋转和平衡调整过程,每一步都详细说明操作内容和原因。
(1)初始状态:空树
(2)插入第一个节点(值 = 10)
操作:插入新节点 10
规则处理:新节点默认为红色,但根据规则 2(根节点必须为黑色),将其改为黑色。
(3)插入第二个节点(值 = 5)
操作:按 BST 规则,5<10,插入为左子节点,默认为红色。
检查规则:无违反(根为黑,无连续红节点)。
(4)插入第三个节点(值 = 15)
操作:15>10,插入为右子节点,默认为红色
检查规则:无违反(10 为黑,15 为红,无连续红)
(5)插入第四个节点(值 = 3)
操作:3<5,插入为 5 的左子节点,默认为红色
检查规则:父节点 5 是红色,新节点 3 也是红色,违反规则 4(连续红节点)
家族关系:
(1)新节点 3 ®
(2)父节点 5 ®
(3)祖父节点 10 (B)
(4)叔叔节点 15 ®(叔叔为红色)
调整步骤:
(1)将父节点 5 和叔叔节点 15 改为黑色
(2)将祖父节点 10 改为红色
(3)因 10 是根节点,需再改回黑色(符合规则 2)
调整后结果:
(6)插入第五个节点(值 = 1)
操作:1<3,插入为 3 的左子节点,默认为红色
检查规则:父节点 3 是红色,新节点 1 也是红色,违反规则 4
家族关系:
(1)1®,3®
(2)5 (B),NIL (B)(叔叔为空,视为黑色)
结构:5 左→ 3左→ 1(左左结构)
调整步骤:
(1)右旋祖父节点 5
(2)将原父节点 3 改为黑色
(3)将原祖父节点 5 改为红色
调整过程:
(1)右旋前(违反规则 4)
(2)右旋后(未着色)
(3)右旋后(着色后)
(7)插入第六个节点(值=7)
操作:7>5,插入为5的右子节点,默认为红色
检查规则:父节点5是红色,新节点7也是红色,违反规则4
家族关系:
(1)7®,5®
(2)3(B),1®(叔叔为红色)
调整步骤:
(1)将父节点5和叔叔节点1改为黑色
(2)将祖父节点3改为红色
调整后结果:
(8)插入第七个节点(值=12)
新节点12®,父节点15(B),无冲突。
(9)插入第八个节点(值=18)
操作:18>15,插入为15的右子节点,默认为红色
检查规则:父节点15是黑色,无违反规则
结果:
(10)插入第九个节点(值=17)
(1)17<18,为18的左子节点
(2)新节点17®,父节点18®,连续红色(违反规则4)
(3)祖父15(B),叔叔12®(叔叔为红)
调整:父18、叔12改黑,祖父15改红 。
(11)插入第十个节点(值=20)
- 操作:20>18,插入为18的右子节点,默认为红色
- 检查规则:父节点18是黑色,无违反规则
- 最终结果:
总结操作要点
(1)插入原则:新节点默认为红色,减少对黑色高度的影响
(2)调整时机:仅当插入后出现连续红色节点时需要调整
(3)调整策略:
①叔叔为红:通过着色解决(父叔变黑,祖父变红)
②叔叔为黑:通过旋转+着色解决(根据结构左旋/右旋)
(4)根节点保障:任何时候根节点必须为黑色
通过以上步骤可以看到,从空树开始,红黑树通过自动平衡机制,始终保持着近似平衡的结构,确保所有操作的时间复杂度为O(log n)。