java算法母题30道-第2篇-链表操作
·
算法母题30道(第2篇):链表操作(6-10)
系列说明:本系列共6篇文章,涵盖30道经典算法母题,使用Java实现
适用场景:算法学习、面试准备、编程能力提升
📋 30道算法母题总览
第1篇:数组与字符串基础(1-5)
- 两数之和
- 三数之和
- 盛最多水的容器
- 接雨水
- 最长无重复字符子串
第2篇:链表操作(6-10)⭐ 本篇
- 反转链表
- 合并两个有序链表
- 链表中倒数第k个节点
- 判断链表是否有环
- 合并K个有序链表
第3篇:栈与队列(11-15)
- 有效的括号
- 用栈实现队列
- 最小栈
- 滑动窗口最大值
- 每日温度
第4篇:二叉树遍历(16-20)
- 二叉树的前序遍历
- 二叉树的中序遍历
- 二叉树的后序遍历
- 二叉树的层序遍历
- 二叉树的最大深度
第5篇:动态规划(21-25)
- 爬楼梯
- 最长递增子序列
- 最长公共子序列
- 背包问题(0-1背包)
- 编辑距离
第6篇:搜索与排序(26-30)
- 二分查找
- 快速排序
- 归并排序
- 岛屿数量(DFS/BFS)
- 全排列(回溯)
链表节点定义
// 所有链表题目的基础节点定义
class ListNode {
int val;
ListNode next;
ListNode() {}
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode next) {
this.val = val;
this.next = next;
}
}
第6题:反转链表
📝 题目描述
给你单链表的头节点 head,请你反转链表,并返回反转后的链表。
示例:
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]
输入:head = [1,2]
输出:[2,1]
输入:head = []
输出:[]
💡 解题思路
迭代法(双指针):
- 使用两个指针:
prev(前一个节点)和curr(当前节点) - 在遍历链表时,将当前节点的
next指向前一个节点 - 需要先保存下一个节点,避免链表断裂
- 继续向前移动,直到遍历完整个链表
递归法:
- 递归到链表末尾
- 从后往前反转指针
- 返回新的头节点
🎯 日常应用
- 浏览器历史:前进后退功能的实现
- 撤销操作:编辑器的 undo 功能
- 数据流处理:反向处理数据序列
- 缓存管理:LRU 缓存的实现基础
🔧 代码解决的问题
- 链表的逆序操作
- 指针的正确重定向
- 避免内存泄漏和链表断裂
💻 Java实现代码
public class ReverseLinkedList {
/**
* 反转链表 - 迭代法(推荐)
* @param head 链表头节点
* @return 反转后的链表头节点
*/
public ListNode reverseList(ListNode head) {
ListNode prev = null;
ListNode curr = head;
while (curr != null) {
// 保存下一个节点
ListNode nextTemp = curr.next;
// 反转当前节点的指针
curr.next = prev;
// 移动指针
prev = curr;
curr = nextTemp;
}
// prev 成为新的头节点
return prev;
}
/**
* 反转链表 - 递归法
*/
public ListNode reverseListRecursive(ListNode head) {
// 基本情况:空链表或只有一个节点
if (head == null || head.next == null) {
return head;
}
// 递归反转后面的节点
ListNode newHead = reverseListRecursive(head.next);
// 反转当前节点和下一个节点的连接
head.next.next = head;
head.next = null;
return newHead;
}
// 辅助方法:创建链表
public ListNode createList(int[] arr) {
if (arr == null || arr.length == 0) return null;
ListNode head = new ListNode(arr[0]);
ListNode curr = head;
for (int i = 1; i < arr.length; i++) {
curr.next = new ListNode(arr[i]);
curr = curr.next;
}
return head;
}
// 辅助方法:打印链表
public void printList(ListNode head) {
ListNode curr = head;
while (curr != null) {
System.out.print(curr.val + " -> ");
curr = curr.next;
}
System.out.println("null");
}
// 测试方法
public static void main(String[] args) {
ReverseLinkedList solution = new ReverseLinkedList();
int[] arr = {1, 2, 3, 4, 5};
ListNode head = solution.createList(arr);
System.out.print("原链表: ");
solution.printList(head);
ListNode reversed = solution.reverseList(head);
System.out.print("反转后: ");
solution.printList(reversed);
}
}
⏱️ 复杂度分析
-
时间复杂度:O(n)
- 需要遍历链表一次
- 每个节点处理一次
-
空间复杂度:
- 迭代法:O(1) - 只使用常数额外空间
- 递归法:O(n) - 递归调用栈的深度为 n
第7题:合并两个有序链表
📝 题目描述
将两个升序链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例:
输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]
输入:l1 = [], l2 = []
输出:[]
输入:l1 = [], l2 = [0]
输出:[0]
💡 解题思路
迭代法:
- 创建一个虚拟头节点
dummy - 使用指针
curr指向当前节点 - 比较两个链表的当前节点值
- 将较小的节点连接到结果链表
- 移动对应链表的指针
- 处理剩余部分(其中一个链表可能先遍历完)
递归法:
- 比较两个链表头节点的值
- 较小的节点作为当前节点
- 递归合并剩余部分
🎯 日常应用
- 归并排序:合并两个已排序的子序列
- 数据流合并:合并多个有序数据源
- 优先级队列:合并多个优先队列
- 日志处理:合并多个时间有序的日志文件
🔧 代码解决的问题
- 有序序列的合并
- 避免创建新节点(复用原节点)
- 处理边界情况(空链表)
💻 Java实现代码
public class MergeTwoSortedLists {
/**
* 合并两个有序链表 - 迭代法
* @param l1 第一个有序链表
* @param l2 第二个有序链表
* @return 合并后的有序链表
*/
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
// 虚拟头节点
ListNode dummy = new ListNode(-1);
ListNode curr = dummy;
// 比较两个链表的节点
while (l1 != null && l2 != null) {
if (l1.val <= l2.val) {
curr.next = l1;
l1 = l1.next;
} else {
curr.next = l2;
l2 = l2.next;
}
curr = curr.next;
}
// 连接剩余部分
curr.next = (l1 != null) ? l1 : l2;
return dummy.next;
}
/**
* 合并两个有序链表 - 递归法
*/
public ListNode mergeTwoListsRecursive(ListNode l1, ListNode l2) {
// 基本情况
if (l1 == null) return l2;
if (l2 == null) return l1;
// 递归合并
if (l1.val <= l2.val) {
l1.next = mergeTwoListsRecursive(l1.next, l2);
return l1;
} else {
l2.next = mergeTwoListsRecursive(l1, l2.next);
return l2;
}
}
// 辅助方法:创建链表
public ListNode createList(int[] arr) {
if (arr == null || arr.length == 0) return null;
ListNode head = new ListNode(arr[0]);
ListNode curr = head;
for (int i = 1; i < arr.length; i++) {
curr.next = new ListNode(arr[i]);
curr = curr.next;
}
return head;
}
// 辅助方法:打印链表
public void printList(ListNode head) {
ListNode curr = head;
while (curr != null) {
System.out.print(curr.val + " -> ");
curr = curr.next;
}
System.out.println("null");
}
// 测试方法
public static void main(String[] args) {
MergeTwoSortedLists solution = new MergeTwoSortedLists();
int[] arr1 = {1, 2, 4};
int[] arr2 = {1, 3, 4};
ListNode l1 = solution.createList(arr1);
ListNode l2 = solution.createList(arr2);
System.out.print("链表1: ");
solution.printList(l1);
System.out.print("链表2: ");
solution.printList(l2);
ListNode merged = solution.mergeTwoLists(l1, l2);
System.out.print("合并后: ");
solution.printList(merged);
}
}
⏱️ 复杂度分析
-
时间复杂度:O(m + n)
- m 和 n 分别是两个链表的长度
- 需要遍历两个链表的所有节点
-
空间复杂度:
- 迭代法:O(1) - 只使用常数额外空间
- 递归法:O(m + n) - 递归调用栈的深度
第8题:链表中倒数第k个节点
📝 题目描述
输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。
示例:
给定一个链表: 1->2->3->4->5, 和 k = 2.
返回链表 4->5.
💡 解题思路
快慢指针法(推荐):
- 使用两个指针
fast和slow,都指向头节点 - 先让
fast走 k 步 - 然后
fast和slow同时移动 - 当
fast到达末尾时,slow正好在倒数第 k 个节点
为什么要这样做?
- 链表只能从头到尾遍历,无法直接定位倒数位置
- 快慢指针保持 k 的距离,相当于一个"窗口"
- 当快指针到末尾,慢指针自然在倒数第 k 个位置
🎯 日常应用
- 分页查询:获取倒数第 N 条记录
- 缓存管理:LRU 缓存中定位特定位置
- 日志分析:获取最近的 N 条日志
- 排行榜:获取倒数排名
🔧 代码解决的问题
- 单向链表的反向定位
- 避免两次遍历链表
- 优化时间和空间复杂度
💻 Java实现代码
public class KthNodeFromEnd {
/**
* 链表中倒数第k个节点 - 快慢指针法
* @param head 链表头节点
* @param k 倒数第k个
* @return 倒数第k个节点
*/
public ListNode getKthFromEnd(ListNode head, int k) {
if (head == null || k <= 0) {
return null;
}
ListNode fast = head;
ListNode slow = head;
// fast 先走 k 步
for (int i = 0; i < k; i++) {
if (fast == null) {
// k 大于链表长度
return null;
}
fast = fast.next;
}
// fast 和 slow 同时移动
while (fast != null) {
fast = fast.next;
slow = slow.next;
}
// slow 此时在倒数第 k 个节点
return slow;
}
/**
* 链表中倒数第k个节点 - 两次遍历法(易理解)
*/
public ListNode getKthFromEndTwoPass(ListNode head, int k) {
if (head == null || k <= 0) {
return null;
}
// 第一次遍历:计算链表长度
int length = 0;
ListNode curr = head;
while (curr != null) {
length++;
curr = curr.next;
}
// 检查 k 是否合法
if (k > length) {
return null;
}
// 第二次遍历:找到倒数第 k 个节点
// 即正数第 (length - k + 1) 个节点
curr = head;
for (int i = 0; i < length - k; i++) {
curr = curr.next;
}
return curr;
}
// 辅助方法:创建链表
public ListNode createList(int[] arr) {
if (arr == null || arr.length == 0) return null;
ListNode head = new ListNode(arr[0]);
ListNode curr = head;
for (int i = 1; i < arr.length; i++) {
curr.next = new ListNode(arr[i]);
curr = curr.next;
}
return head;
}
// 辅助方法:打印链表
public void printList(ListNode head) {
ListNode curr = head;
while (curr != null) {
System.out.print(curr.val + " -> ");
curr = curr.next;
}
System.out.println("null");
}
// 测试方法
public static void main(String[] args) {
KthNodeFromEnd solution = new KthNodeFromEnd();
int[] arr = {1, 2, 3, 4, 5};
ListNode head = solution.createList(arr);
System.out.print("链表: ");
solution.printList(head);
int k = 2;
ListNode result = solution.getKthFromEnd(head, k);
System.out.println("倒数第 " + k + " 个节点: " + (result != null ? result.val : "null"));
System.out.print("从该节点开始的链表: ");
solution.printList(result);
}
}
⏱️ 复杂度分析
快慢指针法:
- 时间复杂度:O(n) - 只需要遍历一次链表
- 空间复杂度:O(1) - 只使用两个指针
两次遍历法:
- 时间复杂度:O(n) - 需要遍历两次链表
- 空间复杂度:O(1) - 只使用常数额外空间
第9题:判断链表是否有环
📝 题目描述
给定一个链表,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。
示例:
输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。
输入:head = [1,2], pos = 0
输出:true
解释:链表中有一个环,其尾部连接到第一个节点。
输入:head = [1], pos = -1
输出:false
解释:链表中没有环。
解题思路
快慢指针法(Floyd 判圈算法):
- 使用两个指针:
slow(慢指针)和fast(快指针) slow每次走一步,fast每次走两步- 如果链表有环,快慢指针最终会相遇
- 如果
fast到达末尾(null),说明无环
为什么快慢指针会相遇?
- 如果有环,快指针会在环内追赶慢指针
- 每次循环,快指针比慢指针多走一步
- 相对速度为 1,最终一定会追上
扩展:找到环的入口
- 快慢指针相遇后,将
slow重置到链表头 slow和fast都每次走一步- 它们再次相遇的位置就是环的入口
🎯 日常应用
- 死锁检测:操作系统中的资源循环等待
- 游戏开发:检测循环路径或无限循环
- 数据验证:检查数据结构的一致性
- 网络路由:检测路由环路
代码解决的问题
- 检测循环依赖
- 避免无限循环
- 优化空间复杂度(不用哈希表)
💻 Java实现代码
public class LinkedListCycle {
/**
* 判断链表是否有环 - 快慢指针法
* @param head 链表头节点
* @return 是否有环
*/
public boolean hasCycle(ListNode head) {
if (head == null || head.next == null) {
return false;
}
ListNode slow = head;
ListNode fast = head;
while (fast != null && fast.next != null) {
slow = slow.next; // 慢指针走一步
fast = fast.next.next; // 快指针走两步
// 快慢指针相遇,说明有环
if (slow == fast) {
return true;
}
}
// fast 到达末尾,说明无环
return false;
}
/**
* 进阶:找到环的入口节点
* @param head 链表头节点
* @return 环的入口节点,无环则返回 null
*/
public ListNode detectCycle(ListNode head) {
if (head == null || head.next == null) {
return null;
}
ListNode slow = head;
ListNode fast = head;
boolean hasCycle = false;
// 第一步:判断是否有环
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) {
hasCycle = true;
break;
}
}
// 无环
if (!hasCycle) {
return null;
}
// 第二步:找到环的入口
// 将 slow 重置到链表头
slow = head;
while (slow != fast) {
slow = slow.next;
fast = fast.next;
}
// 相遇点就是环的入口
return slow;
}
/**
* 判断链表是否有环 - 哈希表法(易理解但空间复杂度高)
*/
public boolean hasCycleHashSet(ListNode head) {
java.util.Set<ListNode> visited = new java.util.HashSet<>();
ListNode curr = head;
while (curr != null) {
if (visited.contains(curr)) {
return true;
}
visited.add(curr);
curr = curr.next;
}
return false;
}
// 测试方法
public static void main(String[] args) {
LinkedListCycle solution = new LinkedListCycle();
// 创建有环链表: 3->2->0->-4->2
ListNode node1 = new ListNode(3);
ListNode node2 = new ListNode(2);
ListNode node3 = new ListNode(0);
ListNode node4 = new ListNode(-4);
node1.next = node2;
node2.next = node3;
node3.next = node4;
node4.next = node2; // 形成环
System.out.println("有环链表检测结果: " + solution.hasCycle(node1));
System.out.println("环的入口节点值: " +
(solution.detectCycle(node1) != null ? solution.detectCycle(node1).val : "null"));
// 创建无环链表: 1->2->3
ListNode head2 = new ListNode(1);
head2.next = new ListNode(2);
head2.next.next = new ListNode(3);
System.out.println("无环链表检测结果: " + solution.hasCycle(head2));
}
}
️ 复杂度分析
快慢指针法:
-
时间复杂度:O(n)
- 无环时:快指针遍历 n/2 次
- 有环时:最多遍历环的长度次
-
空间复杂度:O(1)
- 只使用两个指针
哈希表法:
- 时间复杂度:O(n)
- 空间复杂度:O(n)
- 需要存储所有访问过的节点
第10题:合并K个有序链表
题目描述
给你一个链表数组,每个链表都已经按升序排列。请你将所有链表合并到一个升序链表中,返回合并后的链表。
示例:
输入:lists = [[1,4,5],[1,3,4],[2,6]]
输出:[1,1,2,3,4,4,5,6]
解释:链表数组如下:
[
1->4->5,
1->3->4,
2->6
]
将它们合并到一个有序链表中得到:
1->1->2->3->4->4->5->6
💡 解题思路
方法1:优先队列(最小堆)
- 将每个链表的头节点放入最小堆
- 每次取出堆顶元素(最小值)
- 将该节点的下一个节点(如果不为空)加入堆
- 重复直到堆为空
方法2:分治法
- 将 K 个链表两两合并
- 第一轮合并后剩下 K/2 个链表
- 第二轮合并后剩下 K/4 个链表
- 重复直到只剩一个链表
方法3:顺序合并
- 依次合并两个链表
- 将结果与下一个链表合并
- 重复直到所有链表合并完成
日常应用
- 多路归并:合并多个有序数据流
- 外部排序:大文件的排序和合并
- 日志聚合:合并多个服务器的日志
- 数据库查询:合并多个索引的结果
代码解决的问题
- 多路归并问题
- 优先级调度
- 分治思想的应用
💻 Java实现代码
import java.util.PriorityQueue;
public class MergeKSortedLists {
/**
* 合并K个有序链表 - 优先队列法(推荐)
* @param lists K个有序链表
* @return 合并后的有序链表
*/
public ListNode mergeKLists(ListNode[] lists) {
if (lists == null || lists.length == 0) {
return null;
}
// 最小堆,按照节点值排序
PriorityQueue<ListNode> pq = new PriorityQueue<>((a, b) -> a.val - b.val);
// 将每个链表的头节点加入堆
for (ListNode head : lists) {
if (head != null) {
pq.offer(head);
}
}
// 虚拟头节点
ListNode dummy = new ListNode(-1);
ListNode curr = dummy;
// 不断从堆中取出最小节点
while (!pq.isEmpty()) {
ListNode minNode = pq.poll();
curr.next = minNode;
curr = curr.next;
// 将下一个节点加入堆
if (minNode.next != null) {
pq.offer(minNode.next);
}
}
return dummy.next;
}
/**
* 合并K个有序链表 - 分治法
*/
public ListNode mergeKListsDivide(ListNode[] lists) {
if (lists == null || lists.length == 0) {
return null;
}
return merge(lists, 0, lists.length - 1);
}
private ListNode merge(ListNode[] lists, int left, int right) {
if (left == right) {
return lists[left];
}
int mid = left + (right - left) / 2;
ListNode l1 = merge(lists, left, mid);
ListNode l2 = merge(lists, mid + 1, right);
return mergeTwoLists(l1, l2);
}
private ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode(-1);
ListNode curr = dummy;
while (l1 != null && l2 != null) {
if (l1.val <= l2.val) {
curr.next = l1;
l1 = l1.next;
} else {
curr.next = l2;
l2 = l2.next;
}
curr = curr.next;
}
curr.next = (l1 != null) ? l1 : l2;
return dummy.next;
}
// 辅助方法:创建链表
public ListNode createList(int[] arr) {
if (arr == null || arr.length == 0) return null;
ListNode head = new ListNode(arr[0]);
ListNode curr = head;
for (int i = 1; i < arr.length; i++) {
curr.next = new ListNode(arr[i]);
curr = curr.next;
}
return head;
}
// 辅助方法:打印链表
public void printList(ListNode head) {
ListNode curr = head;
while (curr != null) {
System.out.print(curr.val + " -> ");
curr = curr.next;
}
System.out.println("null");
}
// 测试方法
public static void main(String[] args) {
MergeKSortedLists solution = new MergeKSortedLists();
int[][] arrays = {
{1, 4, 5},
{1, 3, 4},
{2, 6}
};
ListNode[] lists = new ListNode[arrays.length];
for (int i = 0; i < arrays.length; i++) {
lists[i] = solution.createList(arrays[i]);
}
System.out.println("输入链表数组:");
for (int i = 0; i < lists.length; i++) {
System.out.print("链表" + (i+1) + ": ");
solution.printList(lists[i]);
}
ListNode merged = solution.mergeKLists(lists);
System.out.print("合并后: ");
solution.printList(merged);
}
}
⏱️ 复杂度分析
优先队列法:
-
时间复杂度:O(N log K)
- N 是所有节点的总数
- K 是链表的个数
- 每个节点入堆和出堆一次,堆操作为 O(log K)
-
空间复杂度:O(K)
- 堆中最多存储 K 个节点
分治法:
-
时间复杂度:O(N log K)
- 递归树的高度为 log K
- 每层合并所有节点,总时间为 O(N)
-
空间复杂度:O(log K)
- 递归调用栈的深度为 log K
总结与对比
链表操作技巧总结
| 题目 | 核心技巧 | 关键数据结构 | 时间复杂度 | 空间复杂度 |
|---|---|---|---|---|
| 反转链表 | 双指针/递归 | 链表 | O(n) | O(1)/O(n) |
| 合并两个有序链表 | 迭代/递归 | 链表 | O(m+n) | O(1)/O(m+n) |
| 倒数第k个节点 | 快慢指针 | 链表 | O(n) | O(1) |
| 判断链表有环 | 快慢指针 | 链表 | O(n) | O(1) |
| 合并K个有序链表 | 优先队列/分治 | 堆/链表 | O(N log K) | O(K)/O(log K) |
通用解题模板
1. 快慢指针模板
ListNode slow = head, fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
// 检测相遇
if (slow == fast) {
// 有环或其他逻辑
}
}
2. 链表反转模板
ListNode prev = null, curr = head;
while (curr != null) {
ListNode nextTemp = curr.next;
curr.next = prev;
prev = curr;
curr = nextTemp;
}
return prev;
3. 合并链表模板
ListNode dummy = new ListNode(-1);
ListNode curr = dummy;
while (l1 != null && l2 != null) {
if (l1.val <= l2.val) {
curr.next = l1;
l1 = l1.next;
} else {
curr.next = l2;
l2 = l2.next;
}
curr = curr.next;
}
curr.next = (l1 != null) ? l1 : l2;
return dummy.next;
4. 虚拟头节点技巧
// 避免处理头节点的特殊情况
ListNode dummy = new ListNode(-1);
ListNode curr = dummy;
// 操作完成后返回 dummy.next
return dummy.next;
📚 下一篇预告
第3篇:栈与队列(11-15)
- 有效的括号
- 用栈实现队列
- 最小栈
- 滑动窗口最大值
- 每日温度
如果觉得这篇文章对您有帮助,欢迎:
- 👍 点赞支持
- 💬 留言讨论
- ⭐ 收藏备用
有任何算法问题,欢迎在评论区交流!
更多推荐


所有评论(0)