一、题目是啥?一句话说清
给你一个链表和两个整数 left 和 right,反转从第 left 个节点到第 right 个节点的子链表,并返回反转后的链表。其他部分保持不变。
示例:
- 输入:head = [1,2,3,4,5], left = 2, right = 4
- 输出:[1,4,3,2,5](反转了从第2到第4个节点)
二、解题核心
使用哑节点简化操作,找到要反转子链表的前一个节点,断开子链表,反转它,然后重新连接回原链表。 这就像把链表的一段剪下来,反转后再缝回去。
三、关键在哪里?(3个核心点)
想理解并解决这道题,必须抓住以下三个关键点:
1. 哑节点(Dummy Node)的使用
- 是什么:在链表头部添加一个哑节点,其 next 指向头节点。
- 为什么重要:当 left 为 1 时,头节点会被反转,哑节点可以避免处理头节点变化的特殊情况,使代码更统一。
2. 找到关键节点
- 是什么:找到要反转子链表的前一个节点(pre)、子链表的开始节点(start)和结束节点(end)。
- 为什么重要:只有准确找到这些节点,才能正确断开和连接子链表。
3. 反转子链表并重新连接
- 是什么:断开子链表后,反转它,然后将反转后的子链表头连接到 pre 的 next,将反转后的子链表尾连接到原链表的后续节点。
- 为什么重要:如果连接错误,链表会断开或形成环。
四、看图理解流程(通俗理解版本)
让我们用链表 1 → 2 → 3 → 4 → 5 和 left=2, right=4 的例子来可视化过程:
-
初始化:
- 创建哑节点 dummy,其 next 指向头节点 1。
- 初始状态:dummy → 1 → 2 → 3 → 4 → 5
-
找到 pre 节点:
- pre 节点是反转部分的前一个节点,即第 left-1 个节点。这里 left=2,所以 pre 是第 1 个节点,即节点 1。
- 从 dummy 移动 left-1=1 步,pre 指向节点 1。
-
找到 start 和 end 节点:
- start 节点是 pre 的 next,即节点 2。
- end 节点是反转部分的最后一个节点,从 start 移动 right-left=2 步,即节点 4。
-
断开子链表:
- 记录 end 的下一个节点 succ,即节点 5。
- 将 end 的 next 设为 null,断开子链表:子链表为 2 → 3 → 4 → null
-
反转子链表:
- 反转子链表:4 → 3 → 2 → null
- 反转后的头节点是 4,尾节点是 2。
-
重新连接:
- 将 pre 的 next(即节点 1 的 next)指向反转后的头节点 4。
- 将反转后的尾节点 2 的 next 指向 succ(节点 5)。
- 最终链表:dummy → 1 → 4 → 3 → 2 → 5
-
返回结果:返回 dummy.next,即节点 1。
五、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* reverseBetween(ListNode* head, int left, int right) {
// 如果链表为空或 left 等于 right,不需要反转
if (head == nullptr || left == right) {
return head;
}
// 创建哑节点,简化操作
ListNode dummy(0);
dummy.next = head;
ListNode* pre = &dummy; // pre 指向反转部分的前一个节点
// 移动 pre 到第 left-1 个节点
for (int i = 0; i < left - 1; i++) {
pre = pre->next;
}
// start 是反转部分的开始节点
ListNode* start = pre->next;
ListNode* end = start;
// 移动 end 到反转部分的结束节点
for (int i = 0; i < right - left; i++) {
end = end->next;
}
// 记录结束节点的下一个节点
ListNode* succ = end->next;
end->next = nullptr; // 断开子链表
// 反转子链表
ListNode* reversedHead = reverseList(start);
// 重新连接链表
pre->next = reversedHead; // pre 指向反转后的头节点
start->next = succ; // 反转后的尾节点(原 start)指向 succ
return dummy.next;
}
private:
// 反转链表的辅助函数
ListNode* reverseList(ListNode* head) {
ListNode* prev = nullptr;
ListNode* curr = head;
while (curr != nullptr) {
ListNode* next = curr->next;
curr->next = prev;
prev = curr;
curr = next;
}
return prev;
}
};
// 辅助函数:打印链表
void printList(ListNode* head) {
while (head != nullptr) {
cout << head->val << " ";
head = head->next;
}
cout << endl;
}
// 测试代码
int main() {
// 创建示例链表:1->2->3->4->5
ListNode* head = new ListNode(1);
head->next = new ListNode(2);
head->next->next = new ListNode(3);
head->next->next->next = new ListNode(4);
head->next->next->next->next = new ListNode(5);
Solution solution;
ListNode* result = solution.reverseBetween(head, 2, 4);
printList(result); // 输出:1 4 3 2 5
// 释放内存(实际面试中可能不需要完整释放)
return 0;
}
六、注意事项
- 边界检查:确保 left 和 right 在链表长度范围内,且 left <= right。题目已保证 left <= right,但仍需检查链表是否为空。
- 指针移动:在移动 pre 和 end 指针时,确保不会越界。由于 left 和 right 有效,通常不会越界。
- 反转函数:反转子链表时,使用迭代法,时间复杂度为 O(n),空间复杂度为 O(1)。
- 连接顺序:反转后,原 start 节点成为尾节点,其 next 应指向 succ,以保持链表完整。
- 内存管理:在 C++ 中,没有分配新节点,只是重新连接现有节点,所以不需要额外内存管理。
七、总结
理解此题的关键在于:
- 使用哑节点:处理头节点可能被反转的情况。
- 准确找到关键节点:pre、start、end 和 succ 节点。
- 正确断开和连接:反转子链表后,确保链表不断开。
掌握这三点,你就能高效解决反转链表Ⅱ问题。这道题是链表操作的经典题目,考察了对链表遍历和指针操作的理解。多练习几次,注意细节,就能熟练运用。