目录
[一 红黑树概述](#一 红黑树概述)
[二 红黑树插入原理介绍](#二 红黑树插入原理介绍)
[三 红黑树删除的原理介绍](#三 红黑树删除的原理介绍)
[四 红黑树 Java 实现](#四 红黑树 Java 实现)
[五 代码解释](#五 代码解释)
[1. RedBlackNode 节点类](#1. RedBlackNode 节点类)
[2. insert() 方法](#2. insert() 方法)
[3. handleReorient() 与 rotate()](#3. handleReorient() 与 rotate())
[六 红黑树总结](#六 红黑树总结)
一 红黑树概述
红黑树(Red-Black Tree,简称 RBT)是一种自平衡二叉查找树(Self-Balancing Binary Search Tree)。它在普通二叉查找树(BST)的基础上,通过**"红黑规则"与旋转、变色操作** 保证了树的近似平衡,从而使得插入、删除、查询等操作的时间复杂度稳定在 O(log n)
。
红黑树最早由 Rudolf Bayer 于 1972 年提出,原名为 对称二叉 B 树(Symmetric Binary B-tree) 。后来由 Leo J. Guibas 和 Robert Sedgewick 于 1978 年改进并命名为红黑树。如今,红黑树已成为计算机科学领域中使用最广泛的平衡树之一。
-
红黑树的五条性质
-
每个结点要么是红色,要么是黑色。
-
根结点是黑色。
-
所有叶子结点(NIL或NULL节点)都是黑色。
-
如果一个节点是红色,则它的两个子节点必须是黑色(红节点不能相邻)。
-
从任意一个节点到其所有叶子节点的路径上,黑色节点数相同。
-
第五条性质称为 黑高平衡 ,它是红黑树平衡性的核心。虽然红黑树不是严格意义上的高度平衡树(如AVL树),但其最大高度不会超过 2*log2(n+1)
(最长路径(红黑节点交替)不会超过最短路径(全黑节点)的两倍
),在实际应用中性能非常稳定。
-
红黑树的基本操作
为了在插入和删除后仍能维持红黑树的五大性质,我们需要进行两种基本操作:变色 和 旋转。
-
旋转
旋转是保持BST性质并调整树结构的局部操作。
- 左旋:以某个节点为支点,其右子节点成为新的父节点,支点自身成为新父节点的左子节点。
- 右旋:以某个节点为支点,其左子节点成为新的父节点,支点自身成为新父节点的右子节点。
-
变色
简单地改变节点的颜色,从红变黑或从黑变红。这是最直接的调整方式。
-
二 红黑树插入原理介绍
插入新节点的步骤可以概括为两步:
-
标准BST插入 :首先,像在普通二叉查找树中一样插入新节点。新插入的节点我们总是将其着色为红色。
- 为什么是红色?因为如果插入黑色节点,会立即违反性质5(黑高不一致),修复起来非常困难。插入红色节点可能违反性质2(根节点为黑)或性质4(不能有连续红节点),但修复这些情况相对容易。
-
重新平衡与修复 :如果插入后违反了红黑树的性质,我们需要通过变色和旋转来修复。修复的情况主要取决于新节点的父节点 和叔叔节点(父节点的兄弟节点)的颜色。
我们定义:
-
P:父节点
-
U:叔叔节点
-
G:祖父节点
-
N:新插入的节点
情况1:N是根节点
- 操作:直接将N变为黑色。(违反性质2)
情况2:P是黑色
- 操作:什么都不用做。树仍然是有效的红黑树。(没有违反任何性质)
情况3:P是红色,U也是红色
-
操作:
-
将P和U变为黑色。
-
将G变为红色。
-
将G视为新的当前节点,从情况1开始递归检查。
-
情况4:P是红色,U是黑色(或NIL),且N是P的右子节点,P是G的左子节点
-
操作:
-
以P为支点进行左旋。
-
将P作为新的当前节点,此时情况转变为情况5。
-
情况5:P是红色,U是黑色(或NIL),且N是P的左子节点,P是G的左子节点
-
操作:
-
将P变为黑色。
-
将G变为红色。
-
以G为支点进行右旋。
-
(如果P是G的右子节点,则情况4和5是镜像对称的,操作中的左右旋相反)
三 红黑树删除的原理介绍
删除操作比插入更复杂,但核心思想相似:先执行标准BST删除,然后修复可能被破坏的红黑性质。
-
标准BST删除:
-
如果被删除的节点有两个非NIL子节点,我们通常找到它的后继节点 (右子树中的最小节点),用后继节点的值替换被删除节点的值,然后转而删除这个后继节点。这样问题就转化为删除一个至多只有一个子节点的节点。
-
最终,我们实际删除的节点(记为
D
)最多只有一个子节点(记为C
)。
-
-
重新平衡与修复:
-
如果
D
是红色,直接删除它,用C
替换它,不会破坏任何性质。 -
如果
D
是黑色,而C
是红色,那么直接用红色的C
替换D
,并将C
变为黑色。 -
最复杂的情况 :如果
D
和C
都是黑色(C
可能是NIL节点)。删除D
后,经过D
的路径会少一个黑色节点,破坏了性质5。修复过程需要根据兄弟节点S
的颜色和其子节点的颜色来分多种情况处理,通过变色和旋转将"双重黑色"向上传递或消除。这个过程比插入更繁琐,但核心目标始终是恢复黑高平衡。
-
四 红黑树 Java 实现
下面的代码实现基于 自顶向下插入法(Top-Down Insertion),该方法在插入时提前进行调整,避免自底向上回溯,提高了效率。
package org.algds.tree.ds;
/**
* 红黑树实现 - 自顶向下
*
* @param <T>
*/
public class RedBlackTree<T extends Comparable<? super T>> {
// 1 内部结点定义 ****************************************************************************************************
private static final int BLACK = 1;
private static final int RED = 0;
private static class RedBlackNode<T> {
RedBlackNode(T theElement) {
this(theElement, null, null);
}
RedBlackNode(T theElement, RedBlackNode<T> lt, RedBlackNode<T> rt) {
element = theElement;
left = lt;
right = rt;
color = RedBlackTree.BLACK;
}
T element;
RedBlackNode<T> left;
RedBlackNode<T> right;
int color;
}
// 2 核心结构定义 ****************************************************************************************************
private RedBlackNode<T> header; // 头结点,header.right 引用红黑树根结点
private RedBlackNode<T> nullNode; // 空结点,空对象设计模式
// 自顶向下红黑树不需要叔叔结点,只需要当前结点上游结点引用即可
private RedBlackNode<T> current; // 当前结点
private RedBlackNode<T> parent; // 父节点
private RedBlackNode<T> grand; // 祖父结点
private RedBlackNode<T> great; // 曾祖父结点
public RedBlackTree() {
nullNode = new RedBlackNode<>(null);
nullNode.left = nullNode;
nullNode.right = nullNode;
header = new RedBlackNode<>(null);
header.left = nullNode;
header.right = nullNode;
}
private int compare(T item, RedBlackNode<T> t) {
if (t == header)
return 1;
else
return item.compareTo(t.element);
}
// 3 核心方法区 *****************************************************************************************************
/**
* 向红黑树插入结点
*
* @param item
*/
public void insert(T item) {
current = parent = grand = header; // 自顶向下插入
nullNode.element = item; // 保存临时数据,完成下面退出条件
while (compare(item, current) != 0) { // 退出条件?
// 向下前进一个深度
great = grand;
grand = parent;
parent = current;
current = compare(item, current) < 0 ? current.left : current.right;
// 当遇到两个儿子都是红色结点时 执行旋转+变色动作
if (current.left.color == RED && current.right.color == RED) { // 当前结点的两个儿子是红色
handleReorient(item); // 执行调整
}
}
if (current != nullNode) // 这就说明遍历到了最后也没有发现item结点,所以下面可以插入数据了
return;
current = new RedBlackNode<>(item, nullNode, nullNode);
if (compare(item, parent) < 0) {
parent.left = current;
} else {
parent.right = current;
}
/**
* 插入叶子结点是红色,父节点也是红色将引发调整
*/
handleReorient(item);
}
public void remove(T x) {
throw new UnsupportedOperationException();
}
public T findMin() {
if (isEmpty())
throw new UnderflowException();
RedBlackNode<T> itr = header.right;
while (itr.left != nullNode)
itr = itr.left;
return itr.element;
}
public T findMax() {
if (isEmpty())
throw new UnderflowException();
RedBlackNode<T> itr = header.right;
while (itr.right != nullNode)
itr = itr.right;
return itr.element;
}
public boolean contains(T x) {
nullNode.element = x;
current = header.right;
for (; ; ) {
if (x.compareTo(current.element) < 0)
current = current.left;
else if (x.compareTo(current.element) > 0)
current = current.right;
else if (current != nullNode)
return true;
else
return false;
}
}
public boolean isEmpty() {
return header.right == nullNode;
}
public void makeEmpty() {
header.right = nullNode;
}
public void printTree() {
if (isEmpty())
System.out.println("Empty tree");
else
System.out.print("Red Black tree: ");
printTree(header.right);
}
private void printTree(RedBlackNode<T> t) {
if (t != nullNode) {
printTree(t.left);
System.out.print(t.element + " ");
printTree(t.right);
}
}
// 4 重要方法区(变色 + 旋转) ******************************************************************************************
private void handleReorient(T item) {
// 执行变色动作
current.color = RED;
current.left.color = BLACK;
current.right.color = BLACK;
/**
* 当前结点和父节点都是红色将会引发调整,调整原理如下所示:
*
* 一字型(zig-zig) 带.表示是红色结点
* G P
* / \ / \
* .P S .x .G
* / \ | | / \
* .x B C --> A B S
* | |
* A C
*
* 之字形(zig-zag)
* G x
* / \ / \
* .P S --> .P .G
* | \ | / \ / \
* A .x C A B1 B2 S
* / \ |
* B1 B2 C
*/
if (parent.color == RED) { // 当前结点时红色 并且 父节点也是红色(祖父一定是黑色),违反红黑树规则,需要执行调整
grand.color = RED; // 调整祖父为红色,因为不管是一字型还是之字形旋转后都是变为红色结点
/**
* 满足下面两种情况
* G G
* / \
* P 或 P
* \ /
* X X
*/
if ((compare(item, grand) < 0) != (compare(item, parent) < 0)) { // 之字型旋转
parent = rotate(item, grand); // zig-zag 格式需要完成两次旋转,这里执行第一次,传入祖父为了后边建立链
/** 之字形第一次旋转
* G
* /
* x
* /
* P
*/
}
current = rotate(item, great); // zig-zag 的第二次旋转 || 或者 zig-zig格式的一次旋转
current.color = BLACK;
}
header.right.color = BLACK; // 根节点始终是黑色
}
private RedBlackNode<T> rotate(T item, RedBlackNode<T> parent) {
if (compare(item, parent) < 0)
return parent.left = compare(item, parent.left) < 0 ?
rotateWithLeftChild(parent.left) : // LL
rotateWithRightChild(parent.left); // LR (parent.left = P)
else
return parent.right = compare(item, parent.right) < 0 ?
rotateWithLeftChild(parent.right) : // RL
rotateWithRightChild(parent.right); // RR
}
/**
* 右旋转(处理LL情况)
* k2 k1
* / / \
* k1 --> o1 k2
* /
* o1
*/
private RedBlackNode<T> rotateWithLeftChild(RedBlackNode<T> k2) {
RedBlackNode<T> k1 = k2.left;
k2.left = k1.right;
k1.right = k2;
return k1;
}
/**
* 左旋转(处理RR情况)
* k1 k2
* \ / \
* k2 --> k1 o1
* \
* o1
*/
private RedBlackNode<T> rotateWithRightChild(RedBlackNode<T> k1) {
RedBlackNode<T> k2 = k1.right;
k1.right = k2.left;
k2.left = k1;
return k2;
}
// 5 单元测试 *******************************************************************************************************
public static void main(String[] args) {
RedBlackTree<Integer> t = new RedBlackTree<>();
final int NUMS = 50;
final int GAP = 3;
System.out.println("Checking... (no more output means success)");
t.printTree();
for (int i = GAP; i != 0; i = (i + GAP) % NUMS)
t.insert(i);
t.printTree();
if (t.findMin() != 1 || t.findMax() != NUMS - 1)
System.out.println("FindMin or FindMax error!");
for (int i = 1; i < NUMS; i++)
if (!t.contains(i))
System.out.println("Find error1!");
}
}
五 代码解释
1. RedBlackNode 节点类
private static final int BLACK = 1;
private static final int RED = 0;
private static class RedBlackNode<T> {
RedBlackNode(T theElement) {
this(theElement, null, null);
}
RedBlackNode(T theElement, RedBlackNode<T> lt, RedBlackNode<T> rt) {
element = theElement;
left = lt;
right = rt;
color = RedBlackTree.BLACK;
}
T element;
RedBlackNode<T> left;
RedBlackNode<T> right;
int color;
}
每个节点包含:
-
element
:存储的数据; -
left
、right
:左右子节点; -
color
:颜色属性(0=RED, 1=BLACK
)。
该实现使用 nullNode
作为所有空指针的替代物(空对象模式),避免空指针判断。
2. insert()
方法
public void insert(T item) {
current = parent = grand = header; // 自顶向下插入
nullNode.element = item; // 保存临时数据,完成下面退出条件
while (compare(item, current) != 0) { // 退出条件?
// 向下前进一个深度
great = grand;
grand = parent;
parent = current;
current = compare(item, current) < 0 ? current.left : current.right;
// 当遇到两个儿子都是红色结点时 执行旋转+变色动作
if (current.left.color == RED && current.right.color == RED) { // 当前结点的两个儿子是红色
handleReorient(item); // 执行调整
}
}
if (current != nullNode) // 这就说明遍历到了最后也没有发现item结点,所以下面可以插入数据了
return;
current = new RedBlackNode<>(item, nullNode, nullNode);
if (compare(item, parent) < 0) {
parent.left = current;
} else {
parent.right = current;
}
/**
* 插入叶子结点是红色,父节点也是红色将引发调整
*/
handleReorient(item);
}
采用 自顶向下(Top-Down) 插入思想:
-
每向下走一步,都提前检查是否有连续红节点;
-
若遇到"父红+两个红儿子",立即调用
handleReorient()
修复; -
插入完成后再调用一次
handleReorient()
,保证平衡。
这种策略无需递归回溯,逻辑更清晰。
3. handleReorient()
与 rotate()
private void handleReorient(T item) {
// 执行变色动作
current.color = RED;
current.left.color = BLACK;
current.right.color = BLACK;
/**
* 当前结点和父节点都是红色将会引发调整,调整原理如下所示:
*
* 一字型(zig-zig) 带.表示是红色结点
* G P
* / \ / \
* .P S .x .G
* / \ | | / \
* .x B C --> A B S
* | |
* A C
*
* 之字形(zig-zag)
* G x
* / \ / \
* .P S --> .P .G
* | \ | / \ / \
* A .x C A B1 B2 S
* / \ |
* B1 B2 C
*/
if (parent.color == RED) { // 当前结点时红色 并且 父节点也是红色(祖父一定是黑色),违反红黑树规则,需要执行调整
grand.color = RED; // 调整祖父为红色,因为不管是一字型还是之字形旋转后都是变为红色结点
/**
* 满足下面两种情况
* G G
* / \
* P 或 P
* \ /
* X X
*/
if ((compare(item, grand) < 0) != (compare(item, parent) < 0)) { // 之字型旋转
parent = rotate(item, grand); // zig-zag 格式需要完成两次旋转,这里执行第一次,传入祖父为了后边建立链
/** 之字形第一次旋转
* G
* /
* x
* /
* P
*/
}
current = rotate(item, great); // zig-zag 的第二次旋转 || 或者 zig-zig格式的一次旋转
current.color = BLACK;
}
header.right.color = BLACK; // 根节点始终是黑色
}
private RedBlackNode<T> rotate(T item, RedBlackNode<T> parent) {
if (compare(item, parent) < 0)
return parent.left = compare(item, parent.left) < 0 ?
rotateWithLeftChild(parent.left) : // LL
rotateWithRightChild(parent.left); // LR (parent.left = P)
else
return parent.right = compare(item, parent.right) < 0 ?
rotateWithLeftChild(parent.right) : // RL
rotateWithRightChild(parent.right); // RR
}
/**
* 右旋转(处理LL情况)
* k2 k1
* / / \
* k1 --> o1 k2
* /
* o1
*/
private RedBlackNode<T> rotateWithLeftChild(RedBlackNode<T> k2) {
RedBlackNode<T> k1 = k2.left;
k2.left = k1.right;
k1.right = k2;
return k1;
}
/**
* 左旋转(处理RR情况)
* k1 k2
* \ / \
* k2 --> k1 o1
* \
* o1
*/
private RedBlackNode<T> rotateWithRightChild(RedBlackNode<T> k1) {
RedBlackNode<T> k2 = k1.right;
k1.right = k2.left;
k2.left = k1;
return k2;
}
该方法是红黑树插入的核心:
-
先变色:当前节点红、左右儿子黑;
-
若父节点红 → 触发旋转调整;
-
根据插入位置判断是 "之字型(Zig-Zag)" 还是 "一字型(Zig-Zig)";
-
rotate()
根据方向选择合适旋转(LL/LR/RL/RR),并返回新子树根; -
最后根节点染黑。
通过这套机制,红黑树始终保持红黑性质。
六 红黑树总结
红黑树作为一种高效的自平衡搜索树,在理论与工程中都极具重要性。与 AVL 树相比,红黑树牺牲了一部分"严格平衡性",但换来了 更少的旋转次数和更高的插入删除性能。
对比项 | 红黑树 | AVL树 |
---|---|---|
平衡性 | 较弱(近似平衡) | 严格平衡 |
查找性能 | 略逊一筹 | 最优 |
插入性能 | 更高(少旋转) | 较低 |
删除性能 | 更高 | 较复杂 |
应用场景 | 通用集合结构、语言标准库 | 实时搜索或频繁查找场景 |
红黑树是 算法工程化的典范:它用极小的实现代价,保证了平衡查找树的性能稳定性。通过颜色和局部旋转的协同,使得树在动态操作下依旧保持高效结构。