bigywater.tech
算法与数据结构 · LeetCode 24

LeetCode 24. 两两交换链表中的节点

给定一个链表,把相邻的两个结点两两交换,返回新的头结点。要求真的交换结点,不能只交换结点里的值。链表长度是奇数时,最后一个结点保持原位。这道题没有算法上的技巧,全部难点在指针操作:每交换一对要改三个指针,顺序错了链表就断了。本文用真实的错误版本展示漏掉一步指针会得到什么,给出五种语言的实现,并把每一步改指针做成分步演示。

5,000
组随机对拍,与暴力解的结果全部一致
6 步
分步演示,每一帧都来自解法的真实执行,另附 5 种语言实现

题意与示例

完整题目见官方页面(链接在文末)。链表在测试里用整数数组表示。基准解直接在数组上交换相邻元素,与改指针是两种独立的写法。一共 10 组用例,覆盖:官方示例、空链表、单个结点、两个结点、奇数长度、全部相同的值:

真实实测 · 本机执行输出
输入 [[1,2,3,4]]
输出 [2,1,4,3]
输入 [[]]
输出 []
输入 [[1]]
输出 [1]
输入 [[1,2]]
输出 [2,1]
输入 [[1,2,3]]
输出 [2,1,3]
输入 [[1,2,3,4,5]]
输出 [2,1,4,3,5]

陷阱:漏掉一步指针,链表会丢结点

交换 prev 后面的一对 a、b 需要三步:让 a 接上 b 后面的部分,让 b 指向 a,让 prev 指向 b。第三步最容易漏,因为前两步已经把 a、b 的方向换过来了,看起来"已经交换完了"。下面是漏掉第三步的真实运行结果:

真实实测 · 本机执行输出
# 漏掉「让前一个结点指向 b」这一步的写法
[1, 2]: 漏一步 -> [1]    solve -> [2, 1]
[1, 2, 3, 4]: 漏一步 -> [1, 3]    solve -> [2, 1, 4, 3]
[1, 2, 3, 4, 5, 6]: 漏一步 -> [1, 3, 5]    solve -> [2, 1, 4, 3, 6, 5]

漏掉第三步之后,prev 仍然指向 a,b 被甩在了链表外面:[1, 2, 3, 4] 只剩 [1, 3],每一对里的第二个结点都丢了。结果不是乱序,而是缺结点,检查时容易被误认为是别的问题。

哑结点加三步改指针:五种语言的实现

先建一个哑结点,prev 指向它。只要 prev 后面还有两个结点,就取这一对 a、b,按上面三步改指针,然后让 prev 指向 a(它现在是这一对的第二个),继续下一对。哑结点让"头结点也被交换"这个情况不需要特判。Python、Java、Go、JavaScript、C++ 都是真实编译运行的。

