【刷题之路Ⅱ】LeetCode 92. 反转链表 II
- 一、题目描述
- 二、解题
- 1、方法1——穿针引线法
- 1.1、思路分析
- 1.2、代码实现
- 2、方法2——针对进阶的头插法
- 2.1、思路分析
- 2.2、代码实现
一、题目描述
原题连接: 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
进阶: 你可以使用一趟扫描完成反转吗?
二、解题
1、方法1——穿针引线法
1.1、思路分析
其实这一题与"206. 反转链表"的区别就在于,206题是要求我们把整个链表都反转,而这一题只要求我们把链表的一部分反转。所以反转部分我们就沿用206题的方法即可,还不知道思路的可以转到【刷题之路】LeetCode 206. 反转链表。
而为了反转之后还能连接回原链表,我们分别记录反转部分的链表的表头的前一个节点和表尾的后一个节点,如下图中绿色节点标注:
而又因为反转部分可能还包含了头节点,所以为了方便操作我们需要额外的创建一个辅助的哑结点,指向我们链表的头节点:
然后我们一共需要四个指针:pre、leftNode、rightNode,next,分别记录反转部分的前一个节点、反转部分的头节点、分转部分的尾节点和反转部分的后一个节点:
而当我们把反转部分的链表反转完毕之后,只需要更改相应的指针的指向即可:
即pre->next = rightNode,leftNode->next = next。
(这里反转部分的头节点和尾节点的位置虽然发生了变化,但rightNode和leftNode的指向并没有变化)。
1.2、代码实现
有了以上思路,那我们写起代码来也就水到渠成了:
// 沿用206题的迭代版的链表反转方法
void reverseList(struct ListNode* head){
if (NULL == head) {
return NULL;
}
struct ListNode *Pre = NULL;
struct ListNode *Cur = head;
struct ListNode *Next = head->next;
while (Cur) {
// 翻转
Cur->next = Pre;
// 迭代
Pre = Cur;
Cur = Next;
if (Next) {
Next = Next->next;
}
}
}
struct ListNode* reverseBetween(struct ListNode* head, int left, int right) {
if (NULL == head || NULL == head->next) {
return head;
}
// 先创建一个辅助的哑结点
struct ListNode *dumbNode = (struct ListNode*)malloc(sizeof(struct ListNode));
dumbNode->val = -1;
dumbNode->next = head;
struct ListNode *cur = dumbNode;
struct ListNode *leftNode = head;
struct ListNode *rightNode = NULL;
struct ListNode *pre = dumbNode; // 记录leftNode的前一个节点
// 因为头节点也可能包含在反转部分,所以pre初始化时应该指向dumbNode
struct ListNode *next = NULL; // 记录rightNode的下一个节点
int len = 0; // 记录链表的长度
// 先找到各个节点
while (cur) {
len++;
if (len == left) {
pre = cur;
leftNode = pre->next;
}
if (len == right + 1) {
rightNode = cur;
next = rightNode->next;
}
cur = cur->next;
}
// 断开链表
pre->next = NULL;
rightNode->next = NULL;
// 反转链表
reverseList(leftNode);
// 接回原来的链表
pre->next = rightNode;
leftNode->next = next;
return dumbNode->next;
}
2、方法2——针对进阶的头插法
方法1的效率其实并不是很高,因为如果left和right刚好是原链表的头和尾时,我们前后一共需要遍历两次链表,分别是找各个节点一次和反转链表一次。
那有没有一种方法能一次遍历解决问题呢?
2.1、思路分析
针对进阶提出的:你可以使用一趟扫描完成反转吗?,其实有一种解决方案就是“头插”。
我们可以将反转部分的各个节点头插到反转部分的前一个节点,具体做法如下:
我们需要三个指针pre、cur和next,pre指针永远指向反转部分的前一个节点,cur则用来遍历反转部分,next永远指向cur的下一个节点:
而接下来的反转操作就要有些讲究了:
第一步,先将cur的next指向next的next:
第二步,将next的next指向pre->next:
注意这里的next的next指向的一定是pre的next,而不是cur,因为cur的位置是一直往后走的,只有指向pre的next才是头插。
第三步,将pre的next指向next
最后我们的cur其实并不用再往后走了。
以上操作总共需要执行right - left次,因为一共有right - left个节点需要反转。
最后返回dumbNode的next即可。
2.2、代码实现
有了以上思路,那我们写起代码来也就水到渠成了:
struct ListNode* reverseBetween(struct ListNode* head, int left, int right) {
if (NULL == head || NULL == head->next) {
return head;
}
// 先创建一个辅助的哑结点
struct ListNode *dumbNode = (struct ListNode*)malloc(sizeof(struct ListNode));
dumbNode->val = -1;
dumbNode->next = head;
struct ListNode *cur = dumbNode;
struct ListNode *pre = dumbNode; // 记录反转部分的前一个节点
struct ListNode *next = NULL; // cur的下一个节点
// 先定位pre,只需让pre往后走left - 1次即可
int i = 0;
for (i = 0; i < left - 1; i++) {
pre = pre->next;
}
cur = pre->next;
next = cur->next;
// 再进行反转,总共需要反转right - left次
for (i = 0; i < right - left; i++) {
next = cur->next;
cur->next = next->next;
next->next = pre->next;
pre->next = next;
}
return dumbNode->next;
}