力扣解题-92. 反转链表 II

0 阅读7分钟

力扣解题-92. 反转链表 II

给你单链表的头指针 head 和两个整数 left 和 right ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表 。

示例 1:

image.png

输入:head = [1,2,3,4,5], left = 2, right = 4

输出:[1,4,3,2,5]

示例 2:

输入:head = [5], left = 1, right = 1

输出:[5]

提示:

链表中节点数目为 n

1 <= n <= 500

-500 <= Node.val <= 500

1 <= left <= right <= n

进阶: 你可以使用一趟扫描完成反转吗?

Related Topics

链表


第一次解答

解题思路

核心方法:分段反转法(迭代版),将链表分为“前段(left前)、反转段(left-right)、后段(right后)”三部分,先定位反转段的前驱节点,再迭代反转指定区间的节点,最后重新拼接三段链表,逻辑清晰且能通过一趟扫描完成反转(满足进阶要求)。

核心逻辑拆解

反转指定区间链表的核心是“精准定位+局部反转+重新拼接”:

  1. 边界优化:若left == right(无需反转),直接返回原链表,避免无效操作;
  2. 哑节点初始化:创建dummy哑节点(dummy.next = head),解决left=1时反转段无前驱节点的边界问题;
  3. 定位反转段前驱
    • 定义pre指针从dummy开始,遍历left-1次,最终pre指向“left位置节点的前一个节点”(如示例1中left=2,pre最终指向1);
  4. 局部迭代反转
    • start指针指向反转段的起始节点(pre.next,示例1中为2);
    • curr初始化为startprev初始化为null(反转链表的经典双指针);
    • 遍历right-left+1次(覆盖left到right的所有节点),执行标准链表反转操作:
      • 保存curr.next(避免断链);
      • curr.next指向prev(反转指针);
      • prev移动到currcurr移动到保存的next
    • 遍历完成后:prev指向反转段的新头节点(示例1中为4),curr指向后段的起始节点(示例1中为5);
  5. 重新拼接链表
    • pre.next = prev:将前段的尾节点(pre)指向反转段的新头(prev);
    • start.next = curr:将反转段的原起始节点(现在是反转段的尾)指向后段的起始节点(curr);
  6. 返回结果:返回dummy.next(跳过哑节点,得到完整链表)。
具体步骤(以示例1 head=[1,2,3,4,5]、left=2、right=4为例)
步骤操作pre指向start指向prev指向curr指向说明
1初始化dummy→1→2→3→4→5dummy---哑节点避免头节点边界问题
2遍历1次(left-1=1)1---定位反转段前驱
3start=pre.next=212--标记反转段起始
4第1次反转(i=0)12232.next=null
5第2次反转(i=1)12343.next=2
6第3次反转(i=2)12454.next=3
7pre.next=prev=41245前段→反转段新头
8start.next=curr=51245反转段尾→后段
最终链表:1→4→3→2→5,与示例结果一致。
性能说明
  • 时间复杂度:O(n)(仅一趟扫描,遍历节点数=left-1 + right-left+1 = right ≤ n),满足进阶的“一趟扫描”要求;
  • 空间复杂度:O(1)(仅使用几个指针变量,无额外存储);
  • 优势:
    1. 分段处理逻辑清晰,反转过程复用经典链表反转逻辑,易理解;
    2. 哑节点完美解决left=1的边界问题;
    3. 一趟扫描完成所有操作,执行效率高。
    public ListNode reverseBetween(ListNode head, int left, int right) {
        if(left==right){
            return head;
        }
        ListNode dummy = new ListNode(0);
        dummy.next=head;
        ListNode pre=dummy;
        //找到left的前一个节点
        for(int i=1;i<left;i++){
            pre=pre.next;
        }
        //找到反转开始点
        ListNode start=pre.next;
        ListNode curr=start;
        ListNode prev=null;
        //反转从left到right的部分
        for(int i=0;i<=right-left;i++){
            ListNode next=curr.next;
            curr.next=prev;
            prev=curr;
            curr=next;
        }
        //第一次反转 处理2  next->3 2->null prev->2 curr->3
        //第二次反转 处理3  next->4 3->2 prev->3 curr->4
        //第三次反转 处理4  next->5 4->3 prev->4 curr->5
        //得出新链接 prev,尾巴是curr

        //重新连接三段
        pre.next = prev;      // 前段 → 新头
        start.next = curr;         // 新尾 → 后段

        return dummy.next;
    }

示例解答

解题思路

解法1:头插法(一趟扫描优化版)

