红黑树详细介绍(五大规则、保持平衡操作、Java实现)

文章目录

红黑树

介绍

红黑树是另一种重要的平衡二叉树,通过颜色标记和旋转维护平衡。

由于其自平衡的特性,保证了最坏情形下在 O(logn) 时间复杂度内完成查找、增加、删除等操作,性能表现稳定。

五大规则

红黑树之所以能保持平衡,是因为它遵循以下五个基本规则。这些规则是定义红黑树的核心:

  1. 节点是红色或黑色:每个节点都有一个颜色属性,非红即黑。
  2. 根节点是黑色:树的顶端根节点必须是黑色的。
  3. 所有叶子节点都是黑色:这里的叶子节点指的是空指针(NIL或NULL节点),被视为黑色的哨兵节点。
  4. 红色节点的子节点必须是黑色 :这意味着不会有两个连续的红色节点出现。这是保证平衡的关键规则之一。
  5. 从任意节点到其每个叶子节点的所有路径都包含相同数目的黑色节点:这个数量被称为该节点的"黑高"。这条规则确保了没有一条路径会比其他路径长出两倍以上,是平衡性的最强保证。

规则4和规则5是精髓

  • 规则4限制了红色节点的连续出现,控制了树的"横向"蔓延。
  • 规则5确保了从根到叶子的所有路径中,黑色节点的数量是严格相等的,这就像是为树的深度设定了一个基准。

保持平衡

当插入或删除节点可能破坏上述规则时,红黑树需要通过两种基本操作来修复平衡:

  1. 变色:改变节点的颜色(红变黑,或黑变红)。这是最直接、代价最小的调整方式。
  2. 旋转:通过改变树的结构来重新平衡。
  • 左旋:以某个节点为支点,让其右子节点成为新的父节点。
  • 右旋:以某个节点为支点,让其左子节点成为新的父节点。

旋转操作是局部操作,时间复杂度是O(1),不会影响整棵树的大局。

插入操作(简述)

新插入的节点通常被标记为红色(因为标记为黑色会立刻违反规则5,影响太大)。插入后,检查是否破坏了规则:

  • 情况1:如果新节点的父节点是黑色,没有违反任何规则,无需调整。

  • 情况2:如果父节点是红色(违反了规则4),则需要根据其"叔叔节点"(父节点的兄弟节点)的颜色进行进一步处理:

  • 情况2.1 :叔叔节点是红色。通过将父节点和叔叔节点变黑,祖父节点变红来解决。然后可能将问题向上传递到祖父节点。

  • 情况2.2 :叔叔节点是黑色 (或NIL)。这种情况需要通过旋转变色的组合来解决。可能是左左、左右、右右、右左等不同情况,通过相应的旋转(单旋或双旋)来调整结构。

对比AVL树

好的,我们用一个系统、易懂的方式来介绍一下红黑树。

一、红黑树是什么?

简单来说,红黑树是一种自平衡的二叉查找树

  • 二叉查找树:首先,它具备二叉查找树的所有特性:对于任意节点,其左子树所有节点的值都小于它,右子树所有节点的值都大于它。这使得查找、插入、删除等操作非常高效(平均时间复杂度O(log n))。
  • 自平衡:这是红黑树的核心。普通的二叉查找树在插入或删除节点时,如果数据是顺序插入的(如1,2,3,4,5),树会退化成一条"链",查找效率会降至O(n)。红黑树通过一套严格的规则和相应的调整操作(变色、旋转),确保树始终保持"大致平衡",从而保证在最坏情况下的操作效率也是O(log n)。

你可以把它想象成一个能自动整理书籍的超智能书架。无论你按什么顺序往书架上放书(比如按照书名拼音顺序、出版日期乱序等),这个书架都会通过内部机制自动调整,让书与书之间的间隔保持相对均匀,你总能快速地找到任何一本书。


二、红黑树的五大规则

