【LeetCode】92. 反转链表 II
题目
【LeetCode】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
解题思路
核心要点:
- 仅反转链表中从
left
到right
的部分节点 - 保持其他节点的相对位置不变
- 需要正确处理反转部分与前后未反转部分的连接
下面介绍两种高效解法,均能在一次遍历中完成操作:
解法一:穿针引线法(逐个插入)
在需要反转的区间里,每遍历到一个节点,让这个新节点来到反转部分的起始位置。
/*** Definition for singly-linked list.* public class ListNode {* int val;* ListNode next;* ListNode() {}* ListNode(int val) { this.val = val; }* ListNode(int val, ListNode next) { this.val = val; this.next = next; }* }*/
class Solution {public ListNode reverseBetween(ListNode head, int left, int right) {// 创建虚拟头节点,简化头节点处理ListNode dummy = new ListNode(0);dummy.next = head;// p0 指向 left 节点的前一个节点ListNode p0 = dummy;for (int i = 1; i < left; i++) {p0 = p0.next;}// curr 是 left 位置的节点ListNode curr = p0.next;ListNode next; // 保存 curr 的下一个节点// 从 left 到 right 需要反转 right-left 次for (int i = left; i < right; i++) {next = curr.next; // 保存下一个节点// 步骤1:curr 跳过 next,直接指向 next 的下一个节点curr.next = next.next;// 步骤2:next 节点插入到 p0 的后面(成为反转部分的新头)next.next = p0.next;// 步骤3:p0 指向 next,完成插入p0.next = next;}return dummy.next;}
}
解法二:标准反转法(三指针)
这种方法先定位到反转范围,然后用标准的三指针反转法完成局部反转,最后重新连接链表。
/*** Definition for singly-linked list.* public class ListNode {* int val;* ListNode next;* ListNode() {}* ListNode(int val) { this.val = val; }* ListNode(int val, ListNode next) { this.val = val; this.next = next; }* }*/
class Solution {public ListNode reverseBetween(ListNode head, int left, int right) {// 创建哨兵节点ListNode sentinel = new ListNode(0, head);// p0 指向 left 节点的前一个节点ListNode p0 = sentinel;for (int i = 0; i < left - 1; i++) {p0 = p0.next;}// 初始化反转指针ListNode pre = null;ListNode cur = p0.next; // cur 指向 left 节点// 反转 left 到 right 的节点(共 right-left+1 个节点)for (int i = 0; i < right - left + 1; i++) {ListNode nxt = cur.next; // 保存下一个节点cur.next = pre; // 反转指针pre = cur; // pre 前移cur = nxt; // cur 前移}// 连接反转后的链表p0.next.next = cur; // 原 left 节点(现在是反转尾)连接到 right+1 节点p0.next = pre; // p0 连接到反转后的新头(原 right 节点)return sentinel.next;}
}
解法二详解
同样以 head = [1,2,3,4,5], left=2, right=4
为例:
- 定位p0:
p0
移动到节点1(left前一个节点) - 初始化指针:
pre=null
,cur=2
(left节点) - 反转过程(循环3次):
- 第1次:2→null,
pre=2
,cur=3
- 第2次:3→2,
pre=3
,cur=4
- 第3次:4→3,
pre=4
,cur=5
- 反转后局部:4→3→2
- 第1次:2→null,
- 重新连接:
p0.next.next = cur
→ 2→5(反转尾连接到right+1节点)p0.next = pre
→ 1→4(p0连接到反转新头)- 结果:1→4→3→2→5(完成反转)
这种方法先完整反转局部链表,再进行连接,逻辑更符合常规的链表反转思路。
复杂度分析
两种方法的复杂度相同:
- 时间复杂度:O(n),只需一次遍历链表
- 空间复杂度:O(1),仅使用常数个额外指针
关键要点
- 虚拟头节点(哨兵节点)是处理链表头节点可能被反转的有效技巧
- 定位
left
前的节点(p0
)是连接反转部分的关键 - 两种方法都只需一次遍历,在遍历过程中判断并处理
left
到right
范围的节点 - 反转后必须正确连接三部分:
p0
之前的部分、反转部分、right
之后的部分