LC 24 两两交换链表中的节点:我从头到尾实现了一遍,这是所有踩坑记录
代码随想录资源
- 题目链接:两两交换链表中的节点 - 力扣(LeetCode)
- 文章链接:两两交换链表中的节点 | 代码随想录
- 视频链接:两两交换链表中的节点 | Bilibili
题目在说什么
给你一个链表,两两交换其中相邻的节点,返回交换后的链表。注意是交换节点本身(改指针),不能只改节点里的值。
写这道题让我真正理解了指针操作的执行顺序——什么时候必须先保存再改、什么时候可以换序。
我做的第一个决策
这道题的解法分裂出三个分支:
- 带 dummy 的迭代:代码随想录的写法,用虚拟头结点消除头结点的特殊性
- 不用 dummy 的迭代:自己尝试能不能省掉 dummy,结果发现要多维护一个
newHead和一个if (temp) - 递归:把当前一对交换和剩余子问题分开,代码最短
我决定三种都写进去,因为它们的递进关系本身就是最好的学习路径:先学会标准写法(带 dummy)→ 再理解 dummy 到底省了什么(无 dummy)→ 最后切换到另一种思维模型(递归)。
实现过程
方案 1:带 dummy 的迭代版
核心思路:给头前面补一个 dummy 节点,这样每一对节点的交换代码一模一样,不需要单独处理第一对。
每轮循环处理 prev -> a -> b -> next 这一段,改成 prev -> b -> a -> next,需要三条指针操作:
a.next = next—— a 先接上后半段b.next = a—— b 指向 a,完成局部反转prev.next = b—— 前驱指向新的头节点 b
然后 prev 移到 a(交换后的尾节点),进入下一轮。
var swapPairs = function(head) {
let dummy = new ListNode(0, head);
let cur = dummy;
while (cur.next && cur.next.next) {
let node1 = cur.next;
let node2 = cur.next.next;
cur.next = node2;
node1.next = node2.next;
node2.next = node1;
cur = node1;
}
return dummy.next;
};
我一开始写的是:
// 错误版本
var swapPairs = function(head) {
let dummy = new ListNode(0, head), cur = dummy;
while (cur.next && cur.next.next) {
let temp = cur.next, x = cur.next.next;
temp.next = x.next;
x.next = temp;
cur.next = x.next; // ← bug
}
return dummy.next;
};
两个问题:
问题 1:cur.next = x.next 应是 cur.next = x
我当时想的是「让前驱指向被保存下来的后半段」,但 x.next 在执行完 x.next = temp 后已经被改成 temp 了,所以 cur.next = x.next 等于让 cur.next 指回了原来的第一个节点,白换了。
正确做法是直接让 cur.next 指向 x(交换后的新头)。
问题 2:缺少 cur = node1(或 cur = temp)推进
没有这行,cur 始终指向 dummy,每次循环都在处理前两个节点,链表会在 1 -> 2 之间反复交换形成死循环。
修复后的正确代码见最终完整代码。
方案 2:不用 dummy 的迭代版
理解了 dummy 省了什么之后,尝试不用 dummy 写一遍来验证理解。
var swapPairs = function(head) {
if (!head || !head.next) return head;
let newHead = head.next;
let cur = head;
let temp = null;
while (cur && cur.next) {
let node1 = cur, node2 = cur.next;
node1.next = node2.next;
node2.next = node1;
if (temp) temp.next = node2;
cur = node1.next;
temp = node1;
}
return newHead;
};
和带 dummy 版的区别:
- 需要单独用一个
newHead记住新的头结点(原来的第二个节点) - 需要一个
if (temp)判断当前是不是第一对——第一对的前驱temp为 null,不需要接线(就是将新的交换过的节点与之前的交换完的节点进行连接) - 除此之外,指针交换的逻辑完全一样
这个对比让我看清了:dummy 解决的问题就是省掉一个 if 判断和一个 newHead 变量。不是算法上的差别,是代码简洁性的差别,当然,也更加便于理解。
方案 3:递归
递归不需要 dummy,因为 base case if (!head || !head.next) return head 直接处理了空和单节点的边界。
var swapPairs = function(head) {
if (!head || !head.next) return head;
let first = head;
let second = head.next;
first.next = swapPairs(second.next);
second.next = first;
return second;
};
思路:每次只处理一对,把剩下的部分扔给递归,返回的结果接在 first 后面,然后 second 成为新的头。
递归的好处是代码短、思路声明式。代价是空间复杂度 O(n)(调用栈深度等于节点数的一半)。
方案对比
| 方案 | 空间 | 代码量 | 关键技巧 |
|---|---|---|---|
| 带 dummy 迭代 | O(1) | 10 行 | dummy 统一头结点处理 |
| 不用 dummy 迭代 | O(1) | 12 行 | 单独记 newHead + if(temp) |
| 递归 | O(n) | 6 行 | base case 处理边界 |
踩坑合集
| # | 问题 | 根因 | 怎么发现的 | 修复 |
|---|---|---|---|---|
| 1 | cur.next = x.next 写成 x.next | 以为带上 .next 才能接上后半段,没意识到 x.next 已经被改了 | 人工 trace 一遍发现 cur.next 指回了第一个节点 | cur.next = x |
| 2 | 漏掉 cur = node1 推进 | 以为循环会自动推进 | 跑一遍发现死循环 | 每轮末尾 cur = node1 |
| 3 | node2.next = node1 必须在 node1.next = node2.next 之后 | 否则 node2.next 的原值会丢失 | 自己意识到顺序约束 | 保持正确顺序 |
这三个坑其实指向同一个教训:改指针前,先问自己——我接下来要改的这个 .next 原指向谁,还有没有人需要那个旧值。
最终完整代码
/**
* 方案 1:带 dummy 的迭代版(推荐)
* 空间 O(1),统一了头结点处理
*/
var swapPairs = function(head) {
let dummy = new ListNode(0, head);
let cur = dummy;
while (cur.next && cur.next.next) {
let node1 = cur.next; // a
let node2 = cur.next.next; // b
cur.next = node2; // prev -> b
node1.next = node2.next; // a -> next(先保存后半段)
node2.next = node1; // b -> a
cur = node1; // prev 移到 a
}
return dummy.next;
};
/**
* 方案 2:不用 dummy 的迭代版
* 空间 O(1),需要单独处理第一对
*/
var swapPairs = function(head) {
if (!head || !head.next) return head;
let newHead = head.next;
let cur = head;
let temp = null;
while (cur && cur.next) {
let node1 = cur, node2 = cur.next;
node1.next = node2.next;
node2.next = node1;
if (temp) temp.next = node2;
cur = node1.next;
temp = node1;
}
return newHead;
};
/**
* 方案 3:递归版
* 空间 O(n),代码最简洁
*/
var swapPairs = function(head) {
if (!head || !head.next) return head;
let first = head;
let second = head.next;
first.next = swapPairs(second.next);
second.next = first;
return second;
};