TreeSet 与红黑树排序 | JavaSE
TreeSet 与红黑树排序
一、学习目标
完成本章学习后,你应该能够:
- 能够解释
TreeSet的核心定位:去重 + 自动排序。 - 能够说明 TreeSet 与 TreeMap、红黑树之间的关系。
- 能够使用 TreeSet 对 Integer、Double、String 等具有自然顺序的元素进行排序。
- 能够解释自然排序(Natural Ordering)与外部比较器排序的基本区别。
- 能够解释 TreeSet 为什么不能随意保存“不具备比较规则”的自定义对象。
- 能够理解 TreeSet 判断集合意义上的重复依赖比较结果。
- 能够解释“比较结果为 0”为什么直接影响 TreeSet 去重。
- 能够理解 TreeSet 基本操作为什么能够达到
O(log n)级别。 - 能够区分 TreeSet 的红黑树与 HashSet 中“哈希桶树化”的红黑树。
二、核心知识
2.1 TreeSet 是什么
TreeSet<E> 是 Set 集合体系中的一个重要实现类。
它的核心特点可以概括为:
不重复
+
无 List 式索引
+
根据比较规则排序
例如:
Set<Integer> set = new TreeSet<>();
set.add(30);
set.add(10);
set.add(50);
set.add(20);
System.out.println(set);
TreeSet 不会按照:
30 → 10 → 50 → 20
保存遍历顺序。
而会根据整数自身的比较规则形成:
10 → 20 → 30 → 50
因此:
TreeSet 是一个排序 Set。
2.2 TreeSet 与前三种 Set 的区别
可以先整体比较:
| 集合 | 去重 | 遇见顺序 | 自动排序 |
| --------------- | ---- | -------------- | -------- |
| HashSet | 是 | 不保证 | 否 |
| LinkedHashSet | 是 | 确定 | 否 |
| TreeSet | 是 | 由比较规则决定 | 是 |
因此数据结构选择的核心问题不同:
只关心去重
→ HashSet
去重 + 保留遇见顺序
→ LinkedHashSet
去重 + 根据大小规则排序
→ TreeSet
2.3 TreeSet 底层依赖 TreeMap
与 HashSet 类似,TreeSet 并没有独立重新实现所有树结构逻辑。
TreeSet 底层基于:
TreeMap
可以建立:
TreeSet
│
│ 底层使用
▼
TreeMap
│
▼
红黑树
因此:
TreeSet 本质上借助 TreeMap 的 key 排序能力实现有序 Set。
类似 HashSet:
HashSet
↓
HashMap
TreeSet:
TreeSet
↓
TreeMap
这是集合框架中非常漂亮的一种复用设计。
2.4 TreeMap 底层是红黑树
TreeMap 是:
基于红黑树(Red-Black Tree)的有序 Map 实现。
因此 TreeSet 底层最终也利用红黑树维护元素顺序。
抽象成:
30
/ \
10 50
\ /
20 40
这不是普通二叉搜索树的简单版本,而是:
一种带有平衡约束的自平衡二叉搜索树。
它通过:
节点颜色
+
旋转
+
重新着色
等机制控制树的高度。
本章的目标不是从零手写红黑树,而是理解:
TreeSet 为什么能持续保持“可排序 + 较高查询效率”。
2.5 为什么不能使用普通二叉搜索树就结束
如果普通二叉搜索树依次插入:
10
20
30
40
50
可能退化为:
10
\
20
\
30
\
40
\
50
这已经非常像:
链表
树高可能达到:
O(n)
查找性能也随之下降。
而红黑树通过平衡规则让树高保持在受控范围,从而使:
add
remove
contains
拥有稳定的:
O(log n)
级别性能。
2.6 TreeSet 排序必须有比较规则
如果 TreeSet 想决定:
A 放左边还是右边?
就必须知道:
A 和 B 谁大谁小?
所以 TreeSet 中的元素必须:
能够相互比较。
比较规则来源主要有两种:
元素自己的自然排序
或
TreeSet 外部提供的 Comparator
完整的 Comparable 与 Comparator 体系将在下一章专门学习。
本章先建立 TreeSet 使用模型。
2.7 什么是自然排序
有一些 Java 类型本身已经定义了:
Comparable
因此具有自然顺序。
例如:
Integer
Double
String
它们可以直接放入:
new TreeSet<>()
然后 TreeSet 使用元素自身的:
compareTo()
进行比较。
例如:
TreeSet<Integer> numbers = new TreeSet<>();
numbers.add(30);
numbers.add(10);
numbers.add(20);
结果:
10
20
30
2.8 Double 的自然排序
例如:
TreeSet<Double> numbers = new TreeSet<>();
numbers.add(3.14);
numbers.add(1.5);
numbers.add(9.8);
numbers.add(2.6);
TreeSet 会使用 Double 的自然比较规则排列元素。
2.9 String 是如何排序的
原始教学资料常把 String 简化描述成:
按首字符编号排序。
这不够准确。
String 的自然顺序实际上采用:
字典序比较(Lexicographical Comparison)
例如:
TreeSet<String> words = new TreeSet<>();
words.add("ab");
words.add("aa");
words.add("b");
words.add("a");
排序不能只看:
第一个字符
如果第一个字符相同,还会继续比较后面的字符。
概念上:
"a"
"aa"
"ab"
"b"
会按照整个字符串的字典序进行比较。
因此:
TreeSet 排序 String 不是“只比较第一个字符”。
2.10 自定义对象为什么可能无法直接加入 TreeSet
例如:
class Student {
private String name;
private int age;
}
然后:
TreeSet<Student> students = new TreeSet<>();
students.add(new Student("张三", 20));
students.add(new Student("李四", 18));
问题出现了:
张三和李四到底谁应该排在前面?
可以:
- 按年龄;
- 按姓名;
- 按成绩;
- 按学号;
TreeSet 无法凭空猜测你的业务规则。
因此,如果没有能够使用的比较规则,运行时可能出现:
ClassCastException
所以:
自定义对象进入 TreeSet 前,必须解决“如何比较”这个问题。
2.11 TreeSet 的两种排序来源
当前先建立基本结构:
方式一:自然排序
类自己实现:
Comparable<T>
提供:
compareTo()
方式二:比较器排序
创建 TreeSet 时传入:
Comparator<T>
例如:
TreeSet<Student> students =
new TreeSet<>(comparator);
详细语法、设计原则、多字段排序将在下一章系统展开。
2.12 TreeSet 的“重复”由比较规则决定
这是 TreeSet 与 HashSet 最大的思维差异之一。
HashSet 重点依赖:
hashCode
+
equals
TreeSet 则通过:
compareTo
或
Comparator.compare
比较元素。
如果比较结果:
< 0
说明第一个元素排在第二个之前。
如果:
> 0
说明第一个元素排在第二个之后。
而如果:
== 0
TreeSet 会把两个元素从排序集合角度视为:
同一位置上的元素
因此只保留一个。
这意味着:
TreeSet 的排序规则同时影响排序,也影响去重。
这是本章最重要的知识点之一。
三、使用方法
3.1 Integer 排序
import java.util.TreeSet;
public class TreeSetDemo {
public static void main(String[] args) {
TreeSet<Integer> numbers = new TreeSet<>();
numbers.add(30);
numbers.add(10);
numbers.add(50);
numbers.add(20);
numbers.add(30);
System.out.println(numbers);
}
}
其中第二个:
30
不会形成重复元素。
3.2 String 排序
TreeSet<String> names = new TreeSet<>();
names.add("Tom");
names.add("Alice");
names.add("Bob");
names.add("Amy");
System.out.println(names);
String 自身具有自然比较规则,因此可以直接排序。
3.3 获取最小和最大元素
TreeSet 不只是普通 Set。
它实现了:
NavigableSet
因此具有大量“导航式”查询能力。
例如:
TreeSet<Integer> numbers = new TreeSet<>();
numbers.add(10);
numbers.add(30);
numbers.add(20);
numbers.add(40);
System.out.println(numbers.first());
System.out.println(numbers.last());
其中:
first()
返回当前比较顺序中的最小元素。
last()
返回最大元素。
3.4 lower()
numbers.lower(30);
表示:
查找严格小于 30 的最大元素。
例如集合:
10 20 30 40
结果为:
20
3.5 floor()
numbers.floor(30);
表示:
查找小于等于 30 的最大元素。
可能返回:
30
3.6 higher()
numbers.higher(30);
表示:
查找严格大于 30 的最小元素。
结果:
40
3.7 ceiling()
numbers.ceiling(30);
表示:
查找大于等于 30 的最小元素。
可能返回:
30
可以把四个 API 记成:
lower <
floor <=
higher >
ceiling >=
3.8 降序视图
TreeSet 可以得到:
descendingSet()
例如:
TreeSet<Integer> numbers = new TreeSet<>();
numbers.add(10);
numbers.add(20);
numbers.add(30);
System.out.println(numbers.descendingSet());
原顺序:
10 20 30
降序视图:
30 20 10
3.9 使用简单 Comparator
例如暂时需要整数降序:
TreeSet<Integer> numbers =
new TreeSet<>((a, b) -> Integer.compare(b, a));
这样 TreeSet 的排序方向就被改变。
但是:
Comparator 的完整规范、Lambda 写法、多字段排序和规则组合属于下一章 05-13。
本章只需要知道:
TreeSet 可以从构造器获得外部比较规则。
四、原理与进阶
4.1 TreeSet 添加元素的大致流程
考虑:
set.add(30);
set.add(10);
set.add(50);
set.add(20);
可以概念化为:
准备加入新元素
↓
与树中元素比较
↓
小于当前节点?
↓ ↓
是 否
向左 向右
↓
不断比较定位
↓
找到插入位置
↓
插入节点
↓
必要时进行红黑树平衡调整
所以 TreeSet 不需要:
先全部存进去
最后统一 sort()
而是在集合结构维护过程中持续保持比较顺序。
4.2 红黑树的核心目标
红黑树并不是为了:
让所有路径绝对一样高
它是一种近似平衡结构。
核心目标是:
防止普通二叉搜索树严重退化。
因此 TreeSet 的:
add()
contains()
remove()
可以稳定在:
O(log n)
级别。
4.3 TreeSet 的红黑树与 HashSet 的红黑树有什么区别
这是非常重要的区别。
HashSet
核心结构首先是:
哈希表
只有当:
某个哈希桶冲突严重
并达到特定条件时,这个桶才可能:
链表 → 红黑树
红黑树属于:
哈希冲突优化结构。
TreeSet
TreeSet 本身就是:
基于 TreeMap
而 TreeMap 的核心数据结构就是:
红黑树
TreeSet 使用红黑树的目的主要是:
维持比较顺序
+
高效查找
所以二者虽然都出现:
红黑树
但场景完全不同。
4.4 为什么比较结果为 0 会影响去重
假设两个 Student:
张三 18岁
李四 18岁
如果比较器只写:
Integer.compare(s1.getAge(), s2.getAge())
那么两个对象年龄相同:
compare == 0
TreeSet 就会认为:
在这套排序规则中二者等价。
第二个对象可能无法加入。
这意味着:
如果你的业务希望两个同龄学生都保留下来,比较规则不能只比较年龄。
通常还需要:
年龄
↓
姓名
↓
学号
等后续条件继续比较。
详细的“多级比较器设计”将在下一章完成。
4.5 TreeSet 与 equals 的关系
普通 Set 契约从:
equals()
角度定义元素是否相等。
但 TreeSet 实际排序、查找和元素等价判断使用的是:
compareTo()
或者:
Comparator.compare()
因此官方建议:
TreeSet 使用的排序规则应该尽量与 equals 保持一致。
所谓:
consistent with equals
大致要求:
compare(a, b) == 0
与:
a.equals(b)
表达相同的相等关系。
如果二者不一致,TreeSet 本身仍然可能运行,但可能违反一般 Set 契约所期待的相等性语义。
4.6 TreeSet 是否依赖 hashCode
TreeSet 的核心元素定位不是:
hashCode → 哈希桶
而是:
比较 → 左子树 / 右子树
所以:
HashSet
和:
TreeSet
是两套不同的数据结构模型。
可以记成:
HashSet
→ hashCode + equals
→ 哈希表
TreeSet
→ compareTo / Comparator
→ 红黑树
4.7 String 排序为什么不是“只比较首字符”
例如:
"aa"
"ab"
首字符都是:
'a'
如果真的只比较首字符,它们就无法区分。
String 的字典序会继续比较下一字符:
'a' == 'a'
↓
继续
'a' < 'b'
于是:
"aa" < "ab"
如果前面所有字符都相同:
"a"
"aa"
较短的字符串排在前面。
因此:
String 自然排序比较的是完整字符序列。
4.8 null 与 TreeSet
自然排序 TreeSet:
TreeSet<String> set = new TreeSet<>();
通常不能直接加入:
null
因为 TreeSet 需要比较元素,而:
null
没有自然排序能力。
调用相关操作时可能抛出:
NullPointerException
如果显式 Comparator 专门设计了 null 比较规则,则属于另外一种情况。
因此不能机械记成:
所有 Set 都能存 null
必须看具体实现和比较器规则。
4.9 JDK 21 的 SequencedSet 与 TreeSet
JDK 21 中 TreeSet 也属于:
SequencedSet
因为它有明确遇见顺序:
按照比较规则从小到大
所以可以:
getFirst()
getLast()
removeFirst()
removeLast()
reversed()
但是 TreeSet 不允许你随意:
addFirst()
addLast()
因为:
TreeSet 中元素位置必须由比较规则决定。
例如:
TreeSet<Integer> set = new TreeSet<>();
set.addFirst(100);
不能强迫:
100
排在:
1
前面,因为这会破坏 TreeSet 的排序约束。
因此 JDK 21 中:
TreeSet.addFirst()
TreeSet.addLast()
会抛出:
UnsupportedOperationException
这是一个非常典型的:
“接口有这个能力,但具体实现因为语义限制不支持该可选操作”
的例子。
五、实践应用
5.1 不重复且自动排序的数字
例如:
考试分数集合
只希望保存不同分数,并自动按大小排列:
TreeSet<Integer> scores = new TreeSet<>();
非常适合。
5.2 排名候选数据
如果对象:
不允许比较规则上的重复
+
需要持续保持排序
可以考虑 TreeSet。
但排行榜通常还涉及:
同分用户
排名规则
用户唯一性
排名变化
所以 TreeSet 是否适合,需要仔细设计 Comparator,而不能只说:
排行榜 = TreeSet
5.3 区间查询
TreeSet 实现:
NavigableSet
因此非常适合:
找到小于某值的最大元素
找到大于某值的最小元素
取某个范围的数据
例如:
价格区间
成绩区间
时间点集合
版本号集合
这些都属于有序集合擅长的问题。
5.4 TreeSet 与 HashSet 怎么选
如果业务是:
只需要唯一性 + 快速成员判断
优先考虑:
HashSet
如果业务是:
唯一性
+
必须始终保持比较顺序
考虑:
TreeSet
原因是 TreeSet 的基本操作:
O(log n)
而哈希分布良好的 HashSet 基本操作平均:
O(1)
因此:
不需要排序时,不要因为 TreeSet “更高级”就默认使用 TreeSet。
六、常见问题
6.1 TreeSet 是否按照添加顺序保存?
不是。
TreeSet 的顺序由:
自然排序
或
Comparator
决定。
与用户添加顺序无关。
6.2 TreeSet 默认都是升序吗?
如果使用:
new TreeSet<>()
则采用元素的自然排序。
很多常见数字类型的自然顺序表现为:
从小到大
但“默认升序”只是结果表现,真正的机制是:
使用元素的自然顺序。
6.3 String 是否只按照首字符排序?
不是。
String 使用完整字符串的:
字典序
比较。
6.4 为什么 Student 放入 TreeSet 可能报错?
如果:
Student 没有自然比较规则
同时 TreeSet 又没有获得:
Comparator<Student>
就无法确定 Student 之间的大小关系。
因此可能出现:
ClassCastException
6.5 TreeSet 如何判断重复?
它根据:
compareTo()
或者:
Comparator.compare()
的比较结果判断排序意义上的等价。
如果比较结果:
0
TreeSet 会将两个元素视为集合中的同一排序位置。
6.6 TreeSet 去重是否主要依赖 hashCode?
不是。
这是 HashSet 的思路。
TreeSet 核心依赖:
比较规则
+
红黑树
6.7 TreeSet 为什么基本操作不是 O(1)?
因为它不是哈希定位。
查找时需要沿:
红黑树
进行多层比较。
树高度是:
O(log n)
所以基本:
add
remove
contains
也是:
O(log n)
6.8 TreeSet 可以存 null 吗?
不能笼统回答。
自然排序 TreeSet 通常不能处理 null,因为 null 无法参加自然比较。
如果自定义 Comparator 明确支持 null,则需要根据那个 Comparator 的具体规则判断。
6.9 TreeSet 为什么不能 addFirst?
因为:
元素位置
必须由:
比较规则
决定。
程序不能一边要求:
从小到大
一边又强制:
把最大的元素塞到最前面
两种语义冲突。
七、练习与验收
7.1 知识问答
- TreeSet 的三个核心特征是什么?
- TreeSet 底层基于哪个 Map?
- TreeMap 底层是什么数据结构?
- 为什么使用红黑树而不是任意普通二叉搜索树?
- TreeSet 的基本 add/remove/contains 时间复杂度是什么?
- 什么是自然排序?
- Integer 为什么可以直接放进 TreeSet?
- String 为什么可以直接放入 TreeSet?
- String 的排序是不是只比较第一个字符?
- 自定义对象为什么可能无法直接放入 TreeSet?
- TreeSet 的比较规则来自哪两种方式?
- 比较结果小于 0、等于 0、大于 0 分别意味着什么?
- 为什么比较结果为 0 会影响 TreeSet 去重?
- TreeSet 与 HashSet 判断重复的主要机制有何区别?
- TreeSet 的红黑树和 HashSet 桶树化有什么区别?
- 为什么 TreeSet 的排序规则最好与 equals 一致?
- JDK 21 下为什么 TreeSet 的
addFirst()不被支持?
7.2 代码阅读
不运行:
TreeSet<Integer> set = new TreeSet<>();
set.add(30);
set.add(10);
set.add(20);
set.add(30);
set.add(5);
System.out.println(set);
System.out.println(set.first());
System.out.println(set.last());
回答:
- 最终集合有几个元素?
- 为什么两个 30 只保留一个?
- 遍历顺序是什么?
first()是什么?last()是什么?- 这些结论与添加顺序有什么关系?
7.3 手写代码
任务一:数字排序
使用 TreeSet 保存:
50 20 10 30 50 40 20
要求:
- 自动去重;
- 自动排序;
- 输出最小值;
- 输出最大值;
- 输出降序视图。
任务二:String 字典序
加入:
ab
aa
b
a
abc
要求:
- 先禁止运行,预测顺序;
- 解释为什么不能只看第一个字符;
- 再运行验证。
任务三:导航查询
建立:
TreeSet<Integer>
包含:
10 20 30 40 50
分别执行:
lower(30)
floor(30)
higher(30)
ceiling(30)
关闭资料写出结果并解释。
7.4 Debug
下面代码:
class Student {
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
}
TreeSet<Student> students = new TreeSet<>();
students.add(new Student("张三", 20));
students.add(new Student("李四", 18));
回答:
- 代码为什么可能运行失败?
- TreeSet 缺少什么信息?
- 有哪两种基本解决方向?
- 为什么 TreeSet 无法自己猜测“按年龄还是按姓名”?
- 如果只按年龄比较,同龄学生可能出现什么问题?
7.5 综合训练
设计一个课程成绩集合。
Student:
name
age
score
业务要求:
成绩高的排前面
成绩相同,年龄小的排前面
年龄仍然相同,再按姓名决定顺序
要求:
- 使用 TreeSet;
- 给 TreeSet 提供比较规则;
- 加入至少 6 个学生;
- 至少设计两个成绩相同的学生;
- 验证所有学生是否都能保留;
- 故意删除最后一个姓名比较条件;
- 观察某些学生为什么可能“消失”。
本题重点观察:
排序规则同时影响 TreeSet 的元素唯一性。
完整的多条件 Comparator 写法将在下一章系统掌握。
7.6 本章验收
关闭资料和 AI 自动补全:
- [ ] 能准确说出 TreeSet 的核心特点。
- [ ] 能画出 TreeSet → TreeMap → 红黑树的关系。
- [ ] 能解释红黑树为什么需要平衡。
- [ ] 能说出 TreeSet 基本操作的
O(log n)复杂度。 - [ ] 能使用 Integer、Double、String 完成自然排序。
- [ ] 能解释 String 的字典序。
- [ ] 能解释为什么 Student 默认可能无法排序。
- [ ] 能说出 Comparable 与 Comparator 两种排序来源。
- [ ] 能解释比较结果为 0 对 TreeSet 去重的影响。
- [ ] 能区分 HashSet 的 hashCode/equals 与 TreeSet 的比较规则。
- [ ] 能区分 TreeSet 红黑树与 HashSet 哈希桶树化。
- [ ] 能使用
first、last、lower、floor、higher、ceiling。 - [ ] 能解释 JDK 21 中 TreeSet 为什么不能显式
addFirst/addLast。