本文最后更新于 2026-01-18,文章内容可能已经过时。

「两两交换链表中的节点」是 LeetCode 中经典的中等难度题目,也是链表指针操作的核心考题。这道题的关键痛点是:如何在不创建新节点的前提下,通过调整指针指向完成相邻节点的两两交换,同时处理好链表首尾的边界情况 —— 核心考察对链表指针指向关系的精准把控能力。

实际开发中,类似链表节点重排、数据分片交换等场景,都能用到它的核心逻辑。今天我们从最直观的暴力思路出发,一步步优化到迭代和递归两种最优解,带你吃透链表两两交换的实现精髓~

📌 题目重述

给你一个单链表的头节点 head,请你两两交换链表中相邻的节点,并返回交换后链表的头节点。要求:不能修改节点的值(仅能调整指针指向),且不能创建新节点(只能复用原有节点)。

举个例子:

链表两两交换节点 图形化展示
输入:
1
2
3
4
输出:
2
1
4
3
输入:
1
输出:
1
输入:
输出:
输入:
1
2
3
输出:
2
1
3

🚶 阶梯思路拆解

第一步:暴力思路(数组存储 + 重构链表)🥾

最直观的想法是:先把链表的所有节点存入数组,然后遍历数组两两交换元素的位置,最后用交换后的数组重构新链表。这种方法无需复杂的指针操作,适合新手理解,但需要额外的 O (n) 空间(n 为链表长度),且违背不创建新节点的隐含最优要求。

💡 核心逻辑

  1. 遍历链表,将所有节点按顺序存入数组;

  2. 遍历数组,两两交换相邻元素的位置(索引 0 和 1、2 和 3……);若数组长度为奇数,最后一个元素不交换

  3. 若数组为空,返回 null;

  4. 重构链表:将数组中交换后的节点依次拼接,最后一个节点的 next 设为 null;

  5. 返回新链表的头节点(数组第一个元素)。

📊 图文演示(以 head= 1→2→3→4 为例)

(如图所示)暴力重构过程:

链表交换演示
步骤1:数组存储节点
将原链表的节点依次存入数组,保留节点引用:
[
1
,
2
,
3
,
4
]
步骤2:两两交换数组元素
按索引两两交换(0↔1,2↔3):
[
1
2
,
3
4
]
交换后数组:
[
2
,
1
,
4
,
3
]
步骤3:重构链表
按交换后数组顺序重新构建链表指针:
2
1
4
3
null

✅ 代码实现(Java)

import java.util.ArrayList;
import java.util.List;

public class Solution {
    public ListNode swapPairs(ListNode head) {
        // 步骤1:处理空链表
        if (head == null) {
            return null;
        }

        // 步骤2:将链表节点存入数组
        List<ListNode> nodeList = new ArrayList<>();
        ListNode curr = head;
        while (curr != null) {
            nodeList.add(curr);
            curr = curr.next;
        }

        // 步骤3:两两交换数组中的节点
        int len = nodeList.size();
        for (int i = 0; i < len - 1; i += 2) {
            // 交换相邻两个节点的位置
            ListNode temp = nodeList.get(i);
            nodeList.set(i, nodeList.get(i+1));
            nodeList.set(i+1, temp);
        }

        // 步骤4:重构链表
        for (int i = 0; i < len - 1; i++) {
            nodeList.get(i).next = nodeList.get(i+1);
        }
        // 最后一个节点的next置空,避免环
        nodeList.get(len - 1).next = null;

        // 步骤5:返回交换后的头节点
        return nodeList.get(0);
    }
}

⚙️ 复杂度分析

复杂度类型

计算结果

说明

时间复杂度

O(n)

遍历链表存数组 O (n) + 交换数组元素 O (n) + 重构链表 O (n),整体线性时间

空间复杂度

O(n)

数组存储 n 个节点,空间开销 O (n)

🚫 遇到的问题

  1. 空间开销大:需要额外的数组存储所有节点,不符合 O (1) 额外空间的最优要求;

  2. 逻辑冗余:明明可以直接调整指针完成交换,却绕路存储和重构,效率低;

  3. 隐含风险:重构链表时若忘记将最后一个节点的 next 置空,可能导致链表成环;

  4. 违背题意精髓:题目核心考察指针操作,该方法未体现任何链表指针调整的技巧。

第二步:最优解法(迭代法 / 虚拟头节点)🔑

