给定一个链表,把相邻的两个结点两两交换,返回新的头结点。要求真的交换结点,不能只交换结点里的值。链表长度是奇数时,最后一个结点保持原位。这道题没有算法上的技巧,全部难点在指针操作:每交换一对要改三个指针,顺序错了链表就断了。本文用真实的错误版本展示漏掉一步指针会得到什么,给出五种语言的实现,并把每一步改指针做成分步演示。
完整题目见官方页面(链接在文末)。链表在测试里用整数数组表示。基准解直接在数组上交换相邻元素,与改指针是两种独立的写法。一共 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
$ 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; } }
$ 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 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; }
$ 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; } };
$ 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 个结点交换完两对之后,最后一个结点没有搭档,保持原位。