红黑树(Red-Black Tree)是一种自平衡的二叉搜索树(BST),它通过一系列规则保证树的高度始终维持在 O(log n) 级别,从而确保插入、删除、查找等操作的时间复杂度稳定在 O(log n)。它既解决了普通二叉搜索树在极端情况下(如有序插入)退化为链表、性能骤降的问题,又比AVL树(另一种平衡二叉树)的调整成本更低,是Java集合框架中 TreeMap、TreeSet 的底层核心实现,也是面试中高频考察的重点。
本文将从"为什么需要红黑树"出发,层层拆解红黑树的核心特性、插入/删除的完整流程(含修复逻辑)、底层原理及实际应用,全程结合示例和图解,逻辑清晰、细节拉满,帮你彻底掌握红黑树。
一、前置知识:为什么需要红黑树?
在了解红黑树之前,我们先回顾两个关键概念,理解红黑树的设计初衷:
1. 二叉搜索树(BST)的缺陷
二叉搜索树的核心规则:左子树所有节点值 < 根节点值,右子树所有节点值 > 根节点值,左右子树也均为二叉搜索树。其查找、插入、删除的时间复杂度依赖于树的高度------理想情况下(平衡树),高度为 log₂n,时间复杂度 O(log n);但在极端情况下(如插入有序数据),会退化为单链表,高度变为 n,时间复杂度骤降为 O(n),性能极差。
示例:插入 1、2、3、4、5 到普通BST,最终树的结构为 1→2→3→4→5(链表),查找5需要遍历5个节点,效率极低。
2. 平衡二叉树的需求
为了解决BST的缺陷,需要一种"自平衡"机制:当插入/删除节点导致树不平衡时,通过一系列操作(旋转、变色)调整树的结构,确保树的高度始终维持在 O(log n)。
常见的平衡二叉树有两种:
-
AVL树:要求左右子树的高度差(平衡因子)不超过1,调整频繁,适合查找密集、插入删除较少的场景;
-
红黑树:不追求"绝对平衡",只要求满足特定规则(红黑规则),调整频率低于AVL树,适合插入删除频繁的场景(如TreeMap、TreeSet)。
红黑树的核心优势:以"相对平衡"换取"更少的调整操作",兼顾查询和插入删除性能,是工业界应用最广泛的平衡二叉树。
二、红黑树的核心定义与规则
红黑树本质是一棵二叉搜索树,在此基础上增加了5条"红黑规则",通过这些规则强制树的高度保持平衡。首先明确两个基础设定:
-
红黑树的每个节点,要么是红色,要么是黑色(无其他颜色);
-
为了简化边界处理,红黑树会引入一个"哨兵节点"(NIL节点),所有叶子节点(空节点)都指向这个哨兵节点,哨兵节点固定为黑色。
核心红黑规则(5条,必须同时满足)
这5条规则是红黑树平衡的核心,任何操作后一旦破坏规则,必须通过"旋转+变色"修复,缺一不可:
-
根节点必须是黑色:保证树的顶层结构稳定,避免根节点为红色带来的连锁规则破坏。
-
哨兵节点(NIL)必须是黑色:叶子节点的空指针统一指向哨兵,简化边界判断(比如判断节点是否为叶子,只需判断是否指向NIL)。
-
如果一个节点是红色,那么它的两个子节点必须是黑色:禁止"红-红相邻",避免出现连续的红色节点,防止树的高度失衡。
-
从任意一个节点出发,到其所有后代哨兵节点的路径上,黑色节点的数量都相等(称为"黑高相等"):这是红黑树平衡的核心,确保最长路径(红黑交替)的长度不超过最短路径(全黑)的2倍,从而保证树的高度为 O(log n)。
-
每一个新插入的节点,初始颜色为红色:插入红色节点,大概率不会破坏"黑高相等"规则(仅可能破坏规则3:红-红相邻),减少修复成本;若插入黑色节点,必然破坏规则4,修复成本更高。
关键概念:黑高
黑高(Black Height):从某个节点到其后代哨兵节点的路径上,黑色节点的数量(不包含当前节点,包含哨兵节点)。根据规则4,红黑树中所有叶子节点(哨兵)的黑高都相等。
示例:若根节点为黑色,其左子节点为红色、右子节点为黑色,那么根节点的黑高 = 左子树黑高(1,哨兵)= 右子树黑高(1,哨兵),满足规则4。
三、红黑树的核心操作:旋转(基础)
红黑树的调整操作,核心是"旋转"------通过旋转改变节点的位置,不破坏二叉搜索树的规则(左小右大),同时配合"变色"修复红黑规则。旋转分为两种:左旋转和右旋转,两者是对称操作。
旋转的核心目的:调整节点的位置,消除"红-红相邻",恢复红黑规则,维持树的平衡。
1. 右旋转(Right Rotation)
场景:节点 x 的左子节点 y 存在,且需要将 x 向右"旋转",让 y 成为新的父节点,x 成为 y 的右子节点。
右旋转步骤(结合图解理解):
-
设 x 的左子节点为 y,y 的右子节点为 T2(可能是NIL节点);
-
将 T2 的父节点改为 x,同时将 x 的左子节点改为 T2;
-
将 y 的父节点改为 x 的父节点(若 x 是根节点,则 y 成为新根;否则,若 x 是其父节点的左子节点,y 也成为左子节点,反之亦然);
-
将 x 的父节点改为 y;
-
将 y 的右子节点改为 x。
核心原则:旋转后,二叉搜索树的规则不变(左小右大),仅节点位置调整。
2. 左旋转(Left Rotation)
场景:节点 y 的右子节点 x 存在,且需要将 y 向左"旋转",让 x 成为新的父节点,y 成为 x 的左子节点。
左旋转步骤(与右旋转对称):
-
设 y 的右子节点为 x,x 的左子节点为 T2;
-
将 T2 的父节点改为 y,同时将 y 的右子节点改为 T2;
-
将 x 的父节点改为 y 的父节点(若 y 是根节点,则 x 成为新根;否则,若 y 是其父节点的右子节点,x 也成为右子节点,反之亦然);
-
将 y 的父节点改为 x;
-
将 x 的左子节点改为 y。
旋转总结
旋转是红黑树调整的"工具",本身不改变节点颜色,只改变节点位置。后续插入、删除后的修复,都是"旋转+变色"的组合操作,核心是利用旋转调整节点层级,利用变色修复红黑规则。
四、红黑树的核心操作:插入(含修复逻辑)
红黑树的插入流程分为两步:第一步,按二叉搜索树的规则插入新节点 ;第二步,检查红黑规则是否被破坏,若破坏则进行修复。
重点:新插入节点的初始颜色为红色(规则5),因此插入后可能破坏的规则只有 规则3(红-红相邻) 和 规则1(根节点为红色)(若插入的是根节点),其他规则不会被破坏。
步骤1:按BST规则插入新节点
插入逻辑与普通二叉搜索树一致,核心是"左小右大",找到合适的插入位置:
-
若红黑树为空(只有哨兵节点),则新节点成为根节点,颜色改为黑色(修复规则1);
-
若树非空,从根节点出发,比较新节点值与当前节点值:
-
新节点值 < 当前节点值:向左子树遍历,直到找到左子节点为哨兵的位置,插入新节点;
-
新节点值 > 当前节点值:向右子树遍历,直到找到右子节点为哨兵的位置,插入新节点;
-
新节点值 == 当前节点值:红黑树不允许重复节点(与TreeMap/TreeSet的去重逻辑一致),直接忽略。
-
-
新节点的左右子节点均指向哨兵节点,初始颜色为红色。
步骤2:插入后的修复逻辑(核心)
插入新节点(红色)后,只有两种情况需要修复:
情况1:新节点是根节点 → 直接将颜色改为黑色(修复规则1),无需其他操作;
情况2:新节点的父节点是红色 → 破坏规则3(红-红相邻),此时需要根据"叔叔节点"(父节点的兄弟节点)的颜色,分3种场景修复。
先明确几个称谓(便于理解):
-
新节点:z(红色);
-
父节点:p(z)(红色,否则不会破坏规则3);
-
祖父节点:g(z)(p(z)的父节点,必然是黑色------因为p(z)是红色,若g(z)也是红色,会提前破坏规则3,不可能存在);
-
叔叔节点:u(z)(g(z)的另一个子节点,即p(z)的兄弟节点)。
场景1:叔叔节点u(z)是红色
核心逻辑:变色即可修复,无需旋转(因为叔叔节点是红色,可通过变色维持黑高相等)。
-
将父节点p(z)改为黑色;
-
将叔叔节点u(z)改为黑色;
-
将祖父节点g(z)改为红色;
-
将z指向g(z),继续向上检查(因为g(z)改为红色后,可能与它的父节点形成红-红相邻,需要递归修复)。
原因:变色后,原z所在路径的黑高不变(p(z)从红变黑,g(z)从黑变红,总黑高不变),同时消除了z与p(z)的红-红相邻;但g(z)变红后,可能与它的父节点冲突,因此需要继续向上检查。
场景2:叔叔节点u(z)是黑色,且z是父节点p(z)的右子节点
核心逻辑:先旋转(将z变为父节点的左子节点),转化为场景3,再进行后续修复。
-
将z指向p(z)(即将父节点作为新的z);
-
对新z进行左旋转(目的:让z成为左子节点,转化为场景3);
-
进入场景3,继续修复。
场景3:叔叔节点u(z)是黑色,且z是父节点p(z)的左子节点
核心逻辑:旋转+变色,一次性修复,无需继续向上检查。
-
将父节点p(z)改为黑色;
-
将祖父节点g(z)改为红色;
-
对祖父节点g(z)进行右旋转(目的:调整节点层级,维持黑高相等);
-
修复完成,无需继续向上检查(因为旋转后,g(z)变为红色,但它的父节点是黑色,不会形成红-红相邻)。
插入修复总结
插入修复的核心是"消除红-红相邻",优先通过变色修复(场景1),无法变色时通过旋转转化场景(场景2→场景3),再通过旋转+变色修复。整个过程最多旋转2次,效率极高。
示例:插入 10、20、30、15 到红黑树的完整流程(简化,省略哨兵节点):
-
插入10:根节点,改为黑色 → 树结构:10(黑);
-
插入20:右子节点,红色 → 树结构:10(黑)→20(红),无红-红相邻,无需修复;
-
插入30:20的右子节点,红色 → 父节点20是红色,叔叔节点(10的左子节点,哨兵,黑色),z是右子节点(场景2);
-
z指向20,对20左旋转 → 树结构:20(红)→10(黑)、30(红);
-
此时z是20的左子节点(10),叔叔节点(30的右子节点,哨兵,黑色),进入场景3;
-
将20改为黑色,10改为红色,对10右旋转 → 最终树结构:20(黑)→10(红)、30(红),满足所有规则。
-
-
插入15:10的右子节点,红色 → 父节点10是红色,叔叔节点(20的右子节点30,红色),进入场景1;
-
10改为黑色,30改为黑色,20改为红色 → 树结构:20(红)→10(黑)→15(红)、30(黑);
-
z指向20(红色),父节点是根节点(无父节点),将20改为黑色 → 最终树结构:20(黑)→10(黑)→15(红)、30(黑),满足所有规则。
-
五、红黑树的核心操作:删除(含修复逻辑)
红黑树的删除操作比插入更复杂,核心原因是:删除黑色节点会破坏"黑高相等"规则(规则4),修复难度更高;而删除红色节点不会破坏任何规则(因为红色节点不影响黑高)。
删除流程分为三步:第一步,按BST规则删除节点 ;第二步,确定"替代节点"(用于替代被删除节点的位置),判断是否需要修复 ;第三步,修复红黑规则。
步骤1:按BST规则删除节点
与普通BST删除逻辑一致,根据被删除节点的子节点数量,分为3种情况:
-
被删除节点z无左子节点(只有右子节点或哨兵):用z的右子节点替代z的位置;
-
被删除节点z无右子节点(只有左子节点):用z的左子节点替代z的位置;
-
被删除节点z有左右两个子节点:找到z的"后继节点"(右子树中最小的节点,记为y),用y替代z的位置,再删除y(y只有右子节点或无子女,转化为情况1或2)。
步骤2:判断是否需要修复
删除后,只有一种情况需要修复:被删除的节点是黑色,且替代节点也是黑色。
原因:
-
删除红色节点:不影响黑高,也不会出现红-红相邻,无需修复;
-
删除黑色节点,替代节点是红色:将替代节点改为黑色,即可维持黑高相等,无需进一步修复;
-
删除黑色节点,替代节点是黑色:会导致该路径的黑高减少1,破坏规则4,需要修复(称为"黑色 deficit",黑色缺失)。
后续修复的核心:解决"黑色缺失",恢复所有路径的黑高相等。
步骤3:删除后的修复逻辑(核心)
设替代节点为x(黑色,存在黑色缺失),x的兄弟节点为w(x父节点的另一个子节点),修复逻辑根据w的颜色,分4种场景。
场景1:兄弟节点w是红色
核心逻辑:变色+旋转,将w变为黑色,转化为后续场景(w为黑色)。
-
将w改为黑色;
-
将x的父节点p(x)改为红色;
-
对p(x)进行左旋转(若x是左子节点)或右旋转(若x是右子节点);
-
更新w为x的新兄弟节点(此时w为黑色),进入后续场景。
场景2:兄弟节点w是黑色,且w的两个子节点都是黑色
核心逻辑:将w的黑色"借"给x,消除x的黑色缺失,再向上检查父节点。
-
将w改为红色;
-
将x指向p(x)(此时x的黑色缺失转移到p(x));
-
继续向上检查,直到x是根节点(此时黑色缺失消失)或x是红色(将x改为黑色,修复完成)。
场景3:兄弟节点w是黑色,w的左子节点是红色,右子节点是黑色(x是左子节点)
核心逻辑:旋转+变色,将w的右子节点变为红色,转化为场景4。
-
将w的左子节点改为黑色;
-
将w改为红色;
-
对w进行右旋转;
-
更新w为x的新兄弟节点(此时w的右子节点是红色),进入场景4。
场景4:兄弟节点w是黑色,w的右子节点是红色(x是左子节点)
核心逻辑:旋转+变色,一次性消除黑色缺失,无需继续向上检查。
-
将w的颜色改为p(x)的颜色;
-
将p(x)改为黑色;
-
将w的右子节点改为黑色;
-
对p(x)进行左旋转;
-
将x指向根节点,修复完成(黑色缺失消除)。
删除修复总结
删除修复的核心是"弥补黑色缺失",通过变色和旋转,将黑色缺失逐步向上转移,直到消除。整个过程最多旋转3次,效率依然很高。相比插入修复,删除修复的场景更多,但核心逻辑仍是"维持黑高相等"和"禁止红-红相邻"。
六、红黑树的底层原理与性能分析
1. 为什么红黑树能保证高度为 O(log n)?
根据红黑规则4(所有路径黑高相等),设红黑树的黑高为h(根节点到哨兵的黑色节点数),则:
-
最短路径(全黑节点)的长度为h;
-
最长路径(红黑交替)的长度为2h(因为禁止红-红相邻,最多红黑交替)。
因此,红黑树的高度不会超过2h。而黑高h与节点数n的关系为:n ≥ 2ʰ - 1(全黑树的节点数),即h ≤ log₂(n+1),因此红黑树的高度 ≤ 2log₂(n+1),属于 O(log n) 级别。
2. 红黑树与AVL树的对比
两者都是自平衡二叉搜索树,核心区别在于"平衡标准"和"调整频率":
| 对比维度 | 红黑树 | AVL树 |
|---|---|---|
| 平衡标准 | 满足5条红黑规则,相对平衡(黑高相等) | 左右子树高度差≤1,绝对平衡 |
| 调整频率 | 插入最多旋转2次,删除最多旋转3次,调整少 | 插入/删除可能需要多次旋转(最多O(log n)次),调整频繁 |
| 空间复杂度 | 只需存储节点颜色(1bit),空间开销小 | 需要存储平衡因子(或高度),空间开销略大 |
| 适用场景 | 插入删除频繁(如TreeMap、TreeSet) | 查找密集(如数据库索引) |
3. 红黑树的实际应用
红黑树是工业界应用最广泛的平衡二叉树,常见应用场景:
-
Java集合框架:TreeMap(键值对排序)、TreeSet(元素去重排序)的底层实现;
-
Linux内核:进程调度、内存管理中用于高效查找;
-
数据库:部分数据库的索引结构(如MySQL的InnoDB引擎,虽核心是B+树,但内部部分逻辑依赖红黑树);
-
其他:需要有序存储、且插入删除频繁的场景(如缓存淘汰策略)。
七、红黑树的Java简化实现(核心代码)
下面给出红黑树的简化实现(只包含核心的插入、旋转逻辑,省略删除和哨兵节点细节,便于理解),重点关注节点结构、插入流程和修复逻辑:
java
// 红黑树节点类
class RedBlackNode {
int value;
RedBlackNode left;
RedBlackNode right;
RedBlackNode parent;
boolean isRed; // true:红色,false:黑色
public RedBlackNode(int value) {
this.value = value;
this.isRed = true; // 新节点默认红色
this.left = null;
this.right = null;
this.parent = null;
}
}
// 红黑树类
public class RedBlackTree {
private RedBlackNode root;
// 右旋转
private void rightRotate(RedBlackNode x) {
RedBlackNode y = x.left;
x.left = y.right;
if (y.right != null) {
y.right.parent = x;
}
y.parent = x.parent;
if (x.parent == null) { // x是根节点
this.root = y;
} else if (x == x.parent.left) { // x是左子节点
x.parent.left = y;
} else { // x是右子节点
x.parent.right = y;
}
y.right = x;
x.parent = y;
}
// 左旋转
private void leftRotate(RedBlackNode y) {
RedBlackNode x = y.right;
y.right = x.left;
if (x.left != null) {
x.left.parent = y;
}
x.parent = y.parent;
if (y.parent == null) { // y是根节点
this.root = x;
} else if (y == y.parent.left) { // y是左子节点
y.parent.left = x;
} else { // y是右子节点
y.parent.right = x;
}
x.left = y;
y.parent = x;
}
// 插入修复
private void insertFixup(RedBlackNode z) {
while (z.parent != null && z.parent.isRed) { // 父节点是红色,破坏规则3
if (z.parent == z.parent.parent.left) { // 父节点是祖父节点的左子节点
RedBlackNode uncle = z.parent.parent.right; // 叔叔节点(右子节点)
if (uncle != null && uncle.isRed) { // 场景1:叔叔是红色
z.parent.isRed = false;
uncle.isRed = false;
z.parent.parent.isRed = true;
z = z.parent.parent; // 向上检查
} else { // 叔叔是黑色
if (z == z.parent.right) { // 场景2:z是右子节点
z = z.parent;
leftRotate(z);
}
// 场景3:z是左子节点
z.parent.isRed = false;
z.parent.parent.isRed = true;
rightRotate(z.parent.parent);
}
} else { // 父节点是祖父节点的右子节点(对称逻辑)
RedBlackNode uncle = z.parent.parent.left;
if (uncle != null && uncle.isRed) {
z.parent.isRed = false;
uncle.isRed = false;
z.parent.parent.isRed = true;
z = z.parent.parent;
} else {
if (z == z.parent.left) {
z = z.parent;
rightRotate(z);
}
z.parent.isRed = false;
z.parent.parent.isRed = true;
leftRotate(z.parent.parent);
}
}
}
// 确保根节点是黑色
this.root.isRed = false;
}
// 插入节点(BST规则)
public void insert(int value) {
RedBlackNode z = new RedBlackNode(value);
RedBlackNode parent = null;
RedBlackNode current = this.root;
// 找到插入位置
while (current != null) {
parent = current;
if (z.value < current.value) {
current = current.left;
} else if (z.value > current.value) {
current = current.right;
} else {
return; // 重复节点,直接返回
}
}
z.parent = parent;
if (parent == null) { // 树为空,z是根节点
this.root = z;
} else if (z.value < parent.value) {
parent.left = z;
} else {
parent.right = z;
}
// 插入后修复
insertFixup(z);
}
// 中序遍历(验证红黑树的有序性)
public void inOrder(RedBlackNode node) {
if (node != null) {
inOrder(node.left);
System.out.print(node.value + "(" + (node.isRed ? "红" : "黑") + ") ");
inOrder(node.right);
}
}
// 测试
public static void main(String[] args) {
RedBlackTree rbt = new RedBlackTree();
int[] values = {10, 20, 30, 15, 25};
for (int val : values) {
rbt.insert(val);
}
System.out.println("红黑树中序遍历(有序):");
rbt.inOrder(rbt.root);
// 输出示例:10(黑) 15(红) 20(黑) 25(红) 30(黑)
}
}
说明:该实现省略了哨兵节点(用null代替),简化了删除逻辑,重点展示了插入、旋转和修复的核心流程,可直接运行测试,直观感受红黑树的结构变化。
八、常见面试题(高频考点)
1. 红黑树的5条规则是什么?
核心记忆:根黑、哨兵黑、红子黑、黑高相等、新节点红(具体见本文第二部分)。
2. 红黑树为什么能保证O(log n)的时间复杂度?
因为红黑树的黑高h ≤ log₂(n+1),树的高度 ≤ 2h,因此高度是O(log n),而所有操作(插入、删除、查找)都依赖树的高度,因此时间复杂度为O(log n)。
3. 红黑树插入时,为什么新节点初始颜色是红色?
插入红色节点,只会破坏规则3(红-红相邻),修复成本低;若插入黑色节点,会直接破坏规则4(黑高相等),修复成本极高(需要调整多条路径的黑高)。
4. 红黑树与AVL树的区别?
核心区别:平衡标准不同(红黑树相对平衡,AVL树绝对平衡)、调整频率不同(红黑树调整少,AVL树调整频繁)、适用场景不同(红黑树适合插入删除,AVL树适合查找)。
5. TreeMap的底层为什么用红黑树,而不是AVL树?
因为TreeMap需要支持频繁的插入、删除操作,红黑树的调整频率低于AVL树,能更好地兼顾插入删除和查询性能;而AVL树的绝对平衡带来的查询优势,在TreeMap的使用场景中不明显。
九、总结
红黑树的核心是"通过5条规则维持相对平衡,确保树的高度为O(log n),从而保证高效的插入、删除、查找操作"。其本质是二叉搜索树+红黑规则+旋转/变色调整,核心难点在于插入和删除后的修复逻辑------但只要抓住"消除红-红相邻"(插入)和"弥补黑色缺失"(删除)这两个核心目标,就能理清所有修复场景。
掌握红黑树,不仅能应对面试中的高频问题,更能理解Java集合框架(TreeMap/TreeSet)的底层实现逻辑,为后续学习更复杂的数据结构(如B+树、跳表)打下基础。
最后记住:红黑树的核心不是"红色"和"黑色",而是通过颜色和旋转,实现"相对平衡",兼顾性能和调整成本------这也是它能成为工业界首选平衡二叉树的原因。