一、题目是啥?一句话说清
给定一个链表,交换每两个相邻的节点(不能只改值,要实际交换节点),并返回交换后的链表。
示例:
- 输入:1 → 2 → 3 → 4
- 输出:2 → 1 → 4 → 3
二、解题核心
使用虚拟头节点简化操作,然后遍历链表,每次交换两个相邻节点,并正确更新指针以保持链表连接。 这就像排队时,每两个人互换位置,但要注意换完后重新连接好队伍,不能断开。
三、关键在哪里?(3个核心点)
想理解并解决这道题,必须抓住以下三个关键点:
1. 虚拟头节点(Dummy Node)的使用
- 是什么:在原始链表前添加一个不存储实际数据的节点。
- 为什么重要:交换后头节点可能改变(例如原来头节点是1,交换后变成2),使用虚拟头节点可以避免单独处理头节点变化的特殊情况。
2. 指针操作的顺序
- 是什么:交换节点时,需要调整多个指针的指向顺序。
- 为什么重要:如果指针操作顺序错误,容易导致链表断开或形成环。正确的顺序是:先记录要交换的两个节点,然后让前驱节点指向第二个节点,再让第一个节点指向第二个节点的下一个,最后让第二个节点指向第一个节点。
3. 循环条件的控制
- 是什么:循环需要继续的条件是当前节点后面至少还有两个节点可以交换。
- 为什么重要:如果链表长度是奇数,最后一个节点不需要交换;如果链表为空或只有一个节点,则不需要任何操作。
四、看图理解流程(通俗理解版本)
让我们用链表 1 → 2 → 3 → 4 的例子来可视化过程:
-
初始化:
- 创建虚拟头节点
dummy,其 next 指向头节点 1。 - 设置当前指针
curr指向dummy。 - 初始状态:dummy → 1 → 2 → 3 → 4
- 创建虚拟头节点
-
第一轮交换(交换1和2):
- 记录第一个要交换的节点
first = curr.next(1) - 记录第二个要交换的节点
second = curr.next.next(2) - 执行交换:
curr.next = second(dummy现在指向2)first.next = second.next(1现在指向3)second.next = first(2现在指向1)
- 交换后链表:dummy → 2 → 1 → 3 → 4
- 移动
curr到first(即交换后的第一个节点1),因为下一轮要交换1后面的节点
- 记录第一个要交换的节点
-
第二轮交换(交换3和4):
- 现在
curr指向节点1 - 记录
first = curr.next(3) - 记录
second = curr.next.next(4) - 执行交换:
curr.next = second(1现在指向4)first.next = second.next(3现在指向null)second.next = first(4现在指向3)
- 交换后链表:dummy → 2 → 1 → 4 → 3
- 移动
curr到first(即节点3)
- 现在
-
结束:
curr后面没有两个节点了,循环结束。- 返回
dummy.next,即新头节点2。
五、C++ 代码实现(附详细注释)
#include <iostream>
using namespace std;
// 链表节点定义
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) {
// 创建虚拟头节点,简化操作
ListNode* dummy = new ListNode(0);
dummy->next = head;
ListNode* curr = dummy; // 当前指针,指向要交换的两个节点的前一个节点
// 循环条件:当前节点后面至少有两个节点才交换
while (curr->next != nullptr && curr->next->next != nullptr) {
// 记录要交换的两个节点
ListNode* first = curr->next;
ListNode* second = curr->next->next;
// 执行交换操作
curr->next = second; // 前驱节点指向第二个节点
first->next = second->next; // 第一个节点指向第二个节点的下一个
second->next = first; // 第二个节点指向第一个节点,完成交换
// 移动当前指针到交换后的第一个节点(为下一轮交换做准备)
curr = first;
}
// 返回新链表的头节点
ListNode* newHead = dummy->next;
delete dummy; // 释放虚拟头节点内存
return newHead;
}
};
// 辅助函数:打印链表
void printList(ListNode* head) {
while (head != nullptr) {
cout << head->val << " ";
head = head->next;
}
cout << endl;
}
// 测试代码
int main() {
// 创建示例链表:1->2->3->4
ListNode* head = new ListNode(1, new ListNode(2, new ListNode(3, new ListNode(4))));
Solution solution;
ListNode* result = solution.swapPairs(head);
printList(result); // 输出:2 1 4 3
// 释放内存(实际面试中可能不需要完整释放)
return 0;
}
六、注意事项
- 空链表和单节点链表:如果链表为空或只有一个节点,直接返回原链表,不需要交换。
- 指针操作顺序:交换时一定要先记录要交换的节点,再调整指针,否则容易丢失节点引用。
- 循环后移动当前指针:交换完成后,当前指针要移动到交换后的第一个节点(即原来的第一个节点),以便进行下一轮交换。
- 内存管理:在C++中使用了
new,记得删除虚拟头节点,避免内存泄漏(面试中有时可忽略)。
七、总结
理解此题的关键在于:
- 使用虚拟头节点:简化头节点变化的处理。
- 掌握指针操作顺序:正确调整节点间的连接关系。
- 控制循环条件:确保只有后面有两个节点时才交换。
这道题考察了链表的基本操作和指针运用,是面试中常见的题目。多练习几次,注意指针操作的顺序,就能熟练掌握。