代码随想录 链表 24.两两交换链表中的节点

4 阅读5分钟

LC 24 两两交换链表中的节点:我从头到尾实现了一遍,这是所有踩坑记录

代码随想录资源

题目在说什么

给你一个链表,两两交换其中相邻的节点,返回交换后的链表。注意是交换节点本身(改指针),不能只改节点里的值。

写这道题让我真正理解了指针操作的执行顺序——什么时候必须先保存再改、什么时候可以换序。

我做的第一个决策

这道题的解法分裂出三个分支:

  1. 带 dummy 的迭代:代码随想录的写法,用虚拟头结点消除头结点的特殊性
  2. 不用 dummy 的迭代:自己尝试能不能省掉 dummy,结果发现要多维护一个 newHead 和一个 if (temp)
  3. 递归:把当前一对交换和剩余子问题分开,代码最短

我决定三种都写进去,因为它们的递进关系本身就是最好的学习路径:先学会标准写法(带 dummy)→ 再理解 dummy 到底省了什么(无 dummy)→ 最后切换到另一种思维模型(递归)。

实现过程

方案 1:带 dummy 的迭代版

核心思路:给头前面补一个 dummy 节点,这样每一对节点的交换代码一模一样,不需要单独处理第一对。

每轮循环处理 prev -> a -> b -> next 这一段,改成 prev -> b -> a -> next,需要三条指针操作:

  1. a.next = next —— a 先接上后半段
  2. b.next = a —— b 指向 a,完成局部反转
  3. prev.next = b —— 前驱指向新的头节点 b

然后 prev 移到 a(交换后的尾节点),进入下一轮。

var swapPairs = function(head) {
    let dummy = new ListNode(0, head);
    let cur = dummy;

    while (cur.next && cur.next.next) {
        let node1 = cur.next;
        let node2 = cur.next.next;

        cur.next = node2;
        node1.next = node2.next;
        node2.next = node1;
        cur = node1;
    }

    return dummy.next;
};
我一开始写的是:
// 错误版本
var swapPairs = function(head) {
    let dummy = new ListNode(0, head), cur = dummy;
    while (cur.next && cur.next.next) {
        let temp = cur.next, x = cur.next.next;
        temp.next = x.next;
        x.next = temp;
        cur.next = x.next;  // ← bug
    }
    return dummy.next;
};

两个问题:

问题 1:cur.next = x.next 应是 cur.next = x

我当时想的是「让前驱指向被保存下来的后半段」,但 x.next 在执行完 x.next = temp 后已经被改成 temp 了,所以 cur.next = x.next 等于让 cur.next 指回了原来的第一个节点,白换了。

正确做法是直接让 cur.next 指向 x(交换后的新头)。

问题 2:缺少 cur = node1(或 cur = temp)推进

没有这行,cur 始终指向 dummy,每次循环都在处理前两个节点,链表会在 1 -> 2 之间反复交换形成死循环。

修复后的正确代码见最终完整代码。

方案 2:不用 dummy 的迭代版

理解了 dummy 省了什么之后,尝试不用 dummy 写一遍来验证理解。

var swapPairs = function(head) {
    if (!head || !head.next) return head;

    let newHead = head.next;
    let cur = head;
    let temp = null;

    while (cur && cur.next) {
        let node1 = cur, node2 = cur.next;

        node1.next = node2.next;
        node2.next = node1;
        if (temp) temp.next = node2;

        cur = node1.next;
        temp = node1;
    }

    return newHead;
};

和带 dummy 版的区别:

  1. 需要单独用一个 newHead 记住新的头结点(原来的第二个节点)
  2. 需要一个 if (temp) 判断当前是不是第一对——第一对的前驱 temp 为 null,不需要接线(就是将新的交换过的节点与之前的交换完的节点进行连接)
  3. 除此之外,指针交换的逻辑完全一样

这个对比让我看清了:dummy 解决的问题就是省掉一个 if 判断和一个 newHead 变量。不是算法上的差别,是代码简洁性的差别,当然,也更加便于理解。

方案 3:递归

递归不需要 dummy,因为 base case if (!head || !head.next) return head 直接处理了空和单节点的边界。

var swapPairs = function(head) {
    if (!head || !head.next) return head;

    let first = head;
    let second = head.next;

    first.next = swapPairs(second.next);
    second.next = first;

    return second;
};

思路:每次只处理一对,把剩下的部分扔给递归,返回的结果接在 first 后面,然后 second 成为新的头。

递归的好处是代码短、思路声明式。代价是空间复杂度 O(n)(调用栈深度等于节点数的一半)。

方案对比

方案空间代码量关键技巧
带 dummy 迭代O(1)10 行dummy 统一头结点处理
不用 dummy 迭代O(1)12 行单独记 newHead + if(temp)
递归O(n)6 行base case 处理边界

踩坑合集

#问题根因怎么发现的修复
1cur.next = x.next 写成 x.next以为带上 .next 才能接上后半段,没意识到 x.next 已经被改了人工 trace 一遍发现 cur.next 指回了第一个节点cur.next = x
2漏掉 cur = node1 推进以为循环会自动推进跑一遍发现死循环每轮末尾 cur = node1
3node2.next = node1 必须在 node1.next = node2.next 之后否则 node2.next 的原值会丢失自己意识到顺序约束保持正确顺序

这三个坑其实指向同一个教训:改指针前,先问自己——我接下来要改的这个 .next 原指向谁,还有没有人需要那个旧值。

最终完整代码

/**
 * 方案 1:带 dummy 的迭代版(推荐)
 * 空间 O(1),统一了头结点处理
 */
var swapPairs = function(head) {
    let dummy = new ListNode(0, head);
    let cur = dummy;

    while (cur.next && cur.next.next) {
        let node1 = cur.next;       // a
        let node2 = cur.next.next;  // b

        cur.next = node2;           // prev -> b
        node1.next = node2.next;    // a -> next(先保存后半段)
        node2.next = node1;         // b -> a
        cur = node1;                // prev 移到 a
    }

    return dummy.next;
};

/**
 * 方案 2:不用 dummy 的迭代版
 * 空间 O(1),需要单独处理第一对
 */
var swapPairs = function(head) {
    if (!head || !head.next) return head;

    let newHead = head.next;
    let cur = head;
    let temp = null;

    while (cur && cur.next) {
        let node1 = cur, node2 = cur.next;

        node1.next = node2.next;
        node2.next = node1;
        if (temp) temp.next = node2;

        cur = node1.next;
        temp = node1;
    }

    return newHead;
};

/**
 * 方案 3:递归版
 * 空间 O(n),代码最简洁
 */
var swapPairs = function(head) {
    if (!head || !head.next) return head;

    let first = head;
    let second = head.next;

    first.next = swapPairs(second.next);
    second.next = first;

    return second;
};