红黑树之所以能保持平衡,是因为它遵循以下五个基本规则。这些规则是定义红黑树的核心:

  1. 节点是红色或黑色:每个节点都有一个颜色属性,非红即黑。
  2. 根节点是黑色:树的顶端根节点必须是黑色的。
  3. 所有叶子节点都是黑色:这里的叶子节点指的是空指针(NIL或NULL节点),被视为黑色的哨兵节点。
  4. 红色节点的子节点必须是黑色 :这意味着不会有两个连续的红色节点出现。这是保证平衡的关键规则之一。
  5. 从任意节点到其每个叶子节点的所有路径都包含相同数目的黑色节点:这个数量被称为该节点的"黑高"。这条规则确保了没有一条路径会比其他路径长出两倍以上,是平衡性的最强保证。

规则4和规则5是精髓

  • 规则4限制了红色节点的连续出现,控制了树的"横向"蔓延。
  • 规则5确保了从根到叶子的所有路径中,黑色节点的数量是严格相等的,这就像是为树的深度设定了一个基准。

三、⭐红黑树如何保持平衡?(核心操作)

当插入或删除节点可能破坏上述规则时,红黑树需要通过两种基本操作来修复平衡:

  1. 变色:改变节点的颜色(红变黑,或黑变红)。这是最直接、代价最小的调整方式。
  • 左倾染色:
  • 右倾染色:
  1. 旋转:通过改变树的结构来重新平衡。
  • 左旋:以某个节点为支点,让其右子节点成为新的父节点。
  • 右旋:以某个节点为支点,让其左子节点成为新的父节点。

旋转操作是局部操作,时间复杂度是O(1),不会影响整棵树的大局。

  1. 左旋
  1. 右旋+左旋
  1. 右旋
  1. 左旋+右旋
⭐插入操作的平衡调整(简述)

新插入的节点通常被标记为红色(因为标记为黑色会立刻违反规则5,影响太大)。插入后,检查是否破坏了规则:

  • 情况1:如果新节点的父节点是黑色,没有违反任何规则,无需调整。

  • 情况2:如果父节点是红色(违反了规则4),则需要根据其"叔叔节点"(父节点的兄弟节点)的颜色进行进一步处理:

  • 情况2.1 :叔叔节点是红色。通过将父节点和叔叔节点变黑,祖父节点变红来解决。然后可能将问题向上传递到祖父节点。

  • 情况2.2 :叔叔节点是黑色 (或NIL)。这种情况需要通过旋转变色的组合来解决。可能是左左、左右、右右、右左等不同情况,通过相应的旋转(单旋或双旋)来调整结构。

通过这一系列的变色和旋转,最终让树重新满足所有红黑树规则。


四、红黑树 vs AVL树

AVL树是另一种著名的自平衡二叉查找树,它通过高度差(平衡因子) 来维持更严格的平衡。

特性 红黑树 AVL树
平衡标准 规则4和规则5(较为宽松) 每个节点的左右子树高度差不超过1(非常严格)
平衡度 大致平衡,不是完全平衡 高度平衡,是更严格的平衡
查询效率 查询效率稍低,但稳定在O(log n) 查询效率最高,因为树更平衡
插入/删除效率 更高。因为平衡要求宽松,需要的旋转操作更少 较低。插入/删除后可能需要更多的旋转来维持严格平衡
适用场景 频繁增删的场景,如关联数组(Map、Set) 频繁查询,少增删的场景,如数据库索引

总结选择

  • 如果你的应用需要大量的查找操作,而插入和删除不频繁,AVL树是更好的选择。
  • 在现实中,需要大量插入、删除和查找操作的场景更为常见,因此红黑树的应用更加广泛。

五、Java实现

基本结构
java 复制代码
@Data
public class RBNode<T extends Comparable<T>> {
    private T data;
    private Color color;
    RBNode<T> left, right, parent;

    public RBNode(T data) {
        this.data = data;
        this.color = Color.RED; // 新节点默认为红色
        this.left = this.right = this.parent = null;
    }
}
public class RedBlackTree <T extends Comparable<T>>{
    private RBNode<T> root;
    private final RBNode<T> NIL; // 哨兵节点(叶子节点)

    public RedBlackTree() {
        NIL = new RBNode<>(null);
        NIL.setColor(Color.BLACK);
        root = NIL;
    }
}
左右旋
java 复制代码
// 左旋操作
private void leftRotate(RBNode<T> x) {
    RBNode<T> y = x.right;
    x.right = y.left;

    if (y.left != NIL) {
        y.left.parent = x;
    }

    y.parent = x.parent;

    if (x.parent == NIL) {
        root = y;
    } else if (x == x.parent.left) {
        x.parent.left = y;
    } else {
        x.parent.right = y;
    }

    y.left = x;
    x.parent = y;
}

