← 返回题目列表

如何两两交换链表中的节点?为什么不能只交换节点值?

中等 第 23 / 28 题 更新于 2026/07/30
链表指针操作dummy成对交换

简化版

两两交换链表节点通常用虚拟头节点 dummy 简化头节点变化,每轮处理 prev -> a -> b -> next,把它改成 prev -> b -> a -> next,再让 prev 前进到 a。面试里要强调这是交换节点连接关系,不是只交换节点值。

详细版

这题的核心是维护一段长度为 2 的局部链表。假设当前有 prev -> a -> b -> next,交换后应该变成 prev -> b -> a -> next

常见写法:

  • 新建 dummy 指向 head,避免头两个节点交换时单独处理。
  • 每轮要求 prev.nextprev.next.next 都存在。
  • 记录 a = prev.nextb = a.nextnext = b.next
  • 依次改指针:prev.next = bb.next = aa.next = next
  • 交换完成后,a 已经变成这一组的尾节点,所以下一轮 prev = a

时间复杂度是 O(n),每个节点最多访问常数次;空间复杂度是 O(1)。如果面试官强调“交换节点”,不要只交换 val,因为真实节点可能携带额外引用、对象身份或外部引用。

完整版教学

一、这题真正考的是“局部重连”

两两交换看起来像一道简单模拟题,但它真正考的是你能不能把链表操作缩小到一个稳定局部。链表没有数组下标,不能直接说交换第 i 和第 i+1 个位置;你只能通过前驱节点把下一段摘下来再接回去。

最小局部可以写成:

交换前:prev -> a -> b -> next
交换后:prev -> b -> a -> next

这里 prev 很关键。只盯着 ab 不够,因为交换后的新头是 b,必须让前一段链表指向 b。这也是为什么链表题经常要维护“前驱指针”,它不是为了多写一个变量,而是为了有能力修改当前片段的入口。

二、为什么 dummy 能让头节点也按同一套逻辑处理

如果链表是 1 -> 2 -> 3 -> 4,第一组交换后头节点会从 1 变成 2。没有 dummy 时,你需要单独保存新头,否则函数最后不知道返回谁。

dummy 的作用是在人造的头部增加一个永远存在的前驱:

dummy -> 1 -> 2 -> 3 -> 4
prev = dummy

这样第一组也能统一看成 prev -> a -> b -> next。交换后是 dummy -> 2 -> 1 -> 3 -> 4,最终返回 dummy.next 就行。它减少的不是时间复杂度,而是边界分支数量;分支少,链表断链和漏接的概率就低。

三、指针修改顺序为什么要先保存 next

链表重连最怕“改掉以后找不回后半段”。在 prev -> a -> b -> next 中,如果你先写 b.next = a,再想通过 b.next 找原来的 next,已经不可能了,因为 b.next 被覆盖成了 a

安全顺序是先保存变量:

const a = prev.next;
const b = a.next;
const next = b.next;

prev.next = b;
b.next = a;
a.next = next;
prev = a;

数字例子:0(dummy) -> 1 -> 2 -> 3,如果忘记 next = 3,把 2.next = 1 后,节点 3 就可能从链上丢失。链表题里“先保存后修改”几乎是铁律,因为指针字段本身就是通往后续节点的道路。

四、循环条件为什么是两个节点都存在

两两交换要求当前组必须有 2 个节点。也就是说,prev.next 是第一个节点,prev.next.next 是第二个节点,两者都存在才能交换。

如果链表长度是 5:

1 -> 2 -> 3 -> 4 -> 5
交换后:2 -> 1 -> 4 -> 3 -> 5

最后的 5 没有配对节点,必须原样保留。循环条件写成 while (prev.next && prev.next.next) 就刚好表达这个语义。只判断 prev.next 会让奇数长度链表在最后一轮访问空指针;判断过度又可能漏掉长度正好为 2 的尾段。

五、为什么不建议只交换节点值

在刷题平台里,节点可能只有 valnext,交换值似乎也能得到一样的输出。但面试题如果明确说“交换节点”,通常希望你改的是链表结构。

对比如下:

做法改动对象适合场景隐患
交换值节点内容节点只含简单值,题目允许对象身份没有变化
交换节点next 指针真实链表结构题指针顺序要求更严格
重建链表新建节点允许额外空间时空间 O(n),外部引用丢失

真实工程里,节点可能被别的结构引用,比如 LRU 的双向链表节点还挂在哈希表里。只交换值会让“哪个对象代表哪个缓存项”变得混乱,所以链表结构题要优先回答指针重连。

六、复杂度和可验证性怎么说

每一轮处理 2 个节点,做常数次指针赋值,所以总时间复杂度是 O(n)。除了 dummy 和几个指针变量,没有随输入增长的额外存储,所以空间复杂度是 O(1)。

可验证时可以覆盖 4 类输入:

空链表:null -> null
单节点:1 -> 1
偶数长度:1->2->3->4 -> 2->1->4->3
奇数长度:1->2->3->4->5 -> 2->1->4->3->5

记忆钩子:链表成对交换不是“交换两个值”,而是固定看 prev -> a -> b -> next,先保存 next,再改成 prev -> b -> a -> next

七、常见误区与追问

  • 误区:交换 val 就等于交换节点。 输出值可能一样,但节点对象身份、外部引用和附加字段都没有交换,面试里通常不算结构层面的答案。
  • 误区:第一组需要特殊处理。 用 dummy 后第一组也有前驱节点,可以和后续组使用完全相同的逻辑。
  • 误区:改指针前不用保存 next 一旦覆盖了 b.next,后半段入口可能丢失,链表会断。
  • 追问:为什么交换后 prev = a 因为交换后 a 是这一组的尾节点,下一组应该从 a.next 开始。
  • 追问:递归能做吗? 可以,递归写法把后续链表先两两交换,再让 b.next = aa.next = swappedRest,但调用栈空间是 O(n)。

八、加强记忆

这题记住四个角色就稳了:prev 负责接入新组头,a 是组内第一个节点,b 是组内第二个节点,next 是后半段入口。每轮只做一件事:把 prev -> a -> b -> next 改成 prev -> b -> a -> next。只要返回 dummy.next,奇数尾巴自然留下,头节点变化也自然处理。