Java 黑马程序员学习笔记(进阶篇14)
1. 红黑树
(1) 红黑规则
红黑树通过以下规则保证 “自平衡”(本质是控制树的整体形态,避免极端不平衡):
- 颜色二选一:每个节点的颜色只能是红色或黑色。
- 根黑原则:根节点必须是黑色。
- 叶黑原则:叶节点(用 NIL 表示,无子女 / 父节点的 “空节点”)是黑色。
- 红节点的子节点必黑:若一个节点是红色,它的子节点必须是黑色(禁止 “红 - 红” 连续)。
- 黑高一致:对任意节点,从该节点到其所有后代叶节点的路径上,包含的黑色节点数目相同(“黑高” 相同)。
(2) 默认颜色:新节点默认红色
红黑树添加新节点时,默认将节点颜色设为红色。这样设计的优势是:红色节点插入后更不容易直接违反红黑规则(尤其是 “不能有两个红色节点连续相连” 的规则),从而减少初始调整的概率,提升插入效率。
(3) 添加节点的分支处理规则
根据 “新节点是否为根”“父节点颜色”“叔叔节点颜色”“节点位置(父的左 / 右孩子)”,分支处理以满足红黑规则:
场景分类 | 具体条件 | 处理方式 |
---|---|---|
新节点是根节点 | —— | 直接将新节点颜色改为黑色(满足 “根节点必须是黑色” 的规则)。 |
新节点是非根,且父节点为黑色 | —— | 无需任何操作(不违反红黑规则,5 条核心规则仍成立)。 |
新节点是非根,且父节点为红色 | 需进一步判断 “叔叔节点颜色” 和 “当前节点相对父的位置”,因为违反了 “不能有两个红色节点连续相连” 的规则。 | 1. 叔叔节点为红色:- 父、叔叔设为黑色;- 祖父设为红色;- 若祖父是根,再将祖父改回黑色;- 若祖父非根,将祖父作为新的 “当前节点”,继续上层判断。2. 叔叔节点为黑色,且当前节点是父的右孩子:- 将 “父节点” 作为新的 “当前节点”,执行左旋,转化为后续可处理的形态。3. 叔叔节点为黑色,且当前节点是父的左孩子:- 父设为黑色,祖父设为红色;- 以祖父节点为支点执行右旋,调整结构使规则满足。 |
2. Set 系列集合
(1) 核心特点:
① 整体特性:Set
是 Collection
接口的子接口,具备 无序(存储顺序与添加顺序无关)、不重复(自动去重)、无索引 的特点;方法与 Collection
的 API 基本一致(如 add
、remove
、contains
等)。
② 实现类差异:
HashSet
:无序、不重复、无索引,是最常用的Set
实现类。LinkedHashSet
:有序(保持元素 “插入顺序”)、不重复、无索引,底层通过 “链表 + 哈希表” 维护顺序。TreeSet
:可排序(支持自然排序 / 自定义比较器排序)、不重复、无索引,底层基于红黑树实现排序。
3. HashSet 详解
(1) HashSet 的底层原理
HashSet
底层依赖哈希表(JDK8 后为 “数组 + 链表 + 红黑树” 的组合结构),核心机制与性能优化和红黑树密切相关:
- 数组:称为「哈希表」或「桶(Bucket)」,存储元素的引用(或链表头节点)。
- 链表:当多个元素的「哈希值」对应同一个数组下标时,用链表存储这些元素(解决「哈希冲突」)。
- 红黑树:当链表长度超过 8,且数组长度 ≥ 64 时,链表会自动转为红黑树(将查询效率从 O (n) 优化为 O (logn))。
(2) HashSet 的去重原理
HashSet 之所以能「去重」,依赖 hashCode () 方法和 equals () 方法的协同工作,流程如下:
① 当向 HashSet 中添加元素 e
时,先调用 e.hashCode()
计算「哈希值」,根据哈希值确定元素在数组中的「目标下标」。
② 检查目标下标位置:
- 若该位置为空:直接将元素存入(无重复)。
- 若该位置不为空:遍历该位置的链表 / 红黑树,逐个比较已有元素与
e
:- 先比较「哈希值」:若哈希值不同 → 不是重复元素,直接添加到链表 / 红黑树。
- 若哈希值相同 → 再调用
equals()
比较内容:若equals()
返回true
→ 是重复元素,拒绝添加;若返回false
→ 不是重复元素,添加到链表 / 红黑树。
③ 关键结论:存储自定义对象时,必须重写 hashCode () 和 equals (),否则无法实现去重(默认用 Object 类的方法,比较的是地址值)。
(3) 案例 2:存储自定义对象(需重写 hashCode () 和 equals ())
需求:存储「学生对象」,要求「学号相同则视为重复元素」。
import java.util.HashSet;// 学生类
class Student {private String id; // 学号(唯一标识)private String name;// 构造方法public Student(String id, String name) {this.id = id;this.name = name;}// 重写 toString():便于打印@Overridepublic String toString() {return "Student{id='" + id + "', name='" + name + "'}";}// 核心:重写 hashCode() 和 equals()(根据学号去重)@Overridepublic boolean equals(Object o) {if (this == o) return true; // 地址相同 → 同一对象if (o == null || getClass() != o.getClass()) return false; // 类型不同 → 不相等Student student = (Student) o;return id.equals(student.id); // 学号相同 → 相等}@Overridepublic int hashCode() {return id.hashCode(); // 哈希值由学号决定(保证学号相同的对象哈希值相同)}
}// 测试类
public class HashSetDemo2 {public static void main(String[] args) {HashSet<Student> studentSet = new HashSet<>();// 添加学生(学号相同的视为重复)studentSet.add(new Student("2024001", "张三"));studentSet.add(new Student("2024002", "李四"));studentSet.add(new Student("2024001", "张三")); // 学号相同,重复元素,添加失败// 遍历for (Student s : studentSet) {System.out.println(s); // 输出:// Student{id='2024001', name='张三'}// Student{id='2024002', name='李四'}}}
}
4. LinkedHashSet 详解
(1) 核心定义
LinkedHashSet 是 HashSet 的子类,底层基于「哈希表 + 双向链表」实现,核心特点是「保证元素的插入顺序」(解决 HashSet 的无序问题)。
(2) 核心特点
- 继承 HashSet 的所有特点:无重复、线程不安全、允许存 1 个 null。
- 额外特点:有序性:元素的遍历顺序 ≠ 存储顺序,而是 与插入顺序完全一致(底层双向链表维护顺序)。
(3) 底层原理
LinkedHashSet 底层依赖 LinkedHashMap
实现(构造方法创建 LinkedHashMap),而 LinkedHashMap 的底层是「哈希表(数组 + 链表 / 红黑树) + 双向链表」:
- 哈希表:作用与 HashSet 一致,负责快速存储和去重(依赖 hashCode () 和 equals ())。
- 双向链表:额外维护元素的「插入顺序」—— 每个元素除了存储哈希表的关联信息,还会记录「前驱元素」和「后继元素」的引用,遍历时分直接按双向链表顺序读取,保证插入顺序。
5. TreeSet
(1) 概述
TreeSet 是 Java 集合框架中 Set 接口 的一个实现类,它的特点是:
- 元素唯一(无重复)
- 自动排序(默认升序,或按指定规则排序)
- 底层基于 TreeMap(红黑树结构)
(2) TreeSet 第一种排序方式
① 自然排序(Comparable)
- 元素类必须实现
java.lang.Comparable
接口 - 并重写
compareTo()
方法,定义排序规则 - 这是 TreeSet 的默认排序方式
② 示例:
package demo3;public class Student implements Comparable<Student> {private String name;private int age;public Student(String name, int age) {this.name = name;this.age = age;}@Overridepublic int compareTo(Student o) {// 先按年龄升序排序int result = this.age - o.age;// 如果年龄相同,再按姓名升序排序if (result == 0) {result = this.name.compareTo(o.name);}return result;}@Overridepublic String toString() {return "Student{" +"name='" + name + '\'' +", age=" + age +'}';}
}
(3) TreeSet 第一种排序方式
① 比较器排序(Comparator)
- 创建 TreeSet 时,传入一个
Comparator
接口的实现类对象 - 可以不用修改元素类代码,灵活定义排序规则
② 示例分析:
public int compareTo(Student o) {int sum1 = this.getChinese() + this.getMath() + this.getEnglish(); //不太理解,// 为什么要用this,为什么要用getChinese(),而不是直接chinese,不是同一个类里面吗int sum2 = o.getChinese() + o.getMath() + o.getEnglish(); //不太理解
Q1:为什么用 getChinese()
而不是直接 chinese
?
A1:访问权限问题:
- 如果
chinese
是private
,那么只能在当前类的当前实例中直接访问。 - 对于另一个对象
o
(即使是同一个类),你不能直接访问o.chinese
。 - 所以必须用
o.getChinese()
这种公共方法去获取值。
Q2:为什么要用 this
?
A2:this
代表当前对象(调用 compareTo
方法的那个对象)。
在 compareTo
里,你需要比较两个对象:
this
→ 自己(调用者)o
→ 另一个对象(参数)