Przeglądaj źródła

feat: 新增20260730 LinkedList双链表课堂练习题——5道练习涵盖List/Deque/Queue操作、性能对比实验、手写单链表完善

WanJL 3 tygodni temu
rodzic
commit
44faf6ccdd
2 zmienionych plików z 667 dodań i 0 usunięć
  1. 1 0
      README.md
  2. 666 0
      课堂练习题/20260730-LinkedList双链表练习.md

+ 1 - 0
README.md

@@ -73,3 +73,4 @@
 | 2026-07-27 | [内部类练习](./课堂练习题/20260727-内部类练习.md) | 成员内部类、私有内部类设计原则、静态成员内部类、局部内部类、匿名内部类三种使用方式(多态接收/方法参数/匿名对象直接调用)、匿名内部类 + 接口回调 + 排序/筛选策略模式(学生信息管理系统) |
 | 2026-07-28 | [异常处理练习](./课堂练习题/20260728-异常处理练习.md) | 异常概念与 Throwable 体系、try-catch 多 catch 块捕获(ArrayIndexOutOfBoundsException / NullPointerException / ClassCastException / ArithmeticException)、多 catch 块顺序、throws 抛出、程序鲁棒性、自定义异常(继承 Exception)、异常处理在业务逻辑中的应用(用户注册验证 + 银行 ATM 取款) |
 | 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 性能对比实验、手写单链表增删改查 |

+ 666 - 0
课堂练习题/20260730-LinkedList双链表练习.md

