本文最后更新于 2026-01-18,文章内容可能已经过时。
「两两交换链表中的节点」是 LeetCode 中经典的中等难度题目,也是链表指针操作的核心考题。这道题的关键痛点是:如何在不创建新节点的前提下,通过调整指针指向完成相邻节点的两两交换,同时处理好链表首尾的边界情况 —— 核心考察对链表指针指向关系的精准把控能力。
实际开发中,类似链表节点重排、数据分片交换等场景,都能用到它的核心逻辑。今天我们从最直观的暴力思路出发,一步步优化到迭代和递归两种最优解,带你吃透链表两两交换的实现精髓~
📌 题目重述
给你一个单链表的头节点 head,请你两两交换链表中相邻的节点,并返回交换后链表的头节点。要求:不能修改节点的值(仅能调整指针指向),且不能创建新节点(只能复用原有节点)。
举个例子:
🚶 阶梯思路拆解
第一步:暴力思路(数组存储 + 重构链表)🥾
最直观的想法是:先把链表的所有节点存入数组,然后遍历数组两两交换元素的位置,最后用交换后的数组重构新链表。这种方法无需复杂的指针操作,适合新手理解,但需要额外的 O (n) 空间(n 为链表长度),且违背不创建新节点的隐含最优要求。
💡 核心逻辑
遍历链表,将所有节点按顺序存入数组;
遍历数组,两两交换相邻元素的位置(索引 0 和 1、2 和 3……);若数组长度为奇数,最后一个元素不交换;
若数组为空,返回 null;
重构链表:将数组中交换后的节点依次拼接,最后一个节点的 next 设为 null;
返回新链表的头节点(数组第一个元素)。
📊 图文演示(以 head= 1→2→3→4 为例)
(如图所示)暴力重构过程:
✅ 代码实现(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 (1) 额外空间的最优要求;
逻辑冗余:明明可以直接调整指针完成交换,却绕路存储和重构,效率低;
隐含风险:重构链表时若忘记将最后一个节点的 next 置空,可能导致链表成环;
违背题意精髓:题目核心考察指针操作,该方法未体现任何链表指针调整的技巧。
第二步:最优解法(迭代法 / 虚拟头节点)🔑
这是本题的最优解,也是面试中最常考的解法:利用虚拟头节点简化头节点交换的边界处理,通过前驱指针逐个遍历链表,依次调整相邻两个节点的指针指向,完成两两交换 —— 仅需一次遍历,O (1) 额外空间,且完全复用原有节点。
💡 核心逻辑
创建虚拟头节点(dummy):指向 head,避免单独处理头节点交换的情况;
初始化前驱指针: prev = dummy(始终指向待交换两个节点的前一个节点);
循环交换( prev.next != null && prev.next.next != null):
定义待交换的两个节点: node1 = prev.next, node2 = prev.next.next;
调整指针完成交换:
prev.next = node2(前驱指向第二个节点);
node1.next = node2.next(第一个节点指向第二个节点的后继);
node2.next = node1(第二个节点指向第一个节点);
前驱指针前进: prev = node1(移动到已交换的第二个节点,作为下一组的前驱);
返回 dummy.next(交换后链表的真实头节点)。
📊 图文演示(以 head= 1→2→3→4 为例)
(如图所示)迭代交换过程:
✅ 代码实现(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 (1) 额外空间,完全符合最优解要求;
时间高效:一次遍历完成所有交换,无冗余操作;
边界处理简洁:虚拟头节点避免了头节点交换的特殊判断;
逻辑严谨:指针调整步骤清晰,完全复用原有节点,无内存泄漏风险。
第三步:进阶思路(递归法)🌀
递归法的核心是分治思想:将链表的两两交换拆解为交换当前两个节点 + 递归交换剩余链表。这种方法无需手动管理前驱指针,代码简洁优雅,但需要理解递归的终止条件和回溯拼接逻辑。
💡 核心逻辑
递归终止条件:
若 head null(空链表)或 head.next null(仅有一个节点),返回 head;
递归处理:
定义待交换的两个节点: node1 = head, node2 = head.next;
递归交换剩余链表: node1.next = swapPairs(node2.next);
调整当前两个节点的指针: node2.next = node1;
返回交换后的当前组头节点(node2)。
📊 图文演示(以 head= 1→2→3→4 为例)
(如图所示)递归交换过程:
递归调用: swapPairs(1) → 待交换 node1=1、node2=2 → 递归 swapPairs(3);
递归调用: swapPairs(3) → 待交换 node1=3、node2=4 → 递归 swapPairs(null);
递归终止: swapPairs(null) 返回 null;
回溯拼接:
swapPairs (3):node1 (3).next = null → node2 (4).next = 3 → 返回 4;
swapPairs (1):node1 (1).next = 4 → node2 (2).next = 1 → 返回 2;
最终结果: 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;
}
}⚙️ 复杂度分析
✨ 核心优势与注意点
优势:代码极简,无需虚拟头节点和循环,体现分治思想,面试中展示算法思维加分;
注意点:
空间复杂度高于迭代法(递归栈开销);
递归深度过大会导致栈溢出(但题目中链表长度 ≤ 100,可安全使用);
指针拼接顺序易错:需先递归处理剩余链表,再调整当前节点指针;
适用场景:适合面试中展示递归思维,实际开发中迭代法更常用(空间效率更高)。
第四步:边界情况与易错点
常见边界场景
空链表(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;
头节点交换(核心边界):虚拟头节点完美处理,无需单独判断。
易错点提醒
迭代法:
循环条件遗漏:忘记判断 prev.next.next != null,导致空指针异常(如只剩一个节点时);
指针调整顺序错误:先修改 node2.next 再修改 node1.next,会导致链表断裂;
前驱指针未前进: prev 未移动到 node1,导致无限循环交换同一组节点;
递归法:
终止条件不全:遗漏 head.next == null,导致单节点时仍尝试交换,空指针异常;
递归参数错误:误将 swapPairs(node1.next) 写成 swapPairs(node2.next),拼接错误;
返回值错误:返回 node1 而非 node2,导致交换后的头节点错误;
通用错误:
修改节点值:违背不能修改节点值的要求,面试中直接扣分;
创建新节点:题目要求复用原有节点,新节点会增加空间开销;
链表成环:重构 / 交换时未处理最后一个节点的 next,导致环的产生。
📝 总结
「两两交换链表中的节点」的解题核心是精准调整相邻节点的指针指向,从暴力到最优的思路递进清晰:
暴力解法:数组存储 + 交换 + 重构链表,直观但空间开销大,未体现指针操作精髓;
迭代法:虚拟头节点 + 前驱指针 + 逐组交换,O (n) 时间 + O (1) 空间,是实际开发和面试的首选;
递归法:分治思想 + 回溯拼接,代码简洁但空间复杂度 O (n),适合展示算法思维。
关键技巧
迭代法:记住虚拟头节点→定义待交换节点→三步指针调整→前驱指针前进的固定步骤;
递归法:明确终止条件(空 / 单节点),核心逻辑是先递归剩余链表,再交换当前节点;
通用技巧:指针调整的顺序是关键,需先保留后继节点的指向,再修改核心指针,避免链表断裂。
同类题扩展建议
掌握本题思路后,可以尝试这些进阶题目,都是其核心技巧的延伸:
LeetCode 25. K 个一组翻转链表:本题的进阶版,按 K 个节点为一组翻转链表,核心是指针调整 + 分组处理;
LeetCode 92. 反转链表 II:指定区间内反转链表,虚拟头节点 + 指针调整的经典应用;
LeetCode 206. 反转链表 | 迭代 + 递归双解…:链表反转的基础题,指针调整的入门必学;
LeetCode 143. 重排链表:链表节点重排,结合反转和拼接,指针操作的综合应用。
链表两两交换的核心是指针调整的顺序和逻辑,吃透迭代法的虚拟头节点 + 逐组交换技巧,能轻松应对大多数链表重排、反转类题目~