1. LeetCode链表专题精讲:K个一组翻转、旋转与去重
链表操作是算法面试中的高频考点,也是许多开发者容易翻车的难点。今天我将结合自己刷题300+的经验,详细拆解LeetCode中三道经典的链表题目(25、61、82题),不仅给出标准解法,更会分享我在实际coding中总结的调试技巧和思维模式。这些题目都来自真实面试题库,掌握它们能显著提升你的算法思维水平。
2. 第25题:K个一组翻转链表
2.1 问题本质与核心思路
这道题要求每K个节点为一组进行翻转,最后不足K个的保持原样。看似简单的翻转操作,实际需要考虑多种边界条件:
- 链表长度恰好是K的整数倍
- 剩余节点不足K个
- K=1的特殊情况(相当于不翻转)
- 空链表处理
核心解决思路分为三个关键步骤:
- 遍历定位每组起始和结束节点
- 切断当前组与后续节点的连接
- 执行组内翻转并重新连接
2.2 代码实现与逐行解析
先看完整解法,后面我会解释每个关键点的设计考量:
java复制public ListNode reverseKGroup(ListNode head, int k) {
ListNode dummy = new ListNode(0);
dummy.next = head;
ListNode prev = dummy;
while (true) {
// 定位本组结束节点
ListNode end = prev;
for (int i = 0; i < k && end != null; i++) {
end = end.next;
}
if (end == null) break;
// 切断连接并记录下一组起点
ListNode nextGroup = end.next;
ListNode start = prev.next;
end.next = null;
// 翻转当前组并重新连接
prev.next = reverse(start);
start.next = nextGroup;
// 移动prev到本组新尾部
prev = start;
}
return dummy.next;
}
private ListNode reverse(ListNode head) {
ListNode prev = null;
ListNode curr = head;
while (curr != null) {
ListNode next = curr.next;
curr.next = prev;
prev = curr;
curr = next;
}
return prev;
}
关键点解析:
-
哑节点(dummy)的使用:这是处理链表问题的常用技巧,可以避免对头节点的特殊处理。当我们需要修改头节点时(如第一组翻转后新头会变化),dummy节点提供了稳定的前置引用。
-
end定位的for循环:这个循环有两个终止条件:
- 移动了k次(找到完整的一组)
- end变为null(剩余节点不足k个)
-
切断连接的时机:在翻转前必须切断当前组与下一组的连接,否则翻转时会污染后续节点。这是很多初学者容易忽略的细节。
-
翻转后的连接处理:翻转后原来的start节点会变成组尾,需要将其next指向下一组的起点(nextGroup)。同时prev需要更新到这个新尾部,为下一轮翻转做准备。
2.3 调试技巧与边界测试
在实际编写时,我推荐使用这些测试案例验证代码:
- 空链表
- k=1的情况
- 链表长度正好是k的倍数(如5个节点k=5)
- 链表长度比k多1(如5个节点k=2)
- 超大k值(k>链表长度)
调试时可以在关键位置打印链表状态,例如在翻转前后打印prev到end之间的节点值。我常用的调试语句是:
java复制System.out.println("Current group: " + printList(start, end));
3. 第61题:旋转链表
3.1 问题转化与数学建模
旋转链表的关键是将链表尾部k个节点移动到头部。直接思路可能想到多次移动单个节点,但这样时间复杂度会达到O(kn)。更聪明的做法是通过数学建模找到新头节点位置:
- 计算链表长度n
- 有效旋转次数k' = k % n(处理k>n的情况)
- 新头节点位置 = n - k'
- 将尾部k'个节点整体移动到头部
3.2 代码实现与优化技巧
java复制public ListNode rotateRight(ListNode head, int k) {
if (head == null || k == 0) return head;
// 计算长度并找到尾节点
int n = 1;
ListNode tail = head;
while (tail.next != null) {
tail = tail.next;
n++;
}
// 计算有效旋转次数
k = k % n;
if (k == 0) return head;
// 找到新尾节点
ListNode newTail = head;
for (int i = 0; i < n - k - 1; i++) {
newTail = newTail.next;
}
// 重组链表
ListNode newHead = newTail.next;
newTail.next = null;
tail.next = head;
return newHead;
}
性能优化点:
- 一次遍历计算长度:在寻找尾节点的同时计算长度,避免单独遍历
- 取模运算的妙用:k = k % n 同时处理了k=0、k<n和k>n三种情况
- 指针操作的顺序:必须先记录newHead再断开newTail.next,否则会丢失引用
3.3 常见错误与验证方法
这道题容易出现的错误包括:
- 未处理k=0或k=n的情况
- 未正确计算新尾节点位置(应该是n-k-1而非n-k)
- 忘记将原尾节点指向原头节点形成环
验证时可以绘制指针变化图:
code复制原始:1->2->3->4->5, k=2
新尾:3 (第n-k=3个节点)
新头:4
重组后:4->5->1->2->3
4. 第82题:删除排序链表中的重复元素II
4.1 双指针法与哨兵技巧
这道题要求删除所有重复出现的元素,与保留一个的版本不同。我们需要:
- 使用哑节点处理头节点可能被删除的情况
- 维护prev指针指向最后一个确定不重复的节点
- 使用curr指针扫描可能的重复序列
4.2 完整实现与逻辑分析
java复制public ListNode deleteDuplicates(ListNode head) {
ListNode dummy = new ListNode(0);
dummy.next = head;
ListNode prev = dummy;
ListNode curr = head;
while (curr != null) {
boolean duplicate = false;
// 探测当前节点是否有重复
while (curr.next != null && curr.val == curr.next.val) {
curr = curr.next;
duplicate = true;
}
if (duplicate) {
prev.next = curr.next; // 跳过所有重复节点
} else {
prev = prev.next; // 移动prev到不重复节点
}
curr = curr.next;
}
return dummy.next;
}
关键逻辑解析:
- 重复检测循环:内层while通过比较curr和curr.next来检测重复,直到找到不相同的节点
- 删除操作:当发现重复时,prev.next直接指向curr.next,跳过整个重复序列
- 指针移动规则:只有确认不重复时才移动prev,curr每次都前进
4.4 边界情况处理
需要特别注意这些特殊情况:
- 空链表
- 全重复链表(如1->1->1)
- 头节点重复(如1->1->2->3)
- 尾节点重复(如1->2->3->3)
- 多个分散重复(如1->1->2->3->3->4)
实际调试时,建议在纸上画出指针变化过程。我常用的验证序列是:
1->2->3->3->4->4->5
应该变为1->2->5
5. 链表问题通用解题技巧
经过这三道题的训练,我总结出链表问题的通用解法模式:
-
哑节点(dummy node)的使用:90%的链表问题都需要它来处理头节点可能变化的情况
-
多指针策略:通常需要2-3个指针协同工作(prev/curr/next等)
-
循环不变量的维护:明确每个指针在循环中应该保持的性质,比如prev始终指向已处理部分的末尾
-
可视化调试:在纸上画出指针变化过程,比单纯看代码更直观
-
边界测试:必须测试空链表、单节点链表、全重复链表等特殊情况
对于想进一步练习的同学,我推荐尝试这些变种题目:
- 反转链表II(指定区间反转)
- 重排链表(L0→Ln→L1→Ln-1→...)
- 链表排序(要求O(nlogn)时间复杂度)
记住,链表问题的核心在于指针操作和边界处理。多画图、多调试,这些技巧会成为你的第二本能。