本文纲要
- 红黑树概述
- 红黑树的五条红黑规则
- 添加节点默认颜色的选择
- 添加节点后如何保证红黑规则
4.1 父节点为黑色
4.2 父节点为红色,叔叔节点为红色
4.3 父节点为红色,叔叔节点为黑色 - 红黑树与
TreeSet实战练习
5.1 项目结构
5.2Student类实现自然排序
5.3 测试代码
5.4 底层红黑树插入过程图解 - 小结
红黑树概述
红黑树(Red-Black Tree) 是一种自平衡的二叉查找树,在计算机科学中被广泛应用。它最早在1972年由鲁道夫·贝尔提出,当时被称为“平衡二叉B树”,到1978年被修改为现在的形式并命名为红黑树。
红黑树的每个节点都有一个存储位来表示其颜色,只能是红色或黑色。它并非像AVL树那样严格的高度平衡,而是通过自己定义的“红黑规则”来维持一种相对宽松的平衡。这使得它在插入、删除时的旋转次数更少,整体性能更优。
提示:与AVL树相比,红黑树不会因为子树高度差超过1就立即旋转,因此在频繁插入删除的场景中效率更高。
红黑树的五条红黑规则
红黑树必须时刻满足以下五条规则:
- 每个节点是红色或者黑色。
- 根节点必须是黑色。
- 每个叶子节点(
NIL)都是黑色的。 如果一个节点没有子节点或父节点,其对应指针属性值为NIL,这些NIL被视为叶子节点,且必须为黑色。 - 红色节点的子节点必须是黑色。 (不能出现两个连续红色节点)
- 对每个节点,从该节点到其所有后代叶子节点的简单路径上,均包含相同数目的黑色节点。 (“简单路径”是指不能回头的路径)
红黑树节点结构
相较于普通二叉树的节点,红黑树的节点多了一个颜色属性:
class RBNode {
int key;
Color color; // RED 或 BLACK
RBNode parent;
RBNode left;
RBNode right;
}
下面用一个具体红黑树的结构图来说明这些规则:
- 所有叶子都是黑色NIL。
- 根节点13是黑色。
- 红色节点(8,17)的子节点(1,11,15,25)全是黑色 → 没有连续红色。
- 从任一节点到叶子NIL的黑色节点数目相同。
添加节点默认颜色的选择
在红黑树中插入新节点时,默认颜色应该设置为红色还是黑色呢?我们通过对比分析:
1 ) 假设默认颜色为黑色
插入顺序:20(根), 18, 23
- 插入20(黑) — 符合规则。
- 插入18(黑) — 20的左子树黑高增加,破坏了规则5(左右路径黑色节点数不等),需要将18改为红色。
- 插入23(黑) — 同样破坏规则5,又需要将23改为红色。
结果:插入3个节点需要调整2次。
2 ) 假设默认颜色为红色
插入顺序:20, 18, 23
- 插入20(红) — 破坏规则2(根必须黑),直接变色为黑即可。
- 插入18(红) — 父节点20为黑色,无需调整。
- 插入23(红) — 父节点20为黑色,无需调整。
结果:插入3个节点只需要调整1次(仅根节点变色)。
显然,默认颜色为红色效率更高,红黑树也正是这样设计的。
添加节点后如何保证红黑规则
当插入新节点(默认为红色)后可能破坏红黑规则,此时需要根据父节点颜色以及叔叔节点颜色进行修复。主要分为以下几种情况:
1 ) 父节点为黑色
结论:什么都不需要做,插入红色节点不会破坏任何规则。
2 ) 父节点为红色,叔叔节点为红色
修复步骤(三步走):
- 将父节点设置为黑色。
- 将叔叔节点设置为黑色。
- 将祖父节点设置为红色。
- 如果祖父节点是根节点,则再将其变回黑色。
示意图:假设插入22节点后,结构如下(父23红,叔18红):
修复后:父23变黑,叔18变黑,祖父20变红。由于20是根,再变回黑。
3 ) 父节点为红色,叔叔节点为黑色
这是最复杂的情况,需要旋转 + 变色。修复步骤:
- 将父节点设置为黑色。
- 将祖父节点设置为红色(注意:叔叔节点颜色不变)。
- 以祖父节点为支点进行旋转:
- 如果新节点在 左子树的左侧(LL型) → 进行右旋。
- 如果新节点在 右子树的右侧(RR型) → 进行左旋。
- 如果是内侧插入(LR或RL)则需要先旋转子树变成外侧情况,再按上述处理。
示例:在已有树中插入14,父15红,叔NIL黑(NIL视为黑色)。
插入后结构(15,16,18关系,14在15的左边):
修复过程:
父节点15变黑。
祖父节点16变红。
以16为支点进行右旋:15上升为子树根,16变成15的右子节点。
右旋示意图:
归纳成一张完整的插入修复流程图:
红黑树与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()。记混了也没关系,写完运行看结果,不符合期望再调换 this 和 o 即可。
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) 作为根节点
根节点不能为红色 → 直接变黑:
第二步:插入 s2 (270),总分 270 > 240,按降序规则 (o - this) 结果为负,应放在左边
父节点为黑色 → 无需调整。
第三步:插入 s3 (300),300 > 240 且 > 270,最终放在270的左边
此时出现连续红色节点(270红,300红),触发修复:
- 父节点270红,叔叔节点是NIL(黑) → 属于“父红叔黑”情况。
- 且新节点在左子树的左侧(LL型) → 需要进行右旋。
修复步骤:
- 父节点270变黑。
- 祖父节点240变红。
- 以祖父240为支点进行右旋 → 270上升为根,240降为270的右孩子。
注意:270变黑后成为新根,如果是根必须保持黑色,已经满足;240变红后不再是根,合法。所有规则重新得到满足。
中序遍历红黑树的顺序是:左子树 → 当前节点 → 右子树,因此输出结果为:300 → 270 → 240,即从大到小排列。如果要升序,只需修改 compareTo 中的相减顺序,底层红黑树的左右结构将会镜像反转。
小结
- 红黑树是一种自平衡二叉查找树,通过“红黑规则”而非绝对高度差来维持平衡。
- 五条红黑规则必须牢记,尤其是“黑高相同”和“无连续红色”。
- 插入节点时默认颜色为红色,效率更高。
- 插入后的修复依赖于父节点与叔叔节点的颜色,核心操作包括:
- 变色
- 左旋 / 右旋
- Java中的
TreeSet底层就是红黑树,存入的元素必须实现Comparable或传入Comparator才能自动排序。 - 自然排序方法
compareTo中,this代表新元素,o代表已存在元素,灵活调换两者即可改变升/降序。
红黑树是面试和实际开发中的重点数据结构,理解其平衡原理和旋转过程对后续学习 TreeMap、HashMap(链表转红黑树)等都大有裨益。建议结合动图或亲手画出插入过程来加深印象,切勿死记硬背。
本文基于红黑树经典理论及TreeSet底层实现编写,配合代码实战,助你快速入门。
3166

被折叠的 条评论
为什么被折叠?



