Java基础快速入门: 红黑树与TreeSet

本文纲要

  1. 红黑树概述
  2. 红黑树的五条红黑规则
  3. 添加节点默认颜色的选择
  4. 添加节点后如何保证红黑规则
    4.1 父节点为黑色
    4.2 父节点为红色,叔叔节点为红色
    4.3 父节点为红色,叔叔节点为黑色
  5. 红黑树与TreeSet实战练习
    5.1 项目结构
    5.2 Student类实现自然排序
    5.3 测试代码
    5.4 底层红黑树插入过程图解
  6. 小结

红黑树概述

红黑树(Red-Black Tree) 是一种自平衡的二叉查找树,在计算机科学中被广泛应用。它最早在1972年由鲁道夫·贝尔提出,当时被称为“平衡二叉B树”,到1978年被修改为现在的形式并命名为红黑树。

红黑树的每个节点都有一个存储位来表示其颜色,只能是红色或黑色。它并非像AVL树那样严格的高度平衡,而是通过自己定义的“红黑规则”来维持一种相对宽松的平衡。这使得它在插入、删除时的旋转次数更少,整体性能更优。

红黑树

是二叉查找树

自平衡,非高度平衡

每个节点带颜色: 红/黑

通过红黑规则维持平衡

左子树 < 节点 < 右子树

平衡条件比AVL树宽松

旋转次数相对较少

提示:与AVL树相比,红黑树不会因为子树高度差超过1就立即旋转,因此在频繁插入删除的场景中效率更高。

红黑树的五条红黑规则

红黑树必须时刻满足以下五条规则:

  1. 每个节点是红色或者黑色。
  2. 根节点必须是黑色。
  3. 每个叶子节点(NIL)都是黑色的。 如果一个节点没有子节点或父节点,其对应指针属性值为NIL,这些NIL被视为叶子节点,且必须为黑色。
  4. 红色节点的子节点必须是黑色。 (不能出现两个连续红色节点)
  5. 对每个节点,从该节点到其所有后代叶子节点的简单路径上,均包含相同数目的黑色节点。 (“简单路径”是指不能回头的路径)

红黑树节点结构

相较于普通二叉树的节点,红黑树的节点多了一个颜色属性:

class RBNode {
    int key;
    Color color; // RED 或 BLACK 
    RBNode parent;
    RBNode left;
    RBNode right;
}

下面用一个具体红黑树的结构图来说明这些规则:

1 黑

8 红

11 黑

13 黑

15 黑

17 红

25 黑

NIL 黑

NIL 黑

NIL 黑

NIL 黑

NIL 黑

NIL 黑

NIL 黑

NIL 黑

  • 所有叶子都是黑色NIL。
  • 根节点13是黑色。
  • 红色节点(8,17)的子节点(1,11,15,25)全是黑色 → 没有连续红色。
  • 从任一节点到叶子NIL的黑色节点数目相同。

添加节点默认颜色的选择

在红黑树中插入新节点时,默认颜色应该设置为红色还是黑色呢?我们通过对比分析:

1 ) 假设默认颜色为黑色

插入顺序:20(根), 18, 23

  1. 插入20(黑) — 符合规则。
  2. 插入18(黑) — 20的左子树黑高增加,破坏了规则5(左右路径黑色节点数不等),需要将18改为红色。
  3. 插入23(黑) — 同样破坏规则5,又需要将23改为红色。

结果:插入3个节点需要调整2次。

2 ) 假设默认颜色为红色

插入顺序:20, 18, 23

  1. 插入20(红) — 破坏规则2(根必须黑),直接变色为黑即可。
  2. 插入18(红) — 父节点20为黑色,无需调整。
  3. 插入23(红) — 父节点20为黑色,无需调整。

结果:插入3个节点只需要调整1次(仅根节点变色)。

显然,默认颜色为红色效率更高,红黑树也正是这样设计的。

新节点默认颜色

红色

根节点?

直接变黑

父节点黑色?

无需调整

需要进一步修复

添加节点后如何保证红黑规则

