力扣解题-92. 反转链表 II
给你单链表的头指针 head 和两个整数 left 和 right ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表 。
示例 1:
输入: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后)”三部分,先定位反转段的前驱节点,再迭代反转指定区间的节点,最后重新拼接三段链表,逻辑清晰且能通过一趟扫描完成反转(满足进阶要求)。
核心逻辑拆解
反转指定区间链表的核心是“精准定位+局部反转+重新拼接”:
- 边界优化:若
left == right(无需反转),直接返回原链表,避免无效操作; - 哑节点初始化:创建
dummy哑节点(dummy.next = head),解决left=1时反转段无前驱节点的边界问题; - 定位反转段前驱:
- 定义
pre指针从dummy开始,遍历left-1次,最终pre指向“left位置节点的前一个节点”(如示例1中left=2,pre最终指向1);
- 定义
- 局部迭代反转:
start指针指向反转段的起始节点(pre.next,示例1中为2);curr初始化为start,prev初始化为null(反转链表的经典双指针);- 遍历
right-left+1次(覆盖left到right的所有节点),执行标准链表反转操作:- 保存
curr.next(避免断链); curr.next指向prev(反转指针);prev移动到curr,curr移动到保存的next;
- 保存
- 遍历完成后:
prev指向反转段的新头节点(示例1中为4),curr指向后段的起始节点(示例1中为5);
- 重新拼接链表:
pre.next = prev:将前段的尾节点(pre)指向反转段的新头(prev);start.next = curr:将反转段的原起始节点(现在是反转段的尾)指向后段的起始节点(curr);
- 返回结果:返回
dummy.next(跳过哑节点,得到完整链表)。
具体步骤(以示例1 head=[1,2,3,4,5]、left=2、right=4为例)
| 步骤 | 操作 | pre指向 | start指向 | prev指向 | curr指向 | 说明 |
|---|---|---|---|---|---|---|
| 1 | 初始化dummy→1→2→3→4→5 | dummy | - | - | - | 哑节点避免头节点边界问题 |
| 2 | 遍历1次(left-1=1) | 1 | - | - | - | 定位反转段前驱 |
| 3 | start=pre.next=2 | 1 | 2 | - | - | 标记反转段起始 |
| 4 | 第1次反转(i=0) | 1 | 2 | 2 | 3 | 2.next=null |
| 5 | 第2次反转(i=1) | 1 | 2 | 3 | 4 | 3.next=2 |
| 6 | 第3次反转(i=2) | 1 | 2 | 4 | 5 | 4.next=3 |
| 7 | pre.next=prev=4 | 1 | 2 | 4 | 5 | 前段→反转段新头 |
| 8 | start.next=curr=5 | 1 | 2 | 4 | 5 | 反转段尾→后段 |
| 最终链表:1→4→3→2→5,与示例结果一致。 |
性能说明
- 时间复杂度:O(n)(仅一趟扫描,遍历节点数=left-1 + right-left+1 = right ≤ n),满足进阶的“一趟扫描”要求;
- 空间复杂度:O(1)(仅使用几个指针变量,无额外存储);
- 优势:
- 分段处理逻辑清晰,反转过程复用经典链表反转逻辑,易理解;
- 哑节点完美解决left=1的边界问题;
- 一趟扫描完成所有操作,执行效率高。
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=3 | 1 | 2 | 3 | 1→2→3→4→5 |
| 2 | 第1次头插(i=0) | 1 | 2 | 4 | 1→3→2→4→5 |
| 3 | 第2次头插(i=1) | 1 | 2 | 5 | 1→4→3→2→5 |
| 4 | 循环结束 | 1 | 2 | 5 | 最终链表 |
优势说明
- 时间复杂度:O(n)(一趟扫描,遍历节点数=left-1 + right-left = right-1 ≤ n);
- 空间复杂度:O(1),与原解法一致;
- 核心优势:
- 反转逻辑更高效,无需完整反转区间(减少指针赋值次数);
- 代码更简洁,无需维护prev指针,仅通过固定节点和头插完成反转;
- 同样满足“一趟扫描”的进阶要求。
解法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)(递归调用栈深度);
- 优势:代码简洁,符合分治思想,适合理解递归处理链表的逻辑;
- 局限性:递归栈有额外空间开销,执行效率略低于迭代法。
总结
- 分段迭代反转法(第一次解答):逻辑清晰、边界处理完善,O(n)时间+O(1)空间,一趟扫描完成,是工程首选的经典解法;
- 头插法(最优迭代版):反转逻辑更高效,代码更简洁,同样满足一趟扫描要求,性能略优;
- 递归法:思路优雅,适合理解递归分治思想,但有栈空间开销;
- 关键技巧:
- 核心思想:将局部反转拆解为“定位前驱+局部操作+重新拼接”,哑节点解决头节点边界问题;
- 进阶优化:头插法无需完整反转区间,是局部链表反转的最优操作方式;
- 边界处理:left=1、left=right、right=n等场景需优先判断,避免无效操作。