【算法】92.翻转链表Ⅱ--通俗讲解

89 阅读5分钟

一、题目是啥?一句话说清

给你一个链表和两个整数 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 的例子来可视化过程:

  1. 初始化

    • 创建哑节点 dummy,其 next 指向头节点 1。
    • 初始状态:dummy → 1 → 2 → 3 → 4 → 5
  2. 找到 pre 节点

    • pre 节点是反转部分的前一个节点,即第 left-1 个节点。这里 left=2,所以 pre 是第 1 个节点,即节点 1。
    • 从 dummy 移动 left-1=1 步,pre 指向节点 1。
  3. 找到 start 和 end 节点

    • start 节点是 pre 的 next,即节点 2。
    • end 节点是反转部分的最后一个节点,从 start 移动 right-left=2 步,即节点 4。
  4. 断开子链表

    • 记录 end 的下一个节点 succ,即节点 5。
    • 将 end 的 next 设为 null,断开子链表:子链表为 2 → 3 → 4 → null
  5. 反转子链表

    • 反转子链表:4 → 3 → 2 → null
    • 反转后的头节点是 4,尾节点是 2。
  6. 重新连接

    • 将 pre 的 next(即节点 1 的 next)指向反转后的头节点 4。
    • 将反转后的尾节点 2 的 next 指向 succ(节点 5)。
    • 最终链表:dummy → 1 → 4 → 3 → 2 → 5
  7. 返回结果:返回 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 节点。
  • 正确断开和连接:反转子链表后,确保链表不断开。

掌握这三点,你就能高效解决反转链表Ⅱ问题。这道题是链表操作的经典题目,考察了对链表遍历和指针操作的理解。多练习几次,注意细节,就能熟练运用。