// 右旋操作
private void rightRotate(RBNode<T> y) {
    RBNode<T> x = y.left;
    y.left = x.right;

    if (x.right != NIL) {
        x.right.parent = y;
    }

    x.parent = y.parent;

    if (y.parent == NIL) {
        root = x;
    } else if (y == y.parent.left) {
        y.parent.left = x;
    } else {
        y.parent.right = x;
    }

    x.right = y;
    y.parent = x;
}
插入方法
java 复制代码
// 插入方法
public void insert(T data) {
    RBNode<T> node = new RBNode<>(data);
    node.left = NIL;
    node.right = NIL;
    node.parent = NIL;

    RBNode<T> current = root;
    RBNode<T> parent = NIL;

    // 二叉搜索树插入
    while (current != NIL) {
        parent = current;
        if (node.getData().compareTo(current.getData()) < 0) {
            current = current.left;
        } else {
            current = current.right;
        }
    }

    node.parent = parent;
    if (parent == NIL) {
        root = node;
    } else if (node.getData().compareTo(parent.getData()) < 0) {
        parent.left = node;
    } else {
        parent.right = node;
    }

    fixInsert(node); // 修复红黑树性质
}

// 修复插入后的红黑树性质
private void fixInsert(RBNode<T> node) {
    while (node.parent.getColor() == Color.RED) {
        if (node.parent == node.parent.parent.left) {
            RBNode<T> uncle = node.parent.parent.right;

            // Case 1: 叔叔节点是红色
            if (uncle.getColor() == Color.RED) {
                node.parent.setColor(Color.BLACK);
                uncle.setColor(Color.BLACK) ;
                node.parent.parent.setColor(Color.RED);
                node = node.parent.parent;
            } else {
                // Case 2: 节点是父节点的右孩子
                if (node == node.parent.right) {
                    node = node.parent;
                    leftRotate(node);
                }
                // Case 3: 节点是父节点的左孩子
                node.parent.setColor(Color.BLACK);
                node.parent.parent.setColor(Color.RED);
                rightRotate(node.parent.parent);
            }
        } else {
            // 对称情况(父节点是右孩子)
            RBNode<T> uncle = node.parent.parent.left;

            if (uncle.getColor() == Color.RED) {
                node.parent.setColor(Color.BLACK);
                uncle.setColor(Color.BLACK);
                node.parent.parent.setColor(Color.RED);
                node = node.parent.parent;
            } else {
                if (node == node.parent.left) {
                    node = node.parent;
                    rightRotate(node);
                }
                node.parent.setColor(Color.BLACK);
                node.parent.parent.setColor(Color.RED);
                leftRotate(node.parent.parent);
            }
        }
    }
    root.setColor(Color.BLACK); // 确保根节点为黑色
}
相关推荐
墨染点香6 小时前
LeetCode 刷题【124. 二叉树中的最大路径和、125. 验证回文串】
算法·leetcode·职场和发展
jerryinwuhan6 小时前
机器人模拟器(python)
开发语言·python·机器人
AhriProGramming6 小时前
Flask-SQLAlchemy精读-双语精选文章
python·算法·flask
孤廖6 小时前
吃透 C++ 栈和队列:stack/queue/priority_queue 用法 + 模拟 + STL 标准实现对比
java·开发语言·数据结构·c++·人工智能·深度学习·算法
我命由我123456 小时前
Android 对话框 - 对话框全屏显示(设置 Window 属性、使用自定义样式、继承 DialogFragment 实现、继承 Dialog 实现)
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
驰羽6 小时前
[GO]GORM中的Tag映射规则
开发语言·golang
Full Stack Developme6 小时前
java.net 包详解
java·python·.net
非凡的世界6 小时前
深入理解 PHP 框架里的设计模式
开发语言·设计模式·php
BanyeBirth6 小时前
C++动态规划——LIS(最长不下降子序列)
算法·动态规划