Python · 解法代码class ListNode: def __init__(self, val=0, next=None): self.val = val self.next = next def swap_pairs(head): dummy = ListNode(0, head) prev = dummy # prev 始终指向"待交换的这一对"的前一个结点 while prev.next and prev.next.next: a, b = prev.next, prev.next.next a.next = b.next # 1) a 接到 b 后面的部分 b.next = a # 2) b 指向 a prev.next = b # 3) 前一个结点指向 b prev = a # a 现在是这一对的第二个,下一对从它后面开始 return dummy.next
真实实测 · Python · 前几组用例的输出
$ python3 solution.py   # 统一入口:逐行读取 cases.jsonl,调用上面的 solve,打印 JSON
[[1,2,3,4]]  →  [2,1,4,3]
[[]]  →  []
[[1]]  →  [1]
[[1,2]]  →  [2,1]
[[1,2,3]]  →  [2,1,3]
[[1,2,3,4,5]]  →  [2,1,4,3,5]
Java · 解法代码class ListNode { int val; ListNode next; ListNode(int v) { val = v; } } class Solution { public ListNode swapPairs(ListNode head) { ListNode dummy = new ListNode(0); dummy.next = head; ListNode prev = dummy; // prev 始终指向"待交换的这一对"的前一个结点 while (prev.next != null && prev.next.next != null) { ListNode a = prev.next, b = prev.next.next; a.next = b.next; // 1) a 接到 b 后面的部分 b.next = a; // 2) b 指向 a prev.next = b; // 3) 前一个结点指向 b prev = a; // a 现在是这一对的第二个 } return dummy.next; } }
真实实测 · Java · 前几组用例的输出
$ javac -d build Main.java Solution.java && java -cp build Main < cases.jsonl
[[1,2,3,4]]  →  [2,1,4,3]
[[]]  →  []
[[1]]  →  [1]
[[1,2]]  →  [2,1]
[[1,2,3]]  →  [2,1,3]
[[1,2,3,4,5]]  →  [2,1,4,3,5]
Go · 解法代码type ListNode struct { Val int Next *ListNode } func swapPairs(head *ListNode) *ListNode { dummy := &ListNode{Next: head} prev := dummy // prev 始终指向"待交换的这一对"的前一个结点 for prev.Next != nil && prev.Next.Next != nil { a, b := prev.Next, prev.Next.Next a.Next = b.Next // 1) a 接到 b 后面的部分 b.Next = a // 2) b 指向 a prev.Next = b // 3) 前一个结点指向 b prev = a // a 现在是这一对的第二个 } return dummy.Next }
真实实测 · Go · 前几组用例的输出
$ go build -o bin . && ./bin < cases.jsonl
[[1,2,3,4]]  →  [2,1,4,3]
[[]]  →  []
[[1]]  →  [1]
[[1,2]]  →  [2,1]
[[1,2,3]]  →  [2,1,3]
[[1,2,3,4,5]]  →  [2,1,4,3,5]
JavaScript · 解法代码class ListNode { constructor(val = 0, next = null) { this.val = val; this.next = next; } } function swapPairs(head) { const dummy = new ListNode(0, head); let prev = dummy; // prev 始终指向"待交换的这一对"的前一个结点 while (prev.next && prev.next.next) { const a = prev.next, b = prev.next.next; a.next = b.next; // 1) a 接到 b 后面的部分 b.next = a; // 2) b 指向 a prev.next = b; // 3) 前一个结点指向 b prev = a; // a 现在是这一对的第二个 } return dummy.next; }
真实实测 · JavaScript · 前几组用例的输出
$ node run.js < cases.jsonl
[[1,2,3,4]]  →  [2,1,4,3]
[[]]  →  []
[[1]]  →  [1]
[[1,2]]  →  [2,1]
[[1,2,3]]  →  [2,1,3]
[[1,2,3,4,5]]  →  [2,1,4,3,5]
C++ · 解法代码struct ListNode { int val; ListNode* next; ListNode(int v) : val(v), next(nullptr) {} }; class Solution { public: ListNode* swapPairs(ListNode* head) { ListNode dummy(0); dummy.next = head; ListNode* prev = &dummy; // prev 始终指向"待交换的这一对"的前一个结点 while (prev->next && prev->next->next) { ListNode *a = prev->next, *b = prev->next->next; a->next = b->next; // 1) a 接到 b 后面的部分 b->next = a; // 2) b 指向 a prev->next = b; // 3) 前一个结点指向 b prev = a; // a 现在是这一对的第二个 } return dummy.next; } };
真实实测 · C++ · 前几组用例的输出
$ clang++ -std=c++17 -O2 -o bin sol.cpp && ./bin < cases.jsonl
[[1,2,3,4]]  →  [2,1,4,3]
[[]]  →  []
[[1]]  →  [1]
[[1,2]]  →  [2,1]
[[1,2,3]]  →  [2,1,3]
[[1,2,3,4,5]]  →  [2,1,4,3,5]
真实实测 · 本机执行输出
共 10 组用例(含边界),以暴力解的结果为基准逐组比对:
  Python      通过 10/10
  Java        通过 10/10
  Go          通过 10/10
  JavaScript  通过 10/10
  C++         通过 10/10

随机对拍

Python 版本另外和数组交换做了随机对拍:

真实实测 · 本机执行输出
随机对拍 5000 组(长度 0 到 12 的随机链表(奇数长度、偶数长度、空链表都会出现)),solve 与 brute 的结果全部一致。

另外四种语言只在上面 10 组固定用例上验证过,没有做大规模随机对拍。这道题是线性的,一次遍历,每个结点只访问常数次,所以没有做规模对比实验。

分步演示:每一对怎么交换

下面的演示用 [1, 2, 3, 4, 5]。每一帧都是把上面的解法在这组输入上真实执行记录下来的,运行器还检查了演示的最终结果与 solve 的返回值一致。链表最前面的 dummy 是哑结点,琥珀色的是 prev,蓝色的是这一对。5 个结点交换完两对之后,最后一个结点没有搭档,保持原位。

两两交换链表中的节点:[1, 2, 3, 4, 5]
正在交换的一对 a、bprev

    参考与说明

    • 官方题目页:LeetCode 24. 两两交换链表中的节点。本文只用自己的话概括了题意,完整约束以官方页面为准。
    • 数据来自本机真实运行,五种语言的版本写在页脚。
    • 没有覆盖:递归写法;官方页面上"不修改结点内部的值"这一约束是如何被评测系统检查的。
    京ICP备2026067502号-1京公网安备11010502063487号