当插入新节点(默认为红色)后可能破坏红黑规则,此时需要根据父节点颜色以及叔叔节点颜色进行修复。主要分为以下几种情况:

1 ) 父节点为黑色

结论:什么都不需要做,插入红色节点不会破坏任何规则。

2 ) 父节点为红色,叔叔节点为红色

修复步骤(三步走):

  1. 将父节点设置为黑色。
  2. 将叔叔节点设置为黑色。
  3. 将祖父节点设置为红色。
  4. 如果祖父节点是根节点,则再将其变回黑色。

示意图:假设插入22节点后,结构如下(父23红,叔18红):

18 红

20 黑

22 红

23 红

NIL

NIL

NIL

修复后:父23变黑,叔18变黑,祖父20变红。由于20是根,再变回黑。

18 黑

20 黑

22 红

23 黑

NIL

NIL

NIL

3 ) 父节点为红色,叔叔节点为黑色

这是最复杂的情况,需要旋转 + 变色。修复步骤:

  1. 将父节点设置为黑色。
  2. 将祖父节点设置为红色(注意:叔叔节点颜色不变)。
  3. 以祖父节点为支点进行旋转:
    • 如果新节点在 左子树的左侧(LL型) → 进行右旋。
    • 如果新节点在 右子树的右侧(RR型) → 进行左旋。
    • 如果是内侧插入(LR或RL)则需要先旋转子树变成外侧情况,再按上述处理。

示例:在已有树中插入14,父15红,叔NIL黑(NIL视为黑色)。

插入后结构(15,16,18关系,14在15的左边):

14 红

15 红

16 红

18 黑

19 黑

NIL

NIL

修复过程:
父节点15变黑。
祖父节点16变红。
以16为支点进行右旋:15上升为子树根,16变成15的右子节点。

旋转后

14 红

15 黑

16 红

18 黑

19 黑

NIL

NIL

右旋示意图:

旋转前: 16为支点

16

15

?

14

?

旋转后: 15上升

15

14

16

?

?

归纳成一张完整的插入修复流程图:

黑色

红色

红色

黑色/NIL

外侧LL/RR

内侧LR/RL

插入新节点,默认红色

是否为根节点?

直接变黑色

父节点颜色?

无需任何操作

叔叔节点颜色?

1.父变黑 2.叔变黑 3.祖父变红 4.若祖父为根再变黑

1.父变黑 2.祖父变红 3.根据结构旋转

插入位置

单旋转 右旋/左旋

先旋子树再旋祖父

红黑树与TreeSet实战练习

TreeSet 底层就是基于红黑树实现的,它是一个可以自动排序的集合。
下面我们通过一个练习:创建学生对象并按总分排序,来深入理解红黑树的应用。

1 ) 项目结构

MySet/
└── src/
    └── com/
        └── wb/
            └── treesettest/
                ├── Student.java 
                └── TreeSetTest.java 

2 ) Student类实现自然排序

要让 TreeSet 能够对学生对象排序,必须指定排序规则。这里实现 Comparable 接口(自然排序),重写 compareTo 方法。

package com.wb.treesettest;
 
public class Student implements Comparable<Student>{
    private String name;
    private int chinese;
    private int math;
    private int english;
 
    public Student() {
    }
 
    public Student(String name, int chinese, int math, int english) {
        this.name = name;
        this.chinese = chinese;
        this.math = math;
        this.english = english;
    }
 
    // 省略 getter/setter (保持原有完整代码即可)
 
    @Override 
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", chinese=" + chinese +
                ", math=" + math +
                ", english=" + english +
                '}' + "总分为" + getSum();
    }
 
    // 计算总分 
    public int getSum(){
        return chinese + math + english;
    }
 
    @Override 
    public int compareTo(Student o) {
        // 按照总分从高到低排序 (o - this 表示降序)
        int result = o.getSum() - this.getSum();
        // 次要条件1:总分一样,比较语文成绩 
        result = result == 0 ? o.getChinese() - this.getChinese() : result;
        // 次要条件2:语文一样,比较数学成绩 
        result = result == 0 ? o.getMath() - this.getMath() : result;
        // 次要条件3:数学一样,比较英语成绩 
        result = result == 0 ? o.getEnglish() - this.getEnglish() : result;
        // 次要条件4:成绩都一样,按姓名排序 
        result = result == 0 ? o.getName().compareTo(this.getName()) : result;
        return result;
    }
}

