红黑树-带源码
目录
一 红黑树概述
二 红黑树插入原理介绍
三 红黑树删除的原理介绍
四 红黑树 Java 实现
五 代码解释
1. RedBlackNode 节点类
2. insert() 方法
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;elsereturn 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;elsereturn false;}} public boolean isEmpty() {return header.right == nullNode;} public void makeEmpty() {header.right = nullNode;} public void printTree() {if (isEmpty())System.out.println("Empty tree");elseSystem.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) : // LLrotateWithRightChild(parent.left); // LR (parent.left = P)elsereturn parent.right = compare(item, parent.right) < 0 ?rotateWithLeftChild(parent.right) : // RLrotateWithRightChild(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) : // LLrotateWithRightChild(parent.left); // LR (parent.left = P)elsereturn parent.right = compare(item, parent.right) < 0 ?rotateWithLeftChild(parent.right) : // RLrotateWithRightChild(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树 |
---|---|---|
平衡性 | 较弱(近似平衡) | 严格平衡 |
查找性能 | 略逊一筹 | 最优 |
插入性能 | 更高(少旋转) | 较低 |
删除性能 | 更高 | 较复杂 |
应用场景 | 通用集合结构、语言标准库 | 实时搜索或频繁查找场景 |
红黑树是 算法工程化的典范:它用极小的实现代价,保证了平衡查找树的性能稳定性。通过颜色和局部旋转的协同,使得树在动态操作下依旧保持高效结构。