Просмотр исходного кода

feat: 新增20260803课堂练习题——比较器排序专项练习(Comparable自然排序基础3题+进阶2题:TreeSet/TreeMap排序、匿名内部类Comparator、Collections.sort、Double.compare、compareTo与equals一致性)

WanJL 3 недель назад
Родитель
Сommit
741a3367dd
2 измененных файлов с 524 добавлено и 0 удалено
  1. 1 0
      README.md
  2. 523 0
      课堂练习题/20260803-比较器排序练习.md

+ 1 - 0
README.md

@@ -79,3 +79,4 @@
 | 2026-07-29 | [集合框架练习](./课堂练习题/20260729-集合框架练习.md) | ArrayList 基本操作(增删改查)、ArrayList 底层原理、包装类在集合中的应用、ArrayList 嵌套二维结构、冒泡排序、contains()/indexOf() 方法 |
 | 2026-07-30 | [LinkedList 双链表练习](./课堂练习题/20260730-LinkedList双链表练习.md) | LinkedList 作为 List 的基本操作、Deque 双端队列操作、Queue 队列 FIFO 操作、LinkedList vs ArrayList 性能对比实验、手写单链表增删改查 |
 | 2026-07-31 | [Iterator迭代器与Map集合练习](./课堂练习题/20260731-迭代器与Map集合练习.md) | Iterator 迭代器遍历(for vs 迭代器,hasNext/next)、HashMap 键值对基本操作(put/get/containsKey/containsValue/size/remove/clear)、Map 两种遍历方式(keySet+get / entrySet+getKey/getValue)、自定义类型作为 Key(重写 hashCode/equals)、HashMap 底层原理应用、单词词频统计 |
+| 2026-08-03 | [比较器排序练习](./课堂练习题/20260803-比较器排序练习.md) | Comparable 接口与 compareTo 重写、返回值规则(负数存左/0 重复不存/正数存右)、二次比较(多字段排序)、Comparator 接口与匿名内部类、TreeSet(Comparator) 构造方法、Collections.sort(list, comparator)、Double.compare、降序技巧(交换参数)、TreeMap 按键排序、compareTo 与 equals/hashCode 一致性、TreeSet 自动排序 + 去重、Iterator 迭代器遍历 |

+ 523 - 0
课堂练习题/20260803-比较器排序练习.md