这是本题的最优解,也是面试中最常考的解法:利用虚拟头节点简化头节点交换的边界处理,通过前驱指针逐个遍历链表,依次调整相邻两个节点的指针指向,完成两两交换 —— 仅需一次遍历,O (1) 额外空间,且完全复用原有节点。

💡 核心逻辑

  1. 创建虚拟头节点(dummy):指向 head,避免单独处理头节点交换的情况;

  2. 初始化前驱指针: prev = dummy(始终指向待交换两个节点的前一个节点);

  3. 循环交换( prev.next != null && prev.next.next != null):

    • 定义待交换的两个节点: node1 = prev.next, node2 = prev.next.next;

    • 调整指针完成交换:

      1. prev.next = node2(前驱指向第二个节点);

      2. node1.next = node2.next(第一个节点指向第二个节点的后继);

      3. node2.next = node1(第二个节点指向第一个节点);

    • 前驱指针前进: prev = node1(移动到已交换的第二个节点,作为下一组的前驱);

  4. 返回 dummy.next(交换后链表的真实头节点)。

📊 图文演示(以 head= 1→2→3→4 为例)

(如图所示)迭代交换过程:

步骤

prev 位置

node1

node2

指针调整操作

链表状态

初始

dummy(-1)

1

2

 

dummy→1→2→3→4→null

1

dummy(-1)

1

2

prev.next=22. node1.next=33. node2.next=1

dummy→2→1→3→4→null

2

1

3

4

prev.next=42. node1.next=null3. node2.next=3

dummy→2→1→4→3→null

结束

3

null

null

 

返回 dummy.next=2

✅ 代码实现(Java,迭代最优解)

public class Solution {
    public ListNode swapPairs(ListNode head) {
        // 创建虚拟头节点,简化头节点交换逻辑
        ListNode dummy = new ListNode(-1);
        dummy.next = head;

        // 前驱指针:始终指向待交换两个节点的前一个节点
        ListNode prev = dummy;

        // 循环条件:至少有两个节点可交换
        while (prev.next != null && prev.next.next != null) {
            // 定义待交换的两个节点
            ListNode node1 = prev.next;
            ListNode node2 = prev.next.next;

            // 核心:调整指针完成交换
            prev.next = node2;       // 前驱指向第二个节点
            node1.next = node2.next; // 第一个节点指向第二个节点的后继
            node2.next = node1;      // 第二个节点指向第一个节点

            // 前驱指针前进,准备下一组交换
            prev = node1;
        }

        // 返回交换后的真实头节点
        return dummy.next;
    }
}

⚙️ 复杂度分析

复杂度类型

计算结果

说明

时间复杂度

O(n)

仅一次遍历链表,每个节点处理 O (1),线性时间

空间复杂度

O(1)

仅使用虚拟头节点和几个指针变量,无额外空间开销

✨ 核心优势

  1. 空间最优:O (1) 额外空间,完全符合最优解要求;

  2. 时间高效:一次遍历完成所有交换,无冗余操作;

  3. 边界处理简洁:虚拟头节点避免了头节点交换的特殊判断;

  4. 逻辑严谨:指针调整步骤清晰,完全复用原有节点,无内存泄漏风险。

第三步:进阶思路(递归法)🌀

递归法的核心是分治思想:将链表的两两交换拆解为交换当前两个节点 + 递归交换剩余链表。这种方法无需手动管理前驱指针,代码简洁优雅,但需要理解递归的终止条件和回溯拼接逻辑。

💡 核心逻辑

  1. 递归终止条件:

    • 若 head null(空链表)或 head.next null(仅有一个节点),返回 head;

  2. 递归处理:

    • 定义待交换的两个节点: node1 = head, node2 = head.next;

    • 递归交换剩余链表: node1.next = swapPairs(node2.next);

    • 调整当前两个节点的指针: node2.next = node1;

  3. 返回交换后的当前组头节点(node2)。

📊 图文演示(以 head= 1→2→3→4 为例)

(如图所示)递归交换过程:

  1. 递归调用: swapPairs(1) → 待交换 node1=1、node2=2 → 递归 swapPairs(3);

  2. 递归调用: swapPairs(3) → 待交换 node1=3、node2=4 → 递归 swapPairs(null);

  3. 递归终止: swapPairs(null) 返回 null;

  4. 回溯拼接:

    • swapPairs (3):node1 (3).next = null → node2 (4).next = 3 → 返回 4;

    • swapPairs (1):node1 (1).next = 4 → node2 (2).next = 1 → 返回 2;

  5. 最终结果: 2→1→4→3。

✅ 代码实现(Java,递归解法)