@@ -0,0 +1,666 @@
+# 2026-07-30 LinkedList 双链表专项练习(课堂练习用)
+
+> **说明**:以下习题围绕今日授课核心内容——**LinkedList 双链表** 展开,涵盖 LinkedList 的特点与底层结构、LinkedList 作为 List 的基本操作、LinkedList 作为 Deque/Queue 的双端队列/队列操作、手写简单链表等知识点。
+>
+> 题目分为 **基础练习**(LinkedList 基本操作,3 题)和 **进阶挑战**(链表应用,2 题)。
+
+---
+
+# 第一部分:基础练习
+
+> 以下练习围绕 LinkedList 的 List / Deque / Queue 三种角色展开,由浅入深。
+
+---
+
+## 练习 1:LinkedList 创建与基本操作
+
+**难度**:⭐  
+**知识点**:LinkedList 创建、add()/get()/size()/set()/remove() 方法、与 ArrayList 对比
+
+**场景描述**:使用 LinkedList 存储一个班级的学生姓名,体验 LinkedList 作为 List 的基本操作。
+
+**题目要求**:
+
+在 `LinkedListDemo` 类的 `main` 方法中,完成以下操作:
+
+```java
+import java.util.LinkedList;
+import java.util.List;
+
+public class LinkedListDemo {
+    public static void main(String[] args) {
+        // 1. 创建一个 LinkedList,泛型为 String,存储学生姓名
+        //    提示:LinkedList<String> list = new LinkedList<>();
+
+        // 2. 添加 5 个学生姓名:"张三", "李四", "王五", "赵六", "孙七"
+
+        // 3. 输出当前链表大小(使用 size() 方法)
+
+        // 4. 获取并输出索引为 2 的元素(使用 get(2))
+
+        // 5. 将索引 1 处的"李四"修改为"李思"(使用 set() 方法)
+
+        // 6. 删除索引为 3 的元素"赵六"(使用 remove(index) 方法)
+
+        // 7. 使用 for 循环遍历链表,输出所有学生姓名
+        //    输出格式:"当前学生列表:张三, 李思, 王五, 孙七"
+
+        // 8. 在索引 1 处插入一个新学生"周八"
+        //    提示:list.add(1, "周八");
+
+        // 9. 再次遍历输出
+    }
+}
+```
+
+**预期输出**:
+```
+链表大小:5
+索引 2 的学生:王五
+已修改索引 1:李四 → 李思
+已删除索引 3:赵六
+当前学生列表:张三, 李思, 王五, 孙七
+插入后列表:张三, 周八, 李思, 王五, 孙七
+```
+
+**思考题**:
+1. 上面代码中,如果换成 `ArrayList<String>` 实现,代码需要修改哪些部分?
+2. `LinkedList` 的 `get(index)` 和 `ArrayList` 的 `get(index)` 在性能上有什么区别?为什么?
+
+---
+
+## 练习 2:LinkedList 的双端队列操作(Deque)
+
+**难度**:⭐⭐  
+**知识点**:`addFirst()` / `addLast()` / `getFirst()` / `getLast()` / `removeFirst()` / `removeLast()`
+
+**场景描述**:LinkedList 实现了 `Deque` 接口(双端队列),可以在链表的两端高效操作元素。模拟一个"双端排队"场景——有些人从队尾排队,有些人插队到队首。
+
+**题目要求**:
+
+在 `DequeDemo` 类的 `main` 方法中,完成以下操作:
+
+```java
+import java.util.LinkedList;
+
+public class DequeDemo {
+    public static void main(String[] args) {
+        // 1. 创建一个 LinkedList<String> 表示排队队伍
+
+        // 2. 依次往队尾添加:"张三", "李四", "王五"
+        //    提示:使用 addLast() 或 offerLast()
+
+        // 3. 查看队首和队尾的人(使用 getFirst() / getLast())
+        //    输出:"队首:张三,队尾:王五"
+
+        // 4. 一个新同学"赵六"插队到队首(使用 addFirst())
+
+        // 5. 输出当前队伍:"当前队伍:[赵六, 张三, 李四, 王五]"
+
+        // 6. 叫到队首的人"赵六"离开(使用 removeFirst())
+        //    输出:"赵六已离开"
+
+        // 7. 队尾的人"王五"等不及也离开了(使用 removeLast())
+        //    输出:"王五已离开"
+
+        // 8. 输出最终队伍:"最终队伍:[张三, 李四]"
+
+        // 9. 尝试获取空链表时的队首(创建一个新空链表,调用 getFirst())
+        //    观察程序发生了什么?(提示:NoSuchElementException)
+    }
+}
+```
+
+**预期输出**:
+```
+队首:张三,队尾:王五
+当前队伍:[赵六, 张三, 李四, 王五]
+赵六已离开
+王五已离开
+最终队伍:[张三, 李四]
+```
+
+**异常观察**:
+```
+Exception in thread "main" java.util.NoSuchElementException
+	at java.base/java.util.LinkedList.getFirst(LinkedList.java:250)
+```
+
+**提示**:
+- `addFirst(e)` 等价于 `offerFirst(e)` — 在双端队列头部插入
+- `addLast(e)` 等价于 `offerLast(e)` — 在双端队列尾部插入
+- `getFirst()` 获取头部元素,队列为空则抛 `NoSuchElementException`
+- `getLast()` 获取尾部元素,队列为空则抛 `NoSuchElementException`
+
+**思考题**:
+1. `getFirst()` 与 `peekFirst()` 有什么区别?(提示:空队列时行为不同)
+2. LinkedList 的双端队列操作的时间复杂度是多少?为什么?
+
+---
+
+## 练习 3:LinkedList 的队列操作(Queue)
+
+**难度**:⭐⭐  
+**知识点**:`offer()` / `poll()` / `peek()`、FIFO(先进先出)队列特性
+
+**场景描述**:LinkedList 实现了 `Queue` 接口,可以用作**队列(FIFO)**。模拟银行叫号系统——客户先到先服务。
+
+**题目要求**:
+
+在 `QueueDemo` 类的 `main` 方法中,完成以下操作:
+
+```java
+import java.util.LinkedList;
+import java.util.Queue;
+
+public class QueueDemo {
+    public static void main(String[] args) {
+        // 1. 创建一个 Queue<String>,使用 LinkedList 实现
+        //    提示:Queue<String> queue = new LinkedList<>();
+
+        // 2. 三个客户依次取号排队:
+        //    "客户A" → offer() 入队
+        //    "客户B" → offer() 入队
+        //    "客户C" → offer() 入队
+
+        // 3. 查看当前排队人数(size())
+
+        // 4. 查看队首是谁但不离开队列(peek())
+        //    输出:"当前排队人数:3,队首:客户A"
+
+        // 5. 叫号服务:队首"客户A"出队(poll())
+        //    输出:"请客户A前往1号窗口"
+
+        // 6. 新客户"客户D"取号排队
+
+        // 7. 再次叫号:队首"客户B"出队
+        //    输出:"请客户B前往1号窗口"
+
+        // 8. 输出剩余排队客户
+        //    输出:"剩余排队客户:[客户C, 客户D]"
+
+        // 9. 演示 poll() 与 peek() 的空队列安全行为:
+        //    创建一个空队列,调用 poll() 和 peek()
+        //    观察它们返回什么,而不是抛异常
+    }
+}
+```
+
+**预期输出**:
+```
+当前排队人数:3,队首:客户A
+请客户A前往1号窗口
+请客户B前往1号窗口
+剩余排队客户:[客户C, 客户D]
+空队列 poll() 返回:null
+空队列 peek() 返回:null
+```
+
+**Queue 方法对比**:
+
+| 操作 | 抛出异常 | 返回特殊值 |
+|------|----------|-----------|
+| 插入 | `add(e)` | `offer(e)` |
+| 移除 | `remove()` | `poll()` |
+| 查看 | `element()` | `peek()` |
+
+**思考题**:
+1. `poll()` 和 `remove()` 在空队列时行为有什么不同?
+2. 队列的 FIFO 特性和栈的 LIFO 特性在实际开发中分别适用于什么场景?
+
+---
+
+# 第二部分:进阶挑战
+
+> 以下练习综合运用 LinkedList、链表数据结构、手写链表等知识。
+
+---
+
+## 进阶 1:LinkedList 与 ArrayList 性能对比实验
+
+**难度**:⭐⭐⭐  
+**知识点**:LinkedList vs ArrayList 性能差异、时间复杂度、`add(index)` 头部插入、`get(index)` 随机访问
+
+**场景描述**:通过实验数据直观对比 LinkedList 和 ArrayList 在不同操作上的性能差异,加深对双链表和数组两种数据结构的理解。
+
+**题目要求**:
+
+编写 `PerformanceCompare` 类,完成以下对比实验:
+
+```java
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+
+public class PerformanceCompare {
+    public static void main(String[] args) {
+        int dataSize = 100000;  // 数据量
+
+        // === 实验 1:尾部添加性能对比 ===
+        // 分别用 ArrayList 和 LinkedList 添加 dataSize 个元素,
+        // 记录并比较耗时(使用 System.currentTimeMillis())
+
+        // === 实验 2:头部插入性能对比 ===
+        // 分别在 ArrayList 和 LinkedList 的索引 0 位置插入 10000 个元素
+        // 记录并比较耗时
+        // 提示:list.add(0, element)
+
+        // === 实验 3:随机访问性能对比 ===
+        // 分别在 ArrayList 和 LinkedList 中执行 100000 次 get(index) 随机访问
+        // 记录并比较耗时
+        // 提示:list.get(randomIndex)
+
+        // === 实验 4:遍历性能对比 ===
+        // 分别用 for 循环 + get(index) 遍历 ArrayList 和 LinkedList
+        // 记录并比较耗时
+        // 思考:LinkedList 用 for+get 遍历为什么慢?
+    }
+
+    // 辅助方法:计算并输出耗时
+    public static void printElapsed(String label, long start, long end) {
+        System.out.println(label + ": " + (end - start) + " ms");
+    }
+}
+```
+
+**预期输出示例**(实际数值因机器而异):
+```
+========== LinkedList vs ArrayList 性能对比 ==========
+数据量:100000
+--- 实验 1:尾部添加 ---
+ArrayList 尾部添加:15 ms
+LinkedList 尾部添加:18 ms
+
+--- 实验 2:头部插入(10000 次) ---
+ArrayList 头部插入:892 ms
+LinkedList 头部插入:5 ms
+
+--- 实验 3:随机访问(100000 次) ---
+ArrayList 随机访问:3 ms
+LinkedList 随机访问:9786 ms
+
+--- 实验 4:for+get 遍历 ---
+ArrayList 遍历:4 ms
+LinkedList 遍历:84321 ms
+```
+
+**分析题(笔试面试高频)**:
+1. 为什么 LinkedList 头部插入比 ArrayList 快得多?
+   - 答:LinkedList 头部插入只需修改节点的 `prev` / `next` 指针(O(1)),而 ArrayList 头部插入需要将后续所有元素向后移动(O(n))
+2. 为什么 LinkedList 的随机访问比 ArrayList 慢得多?
+   - 答:ArrayList 底层是数组,支持通过索引直接寻址(O(1));LinkedList 需要从头节点开始逐个遍历找到目标索引(O(n))
+3. 为什么 LinkedList 用 `for + get(i)` 遍历非常慢?
+   - 答:每次 `get(i)` 都从头部开始遍历到第 i 个节点,总时间复杂度为 O(n²)
+4. **如何正确遍历 LinkedList?**
+   - 提示:使用 **foreach** 或 **迭代器(Iterator)**,让链表沿着 `next` 指针逐个访问,时间复杂度 O(n)
+
+---
+
+## 进阶 2:手写单链表 —— 完善 MyLinked 类
+
+**难度**:⭐⭐⭐⭐  
+**知识点**:链表数据结构、泛型、节点操作、链表增删改查
+
+**场景描述**:课堂示例 `MyLinked` 类只实现了添加和遍历两个方法。请基于已有的单链表框架,补全缺失的方法,并编写测试类验证。
+
+**题目要求**:
+
+完善 `MyLinked.java` 中的以下方法:
+
+```java
+package course;
+
+public class MyLinked<N> {
+    private Integer size;
+    private Node<N> firstNode;  // 头节点(哨兵节点,不存有效数据)
+    private Node<N> lastNode;   // 尾节点
+
+    public MyLinked() {
+        firstNode = new Node<>(null, null);
+        lastNode = firstNode;
+    }
+
+    public int size() {
+        return this.size;
+    }
+
+    // ========== 已有方法 ==========
+
+    // 添加节点(尾插法)
+    public void addNode(N value) {
+        Node<N> n = new Node<>(value, null);
+        if (firstNode.next == null) {
+            this.firstNode.next = n;
+            this.lastNode = n;
+        }
+        lastNode.next = n;
+        lastNode = n;
+    }
+
+    // 遍历打印
+    public void print() {
+        Node<N> node = firstNode.next;
+        while (true) {
+            System.out.println(node.value);
+            node = node.next;
+            if (node.next == null) {
+                System.out.println(node.value);
+                break;
+            }
+        }
+    }
+
+    // ========== 待完善方法(请补全代码) ==========
+
+    /**
+     * 获取指定索引位置的节点值
+     * @param index 索引(从 0 开始)
+     * @return 节点值
+     * @throws IndexOutOfBoundsException 如果索引越界
+     */
+    public N get(int index) {
+        // TODO: 检查索引是否越界(index < 0 || index >= size)
+        //       从头节点开始遍历到第 index 个节点
+        //       返回该节点的 value
+        //       提示:Node<N> node = firstNode.next; 然后循环 index 次
+    }
+
+    /**
+     * 删除指定索引位置的节点
+     * @param index 索引(从 0 开始)
+     * @return 被删除的节点值
+     * @throws IndexOutOfBoundsException 如果索引越界
+     */
+    public N remove(int index) {
+        // TODO: 检查索引越界
+        //       找到要删除节点的前一个节点(prevNode)
+        //       让 prevNode.next = 要删除节点的下一个节点
+        //       如果删除的是尾节点,更新 lastNode
+        //       size--
+        //       返回被删除节点的 value
+    }
+
+    /**
+     * 判断链表是否包含某个值
+     * @param value 要查找的值
+     * @return true 如果存在
+     */
+    public boolean contains(N value) {
+        // TODO: 从头节点开始遍历
+        //       如果 value == null,用 node.value == null 判断
+        //       否则用 value.equals(node.value) 判断
+        //       找到返回 true,遍历完没找到返回 false
+    }
+
+    /**
+     * 将链表转换为数组(方便 toString 等操作)
+     * @return 包含所有节点值的数组
+     */
+    @SuppressWarnings("unchecked")
+    public N[] toArray() {
+        // TODO: 创建一个泛型数组 (N[]) new Object[size];
+        //       遍历链表,将每个节点的 value 放入数组
+        //       返回数组
+    }
+
+    /**
+     * 清空链表
+     */
+    public void clear() {
+        // TODO: 将 firstNode.next 设为 null
+        //       lastNode 指向 firstNode
+        //       size = 0
+        //       提示:链表中的节点对象会被 GC 自动回收
+    }
+}
+```
+
+**测试类要求**:
+
+编写 `MyLinkedTest` 类,测试所有完善后的方法:
+
+```java
+package course;
+
+public class MyLinkedTest {
+    public static void main(String[] args) {
+        MyLinked<String> linked = new MyLinked<>();
+
+        // 1. 添加测试
+        linked.addNode("A");
+        linked.addNode("B");
+        linked.addNode("C");
+        linked.addNode("D");
+        System.out.println("链表大小:" + linked.size());  // 4
+
+        // 2. 获取测试
+        System.out.println("索引 0:" + linked.get(0));  // A
+        System.out.println("索引 2:" + linked.get(2));  // C
+
+        // 3. 包含测试
+        System.out.println("包含 A?" + linked.contains("A"));  // true
+        System.out.println("包含 E?" + linked.contains("E"));  // false
+
+        // 4. 删除测试
+        String removed = linked.remove(1);
+        System.out.println("已删除索引 1:" + removed);  // B
+        System.out.println("删除后大小:" + linked.size());  // 3
+
+        // 5. 遍历测试
+        System.out.print("当前链表:");
+        linked.print();  // A C D
+
+        // 6. 清空测试
+        linked.clear();
+        System.out.println("清空后大小:" + linked.size());  // 0
+    }
+}
+```
+
+**预期输出**:
+```
+链表大小:4
+索引 0:A
+索引 2:C
+包含 A?true
+包含 E?false
+已删除索引 1:B
+删除后大小:3
+当前链表:A C D
+清空后大小:0
+```
+
+**提示**:
+1. `get(index)` 方法的边界检查:`index < 0 || index >= size` 时抛 `IndexOutOfBoundsException`
+2. `remove(index)` 需要找到**前一个节点**(prevNode),然后执行 `prevNode.next = nodeToRemove.next`
+3. `contains()` 需要考虑 `null` 值的处理(`value == null` 时用 `==` 判断)
+4. `toArray()` 由于泛型擦除,需要强制转换 `(N[]) new Object[size]`
+5. `clear()` 只需断开头节点引用,JVM 会自动回收其他节点
+
+**附加思考题**:
+1. 为什么 `remove(0)` 删除头节点时需要特殊处理?代码中如何体现?
+2. 如果链表中有重复值,`contains()` 返回第一个找到的,如何实现返回所有匹配的索引?
+3. 手写单链表的 `get(index)` 和 JDK LinkedList 的 `get(index)` 实现思路有什么不同?
+   - 提示:JDK 的双链表可以从头或尾双向搜索(`node(index)` 方法中 `index < (size >> 1)` 时从头找,否则从尾找)
+
+---
+
+# 参考答案要点
+
+## 练习 1 参考
+
+```java
+LinkedList<String> list = new LinkedList<>();
+
+list.add("张三");
+list.add("李四");
+list.add("王五");
+list.add("赵六");
+list.add("孙七");
+
+System.out.println("链表大小:" + list.size());
+System.out.println("索引 2 的学生:" + list.get(2));
+
+list.set(1, "李思");
+System.out.println("已修改索引 1:李四 → 李思");
+
+String removed = list.remove(3);
+System.out.println("已删除索引 3:" + removed);
+
+System.out.print("当前学生列表:");
+for (int i = 0; i < list.size(); i++) {
+    System.out.print(list.get(i));
+    if (i < list.size() - 1) System.out.print(", ");
+}
+System.out.println();
+
+list.add(1, "周八");
+System.out.println("插入后列表:" + list);
+```
+
+## 练习 2 参考
+
+```java
+LinkedList<String> deque = new LinkedList<>();
+
+deque.addLast("张三");
+deque.addLast("李四");
+deque.addLast("王五");
+
+System.out.println("队首:" + deque.getFirst()
+    + ",队尾:" + deque.getLast());
+
+deque.addFirst("赵六");
+System.out.println("当前队伍:" + deque);
+
+System.out.println(deque.removeFirst() + "已离开");
+System.out.println(deque.removeLast() + "已离开");
+System.out.println("最终队伍:" + deque);
+
+// 空队列异常演示
+LinkedList<String> empty = new LinkedList<>();
+try {
+    empty.getFirst();  // 抛出 NoSuchElementException
+} catch (Exception e) {
+    System.out.println("空队列调用 getFirst():" + e);
+}
+```
+
+## 练习 3 参考
+
+```java
+Queue<String> queue = new LinkedList<>();
+
+queue.offer("客户A");
+queue.offer("客户B");
+queue.offer("客户C");
+
+System.out.println("当前排队人数:" + queue.size()
+    + ",队首:" + queue.peek());
+
+System.out.println("请" + queue.poll() + "前往1号窗口");
+queue.offer("客户D");
+System.out.println("请" + queue.poll() + "前往1号窗口");
+System.out.println("剩余排队客户:" + queue);
+
+// 空队列安全行为
+Queue<String> emptyQueue = new LinkedList<>();
+System.out.println("空队列 poll() 返回:" + emptyQueue.poll());
+System.out.println("空队列 peek() 返回:" + emptyQueue.peek());
+```
+
+## 进阶 1 参考(核心代码)
+
+```java
+// 头部插入实验
+List<String> arrayList = new ArrayList<>();
+List<String> linkedList = new LinkedList<>();
+
+// 先预填充数据
+for (int i = 0; i < dataSize; i++) {
+    arrayList.add("元素" + i);
+    linkedList.add("元素" + i);
+}
+
+// ArrayList 头部插入
+long start = System.currentTimeMillis();
+for (int i = 0; i < 10000; i++) {
+    arrayList.add(0, "新元素");
+}
+long end = System.currentTimeMillis();
+printElapsed("ArrayList 头部插入", start, end);
+
+// LinkedList 头部插入
+start = System.currentTimeMillis();
+for (int i = 0; i < 10000; i++) {
+    linkedList.add(0, "新元素");
+}
+end = System.currentTimeMillis();
+printElapsed("LinkedList 头部插入", start, end);
+
+// 正确遍历 LinkedList 的方式:用 Iterator 或 foreach
+start = System.currentTimeMillis();
+for (String s : linkedList) {
+    // 只遍历,不做其他操作
+}
+end = System.currentTimeMillis();
+printElapsed("LinkedList foreach 遍历", start, end);
+```
+
+## 进阶 2 参考(get / remove / contains 方法)
+
+```java
+public N get(int index) {
+    if (index < 0 || index >= size) {
+        throw new IndexOutOfBoundsException("索引越界: " + index);
+    }
+    Node<N> node = firstNode.next;
+    for (int i = 0; i < index; i++) {
+        node = node.next;
+    }
+    return node.value;
+}
+
+public N remove(int index) {
+    if (index < 0 || index >= size) {
+        throw new IndexOutOfBoundsException("索引越界: " + index);
+    }
+    Node<N> prev = firstNode;
+    for (int i = 0; i < index; i++) {
+        prev = prev.next;
+    }
+    Node<N> target = prev.next;
+    prev.next = target.next;
+    if (target == lastNode) {
+        lastNode = prev;
+    }
+    size--;
+    return target.value;
+}
+
+public boolean contains(N value) {
+    Node<N> node = firstNode.next;
+    while (node != null) {
+        if (value == null ? node.value == null : value.equals(node.value)) {
+            return true;
+        }
+        node = node.next;
+    }
+    return false;
+}
+```
+
+---
+
+# 涵盖知识点总览
+
+| 知识点 | 对应练习 | 说明 |
+|--------|---------|------|
+| LinkedList 创建与 List 操作 | 练习 1 | `add()` / `get()` / `set()` / `remove(index)` / `size()` |
+| LinkedList 双端队列操作 | 练习 2 | `addFirst()` / `addLast()` / `getFirst()` / `getLast()` / `removeFirst()` / `removeLast()` |
+| LinkedList 队列操作 | 练习 3 | `offer()` / `poll()` / `peek()`、FIFO 特性 |
+| LinkedList vs ArrayList 性能对比 | 进阶 1 | 头部插入 O(1) vs O(n)、随机访问 O(n) vs O(1)、遍历方式 |
+| 手写单链表增删改查 | 进阶 2 | 泛型、Node 节点、`get(index)`、`remove(index)`、`contains()`、`clear()` |
+| 链表节点结构 | 进阶 2 | 单链表 Node(value + next)vs 双链表 Node(item + next + prev) |
+| 链表遍历的正确方式 | 进阶 1、2 | foreach / Iterator 方式 O(n) vs for+get 方式 O(n²) |
+| 空集合操作的异常处理 | 练习 2、3 | `getFirst()` 抛异常 vs `peek()` 返回 null |