补充说明:compareTo 中的 this 代表当前待插入元素,o 代表红黑树中已存在的元素。使用 o.getSum() - this.getSum() 会实现从大到小(降序)排序。如果需要从小到大(升序),只需改为 this.getSum() - o.getSum()。记混了也没关系,写完运行看结果,不符合期望再调换 thiso 即可。

3 ) 测试代码

package com.wb.treesettest;
 
import java.util.TreeSet;
 
public class TreeSetTest {
    public static void main(String[] args) {
        TreeSet<Student> ts = new TreeSet<>();
 
        Student s1 = new Student("dahei", 80, 80, 80);    // 总分240 
        Student s2 = new Student("erhei", 90, 90, 90);    // 总分270 
        Student s3 = new Student("xiaohei", 100, 100, 100); // 总分300 
 
        ts.add(s1);
        ts.add(s2);
        ts.add(s3);
 
        for (Student student : ts) {
            System.out.println(student);
        }
    }
}

输出结果(从大到小):

Student{name='xiaohei', chinese=100, math=100, english=100}总分为300 
Student{name='erhei', chinese=90, math=90, english=90}总分为270 
Student{name='dahei', chinese=80, math=80, english=80}总分为240 

4 ) 底层红黑树插入过程图解

当我们依次添加 s1(240)s2(270)s3(300) 时,底层红黑树的变化过程如下(使用降序规则,this=待插入o=已存在):
第一步:插入 s1 (240) 作为根节点

240 红

根节点不能为红色 → 直接变黑:

240 黑

第二步:插入 s2 (270),总分 270 > 240,按降序规则 (o - this) 结果为负,应放在左边

240 黑

270 红

NIL

NIL

NIL

父节点为黑色 → 无需调整。

第三步:插入 s3 (300)300 > 240 且 > 270,最终放在270的左边

240 黑

270 红

300 红

NIL

NIL

此时出现连续红色节点(270红,300红),触发修复:

  • 父节点270红,叔叔节点是NIL(黑) → 属于“父红叔黑”情况。
  • 且新节点在左子树的左侧(LL型) → 需要进行右旋

修复步骤:

  1. 父节点270变黑。
  2. 祖父节点240变红。
  3. 以祖父240为支点进行右旋 → 270上升为根,240降为270的右孩子。

右旋后

240 红

270 黑

300 红

NIL

NIL

NIL

NIL

注意:270变黑后成为新根,如果是根必须保持黑色,已经满足;240变红后不再是根,合法。所有规则重新得到满足。

中序遍历红黑树的顺序是:左子树 → 当前节点 → 右子树,因此输出结果为:300 → 270 → 240,即从大到小排列。如果要升序,只需修改 compareTo 中的相减顺序,底层红黑树的左右结构将会镜像反转。

小结

  1. 红黑树是一种自平衡二叉查找树,通过“红黑规则”而非绝对高度差来维持平衡。
  2. 五条红黑规则必须牢记,尤其是“黑高相同”和“无连续红色”。
  3. 插入节点时默认颜色为红色,效率更高。
  4. 插入后的修复依赖于父节点与叔叔节点的颜色,核心操作包括:
    • 变色
    • 左旋 / 右旋
  5. Java中的 TreeSet 底层就是红黑树,存入的元素必须实现 Comparable 或传入 Comparator 才能自动排序。
  6. 自然排序方法 compareTo 中,this 代表新元素,o 代表已存在元素,灵活调换两者即可改变升/降序。

红黑树是面试和实际开发中的重点数据结构,理解其平衡原理和旋转过程对后续学习 TreeMapHashMap(链表转红黑树)等都大有裨益。建议结合动图或亲手画出插入过程来加深印象,切勿死记硬背。

本文基于红黑树经典理论及TreeSet底层实现编写,配合代码实战,助你快速入门。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Wang's Blog

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值