🔗 题目链接:24. 两两交换链表中的节点 - 力扣(LeetCode)
🔍 难度:中等 | 🏷️ 标签:链表、递归、迭代、指针操作
⏱️ 目标时间复杂度:O(n)
💾 空间复杂度:O(n)(递归)、O(1)(迭代)
题目分析
💬 给你一个单向链表,要求两两交换相邻的节点,并返回新的头节点。
❌ 不能修改节点的值,只能通过调整指针实现“交换”。
✅ 示例输入[1,2,3,4]→ 输出[2,1,4,3]
关键点提炼:
- 每两个节点为一组进行交换;
- 若链表长度为奇数,则最后一个节点保持不变;
- 必须原地操作,不额外创建节点;
- 考察对链表结构的理解和指针重连能力;
- 是后续更难题目如「K个一组翻转链表」的基础。
核心算法及代码讲解
本题有两种经典解法:
✅ 递归法:利用函数调用栈自然处理子问题;
✅ 迭代法:使用哑结点 + 双指针模拟交换过程。
我们先从递归入手,理解其背后的思想逻辑。
🔁 递归核心思想(自顶向下)
ListNode* swapPairs(ListNode* head) {
if(head == nullptr || head->next == nullptr){
return head;
}
ListNode* newHead = head->next; // 新头是原第二个节点
head->next = swapPairs(newHead->next); // 递归处理剩余部分
newHead->next = head; // 原第一个接在新头后
return newHead;
}
🧠 分步解析:
| 步骤 | 说明 | ||
|---|---|---|---|
| 1. `if(head == nullptr | head->next == nullptr)` | 边界条件:空链表或只有一个节点时无法交换,直接返回 | |
2. ListNode* newHead = head->next; | 记录新头节点(即当前节点的下一个),这是交换后的头 | ||
3. head->next = swapPairs(newHead->next); | 递归处理剩下的链表(从 newHead->next 开始),返回的是这部分交换后的头 | ||
4. newHead->next = head; | 把原第一个节点接到新头之后,完成局部交换 | ||
5. return newHead; | 返回新链表的头 |
🌀 这种写法本质是“分治 + 指针重连”:把大问题拆成小问题,解决后再拼接。
⚠️ 注意事项:
- 递归深度等于链表长度的一半左右;
- 空间复杂度为 O(n),因为每次递归都会压栈;
- 面试中若被追问空间优化,应主动提出迭代方案。
🔄 迭代核心思想(自底向上)
使用**哑结点(dummy head)**避免对头节点特殊处理。
ListNode* swapPairs(ListNode* head) {
ListNode* dummyHead = new ListNode(0);
dummyHead->next = head;
ListNode* temp = dummyHead;
while (temp->next != nullptr && temp->next->next != nullptr) {
ListNode* node1 = temp->next;
ListNode* node2 = temp->next->next;
temp->next = node2;
node1->next = node2->next;
node2->next = node1;
temp = node1;
}
ListNode* ans = dummyHead->next;
delete dummyHead;
return ans;
}
🧠 分步解析:
| 步骤 | 说明 |
|---|---|
1. 创建 dummyHead 并连接到 head | 解决头节点需要特殊处理的问题 |
2. temp 指向当前待交换的前一个节点 | 初始为 dummyHead |
| 3. 循环判断是否有两个连续节点 | temp->next && temp->next->next |
4. 定义 node1, node2 | 分别表示要交换的两个节点 |
5. temp->next = node2; | 将 temp 的下一个指向 node2(即新位置) |
6. node1->next = node2->next; | 断开 node1 和 node2 的连接,指向后面的节点 |
7. node2->next = node1; | 将 node2 指向 node1,完成交换 |
8. temp = node1; | 移动到下一对的前驱节点 |
9. 最终返回 dummyHead->next | 即新链表的头节点 |
✅ 优势:空间复杂度 O(1),适合大规模数据;
❌ 缺点:需手动管理哑结点内存(C++ 中需delete)。
解题思路
方法一:递归法(推荐用于理解)
- 终止条件:链表为空或只有一个节点 → 不交换,返回原链表。
- 递归关系:
- 先交换当前两个节点;
- 再递归处理后面的部分;
- 最后将前后两段拼接起来。
- 关键技巧:
- 用
swapPairs(newHead->next)处理后续部分; - 返回
newHead作为新的头节点。
- 用
🎯 类比:就像剥洋葱,一层一层往下走,直到最内层再逐层往上还原。
方法二:迭代法(推荐用于面试)
- 引入哑结点:简化边界处理;
- 使用双指针:
temp控制当前位置,node1/node2表示待交换节点; - 循环交换:每轮交换两个节点,并移动
temp到下一组的前驱; - 释放资源:记得删除哑结点以防止内存泄漏。
💡 提示:面试官常会问:“有没有办法不用递归?” → 此时你应该立刻说出迭代解法!
算法分析
| 方法 | 时间复杂度 | 空间复杂度 | 是否推荐 |
|---|---|---|---|
| 递归 | O(n) | O(n) | ✅ 理解用 |
| 迭代 | O(n) | O(1) | ✅ 面试用 |
📈 为什么递归空间是 O(n)?
因为每次递归调用都占用栈空间,最多调用 n/2 次 → 栈深 ≈ n/2 → O(n)
🔍 实际场景选择建议:
- 如果允许牺牲空间换取简洁性,可用递归;
- 如果强调效率与稳定性(如生产环境),优先选迭代。
代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
// Definition for singly-linked list.
struct ListNode {
int val;
ListNode *next;
ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x, ListNode *next) : val(x), next(next) {}
};
class Solution {
public:
// 方法一:递归法
ListNode* swapPairs(ListNode* head) {
// 判空:若为空或只有一个节点,无法交换
if(head == nullptr || head->next == nullptr){
return head;
}
// 新头是原第二个节点
ListNode* newHead = head->next;
// 递归处理剩余部分:head->next 接上交换后的结果
head->next = swapPairs(newHead->next);
// 新头的下一个指向原头节点,完成交换
newHead->next = head;
return newHead;
}
// 方法二:迭代法(推荐)
ListNode* swapPairsIterative(ListNode* head) {
// 创建哑结点,方便统一处理头节点
ListNode* dummyHead = new ListNode(0);
dummyHead->next = head;
ListNode* temp = dummyHead;
// 当还有至少两个节点可交换时继续
while (temp->next != nullptr && temp->next->next != nullptr) {
ListNode* node1 = temp->next; // 第一个节点
ListNode* node2 = temp->next->next; // 第二个节点
// 交换:temp -> node2 -> node1
temp->next = node2;
node1->next = node2->next;
node2->next = node1;
// 移动 temp 到下一组的前驱
temp = node1;
}
// 获取结果并清理内存
ListNode* ans = dummyHead->next;
delete dummyHead;
return ans;
}
};
// 测试
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
// 构造测试链表 [1,2,3,4]
ListNode* head = new ListNode(1);
head->next = new ListNode(2);
head->next->next = new ListNode(3);
head->next->next->next = new ListNode(4);
Solution sol;
ListNode* result = sol.swapPairs(head);
// 打印结果
while(result != nullptr){
cout << result->val << " ";
result = result->next;
}
cout << endl;
return 0;
}
🛠️ 测试说明:
- 输入:
[1,2,3,4]- 输出:
2 1 4 3- 验证了递归法正确性(也可测试迭代法)
🌟 本期完结,下期见!🔥
👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!
💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪
📣 下一期预告:LeetCode 热题 100 第31题 —— 25.K 个一组翻转链表(困难)
🔹 题目:给定一个链表,将其每 K 个节点为一组进行翻转,返回翻转后的链表。
🔹 核心思路:基于本题的“两两交换”,扩展为“K个翻转”,需结合递归 + 指针重连 + 长度统计。
🔹 考点:链表操作、递归思维、边界处理、复杂度控制。
🔹 难度:困难,是链表类题目的巅峰之一,常出现在字节、腾讯、阿里等大厂面试中。
🔹 提示:不要暴力遍历!要学会用“断开-翻转-连接”的三步法!
💡 提示:可以先尝试用递归实现,再优化为迭代,掌握通用模板!
📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!
👉 下一期我们将挑战「K 个一组翻转链表」,带你打通链表操作的最后一公里!🚀