LeetCode 两两交换链表中的节点:Java 递归与迭代解法
一、原题回顾
题目描述: 给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。 你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
示例 1:
输入:head = 输出:
LeetCode 第 24 题“两两交换链表中的节点”。要求在不修改节点值的情况下,通过调整指针成对交换相邻节点。文章提供了递归和迭代两种 Java 解法,重点分析了哑节点的使用及指针操作顺序。迭代法空间复杂度为 O(1),推荐用于生产环境。内容涵盖复杂度分析、常见问题解答及实际应用场景,帮助读者掌握链表核心操作技巧。
题目描述: 给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。 你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
示例 1:
输入:head = 输出:
示例 2:
输入:head = [] 输出:[]
示例 3:
输入:head = [1] 输出:[1]
提示:
[0, 100] 内0 <= Node.val <= 100这道题要求我们成对地交换相邻节点,且不能通过交换 val 值来实现,必须通过调整指针完成真正的节点交换。
next 指针;head == null)head 在交换后不再是头节点,需正确返回新头。node1 成为该对的尾节点,应指向下一组交换后的头节点;将问题分解为:
✅ 优点:代码简洁,逻辑清晰 ❌ 缺点:空间复杂度 O(n),可能栈溢出(尽管 n≤100 安全)
使用循环,每次处理一对节点:
temp 指针,始终指向当前待处理对的前驱;temp.next 和 temp.next.next;temp 到下一对的前驱(即原 node1)。✅ 优点:O(1) 空间,效率高,工业级推荐 ❌ 缺点:指针操作稍显繁琐,需仔细画图
📌 共同技巧:引入'哑节点' 创建
dummyHead,其next = head,确保无论是否交换头节点,都能通过dummyHead.next返回正确结果。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode swapPairs(ListNode head) {
// 递归终止条件:空链表或只剩一个节点
if (head == null || head.next == null) {
return head;
}
// 记录新的头节点(原第二个节点)
ListNode newHead = head.next;
// 递归处理后续节点,并将结果接在原 head 后面
head.next = swapPairs(newHead.next);
// 将 newHead 指向原 head,完成交换
newHead.next = head;
return newHead;
}
}
class Solution {
public ListNode swapPairs(ListNode head) {
// 创建哑节点,简化头节点处理
ListNode dummyHead = new ListNode(0);
dummyHead.next = head;
// temp 始终指向当前待交换对的前驱
ListNode temp = dummyHead;
// 只要后面还有至少两个节点,就继续交换
while (temp.next != null && temp.next.next != null) {
// 获取要交换的两个节点
ListNode node1 = temp.next;
ListNode node2 = temp.next.next;
// 执行交换操作(关键三步)
temp.next = node2; // 前驱指向 node2
node1.next = node2.next; // node1 指向 node2 的下一个
node2.next = node1; // node2 指向 node1
// 移动 temp 到下一对的前驱(即当前对的尾节点)
temp = node1;
}
return dummyHead.next;
}
}
✅ 强烈推荐迭代解法:空间效率高,无递归开销,更适合生产环境。
以 [1,2,3,4] 为例:
head=1, newHead=2 → 1.next = swapPairs(3)head=3, newHead=4 → 3.next = swapPairs(null) = null4.next = 3 → 返回 41.next = 4,2.next = 1 → 返回 22 → 1 → 4 → 3关键点:
head.next = swapPairs(...):将原第一个节点的 next 指向后续交换后的头节点;newHead.next = head:完成当前对的交换。交换前:temp → node1 → node2 → nextGroup...
交换后:temp → node2 → node1 → nextGroup...
三步操作顺序至关重要:
temp.next = node2
→ 先让前驱指向新头(否则会丢失 node2)node1.next = node2.next
→ 让原第一个节点指向下一组(保存后续链表)node2.next = node1
→ 完成当前对的反转⚠️ 错误顺序示例: 若先执行
node2.next = node1,则node2.next被覆盖,无法获取nextGroup!
1 → 2 → 3 → 4dummy → 1 → 2 → 3 → 4dummy → 2 → 1 → 4 → 3dummy.next 即 2,无需特殊判断头节点是否变化。| 方法 | 时间复杂度 | 空间复杂度 | 是否修改节点值 |
|---|---|---|---|
| 递归 | O(n) | O(n) | ❌(仅改指针) |
| 迭代 | O(n) | O(1) | ❌ |
其中
n为链表节点数。
💡 工程建议:优先使用迭代,避免递归潜在风险(即使本题安全)。
val?答:题目明确要求'只能进行节点交换'。在真实系统中,节点可能包含大量数据(如用户对象),交换指针比复制数据高效得多。此外,某些场景下节点是不可变的(immutable),无法修改
val。
temp = node1 是什么意思?答:交换后,
node1成为当前对的尾节点,它自然就是下一对节点的前驱。例如:初始:dummy → 1 → 2 → 3 → 4交换第一对后:dummy → 2 → 1 → 3 → 4此时temp应指向1,以便下一轮交换3和4
答:最后一节点自动保留。例如
[1,2,3]→[2,1,3]。 因为循环条件是temp.next != null && temp.next.next != null,当只剩一个节点时,循环终止。
答:可以,但效率低。遍历链表入栈,然后每次弹出两个节点重新连接。但空间 O(n),且需两次遍历,不推荐。
在迭代开始前检查 head == null || head.next == null,直接返回。但收益微乎其微。
可合并部分操作,但会降低可读性,不建议。
// 示例:单元测试用例
@Test
void testSwapPairs() {
assertEquals(asList(2, 1, 4, 3), toList(swapPairs(toListNode([1, 2, 3, 4]))));
assertEquals(Collections.emptyList(), toList(swapPairs(toListNode([]))));
assertEquals(asList(1), toList(swapPairs(toListNode([1]))));
}
next 指针方向| 维度 | 递归 | 迭代 |
|---|---|---|
| 代码简洁性 | ✅ 高 | ❌ 中 |
| 空间效率 | ❌ O(n) | ✅ O(1) |
| 可读性 | ✅ 直观 | ❌ 需理解指针 |
| 栈溢出风险 | ✅ 存在 | ❌ 无 |
回答: 实际上我们用了四个引用:
temp,node1,node2, 以及隐式的node2.next。 原因是:要完成A→B→C→D到A→C→B→D的转换,必须同时知道:A(前驱,用于连接C)B(原第一个,需指向D)C(原第二个,需指向B)D(后续链表头) 缺少任何一个,都会导致链表断裂。
回答: 这是 LeetCode 第 25 题《K 个一组翻转链表》。 思路:先检查后续是否有 k 个节点;若有,反转这 k 个节点(可用迭代反转子链表);递归或迭代处理下一组;连接各组。 本题是 k=2 的特例。
回答: 不能,且题目假设链表无环。 若存在环,递归会无限调用导致栈溢出;迭代会陷入死循环。 实际工程中,应先用快慢指针检测环(Floyd 算法),再处理。
回答:
Collections.swap()适用于List接口(如ArrayList),但链表(LinkedList)虽实现List,其swap本质仍是交换值,而非节点。 且本题输入是ListNode自定义结构,非 Java 集合。
虽然'两两交换'看似抽象,但其思想在实际系统中广泛应用:
💡 核心价值:培养对内存布局和指针语义的直觉,这是系统编程的基础。
掌握本题后,可挑战以下 LeetCode 题目:
| 题号 | 题目 | 关联点 |
|---|---|---|
| 206. 反转链表 | 链表基本操作 | 指针重定向 |
| 92. 反转链表 II | 区间反转 | 哑节点 + 指针操作 |
| 25. K 个一组翻转链表 | 本题扩展 | 分组处理 |
| 143. 重排链表 | 找中点 + 反转 + 合并 | 综合应用 |
| 86. 分隔链表 | 哑节点 | 链表分隔 |
| 328. 奇偶链表 | 重排 | 指针分离 |
建议按顺序练习,逐步提升链表操作能力。
prev 指针,操作更复杂,但逻辑类似。[1,2,3]、[1,2]、[] 等 case,展现工程素养。'链表题,指针是笔,内存是纸,画图是草稿。' 掌握本题,你就拥有了处理复杂链表变形的钥匙。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online