Q24 两两交换链表中的节点
题目:给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。不能只改节点内部的值,必须实际交换节点。输入:
1 → 2 → 3 → 4,输出:2 → 1 → 4 → 3。
1 我的想法
想写一个函数:接受一个起点,把后面的两个节点"翻转"一下。第一次草稿卡在:
while()空条件、x未定义,编译不过;- 更关键的是:交换完一对之后,不知道这一对怎么接回前面的链、怎么连到下一对;
- 于是用
flag、heade去补,越补越乱。
核心:两两交换 = 每次处理"一对",但必须记住这一对的前一个节点 prev,否则交换完接不回去。用虚拟头 dummy 统一第一对。
2 简要回答
用虚拟头节点 dummy,维护指针 prev(当前这一对的前一个节点)。每一轮:
| |
然后 prev 跳到 a(这一对的新尾巴),继续处理下一对。
- 时间复杂度 $O(n)$
- 空间复杂度 $O(1)$(迭代版;递归版为 $O(n)$ 栈空间)
3 为什么需要 prev
只交换 a、b 本身不难(a->next = b->next; b->next = a;),难的是把这一对接回前面的链。交换完成后,前面的链还指着原来的 a,必须把它的 next 改成 b——所以必须提前握着"这一对的前一个节点"。
这正好呼应"写一个函数接受起点"的想法:函数参数不应该是这一对的第一个节点,而应该是这一对的前一个节点。
4 虚拟头到底是不是"必须"的
不是必须的,它是把"头节点特判"变成"统一处理"的技巧。头节点特殊在:它没有前驱。凡是"通过前一个节点操作"的写法,头节点都没有前一个可用。
什么时候可以用 nullptr 当虚拟头,什么时候必须 new 一个真节点,看一件事——你要不要通过它修改 next:
| 场景 | 能不能用 nullptr | 原因 |
|---|---|---|
反转链表:只读 pre(cur->next = pre) | 可以 | 只是把 pre 的值赋给别人,pre 本身不参与修改 |
两两交换 / 删除节点:要写 prev->next(prev->next = b) | 不行 | nullptr->next 没法赋值,前一个节点必须真实存在 |
不引入 dummy 也能写 swapPairs,但第一对要单独特判:先手动交换 head 和 head->next,再进循环处理后面的对——多一段代码,多一个出错点。dummy 让第一对也走同一个 swapTwo,特殊 case 直接消失。
5 核心代码
| |
用 1→2→3→4 走一遍:
| |
奇数个节点时,最后一对凑不齐(prev->next->next == nullptr),剩下的单个节点不动,天然正确。
6 递归版(呼应 Q206 的"信任")
| |
基例 + “假设后面已交换好” + 只做一步——和反转链表的信任模型完全一样。
6.1 递归版逐层拆解
| |
递归的箭头是从里往外接的:最深处的子调用先接好,再一层层回溯补上"前两个节点"的交换。每一层只保证前两个节点的顺序对,后面整段全部丢给子调用、信任它已排好——和反转链表、fib 是同一个归纳模型。不需要 dummy。
迭代 vs 递归的建链方向:
- 迭代版:从前往后建链,处理完一对推进
prev,需要 dummy 兜第一对。- 递归版:从后往前组装,最深处先接好再回溯补前面的,不需要 dummy。
7 我的草稿为什么乱
| |
更本质的问题:交换完一对后,这一对的新尾巴(原第一个节点)必须接到下一对的头;草稿里没有 prev 这个概念,只能用 flag、heade 硬补,越补越乱。加了 prev + dummy 之后,flag 和 heade 都不需要了。
8 易错点
- 循环条件必须是
prev->next != nullptr && prev->next->next != nullptr:保证这一对真的有两个节点。 - 交换后
prev = prev->next->next,即这一对的新尾巴 a;写错位置会漏节点或死循环。 - 不要只交换 val:题目明确要求实际交换节点。
- 返回
dummy->next,不是head(head 可能已经被换到第二位了)。