Java版LeetCode热题100之「两两交换链表中的节点」详解

Java版LeetCode热题100之「两两交换链表中的节点」详解

本文约9200字,全面深入剖析 LeetCode 第24题《两两交换链表中的节点》。涵盖题目解析、递归与迭代两种解法、复杂度分析、面试高频问答、实际开发应用场景、相关题目推荐等,助你彻底掌握链表操作核心技巧。

一、原题回顾

题目描述:
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。
你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。

示例 1:

输入:head = [1,2,3,4] 输出:[2,1,4,3] 

示例 2:

输入:head = [] 输出:[] 

示例 3:

输入:head = [1] 输出:[1] 

提示:

  • 链表中节点的数目在范围 [0, 100]
  • 0 <= Node.val <= 100

二、原题分析

这道题要求我们成对地交换相邻节点,且不能通过交换 val 值来实现,必须通过调整指针完成真正的节点交换。

核心难点:

  1. 指针操作复杂:每交换一对节点,需要同时更新多个 next 指针;
  2. 边界条件多
    • 空链表(head == null
    • 单节点链表(无法交换)
    • 奇数长度链表(最后一个节点保持不动)
  3. 头节点可能变化:原 head 在交换后不再是头节点,需正确返回新头。

关键观察:

  • 每次操作涉及三个节点:前驱(用于连接)、当前对(node1, node2);
  • 交换后,原 node1 成为该对的尾节点,应指向下一组交换后的头节点;
  • 若使用哑节点(Dummy Node),可统一处理头节点变更问题。

三、答案构思

思路1:递归(自顶向下)

将问题分解为:

  • 交换前两个节点;
  • 递归处理剩余链表;
  • 将第一对与后续结果连接。

✅ 优点:代码简洁,逻辑清晰
❌ 缺点:空间复杂度 O(n),可能栈溢出(尽管 n≤100 安全)

思路2:迭代(自底向上)

使用循环,每次处理一对节点:

  • 维护一个 temp 指针,始终指向当前待处理对的前驱;
  • 交换 temp.nexttemp.next.next
  • 移动 temp 到下一对的前驱(即原 node1)。

✅ 优点:O(1) 空间,效率高,工业级推荐
❌ 缺点:指针操作稍显繁琐,需仔细画图

📌 共同技巧:引入“哑节点”
创建 dummyHead,其 next = head,确保无论是否交换头节点,都能通过 dummyHead.next 返回正确结果。

四、完整答案(Java 实现)

方法一:递归解法

/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */classSolution{publicListNodeswapPairs(ListNode head){// 递归终止条件:空链表或只剩一个节点if(head ==null|| head.next ==null){return head;}// 记录新的头节点(原第二个节点)ListNode newHead = head.next;// 递归处理后续节点,并将结果接在原 head 后面 head.next =swapPairs(newHead.next);// 将 newHead 指向原 head,完成交换 newHead.next = head;return newHead;}}

方法二:迭代解法(推荐!)

classSolution{publicListNodeswapPairs(ListNode head){// 创建哑节点,简化头节点处理ListNode dummyHead =newListNode(0); dummyHead.next = head;// temp 始终指向当前待交换对的前驱ListNode temp = dummyHead;// 只要后面还有至少两个节点,就继续交换while(temp.next !=null&& temp.next.next !=null){// 获取要交换的两个节点ListNode node1 = temp.next;ListNode node2 = temp.next.next;// 执行交换操作(关键三步) temp.next = node2;// 前驱指向 node2 node1.next = node2.next;// node1 指向 node2 的下一个 node2.next = node1;// node2 指向 node1// 移动 temp 到下一对的前驱(即当前对的尾节点) temp = node1;}return dummyHead.next;}}
强烈推荐迭代解法:空间效率高,无递归开销,更适合生产环境。

五、代码分析

1. 递归解法详解

[1,2,3,4] 为例:

  • 第一层:head=1, newHead=21.next = swapPairs(3)
  • 第二层:head=3, newHead=43.next = swapPairs(null) = null
  • 回溯:4.next = 3 → 返回 4
  • 回溯:1.next = 42.next = 1 → 返回 2
  • 最终:2 → 1 → 4 → 3

关键点

  • head.next = swapPairs(...):将原第一个节点的 next 指向后续交换后的头节点
  • newHead.next = head:完成当前对的交换。

2. 迭代解法指针操作(重点!)

交换前:temp → node1 → node2 → nextGroup...
交换后:temp → node2 → node1 → nextGroup...

三步操作顺序至关重要

  1. temp.next = node2
    → 先让前驱指向新头(否则会丢失 node2
  2. node1.next = node2.next
    → 让原第一个节点指向下一组(保存后续链表)
  3. node2.next = node1
    → 完成当前对的反转
⚠️ 错误顺序示例
若先执行 node2.next = node1,则 node2.next 被覆盖,无法获取 nextGroup

3. 哑节点的作用

  • 原链表:1 → 2 → 3 → 4
  • 加哑节点:dummy → 1 → 2 → 3 → 4
  • 交换后:dummy → 2 → 1 → 4 → 3
  • 返回 dummy.next2,无需特殊判断头节点是否变化。

六、时间复杂度和空间复杂度分析

方法时间复杂度空间复杂度是否修改节点值
递归O(n)O(n)❌(仅改指针)
迭代O(n)O(1)
其中 n 为链表节点数。
  • 时间:每个节点被访问一次,O(n)
  • 空间
    • 递归:调用栈深度 ≈ n/2 → O(n)
    • 迭代:仅用常数个指针 → O(1)
💡 工程建议:优先使用迭代,避免递归潜在风险(即使本题安全)。

七、常见问题解答(FAQ)

Q1:为什么不能直接交换 val

:题目明确要求“只能进行节点交换”。在真实系统中,节点可能包含大量数据(如用户对象),交换指针比复制数据高效得多。此外,某些场景下节点是不可变的(immutable),无法修改 val

Q2:迭代中 temp = node1 是什么意思?

:交换后,node1 成为当前对的尾节点,它自然就是下一对节点的前驱。例如:初始:dummy → 1 → 2 → 3 → 4交换第一对后:dummy → 2 → 1 → 3 → 4此时 temp 应指向 1,以便下一轮交换 34

Q3:如果链表长度为奇数怎么办?

:最后一节点自动保留。例如 [1,2,3][2,1,3]
因为循环条件是 temp.next != null && temp.next.next != null,当只剩一个节点时,循环终止。

Q4:能否用栈实现?

:可以,但效率低。遍历链表入栈,然后每次弹出两个节点重新连接。但空间 O(n),且需两次遍历,不推荐。

八、优化思路

1. 提前退出优化

在迭代开始前检查 head == null || head.next == null,直接返回。但收益微乎其微。

2. 减少临时变量

可合并部分操作,但会降低可读性,不建议。

3. 工程化增强

  • 添加日志:记录交换过程
  • 输入校验:虽然题目保证范围,但生产代码应防御性编程
  • 单元测试:覆盖空、单节点、偶数、奇数长度
// 示例:单元测试用例@TestvoidtestSwapPairs(){assertEquals(asList(2,1,4,3),toList(swapPairs(toListNode([1,2,3,4]))));assertEquals(Collections.emptyList(),toList(swapPairs(toListNode([]))));assertEquals(asList(1),toList(swapPairs(toListNode([1]))));}

九、数据结构与算法基础知识点回顾

1. 链表操作核心原则

  • 删除/插入:必须通过前驱节点操作
  • 反转:调整 next 指针方向
  • 交换:本质是指针重定向,非值交换

2. 哑节点(Dummy Node)

  • 作用:消除头节点特殊处理
  • 适用场景:任何可能改变头节点的链表操作(删除、反转、合并等)

3. 递归 vs 迭代

维度递归迭代
代码简洁性✅ 高❌ 中
空间效率❌ O(n)✅ O(1)
可读性✅ 直观❌ 需理解指针
栈溢出风险✅ 存在❌ 无

4. 指针操作黄金法则

  • 先保存:在断开指针前,保存需要的引用
  • 再连接:按依赖顺序更新指针
  • 画图辅助:复杂操作务必画图验证

十、面试官提问环节(模拟)

❓ 问题1:你的迭代解法中,为什么需要三个指针?

回答
实际上我们用了四个引用:temp, node1, node2, 以及隐式的 node2.next
原因是:要完成 A→B→C→DA→C→B→D 的转换,必须同时知道:A(前驱,用于连接 CB(原第一个,需指向 DC(原第二个,需指向 BD(后续链表头)
缺少任何一个,都会导致链表断裂。

❓ 问题2:如果要求每 k 个节点一组反转,你会怎么做?

回答
这是 LeetCode 第25题《K 个一组翻转链表》。
思路:先检查后续是否有 k 个节点;若有,反转这 k 个节点(可用迭代反转子链表);递归或迭代处理下一组;连接各组。
本题是 k=2 的特例。

❓ 问题3:你的解法能处理环形链表吗?

回答
不能,且题目假设链表无环。
若存在环,递归会无限调用导致栈溢出;迭代会陷入死循环。
实际工程中,应先用快慢指针检测环(Floyd 算法),再处理。

❓ 问题4:为什么不用 Collections.swap()?

回答
Collections.swap() 适用于 List 接口(如 ArrayList),但链表(LinkedList)虽实现 List,其 swap 本质仍是交换值,而非节点。
且本题输入是 ListNode 自定义结构,非 Java 集合。

十一、这道算法题在实际开发中的应用

虽然“两两交换”看似抽象,但其思想在实际系统中广泛应用:

1. 网络协议中的数据包重组

  • 某些协议要求数据包成对处理(如 ACK 机制)
  • 链表可表示数据包队列,交换操作模拟重排

2. 音频/视频帧处理

  • 音频采样点或视频帧有时需成对交换以实现特效(如立体声左右声道互换)
  • 若用链表存储帧,此操作可高效完成

3. 任务调度队列

  • 某些调度策略要求交替执行两类任务(A-B-A-B → B-A-B-A)
  • 通过交换队列中相邻任务实现策略切换

4. 链表操作的通用训练

  • 本题是指针操作的经典练习
  • 掌握后可轻松应对更复杂的链表变形(如反转、合并、分隔)
💡 核心价值:培养对内存布局指针语义的直觉,这是系统编程的基础。

十二、相关题目推荐

掌握本题后,可挑战以下 LeetCode 题目:

题号题目关联点
206. 反转链表链表基本操作指针重定向
92. 反转链表 II区间反转哑节点 + 指针操作
25. K 个一组翻转链表本题扩展分组处理
143. 重排链表找中点 + 反转 + 合并综合应用
86. 分隔链表哑节点链表分隔
328. 奇偶链表重排指针分离
建议按顺序练习,逐步提升链表操作能力。

十三、总结与延伸

✅ 本题核心收获

  1. 链表交换的本质:通过指针重定向实现节点位置变更,而非值交换;
  2. 哑节点的威力:统一处理头节点变更,避免大量 if-else;
  3. 递归 vs 迭代:理解两种范式的适用场景,优先选择空间高效的迭代;
  4. 指针操作顺序:必须“先保存,再连接”,画图是避免 bug 的最佳实践。

🔮 延伸思考

  • 双向链表如何交换?
    需额外维护 prev 指针,操作更复杂,但逻辑类似。
  • 能否原地交换而不使用哑节点?
    可以,但需单独处理头节点,代码冗余且易错。
  • 函数式解法?
    在支持模式匹配的语言(如 Scala)中,可用递归+模式匹配优雅实现。

🌟 最后建议

  • 手写代码:在白板上写出迭代解法,确保指针操作无误;
  • 讲清思路:面试时先说“我打算用哑节点+迭代”,再解释三步交换;
  • 主动测试:提出测试 [1,2,3][1,2][] 等 case,展现工程素养。

“链表题,指针是笔,内存是纸,画图是草稿。”
掌握本题,你就拥有了处理复杂链表变形的钥匙。继续前行,算法之路越走越宽!

Read more

【数据结构】感受递归暴力美学:链式二叉树全方位剖析(附源码)

【数据结构】感受递归暴力美学:链式二叉树全方位剖析(附源码)

🔥 @晨非辰Tong: 个人主页 👀专栏:《C语言》、《数据结构与算法入门指南》 💪学习阶段:C语言、数据结构与算法初学者 ⏳“人理解迭代,神理解递归。” 文章目录 * **引言** * 一、介绍链式二叉树 * 1.1 概念 * 1.2 基本结构(结构上的递归性) * 二、遍历接口实现(操作上的递归性) * 2.1 前序遍历(根左右) * 1. 概念 * 2. 代码 * 2.2 中序遍历(左根右) * 1. 概念 * 2.代码 * 2.3 后序遍历 * 1. 概念 * 2. 代码 * 三、其余接口实现(操作上的递归性)

【C++ 刷题入门】图解算法与数据结构 85 题:图书整理I(反转链表)——从C到C++的实战学习

1. 前言 1.1学习背景 大家好!我是刚学完 C 语言、正通过算法题过渡到 C++ 的编程新手。为了系统掌握 C++ 特性与算法基础,我开启了《图解算法与数据结构》85 道经典入门题的刷题计划,这是我的第 1 篇实战笔记。 本篇笔记会完整记录从「C 语言朴素思路」到「C++ 特性优化」的思考过程,同时对比标准的链表解法,帮和我一样的初学者避坑、吃透核心知识点~ 2.2本篇目标吃透「反转链表」的基础算法逻辑;掌握 C++ 中 string、vector、迭代器等核心特性的实战用法;对比「数组解法」与「链表递归解法」的差异,理解算法题的「考察核心」与「实现便捷性」

剑指offer第2版:链表系列

剑指offer第2版:链表系列

一、p58-JZ6 从尾到头打印链表(递归/栈) 从尾到头打印链表_牛客题霸_牛客网  解法1、递归,每访问一个节点时,先递归输出它后面的节点,再输出该节点自身,但是这样的话可能导致函数的调用层级很深,从而导致函数调用栈溢出。 class Solution { public: void print(vector<int>&ret,ListNode* head){ if(head==nullptr) return; print(ret,head->next); ret.emplace_back(head->val); } vector<int> printListFromTailToHead(ListNode* head)

数据结构——链表

数据结构——链表

目录 链表基础知识 链表的概念 基本特点 分类 基本操作(接口) 单链表的实现 定义 创建新节点 增、删、查、改操作 双链表的实现 定义 初始化(创造哨兵节点) 增、删、查、改操作 顺序表和链表的区别  轻松无痛玩转链表 链表基础知识 链表的概念 链表是一种常见的数据结构,用于线性方式存储数据。链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。 基本特点 1. 动态大小:链表的大小可以在运行时动态变化,不需要在创建时指定固定的大小。 2. 元素连接:每个元素(通常称为节点)存储数据,并包含一个或多个指针指向列表中的其他节点。 3. 无需连续内存:由于元素通过指针连接,它们不需要在内存中连续存放,这使得内存使用更加灵活。 4. 插入和删除操作:在链表中插入或删除节点通常比数组更快,