[链表] - 代码随想录 24. 两两交换链表中的节点
题目要求
给定一个单链表,需两两交换相邻节点 ,并返回交换后的链表头节点。
关键约束 :不能仅修改节点的val值,必须通过调整节点的指针关系完成交换(即实际交换节点位置)。
解题思路:虚拟头节点法
链表问题的核心痛点是头节点的特殊处理(如空链表、仅1个节点),而**虚拟头节点(Dummy Node)**是解决这类问题的「银弹」------通过创建一个指向原头节点的虚拟节点,将所有节点的处理逻辑统一。
本题的核心逻辑可总结为「追踪前驱→标记节点→三步交换→移动指针」:
- 初始化指针 :
- 创建
dummy节点(值为0),指向原链表头head,避免头节点的特殊判断; - 用
pre指针追踪每一对节点的前驱 (初始指向dummy),用于连接交换后的节点对。
- 创建
- 循环条件 :当
pre的下一个节点和下下个节点都存在时(即还有可交换的节点对),才进行交换。 - 三步交换 (核心!):
假设当前要交换的节点对是cur(pre.next)和nxt(pre.next.next),通过以下三步完成交换:pre直接指向nxt(让前驱连接第二节点,跳过第一节点);cur指向nxt.next(保存后续链表,避免断链);nxt指向cur(完成交换,第二节点变为当前对的第一个节点)。
- 移动指针 :交换完成后,将
pre移动到cur(当前对的第二个节点),为下一对节点的交换做准备。
实现代码(Java,带详细注释)
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; }
* }
*/
class Solution {
public ListNode swapPairs(ListNode head) {
// 1. 虚拟头节点:指向原链表头,统一所有节点的处理逻辑
ListNode dummy = new ListNode(0, head);
// pre指针:追踪当前要交换的节点对的「前驱」(初始指向dummy)
ListNode pre = dummy;
// 2. 循环条件:还有至少两个节点可交换(pre.next和pre.next.next都不为空)
while (pre.next != null && pre.next.next != null) {
// 标记当前要交换的两个节点:cur(第一节点)、nxt(第二节点)
ListNode cur = pre.next; // pre的下一个节点(交换前的第一节点)
ListNode nxt = pre.next.next; // pre的下下个节点(交换前的第二节点)
// 3. 三步交换:核心逻辑,调整指针关系
pre.next = nxt; // 步骤1:前驱连接第二节点(pre → nxt)
cur.next = nxt.next; // 步骤2:第一节点连接后续链表(cur → nxt.next)
nxt.next = cur; // 步骤3:第二节点连接第一节点(nxt → cur)
// 4. 移动pre指针:到当前对的第二节点(交换后的cur),为下一对做准备
pre = cur;
}
// 返回交换后的链表头:虚拟头节点的下一个节点(dummy.next)
return dummy.next;
}
}
复杂度分析
- 时间复杂度:O(n)。仅遍历链表一次,每个节点最多被访问两次(交换时的指针调整)。
- 空间复杂度 :O(1)。仅用常数个额外指针(
dummy、pre、cur、nxt),无额外空间开销。
关键说明
- 虚拟头节点的价值 :将「头节点交换」与「中间节点交换」的逻辑统一,避免写
if (head == null || head.next == null)的特殊判断。 pre指针的作用 :始终指向当前要交换节点对的前一个节点 ,确保交换后能重新连接链表(比如交换第1、2节点后,pre要移动到第2节点,才能处理第3、4节点)。
通过这种方法,所有边界情况(如空链表、1个节点、偶数个节点、奇数个节点)都能被优雅处理,是链表交换问题的「标准解法」。