@@ -0,0 +1,523 @@
+# 2026-08-03 比较器排序专项练习(课堂练习用)
+
+> **说明**:以下习题围绕今日授课核心内容——**比较器排序(Comparable 自然排序 / Comparator 比较器排序)** 展开,涵盖 Comparable 接口实现与 `compareTo(T o)` 重写、Comparator 接口与匿名内部类、TreeSet/TreeMap 有序集合自动排序、返回值规则(负数存左 / 0 重复不存 / 正数存右)、`Collections.sort(list, comparator)`、`Double.compare`、compareTo 与 equals 一致性等知识点。
+>
+> 题目分为 **基础练习**(Comparable 自然排序 / Comparator 比较器排序 / Collections.sort 排序,3 题)和 **进阶挑战**(比较器综合应用,2 题)。
+
+---
+
+# 第一部分:基础练习
+
+> 以下练习围绕今日授课核心内容展开,由浅入深。
+
+---
+
+## 练习 1:Comparable 自然排序(TreeSet 自动排序 + 去重)
+
+**难度**:⭐
+**知识点**:`Comparable<T>` 接口、`compareTo(T o)` 重写、`TreeSet` 有序 + 去重、返回值规则
+
+**场景描述**:TreeSet 是有序集合,取出的顺序是**排序过**的。不是所有类型都能直接存入 TreeSet——必须实现 `Comparable` 接口(自然排序),或在创建集合时于构造方法中传入 `Comparator`(比较器排序)。本节课 Student 类实现自然排序:**按年龄升序,年龄相同再按姓名排序**。
+
+**题目要求**:
+
+1. 定义 `Student` 类(`name`、`age`、`score` 三个属性),**实现 `Comparable<Student>` 接口**,重写 `compareTo(T o)` 方法:
+
+```java
+public class Student implements Comparable<Student> {
+    private String name;
+    private int age;
+    private double score;
+
+    public Student(String name, int age, double score) {
+        this.name = name;
+        this.age = age;
+        this.score = score;
+    }
+
+    // TODO: 补全 getter / setter / toString 方法
+
+    // TODO: 重写 compareTo —— 按年龄升序,年龄相同再按姓名
+    @Override
+    public int compareTo(Student o) {
+        // 提示:int result = this.age - o.getAge();
+        //       return result == 0 ? this.name.compareTo(o.getName()) : result;
+    }
+}
+```
+
+2. 编写测试类 `NaturalSortTest`:
+
+```java
+import java.util.TreeSet;
+
+public class NaturalSortTest {
+    public static void main(String[] args) {
+        // 1. 创建 TreeSet<Student>(不传 Comparator → 使用自然排序)
+        // 2. 添加 4 名学生:张三20 / 李四22 / 王五20 / 赵六21(注意:张三和王五年龄相同)
+        // 3. foreach 遍历输出,观察自动排序结果
+        // 4. 再添加一名"刘七20",观察:刘七能否存入?为什么?(提示:compareTo 返回值规则)
+    }
+}
+```
+
+**预期输出**:
+```
+张三 20 85
+王五 20 78     ← 年龄 20 与"张三"相同 → 触发姓名二次比较(王 < 张)
+赵六 21 88
+李四 22 90
+```
+
+**思考题**:
+1. 返回值为负数 / 0 / 正数分别代表什么?元素会存入左边、视为重复不存、还是存入右边?
+2. 为什么年龄相同的"张三"和"王五"都能存入?—— compareTo 里做了**二次比较**(姓名),返回值不为 0,所以不视为重复
+3. 如果 compareTo 只按年龄比较,添加"刘七20"会发生什么?(提示:20 与已有节点比较返回 0 → 视为重复不存)
+
+---
+
+## 练习 2:Comparator 比较器排序(TreeSet 构造传入匿名内部类)
+
+**难度**:⭐⭐
+**知识点**:`Comparator<T>` 接口、`compare(o1, o2)` 重写、匿名内部类、`TreeSet(Comparator)` 构造方法
+
+**场景描述**:类设计期间已经实现 Comparable(默认按年龄排序),但现在**临时**需要按别的规则排序(如成绩),又**不修改** Student 类。此时使用**比较器排序**——创建集合对象时,在构造方法中传入 `Comparator` 接口的实现类对象(匿名内部类)。
+
+**题目要求**:
+
+```java
+import java.util.Comparator;
+import java.util.TreeSet;
+
+public class ComparatorSortTest {
+    public static void main(String[] args) {
+        // 1. 创建 TreeSet<Student>,构造方法传入匿名内部类 Comparator<Student>
+        //    排序规则:按成绩(score)升序,成绩相同再按姓名排序
+        //    提示:TreeSet<Student> set = new TreeSet<>(new Comparator<Student>() {
+        //              @Override
+        //              public int compare(Student o1, Student o2) {
+        //                  int result = (int)(o1.getScore() - o2.getScore());
+        //                  return result == 0 ? o1.getName().compareTo(o2.getName()) : result;
+        //              }
+        //          });
+
+        // 2. 添加 4 名学生:张三85 / 李四90 / 王五78 / 赵六88
+        // 3. foreach 遍历输出,观察:这次是按【成绩】排序,而不是年龄
+        // 4. 与练习 1 的输出顺序对比,体会"自然排序 vs 比较器排序"
+    }
+}
+```
+
+**预期输出**:
+```
+王五 20 78
+张三 7 85
+赵六 222 88
+李四 2 90
+```
+
+**思考题**:
+1. 为什么 `new TreeSet<>(new Comparator<Student>(){...})` 就能覆盖默认的自然排序?
+2. Comparator 的 `compare(o1, o2)` 和 Comparable 的 `compareTo(o)` 写法有什么不同?(提示:一个是"两个对象在外部比较",一个是"this 与 o 比较")
+3. 匿名内部类为什么适合用来传 Comparator?(提示:临时规则、用完即弃、无需新建类文件)
+
+---
+
+## 练习 3:Collections.sort 对 List 排序(按年龄升序 / 按成绩降序)
+
+**难度**:⭐⭐
+**知识点**:`Collections.sort(list, comparator)`、匿名内部类、`Double.compare(a, b)`、降序技巧(交换参数)
+
+**场景描述**:`ArrayList` 是**无序**的,想让它按指定规则排序,用 `Collections.sort(list, comparator)`——传入匿名内部类 Comparator,对 List **原地排序**(改变原集合顺序)。
+
+**题目要求**:
+
+```java
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+
+public class ListSortTest {
+    public static void main(String[] args) {
+        // 1. 创建 ArrayList<Student>,添加 4 名学生:张三20-85 / 李四22-90 / 王五19-78 / 赵六21-88
+        // 2. 匿名内部类 Comparator:按年龄升序排序(s1.getAge() - s2.getAge())
+        //    Collections.sort(list, new Comparator<Student>() {...});
+        //    遍历输出
+
+        // 3. 匿名内部类 Comparator:按成绩(double)降序排序
+        //    注意:成绩是 double,不能直接相减强转 int
+        //    提示:Double.compare(s2.getScore(), s1.getScore())  ← 注意参数顺序 = 降序
+        //    遍历输出
+    }
+}
+```
+
+**预期输出**:
+```
+========== 按年龄升序 ==========
+王五 19 78
+张三 20 85
+赵六 21 88
+李四 22 90
+========== 按成绩降序 ==========
+李四 22 90
+赵六 21 88
+张三 20 85
+王五 19 78
+```
+
+**思考题**:
+1. double 类型的成绩为什么不能用 `(int)(s1.getScore() - s2.getScore())`?用 `Double.compare` 有什么好处?(提示:浮点精度 + 强转截断)
+2. `Collections.sort(list, comparator)` 和 `TreeSet` 自动排序的区别是什么?(提示:一个改变已有 List 的顺序,一个在存入时排序;TreeSet 还会去重)
+3. 想让 Comparator 排序方向反过来,最简单的做法是什么?(提示:交换 compare 中两个参数的位置)
+
+---
+
+# 第二部分:进阶挑战
+
+> 以下练习综合运用 Comparable / Comparator、TreeMap、compareTo 与 equals 一致性等知识。
+
+---
+
+## 进阶 1:成绩排行榜(TreeMap + Comparable 键排序)
+
+**难度**:⭐⭐⭐
+**知识点**:`TreeMap<K,V>` 按键排序、自定义类型作为 Key、`entrySet()` 遍历、compareTo/equals 一致性
+
+**场景描述**:TreeMap 是**按键排序**的有序 Map。把 `Student` 对象作为 **Key**、班级作为 Value 存入 TreeMap。因为 Student 实现了 Comparable,TreeMap 会自动按 compareTo 的规则对键排序——实现"成绩排行榜"。
+
+**题目要求**:
+
+1. 定义 `Student` 类(实现 Comparable,**按成绩降序**排序,成绩相同按姓名),修改 compareTo:
+
+```java
+public class Student implements Comparable<Student> {
+    private String name;
+    private int age;
+    private double score;
+
+    // 构造方法、getter/setter、toString(略)
+
+    @Override
+    public int compareTo(Student o) {
+        // TODO: 按成绩降序(注意:谁减谁决定升降序)
+        //       提示:int result = Double.compare(o.getScore(), this.score);  // 降序
+        //             return result == 0 ? this.name.compareTo(o.getName()) : result;
+    }
+}
+```
+
+2. 编写测试类 `ScoreRankingTest`:
+
+```java
+import java.util.Map;
+import java.util.TreeMap;
+
+public class ScoreRankingTest {
+    public static void main(String[] args) {
+        // 1. 创建 TreeMap<Student, String>:学生 → 所在班级
+        //    提示:Map<Student, String> map = new TreeMap<>();
+        //    添加:张三90-"一班"、李四85-"二班"、王五95-"三班"、赵六88-"一班"
+        // 2. entrySet() 遍历输出,观察是否按成绩从高到低排序
+        //    输出格式:"王五 95 - 三班"
+        // 3. 思考:如果两个学生成绩相同且姓名相同(属性完全一致),再 put 会发生什么?
+        //    —— compareTo 返回 0 → 视为同一个键 → 覆盖旧值(去重)
+    }
+}
+```
+
+**预期输出**:
+```
+王五 95 - 三班
+张三 90 - 一班
+赵六 88 - 一班
+李四 85 - 二班
+```
+
+**原理分析(必答)**:
+1. TreeMap 是怎么判断两个键是否重复的?—— 调用键的 `compareTo` 方法,返回 0 即视为重复
+2. 官方为什么强烈推荐"compareTo == 0 时 equals 返回 true、hashCode 一致"?—— 保证排序判定与相等判定保持一致,避免集合去重行为出现矛盾
+3. 如果 Student 没有实现 Comparable 也不传 Comparator,`new TreeMap<Student, String>()` 会怎样?(提示:存入第一个键时抛 ClassCastException)
+
+---
+
+## 进阶 2:员工信息管理系统(多排序规则 + compareTo/equals 一致性 + 综合应用)
+
+**难度**:⭐⭐⭐⭐
+**知识点**:Comparable 与 Comparator 结合、`hashCode()` / `equals()` 重写、compareTo 与 equals 一致性、多维度排序、迭代器遍历
+
+**场景描述**:某公司开发员工信息管理系统。员工类 `Employee`(工号、姓名、薪资、部门)。要求:
+- **默认排序**(自然排序):按薪资降序,薪资相同再按工号
+- 员工加入 TreeSet 后**自动排序**且**自动去重**(工号相同的员工视为同一人)
+- 临时需求:按姓名排序输出一份通讯录(使用 Comparator 比较器排序,不修改 Employee 类)
+
+**题目要求**:
+
+1. 定义 `Employee` 类:
+
+```java
+import java.util.Objects;
+
+public class Employee implements Comparable<Employee> {
+    private String id;      // 工号
+    private String name;    // 姓名
+    private double salary;  // 薪资
+    private String dept;    // 部门
+
+    public Employee(String id, String name, double salary, String dept) {
+        this.id = id;
+        this.name = name;
+        this.salary = salary;
+        this.dept = dept;
+    }
+
+    // TODO: 补全 getter / setter / toString 方法
+
+    // TODO: 重写 compareTo —— 按薪资降序,薪资相同再按工号
+    @Override
+    public int compareTo(Employee o) {
+        // 提示:Double.compare(o.getSalary(), this.salary)  // 降序
+        //       若返回 0,再比较工号:this.id.compareTo(o.getId())
+    }
+
+    // TODO: 重写 equals / hashCode —— 与 compareTo 保持一致(工号相同即视为同一人)
+    @Override
+    public boolean equals(Object o) { ... }
+
+    @Override
+    public int hashCode() { ... }
+}
+```
+
+2. 编写测试类 `EmployeeSystem`:
+
+```java
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.TreeSet;
+
+public class EmployeeSystem {
+    public static void main(String[] args) {
+        // 1. 创建 TreeSet<Employee>(自然排序)
+        //    添加 5 名员工(其中两名工号相同,测试去重):
+        //    E001-张三-12000-技术部 / E002-李四-15000-市场部
+        //    E003-王五-15000-技术部 / E004-赵六-9000-财务部
+        //    E005-刘七-12000-市场部
+
+        // 2. 迭代器遍历输出 → 观察:按薪资降序,薪资相同按工号;工号重复的自动去重
+        //    输出格式:"E001-张三-12000.0-技术部"
+
+        // 3. 临时需求:通讯录按姓名排序 —— 创建 TreeSet<Employee> 传入按姓名排序的 Comparator
+        //    匿名内部类:o1.getName().compareTo(o2.getName())
+        //    迭代器遍历输出
+
+        // 4. 思考:E001-张三(12000) 和 E005-刘七(12000) 薪资相同,为何都能存入?
+        //    —— compareTo 在薪资相同后继续比较工号,返回值不为 0
+    }
+}
+```
+
+**预期输出**:
+```
+========== 默认排序:按薪资降序(薪资相同按工号) ==========
+E002-李四-15000.0-市场部
+E003-王五-15000.0-技术部    ← 与李四薪资相同 → 按工号 E002<E003
+E001-张三-12000.0-技术部
+E005-刘七-12000.0-市场部    ← 与张三薪资相同 → 按工号 E001<E005
+E004-赵六-9000.0-财务部
+========== 通讯录:按姓名排序 ==========
+E002-李四-15000.0-市场部
+E005-刘七-12000.0-市场部
+E003-王五-15000.0-技术部
+E001-张三-12000.0-技术部
+E004-赵六-9000.0-财务部
+```
+
+**原理分析(必答)**:
+1. 为什么 `E002-李四` 和 `E003-王五` 薪资相同(都是 15000)却能都存入 TreeSet?—— compareTo 返回 0 才算重复,薪资相同后还比较了工号,返回值不为 0
+2. 官方推荐"compareTo == 0 → equals 返回 true、hashCode 一致",Employee 中应该按什么来重写 equals/hashCode?—— 按与 compareTo 一致的判定依据(本例为工号)
+3. 为什么两次遍历用迭代器而不是 for + get(i)?—— Set 没有索引,Iterator 是单列集合的通用遍历方式
+
+---
+
+# 参考答案要点
+
+## 练习 1 参考
+
+```java
+public class Student implements Comparable<Student> {
+    private String name;
+    private int age;
+    private double score;
+
+    // 构造方法、getter/setter、toString(略)
+
+    @Override
+    public int compareTo(Student o) {
+        // 按年龄升序
+        int result = this.age - o.getAge();
+        // 年龄相同 → 按姓名升序(二次比较)
+        return result == 0 ? this.name.compareTo(o.getName()) : result;
+    }
+}
+
+// NaturalSortTest
+TreeSet<Student> set = new TreeSet<>();
+set.add(new Student("张三", 20, 85));
+set.add(new Student("李四", 22, 90));
+set.add(new Student("王五", 20, 78));
+set.add(new Student("赵六", 21, 88));
+
+for (Student s : set) {
+    System.out.println(s);
+}
+// 输出顺序:王五20 → 张三20 → 赵六21 → 李四22(年龄升序,20 岁两人按姓名排序)
+```
+
+## 练习 2 参考
+
+```java
+TreeSet<Student> set = new TreeSet<>(new Comparator<Student>() {
+    @Override
+    public int compare(Student o1, Student o2) {
+        // 按成绩升序,成绩相同再按姓名
+        int result = (int) (o1.getScore() - o2.getScore());
+        return result == 0 ? o1.getName().compareTo(o2.getName()) : result;
+    }
+});
+set.add(new Student("张三", 7, 85));
+set.add(new Student("李四", 2, 90));
+set.add(new Student("王五", 20, 78));
+set.add(new Student("赵六", 222, 88));
+
+for (Student s : set) {
+    System.out.println(s);
+}
+// 输出顺序:王五78 → 张三85 → 赵六88 → 李四90(成绩升序,与年龄无关)
+```
+
+## 练习 3 参考
+
+```java
+ArrayList<Student> list = new ArrayList<>();
+list.add(new Student("张三", 20, 85));
+list.add(new Student("李四", 22, 90));
+list.add(new Student("王五", 19, 78));
+list.add(new Student("赵六", 21, 88));
+
+// 按年龄升序
+Collections.sort(list, new Comparator<Student>() {
+    @Override
+    public int compare(Student s1, Student s2) {
+        return s1.getAge() - s2.getAge();
+    }
+});
+for (Student s : list) {
+    System.out.println(s);
+}
+
+// 按成绩降序(Double.compare + 交换参数 = 降序)
+Collections.sort(list, new Comparator<Student>() {
+    @Override
+    public int compare(Student s1, Student s2) {
+        return Double.compare(s2.getScore(), s1.getScore());
+    }
+});
+for (Student s : list) {
+    System.out.println(s);
+}
+```
+
+## 进阶 1 参考
+
+```java
+// Student.compareTo:按成绩降序,成绩相同按姓名
+@Override
+public int compareTo(Student o) {
+    int result = Double.compare(o.getScore(), this.score);   // 降序
+    return result == 0 ? this.name.compareTo(o.getName()) : result;
+}
+
+// ScoreRankingTest
+TreeMap<Student, String> map = new TreeMap<>();
+map.put(new Student("张三", 18, 90), "一班");
+map.put(new Student("李四", 19, 85), "二班");
+map.put(new Student("王五", 20, 95), "三班");
+map.put(new Student("赵六", 18, 88), "一班");
+
+for (Map.Entry<Student, String> me : map.entrySet()) {
+    System.out.println(me.getKey().getName() + " " + me.getKey().getScore() + " - " + me.getValue());
+}
+// 输出顺序:王五95 → 张三90 → 赵六88 → 李四85(成绩降序)
+```
+
+## 进阶 2 参考(核心逻辑)
+
+```java
+// Employee.compareTo:按薪资降序,薪资相同按工号
+@Override
+public int compareTo(Employee o) {
+    int result = Double.compare(o.getSalary(), this.salary);  // 薪资降序
+    return result == 0 ? this.id.compareTo(o.getId()) : result;
+}
+
+// equals / hashCode:与 compareTo 一致,以工号判定
+@Override
+public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+    Employee employee = (Employee) o;
+    return Objects.equals(id, employee.id);
+}
+
+@Override
+public int hashCode() {
+    return Objects.hash(id);
+}
+
+// EmployeeSystem
+TreeSet<Employee> set = new TreeSet<>();
+set.add(new Employee("E001", "张三", 12000, "技术部"));
+set.add(new Employee("E002", "李四", 15000, "市场部"));
+set.add(new Employee("E003", "王五", 15000, "技术部"));
+set.add(new Employee("E004", "赵六", 9000, "财务部"));
+set.add(new Employee("E005", "刘七", 12000, "市场部"));
+
+Iterator<Employee> it = set.iterator();
+while (it.hasNext()) {
+    System.out.println(it.next());
+}
+// 输出:E002 → E003 → E001 → E005 → E004(薪资降序,同薪按工号)
+
+// 通讯录:按姓名排序的临时 Comparator
+TreeSet<Employee> byName = new TreeSet<>(new Comparator<Employee>() {
+    @Override
+    public int compare(Employee o1, Employee o2) {
+        return o1.getName().compareTo(o2.getName());
+    }
+});
+byName.addAll(set);
+for (Employee e : byName) {
+    System.out.println(e);
+}
+```
+
+---
+
+# 涵盖知识点总览
+
+| 知识点 | 对应练习 | 说明 |
+|--------|---------|------|
+| Comparable 接口与 compareTo 重写 | 练习 1、进阶 1、进阶 2 | 实体类内建默认排序规则(自然排序) |
+| 返回值规则(负数/0/正数) | 练习 1、练习 2 | 负数存左、0 重复不存、正数存右 |
+| 二次比较(多字段排序) | 练习 1、进阶 1、进阶 2 | 主排序相同 → 叠加次排序字段,避免误判重复 |
+| Comparator 接口与匿名内部类 | 练习 2、练习 3 | 临时自定义排序,不改实体类 |
+| TreeSet(Comparator) 构造方法 | 练习 2 | 构造方法传入比较器 → 覆盖自然排序 |
+| Collections.sort(list, comparator) | 练习 3 | 对 List 原地排序 |
+| Double.compare(a, b) | 练习 3、进阶 1、进阶 2 | double 比较的标准做法,避免精度问题 |
+| 降序技巧(交换参数) | 练习 3、进阶 2 | compare(s2, s1) / compareTo 中交换比较顺序 |
+| TreeMap 按键排序 | 进阶 1 | 自定义类型作为 Key,按 compareTo 排序并去重 |
+| compareTo 与 equals/hashCode 一致性 | 进阶 1、进阶 2 | compareTo == 0 → equals 返回 true、hashCode 一致 |
+| TreeSet 自动排序 + 去重 | 练习 1、进阶 2 | compareTo 返回 0 视为重复,不存入 |
+| Iterator 迭代器遍历 | 进阶 2 | Set 没有索引,用迭代器统一遍历 |
+