红黑树-带源码

目录

[一 红黑树概述](#一 红黑树概述)

[二 红黑树插入原理介绍](#二 红黑树插入原理介绍)

[三 红黑树删除的原理介绍](#三 红黑树删除的原理介绍)

[四 红黑树 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. GuibasRobert Sedgewick 于 1978 年改进并命名为红黑树。如今,红黑树已成为计算机科学领域中使用最广泛的平衡树之一。

  • 红黑树的五条性质

    1. 每个结点要么是红色,要么是黑色。

    2. 根结点是黑色。

    3. 所有叶子结点(NIL或NULL节点)都是黑色。

    4. 如果一个节点是红色,则它的两个子节点必须是黑色(红节点不能相邻)。

    5. 从任意一个节点到其所有叶子节点的路径上,黑色节点数相同

第五条性质称为 黑高平衡 ,它是红黑树平衡性的核心。虽然红黑树不是严格意义上的高度平衡树(如AVL树),但其最大高度不会超过 2*log2(n+1)(最长路径(红黑节点交替)不会超过最短路径(全黑节点)的两倍),在实际应用中性能非常稳定。

  • 红黑树的基本操作

    为了在插入和删除后仍能维持红黑树的五大性质,我们需要进行两种基本操作:变色旋转

    1. 旋转

      旋转是保持BST性质并调整树结构的局部操作。

      • 左旋:以某个节点为支点,其右子节点成为新的父节点,支点自身成为新父节点的左子节点。
      • 右旋:以某个节点为支点,其左子节点成为新的父节点,支点自身成为新父节点的右子节点。
    1. 变色

      简单地改变节点的颜色,从红变黑或从黑变红。这是最直接的调整方式。

二 红黑树插入原理介绍

插入新节点的步骤可以概括为两步:

  1. 标准BST插入 :首先,像在普通二叉查找树中一样插入新节点。新插入的节点我们总是将其着色为红色

    • 为什么是红色?因为如果插入黑色节点,会立即违反性质5(黑高不一致),修复起来非常困难。插入红色节点可能违反性质2(根节点为黑)或性质4(不能有连续红节点),但修复这些情况相对容易。
  2. 重新平衡与修复 :如果插入后违反了红黑树的性质,我们需要通过变色和旋转来修复。修复的情况主要取决于新节点的父节点叔叔节点(父节点的兄弟节点)的颜色。

我们定义:

  • P:父节点

  • U:叔叔节点

  • G:祖父节点

  • N:新插入的节点

情况1:N是根节点

  • 操作:直接将N变为黑色。(违反性质2)

情况2:P是黑色

  • 操作:什么都不用做。树仍然是有效的红黑树。(没有违反任何性质)

情况3:P是红色,U也是红色

  • 操作

    1. 将P和U变为黑色。

    2. 将G变为红色。

    3. 将G视为新的当前节点,从情况1开始递归检查。

情况4:P是红色,U是黑色(或NIL),且N是P的右子节点,P是G的左子节点

  • 操作

    1. 以P为支点进行左旋。

    2. 将P作为新的当前节点,此时情况转变为情况5。

情况5:P是红色,U是黑色(或NIL),且N是P的左子节点,P是G的左子节点

  • 操作

    1. 将P变为黑色。

    2. 将G变为红色。

    3. 以G为支点进行右旋。

(如果P是G的右子节点,则情况4和5是镜像对称的,操作中的左右旋相反)

三 红黑树删除的原理介绍

删除操作比插入更复杂,但核心思想相似:先执行标准BST删除,然后修复可能被破坏的红黑性质。

  1. 标准BST删除

    • 如果被删除的节点有两个非NIL子节点,我们通常找到它的后继节点 (右子树中的最小节点),用后继节点的值替换被删除节点的值,然后转而删除这个后继节点。这样问题就转化为删除一个至多只有一个子节点的节点

    • 最终,我们实际删除的节点(记为 D)最多只有一个子节点(记为 C)。

  2. 重新平衡与修复

    • 如果 D 是红色,直接删除它,用 C 替换它,不会破坏任何性质。

    • 如果 D 是黑色,而 C 是红色,那么直接用红色的 C 替换 D,并将 C 变为黑色。

    • 最复杂的情况 :如果 DC 都是黑色(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:存储的数据;

  • leftright:左右子节点;

  • 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树
平衡性 较弱(近似平衡) 严格平衡
查找性能 略逊一筹 最优
插入性能 更高(少旋转) 较低
删除性能 更高 较复杂
应用场景 通用集合结构、语言标准库 实时搜索或频繁查找场景

红黑树是 算法工程化的典范:它用极小的实现代价,保证了平衡查找树的性能稳定性。通过颜色和局部旋转的协同,使得树在动态操作下依旧保持高效结构。

相关推荐
Dave.B4 小时前
【VTK实战】vtkDepthImageToPointCloud:从2D深度图到3D点云,手把手教你落地3D扫描/AR场景
算法·计算机视觉·3d·ar·vtk
乐迪信息4 小时前
乐迪信息:煤矿堆煤隐患难排查?AI摄像机实时监控与预警
大数据·人工智能·算法·安全·视觉检测
一语雨在生无可恋敲代码~4 小时前
leetcode724 寻找数组的中心下标
数据结构·算法
科研小白_4 小时前
2025年优化算法:多策略改进蛇优化算法( Improved Snake Optimizer,ISO)
算法
88号技师4 小时前
【2025年10月一区SCI】改进策略:Trend-Aware Mechanism 趋势感知机制(TAM)-附Matlab免费代码
开发语言·算法·数学建模·matlab·优化算法
晨非辰4 小时前
《超越单链表的局限:双链表“哨兵位”设计模式,如何让边界处理代码既优雅又健壮?》
c语言·开发语言·数据结构·c++·算法·面试
胖咕噜的稞达鸭4 小时前
算法入门:专题攻克一---双指针4(三数之和,四数之和)强推好题,极其锻炼算法思维
开发语言·c++·算法
weixin_307779135 小时前
C#实现MySQL→Clickhouse建表语句转换工具
开发语言·数据库·算法·c#·自动化
zc.ovo5 小时前
Kruskal重构树
数据结构·c++·算法·重构·图论