核心方法:头插法反转指定区间,无需完整反转区间内所有节点,而是将反转段中除第一个节点外的所有节点依次“头插”到反转段的起始位置,仅需一趟扫描且反转逻辑更紧凑,空间复杂度仍为O(1)。

核心原理铺垫

头插法的核心是“将后续节点逐个移到反转段头部”:

  • 示例1中反转2-4:先固定2为初始头,将3插到2前面(变为3→2),再将4插到3前面(变为4→3→2),最终得到反转段4→3→2;
  • 该方法无需保存反转后的尾节点,直接通过指针调整完成局部反转。
代码实现
public ListNode reverseBetween(ListNode head, int left, int right) {
    if (left == right) {
        return head;
    }
    // 哑节点处理边界
    ListNode dummy = new ListNode(0);
    dummy.next = head;
    ListNode pre = dummy;
    
    // 定位到反转段前驱
    for (int i = 1; i < left; i++) {
        pre = pre.next;
    }
    
    // curr:当前要插入到头部的节点;固定节点:pre.next(反转段初始头)
    ListNode curr = pre.next.next;
    ListNode fixed = pre.next;
    
    // 头插法反转:共需要插入 right-left 次
    for (int i = 0; i < right - left; i++) {
        // 1. 取出curr(固定节点的下一个节点)
        fixed.next = curr.next;
        // 2. curr插到pre和fixed之间(反转段头部)
        curr.next = pre.next;
        pre.next = curr;
        // 3. 更新curr为下一个要插入的节点
        curr = fixed.next;
    }
    
    return dummy.next;
}
具体步骤(示例1 left=2、right=4)
步骤操作pre指向fixed指向curr指向链表状态
1定位pre=1,fixed=2,curr=31231→2→3→4→5
2第1次头插(i=0)1241→3→2→4→5
3第2次头插(i=1)1251→4→3→2→5
4循环结束125最终链表
优势说明
  • 时间复杂度:O(n)(一趟扫描,遍历节点数=left-1 + right-left = right-1 ≤ n);
  • 空间复杂度:O(1),与原解法一致;
  • 核心优势:
    1. 反转逻辑更高效,无需完整反转区间(减少指针赋值次数);
    2. 代码更简洁,无需维护prev指针,仅通过固定节点和头插完成反转;
    3. 同样满足“一趟扫描”的进阶要求。
解法2:递归法(思路拓展)

核心方法:递归缩小反转区间,通过递归将反转区间逐步缩小到left=1的场景(简化为“反转前N个节点”),再拼接后续链表,逻辑优雅但递归深度最多为n(无栈溢出风险,n≤500)。

代码实现
public ListNode reverseBetween(ListNode head, int left, int right) {
    // 递归终止:left=1时,反转前right个节点
    if (left == 1) {
        return reverseN(head, right);
    }
    // 递归缩小区间:head.next的反转区间为left-1到right-1
    head.next = reverseBetween(head.next, left - 1, right - 1);
    return head;
}

// 辅助函数:反转链表的前n个节点,返回新头
private ListNode reverseN(ListNode head, int n) {
    if (n == 1) {
        // 记录第n+1个节点,用于拼接
        successor = head.next;
        return head;
    }
    // 递归反转前n-1个节点
    ListNode newHead = reverseN(head.next, n - 1);
    // 反转当前节点指针
    head.next.next = head;
    // 拼接后段
    head.next = successor;
    return newHead;
}

// 后段的起始节点(递归中共享)
private ListNode successor = null;
适用场景说明
  • 时间复杂度:O(n)(每个节点仅被访问一次);
  • 空间复杂度:O(n)(递归调用栈深度);
  • 优势:代码简洁,符合分治思想,适合理解递归处理链表的逻辑;
  • 局限性:递归栈有额外空间开销,执行效率略低于迭代法。

总结

  1. 分段迭代反转法(第一次解答):逻辑清晰、边界处理完善,O(n)时间+O(1)空间,一趟扫描完成,是工程首选的经典解法;
  2. 头插法(最优迭代版):反转逻辑更高效,代码更简洁,同样满足一趟扫描要求,性能略优;
  3. 递归法:思路优雅,适合理解递归分治思想,但有栈空间开销;
  4. 关键技巧:
    • 核心思想:将局部反转拆解为“定位前驱+局部操作+重新拼接”,哑节点解决头节点边界问题;
    • 进阶优化:头插法无需完整反转区间,是局部链表反转的最优操作方式;
    • 边界处理:left=1、left=right、right=n等场景需优先判断,避免无效操作。