说明:以下习题围绕今日授课核心内容——LinkedList 双链表 展开,涵盖 LinkedList 的特点与底层结构、LinkedList 作为 List 的基本操作、LinkedList 作为 Deque/Queue/Stack 的双端队列/队列/栈操作、手写简单链表等知识点。
题目分为 基础练习(LinkedList 基本操作,4 题)和 进阶挑战(链表应用,2 题)。
以下练习围绕 LinkedList 的 List / Deque / Queue 三种角色展开,由浅入深。
难度:⭐
知识点:LinkedList 创建、add()/get()/size()/set()/remove() 方法、与 ArrayList 对比
场景描述:使用 LinkedList 存储一个班级的学生姓名,体验 LinkedList 作为 List 的基本操作。
题目要求:
在 LinkedListDemo 类的 main 方法中,完成以下操作:
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:赵六
当前学生列表:张三, 李思, 王五, 孙七
插入后列表:张三, 周八, 李思, 王五, 孙七
思考题:
ArrayList<String> 实现,代码需要修改哪些部分?LinkedList 的 get(index) 和 ArrayList 的 get(index) 在性能上有什么区别?为什么?难度:⭐⭐
知识点:addFirst() / addLast() / getFirst() / getLast() / removeFirst() / removeLast()
场景描述:LinkedList 实现了 Deque 接口(双端队列),可以在链表的两端高效操作元素。模拟一个"双端排队"场景——有些人从队尾排队,有些人插队到队首。
题目要求:
在 DequeDemo 类的 main 方法中,完成以下操作:
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() 获取头部元素,队列为空则抛 NoSuchElementExceptiongetLast() 获取尾部元素,队列为空则抛 NoSuchElementException思考题:
getFirst() 与 peekFirst() 有什么区别?(提示:空队列时行为不同)难度:⭐⭐
知识点:offer() / poll() / peek()、FIFO(先进先出)队列特性
场景描述:LinkedList 实现了 Queue 接口,可以用作队列(FIFO)。模拟银行叫号系统——客户先到先服务。
题目要求:
在 QueueDemo 类的 main 方法中,完成以下操作:
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() |
思考题:
poll() 和 remove() 在空队列时行为有什么不同?难度:⭐⭐
知识点:push() / pop() / peek()、LIFO(后进先出)栈特性、Deque 接口的栈语义
场景描述:LinkedList 实现了 Deque 接口,而 Deque 提供了完整的栈(Stack)操作。Java 官方建议:使用 Deque 代替 Stack 类,因为 Stack 继承自 Vector,性能较差且线程安全开销不必要。模拟一个"浏览器后退"功能——每次访问新页面就入栈,点击后退就从栈顶弹出。
题目要求:
在 StackDemo 类的 main 方法中,完成以下操作:
import java.util.Deque;
import java.util.LinkedList;
public class StackDemo {
public static void main(String[] args) {
// 1. 创建一个 Deque<String>,使用 LinkedList 实现,作为栈使用
// 提示:Deque<String> stack = new LinkedList<>();
// 2. 模拟小明浏览网页:
// "首页" → push() 入栈
// "新闻页" → push() 入栈
// "体育频道" → push() 入栈
// "NBA战报" → push() 入栈
// 3. 查看当前栈顶页面(peek()),不弹出
// 输出:"当前页面:NBA战报"
// 4. 点击后退:依次弹出 3 次(pop())
// 每次输出:"后退到:XXX"
// 顺序应该是:NBA战报 → 体育频道 → 新闻页
// 5. 输出栈中剩余页面
// 输出:"剩余浏览记录:[首页]"
// 6. 小明又访问了新页面:"科技频道" → push()
// "AI专题" → push()
// 7. 再次查看栈顶(peek())
// 8. 连续弹出所有页面(用循环 pop() 直到栈为空)
// 提示:while (!stack.isEmpty()) { ... }
// 9. 尝试在空栈时调用 pop(),观察发生了什么
// 提示:NoSuchElementException(与 Queue 的 poll() 不同)
}
}
预期输出:
========== 浏览器后退模拟 ==========
当前页面:NBA战报
后退到:NBA战报
后退到:体育频道
后退到:新闻页
剩余浏览记录:[首页]
当前页面:AI专题
连续后退:
后退到:AI专题
后退到:科技频道
后退到:首页
浏览记录已清空
关键方法说明:
| 方法 | 栈的等价操作 | 说明 |
|---|---|---|
push(E e) |
入栈(压栈) | 将元素压入栈顶,等价于 addFirst() |
pop() |
出栈(弹栈) | 移除并返回栈顶元素,空栈时抛 NoSuchElementException |
peek() |
查看栈顶 | 返回栈顶元素但不移除,空栈时抛 NoSuchElementException |
LinkedList 三种角色对比:
| 角色 | 接口 | 核心操作 | 数据结构 |
|---|---|---|---|
| 列表(List) | List |
add() / get() / set() / remove(index) |
线性表 |
| 队列(Queue) | Queue |
offer() / poll() / peek() |
FIFO 先进先出 |
| 栈(Stack) | Deque |
push() / pop() / peek() |
LIFO 后进先出 |
思考题:
java.util.Stack 类?Stack 继承自 Vector(线程安全)带来了什么问题?Deque 的 push() 和 addFirst() 本质上是同一个方法,为什么提供两个名字?Deque 实现栈时,pop() 和 poll() 在空栈时行为有何不同?(提示:异常 vs null)以下练习综合运用 LinkedList、链表数据结构、手写链表等知识。
难度:⭐⭐⭐
知识点:LinkedList vs ArrayList 性能差异、时间复杂度、add(index) 头部插入、get(index) 随机访问
场景描述:通过实验数据直观对比 LinkedList 和 ArrayList 在不同操作上的性能差异,加深对双链表和数组两种数据结构的理解。
题目要求:
编写 PerformanceCompare 类,完成以下对比实验:
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
分析题(笔试面试高频):
prev / next 指针(O(1)),而 ArrayList 头部插入需要将后续所有元素向后移动(O(n))for + get(i) 遍历非常慢?
get(i) 都从头部开始遍历到第 i 个节点,总时间复杂度为 O(n²)next 指针逐个访问,时间复杂度 O(n)难度:⭐⭐⭐⭐
知识点:链表数据结构、泛型、节点操作、链表增删改查
场景描述:课堂示例 MyLinked 类只实现了添加和遍历两个方法。请基于已有的单链表框架,补全缺失的方法,并编写测试类验证。
题目要求:
完善 MyLinked.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 类,测试所有完善后的方法:
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
提示:
get(index) 方法的边界检查:index < 0 || index >= size 时抛 IndexOutOfBoundsExceptionremove(index) 需要找到前一个节点(prevNode),然后执行 prevNode.next = nodeToRemove.nextcontains() 需要考虑 null 值的处理(value == null 时用 == 判断)toArray() 由于泛型擦除,需要强制转换 (N[]) new Object[size]clear() 只需断开头节点引用,JVM 会自动回收其他节点附加思考题:
remove(0) 删除头节点时需要特殊处理?代码中如何体现?contains() 返回第一个找到的,如何实现返回所有匹配的索引?get(index) 和 JDK LinkedList 的 get(index) 实现思路有什么不同?
node(index) 方法中 index < (size >> 1) 时从头找,否则从尾找)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);
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);
}
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());
Deque<String> stack = new LinkedList<>();
stack.push("首页");
stack.push("新闻页");
stack.push("体育频道");
stack.push("NBA战报");
System.out.println("当前页面:" + stack.peek());
System.out.println("后退到:" + stack.pop());
System.out.println("后退到:" + stack.pop());
System.out.println("后退到:" + stack.pop());
System.out.println("剩余浏览记录:" + stack);
stack.push("科技频道");
stack.push("AI专题");
System.out.println("当前页面:" + stack.peek());
System.out.println("连续后退:");
while (!stack.isEmpty()) {
System.out.println("后退到:" + stack.pop());
}
System.out.println("浏览记录已清空");
// 空栈异常演示
Deque<String> emptyStack = new LinkedList<>();
try {
emptyStack.pop(); // 抛出 NoSuchElementException
} catch (Exception e) {
System.out.println("空栈调用 pop():" + e.getClass().getSimpleName());
}
// 头部插入实验
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);
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 栈操作 | 练习 4 | push() / pop() / peek()、LIFO 特性、Deque 代替 Stack |
| 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 |