「这是我参与2022首次更文挑战的第8天,活动详情查看:2022首次更文挑战」
反转链表 II(Reverse Linked List II)
LeetCode传送门92. 反转链表 II
题目
给你单链表的头指针 head 和两个整数 left 和 right ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表 。
Given the head of a singly linked list and two integers left and right where left <= right, reverse the nodes of the list from position left to position right, and return the reversed list.
Example:
Input: head = [1,2,3,4,5], left = 2, right = 4
Output: [1,4,3,2,5]
Input: head = [5], left = 1, right = 1
Output: [5]
Constraints:
- The number of nodes in the list is n.
- 1 <= n <= 500
- -500 <= Node.val <= 500
- 1 <= left <= right <= n
思考线
解题思路
我们要如何解答这道题呢?
首先我们要知道如何从头到尾如何去反转一个链表,如果不太清楚如何做的同学可以参考这个我之前录的B站视频链接地址在这里
知道了如何去从头到尾反转一个链表,在这里我们假设left
节点为头部,right
节点为尾部,我们可以反转从left
到right
之间的链表,在这里我们假设翻转后的left~right
链表为reversed
。
翻转完成left
到right
之间的链表以后,留给我们的问题就是如何让翻转后的链表链reversed
接好未翻转的头部
和未翻转的尾部
我们可以分情况考虑:
我们先看头部的变化:
- 若
left !==1
则说明有未反转的头部,我们记录下头部的位置preLeftNode
,在翻转完成后,把preLeftNode
指向翻转后的头部,而此时整个链表的头部没有发生变化仍然是head
- 若
left ===1
则说明没有未反转的头部,则此时的头部发生了变化,头部为反转链表reversed
的头部。 我们再看看尾部会发生的变化: - 若
right === 链表的尾部
, 则说明,反转后的链表reversed
尾部就是我们最终链表的尾部。在这里我们把翻转后的链表reversed
的尾部leftNode
的next
指针指向null
,否则链表会出现环。 - 若
right !== 链表的尾部
, 则说明,反转后的链表reversed
尾部需要链接到原始链表rightNode
的下一个节点。在这里我们把翻转后的链表reversed
的尾部leftNode
的next
指针指向 原始链表的下一个节点即可。
至此,我们完成了链表翻转的逻辑,最后 我们把新的头部返回即可。
代码如下
/**
* Definition for singly-linked list.
* class ListNode {
* val: number
* next: ListNode | null
* constructor(val?: number, next?: ListNode | null) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
* }
*/
function reverseBetween(head: ListNode | null, left: number, right: number): ListNode | null {
let list = head;
let i = 0
let pre: ListNode | null; // 翻转过程中的前一个
let next: ListNode | null; // 翻转过程中的后一个
let preLeftNode: ListNode | null; // 原始链表中left的前一个节点
let leftNode: ListNode | null; // left对应的节点
let rightNode: ListNode | null; // right对应的节点
let rNext: ListNode | null; // 原始链表中right对应的下一个节点
while (list) {
i++;
if (i === left - 1) preLeftNode = list;
if (i === left) {
pre = list;
leftNode = list;
next = pre.next;
}
if (i === right) {
rightNode = list;
if (list.next) rNext = list.next;
break;
}
list = list.next;
}
// 从 left 到right 来反转链表
while (next && pre !== rightNode) {
const temp = next.next;
next.next = pre;
pre = next;
next = temp;
}
let newHead: ListNode | null;;
// 处理头部逻辑
if (preLeftNode) {
newHead = head;
preLeftNode.next = pre;
} else {
newHead = pre;
}
// 处理尾部逻辑
if (rNext) {
leftNode.next = rNext;
}else {
leftNode.next = null;
}
return newHead;
};
以上是我的第一版代码,那么我们怎么对这个思路进行代码优化呢?
为了减少头部的判断我们可以给链表加上一个虚拟头结点dummyHead
这样一来,我们的新头部节点永远指向dummyHead.next
.
而我们尾部的指向呢? 通过观察我们可以发现,rNext
无论是指向null
还是真的有节点,我们都只要把翻转后的节点尾部指向rNext
即可,这样我们可以得到稍微优化后的代码,如下
function reverseBetween(head: ListNode | null, left: number, right: number): ListNode | null {
const dummyHead = new ListNode(0);
dummyHead.next = head;
let i = 0
let pre: ListNode | null; // 翻转过程中的前一个
let next: ListNode | null; // 翻转过程中的后一个
let preLeftNode: ListNode | null; // 原始链表中left的前一个节点
let leftNode: ListNode | null; // left对应的节点
let rightNode: ListNode | null; // right对应的节点
let rNext: ListNode | null = null; // 原始链表中right对应的下一个节点
if (left === 1) preLeftNode = dummyHead;
while (head) {
i++;
if (i === left - 1) preLeftNode = head;
if (i === left) {
pre = head;
leftNode = head;
next = pre.next;
}
if (i === right) {
rightNode = head;
if (head.next) rNext = head.next;
break;
}
head = head.next;
}
// 从 left 到right 来反转链表
while (next && pre !== rightNode) {
const temp = next.next;
next.next = pre;
pre = next;
next = temp;
}
preLeftNode.next = pre;
leftNode.next = rNext;
return dummyHead.next;
};
时间复杂度分析
O(n): 其中n为链表的总节点,最坏的情况下,需要遍历整个链表。
这就是我对本题的解法,如果有疑问或者更好的解答方式,欢迎留言互动。