public class Solution {
    public ListNode swapPairs(ListNode head) {
        // 终止条件:空链表或仅有一个节点,无需交换
        if (head == null || head.next == null) {
            return head;
        }

        // 定义当前组待交换的两个节点
        ListNode node1 = head;
        ListNode node2 = head.next;

        // 递归交换剩余链表,并将结果拼接到当前组第一个节点后
        node1.next = swapPairs(node2.next);

        // 交换当前两个节点的指向
        node2.next = node1;

        // 返回当前组交换后的头节点(第二个节点)
        return node2;
    }
}

⚙️ 复杂度分析

复杂度类型

计算结果

说明

时间复杂度

O(n)

递归遍历链表一次,每个节点处理 O (1),线性时间

空间复杂度

O(n)

递归栈的深度为 n/2(最坏情况),空间开销 O (n)

✨ 核心优势与注意点

  • 优势:代码极简,无需虚拟头节点和循环,体现分治思想,面试中展示算法思维加分;

  • 注意点:

    1. 空间复杂度高于迭代法(递归栈开销);

    2. 递归深度过大会导致栈溢出(但题目中链表长度 ≤ 100,可安全使用);

    3. 指针拼接顺序易错:需先递归处理剩余链表,再调整当前节点指针;

  • 适用场景:适合面试中展示递归思维,实际开发中迭代法更常用(空间效率更高)。

第四步:边界情况与易错点

常见边界场景

  • 空链表(head=null):返回 null,两种解法均能正确处理;

  • 单节点链表(head=1):返回 1,无需交换;

  • 奇数长度链表(head=1→2→3):交换前两个,第三个保留,结果为 2→1→3;

  • 偶数长度链表(head=1→2→3→4→5→6):结果为 2→1→4→3→6→5;

  • 头节点交换(核心边界):虚拟头节点完美处理,无需单独判断。

易错点提醒

  • 迭代法:

    1. 循环条件遗漏:忘记判断 prev.next.next != null,导致空指针异常(如只剩一个节点时);

    2. 指针调整顺序错误:先修改 node2.next 再修改 node1.next,会导致链表断裂;

    3. 前驱指针未前进: prev 未移动到 node1,导致无限循环交换同一组节点;

  • 递归法:

    1. 终止条件不全:遗漏 head.next == null,导致单节点时仍尝试交换,空指针异常;

    2. 递归参数错误:误将 swapPairs(node1.next) 写成 swapPairs(node2.next),拼接错误;

    3. 返回值错误:返回 node1 而非 node2,导致交换后的头节点错误;

  • 通用错误:

    1. 修改节点值:违背不能修改节点值的要求,面试中直接扣分;

    2. 创建新节点:题目要求复用原有节点,新节点会增加空间开销;

    3. 链表成环:重构 / 交换时未处理最后一个节点的 next,导致环的产生。

📝 总结

「两两交换链表中的节点」的解题核心是精准调整相邻节点的指针指向,从暴力到最优的思路递进清晰:

  1. 暴力解法:数组存储 + 交换 + 重构链表,直观但空间开销大,未体现指针操作精髓;

  2. 迭代法:虚拟头节点 + 前驱指针 + 逐组交换,O (n) 时间 + O (1) 空间,是实际开发和面试的首选;

  3. 递归法:分治思想 + 回溯拼接,代码简洁但空间复杂度 O (n),适合展示算法思维。

关键技巧

  • 迭代法:记住虚拟头节点→定义待交换节点→三步指针调整→前驱指针前进的固定步骤;

  • 递归法:明确终止条件(空 / 单节点),核心逻辑是先递归剩余链表,再交换当前节点

  • 通用技巧:指针调整的顺序是关键,需先保留后继节点的指向,再修改核心指针,避免链表断裂。

同类题扩展建议

掌握本题思路后,可以尝试这些进阶题目,都是其核心技巧的延伸:

  1. LeetCode 25. K 个一组翻转链表:本题的进阶版,按 K 个节点为一组翻转链表,核心是指针调整 + 分组处理;

  2. LeetCode 92. 反转链表 II:指定区间内反转链表,虚拟头节点 + 指针调整的经典应用;

  3. LeetCode 206. 反转链表 | 迭代 + 递归双解…:链表反转的基础题,指针调整的入门必学;

  4. LeetCode 143. 重排链表:链表节点重排,结合反转和拼接,指针操作的综合应用。

链表两两交换的核心是指针调整的顺序和逻辑,吃透迭代法的虚拟头节点 + 逐组交换技巧,能轻松应对大多数链表重排、反转类题目~