链表中的“舞伴交换”:两两交换链表中的节点
摘要:本文详解LeetCode 24题“两两交换链表中的节点”。通过“虚拟头节点”与“三指针移动法”,在不修改节点值的前提下,仅通过调整指针指向实现节点两两交换,助你掌握链表操作的精妙逻辑。
📚 核心知识点:指针重连的艺术
在处理链表节点交换问题时,我们最大的挑战在于:链表是单向的,一旦改变了指向,后面的节点可能会“丢失” 。
为了优雅地解决这个问题,我们需要两个核心技巧:
-
虚拟头节点(Dummy Node) :
- 由于头节点
head可能会被交换(变成第二个节点),我们需要一个永远不动的“哨兵”dummy指向头部,最后返回dummy.next即可。 - 它还能统一操作逻辑,避免对第一个节点进行特殊处理。
- 由于头节点
-
三指针/临时变量法:
-
交换两个相邻节点(比如
1->2变成2->1),我们需要操作三个位置的指针:- 前驱节点(prev) :指向交换对之前的那个节点。
- 节点1(first) :交换对的第一个。
- 节点2(second) :交换对的第二个。
-
为了防止链表断开,我们需要借助临时变量保存后续节点。
-
📝 题目解析:LeetCode 24. 两两交换链表中的节点
题目描述:
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(只能进行节点交换)。
示例:
输入:head = [1,2,3,4]
输出:[2,1,4,3]
💡 解题思路:断链重连四步走
想象一下,我们要把 1 -> 2 -> 3 变成 2 -> 1 -> 3。
我们需要一个指针 prev 站在 1 的前面(假设是 dummy)。
交换步骤图解:
-
定位:
prev指向dummy,first指向1,second指向2。 -
第一步(断后路) :
prev.next指向second(2)。- 链表变成:
dummy -> 2,后面断了。
- 链表变成:
-
第二步(存后路) :我们需要记住
2后面的3,所以用临时变量temp = second.next。 -
第三步(内部反转) :
second.next指向first(1)。- 链表变成:
2 -> 1。
- 链表变成:
-
第四步(接后路) :
first.next指向temp(3)。- 链表变成:
dummy -> 2 -> 1 -> 3。
- 链表变成:
-
移动指针:
prev移动到first(也就是现在的 1),准备处理下一对。
💻 代码实现(Python)
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def swapPairs(self, head: Optional[ListNode]) -> Optional[ListNode]:
# 1. 创建虚拟头节点,防止头节点被交换后丢失,也方便统一逻辑
dummy = ListNode(0, head)
# prev 指针始终指向“待交换对”的前一个节点
prev = dummy
# 2. 循环条件:必须保证后面至少有两个节点才能交换
# 如果 prev.next 为空,说明链表结束了
# 如果 prev.next.next 为空,说明只剩一个节点,无法交换
while prev.next and prev.next.next:
# 标记待交换的两个节点
first = prev.next # 节点 1
second = prev.next.next # 节点 2
# --- 核心交换逻辑 (4步变3步写法) ---
# 第一步:prev 指向 second (2)
prev.next = second
# 第二步:first (1) 指向 second 的后面 (3),注意这里要用 temp 或者 second.next
# 这里为了代码简洁,直接利用 second.next 指向 first
first.next = second.next
# 第三步:second (2) 指向 first (1)
second.next = first
# --- 交换完成,移动 prev 指针 ---
# prev 移动到这一对的末尾,也就是 first (1)
prev = first
# 返回真正的头节点
return dummy.next
📊 复杂度分析
| 维度 | 分析 |
|---|---|
| 时间复杂度 | O(N),其中 N 是链表的节点数量。我们只需要遍历一次链表。 |
| 空间复杂度 | O(1),只需要常数空间存储若干变量(dummy, prev, first, second)。 |
📌 迭代法总结
这道题是链表操作的基本功。
- 不要只交换值:题目明确要求交换节点,面试时如果直接交换
val会被扣分。 - 画图是关键:在纸上画出指针指向的变化,避免逻辑混乱。
- Dummy Node 永远的神:只要涉及头节点可能变动的操作,加上
dummy能让代码健壮性提升一个档次。
递归写法:换个维度的“乾坤大挪移”
摘要:承接迭代法,继续详解 LeetCode 24 题的递归解法。通过“宏观语义”思维,将复杂链表拆解为“处理当前一对 + 递归处理剩余部分”,代码量大幅减少,助你掌握递归在链表中的优雅应用。
🤔 为什么要用递归?
刚才我们讲的“迭代法”(也就是画图一步步移动指针),虽然逻辑直观,但代码写起来确实有点繁琐,需要小心处理 prev、next 等各种指针的指向,稍不留神链表就断了。
这时候,递归就登场了。递归的魅力在于**“宏观语义”**——你不需要关心具体的每一步怎么走,只需要告诉计算机:
- 这一层我要做什么(把前两个节点交换)。
- 剩下的事交给谁做(交给下一层递归)。
🚀 递归解题思路:大事化小
核心逻辑:
假设我们要交换 1 -> 2 -> 3 -> 4。
我们可以把这个问题拆解为:
- 交换前两个节点:把
1和2交换,变成2 -> 1。 - 处理剩下的节点:剩下的
3 -> 4怎么处理?其实和上面是一样的逻辑!我们只需要把1(现在的尾节点)指向“处理完剩下的链表后的新头节点”。
递归三部曲:
-
终止条件(Base Case) :
- 如果链表为空(
head == null),没法交换,直接返回null。 - 如果链表只有一个节点(
head.next == null),也没法交换,直接返回head。
- 如果链表为空(
-
当前层逻辑:
- 定义
firstNode为当前头节点(比如1)。 - 定义
secondNode为当前第二个节点(比如2)。 - 我们要让
secondNode指向firstNode,即2 -> 1。
- 定义
-
递归调用(Drill Down) :
firstNode的后面应该接什么呢?应该接“从secondNode.next开始的链表交换后的结果”。- 即:
firstNode.next = swapPairs(secondNode.next)。
💻 代码实现(Python 递归版)
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def swapPairs(self, head: Optional[ListNode]) -> Optional[ListNode]:
# 1. 终止条件:如果链表为空或只有一个节点,不需要交换,直接返回
if not head or not head.next:
return head
# 2. 定义当前要交换的两个节点
first_node = head # 第一个节点 (例如 1)
second_node = head.next # 第二个节点 (例如 2)
# 3. 递归调用:
# 让第一个节点指向“剩余链表交换后的新头节点”
# 比如:1 指向 swapPairs(3->4) 的结果
first_node.next = self.swapPairs(second_node.next)
# 4. 完成当前层的交换:
# 让第二个节点指向第一个节点 (2 -> 1)
second_node.next = first_node
# 5. 返回新的头节点
# 交换后,第二个节点变成了头节点,所以返回 second_node
return second_node
📊 图解递归过程
假设输入 1 -> 2 -> 3 -> 4:
-
第一层递归 (
1, 2) :first是 1,second是 2。- 执行
1.next = swapPairs(3->4)。此时程序暂停,进入下一层。
-
第二层递归 (
3, 4) :first是 3,second是 4。- 执行
3.next = swapPairs(null)。此时程序暂停,进入下一层。
-
第三层递归 (
null) :- 触发终止条件,返回
null。
- 触发终止条件,返回
-
回溯(归的过程) :
- 回到第二层:
3.next = null,4.next = 3。返回节点4(此时链表片段为4->3)。 - 回到第一层:
1.next = 4(接上了刚才返回的片段),2.next = 1。返回节点2。
- 回到第二层:
最终结果:2 -> 1 -> 4 -> 3。
📌 迭代 vs 递归:哪种更好?
| 维度 | 迭代法 (Iterative) | 递归法 (Recursive) |
|---|---|---|
| 代码复杂度 | 较高,需要维护多个指针 (prev, curr) | 极低,逻辑清晰,代码简洁 |
| 空间复杂度 | O(1),只需要常数空间 | O(N),递归调用栈会占用空间 |
| 理解难度 | 容易模拟,适合画图理解 | 需要抽象思维,理解“宏观语义” |
| 适用场景 | 链表很长时推荐使用 | 链表较短,追求代码优雅时使用 |
爱摸鱼的打工仔建议:
面试时,建议先写出迭代法(展示你对指针操作的扎实功底),然后告诉面试官:“其实这道题用递归写会更简洁,我可以演示一下吗?” 这样能展示你思维的多维性,绝对是加分项!