题目描述

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

image-20260928194503518

image-20260928194503519

题意分析

从链表头部开始,把节点按相邻的两个分组,每组内部交换位置,组与组的前后顺序保持不变。必须改变节点之间的连接,不能只交换节点值;返回交换后的头节点。

空链表和单节点链表不变。如果节点总数为奇数,最后落单的节点也保持原位。至少有两个节点时,原来的第二个节点会成为新头,因此原 head 未必还是返回起点。

解法:哨兵节点迭代交换相邻节点

核心思路

[!blue]

每轮只重连一对相邻节点,但需要把交换后的组头接回前面。第一组没有真实前驱,可以先放置一个哨兵 dummy 指向原头节点,令第一组和后续组都按“前驱接组头”的方式处理。

pre 始终指向已经处理部分的尾节点,first = pre.next、second = first.next 是下一组。原来的局部连接是 pre → first → second → 后续链表,目标连接是 pre → second → first → 后续链表。只需改动三条 next,节点值和后续未处理部分都不变。

先执行 first.next = second.next,让交换后要成为组尾的 first 接住后续链表;接着执行 second.next = first,反转本组顺序;最后执行 pre.next = second,把本组的新头接回已处理部分。这个顺序保证在覆盖 second.next 之前,后续链表已经有连接保留下来。

交换后,原来的 first 成为这一组的尾节点,也是下一组的前驱,因此令 pre = first。这样每轮只处理尚未交换的节点,已完成的部分不会再被改动。当 pre 后不足两个节点时结束,落单节点已经连接在尾部,无需额外处理。

解题步骤

  1. 创建 dummy 并令它指向 head,把 pre 初始化为 dummy。
  2. 同时确认 pre.next 和 pre.next.next 非空,保证本轮存在完整的一对节点。
  3. 保存本组的 first、second,依次重连 first.next、second.next、pre.next。
  4. 将 pre 移到交换后的组尾 first,继续处理后面的一组。
  5. 循环结束后返回 dummy.next,它始终指向实际新头。

代码实现

class Solution {
    public ListNode swapPairs(ListNode head) {
        ListNode dummy = new ListNode(0);

        dummy.next = head;
        ListNode pre = dummy;

        while (pre.next != null && pre.next.next != null) {
            ListNode first = pre.next;
            ListNode second = first.next;

            // 先接住下一组,再翻转本组的两条连接。
            first.next = second.next;
            second.next = first;
            pre.next = second;
            // 交换后的第一节点成为组尾,下一组从它之后开始。
            pre = first;
        }

        return dummy.next;
    }
}
func swapPairs(head *ListNode) *ListNode {
    dummy := &ListNode{Next: head}
    pre := dummy

    for pre.Next != nil && pre.Next.Next != nil {
        first := pre.Next
        second := first.Next

        // 先接住下一组,再翻转本组的两条连接。
        first.Next = second.Next
        second.Next = first
        pre.Next = second
        // 交换后的第一节点成为组尾,下一组从它之后开始。
        pre = first
    }
    return dummy.Next
}

复杂度分析

  • 时间复杂度:$O(n)$,n 为节点数,每轮处理两个节点、修改常数条连接。
  • 空间复杂度:$O(1)$,只使用一个哨兵节点和固定数量的指针。

关键点总结

[!green]

  • 哨兵为第一组提供前驱,统一头节点变化与普通位置的重连。
  • 先保留后续链表连接,再反转本组,最后把新组头接回前驱。
  • pre 表示已处理部分的尾部,交换后应移动到原 first。
  • 不完整的一组保持原连接,不需要拆开再接回。

解法:递归交换后续链表

核心思路

[!blue]

把递归函数定义为“将传入链表两两交换,并返回交换后的新头”。如果当前链表不足两个节点,就不存在完整的一对,直接返回原头;这也会保留奇数长度链表末尾落单的节点。

有完整一对时,保存第二个节点 second = head.next。本组后面的部分仍然是同一个问题,所以先对 second.next 开始的后缀递归,得到后缀交换后的新头。

原 head 交换后将成为本组尾节点,因此让 head.next 接向递归返回的后缀新头;再令 second.next = head 完成本组交换,最后返回 second 作为本层的新头。只要后缀已经正确交换,这两条连接就能得到整个链表的正确结果;递归逐次减少两个节点,最终一定到达空链表或单节点的边界。

解题步骤

  1. head 为空或 head.next 为空时,直接返回 head。
  2. 保存本组第二个节点 second,对 second.next 后面的链表递归交换。
  3. 将递归返回的新头赋给 head.next,把本组尾部接回处理好的后缀。
  4. 令 second.next = head,返回本组新头 second。

代码实现

class Solution {
    public ListNode swapPairs(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }

        ListNode second = head.next;
        head.next = swapPairs(second.next);
        second.next = head;
        return second;
    }
}
func swapPairs(head *ListNode) *ListNode {
    if head == nil || head.Next == nil {
        return head
    }
    second := head.Next
    head.Next = swapPairs(second.Next)
    second.Next = head
    return second
}

复杂度分析

  • 时间复杂度:$O(n)$,每层递归处理一对节点,重连操作为常数次。
  • 空间复杂度:$O(n)$,每次递归跳过两个节点,调用栈深度仍与链表长度成正比。

关键点总结

[!green]

  • 递归返回值是后缀交换后的新头,必须接回当前组的尾部。
  • 先保存第二个节点,再处理后缀,避免改变连接后失去本组的新头。
  • 迭代显式维护前驱,递归通过返回新头完成重连;递归会额外占用线性栈空间。

易错点总结

[!yellow]

  • 只检查存在第一个节点,落单时继续访问第二个节点会触发空指针错误。
  • 未保存后续链表就先改 second.next,会丢失原后继;再用它更新 first.next 还可能形成环。
  • 迭代交换后令 pre = second,会停在本组头部,下一轮重复处理已经交换过的节点。
  • 递归把剩余链表处理好后,没有用它的新头更新 head.next,会丢失后缀的正确连接。
  • 返回原 head,会漏掉交换后的新头;迭代返回 dummy.next,递归返回本组原第二个节点。
  • 只交换节点的值,输出看似相同但没有满足实际交换节点的要求。

相似题目

题目 难度 关联与区别
25. K 个一组翻转链表 困难 本题相当于k=2的分组反转,原题需要先确认每组是否有足够节点。
92. 反转链表 II 中等 同样保存前驱和后继后重连局部链段,原题只反转指定区间。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/54774613
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!