「这是我参与11月更文挑战的第7天,活动详情查看:2021最后一次更文挑战」
通过了上一篇文章的翻转链表,我们已经知道了如何将一个链表反转,那么下面我们加深一点难度,思考一下更有挑战性的问题。
我们只反转链表中的一部分,而不是全部,这个我们应该怎么处理呢?
这就是今天这道题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]
这道题理解上应该没有难度,就是在链表中找到指定的两个节点,将这两个节点中间的所有结点反转即可。
我们这时需要将链表分为三个部分,前部,待转区,后部。
反转前:1->2->3->4->5
反转中:pre :1->null ,reverseLink:2->3->4 ,cur :5->null
反转后:pre :1->null ,reverseLink:4->3->2 ,cur :5->null
拼接后:1->4->3->2->5->null
var reverseBetween = function(head, left, right) {
// 因为如果left=1头节点有可能发生变化,
// 使用虚拟头节点可以避免复杂的分类讨论
const dummyNode = new ListNode(-1);
dummyNode.next = head;
// 寻找翻转节点之前的结点pre
let pre = dummyNode;
// 第 1 步:从虚拟头节点走 left - 1 步,来到 left 节点的前一个节点
// 建议写在 for 循环里,语义清晰
for (let i = 0; i < left - 1; i++) {
pre = pre.next;
}
// 寻找翻转结点之后的结点,curr
// 第 2 步:从 pre 再走 right - left + 1 步,来到 right 节点
let rightNode = pre;
for (let i = 0; i < right - left + 1; i++) {
rightNode = rightNode.next;
}
// 第 3 步:切断出一个子链表(截取链表)
let leftNode = pre.next;
let curr = rightNode.next;
// 注意:切断链接
pre.next = null;
rightNode.next = null;
// 第 4 步:看上一篇[翻转结点](<https://www.notion.so/206-8a525b5807ac484c95bb9042b0e0cb63>),反转链表的子区间
reverseLinkedList(leftNode);
// 第 5 步:接回到原来的链表中
pre.next = rightNode;
leftNode.next = curr;
return dummyNode.next;
};
const reverseLinkedList = (head) => {
let pre = null;
let cur = head;
while (cur) {
const next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
}
这是第一种方法,是将所有需要翻转的结点全部遍历出来,统一进行一次翻转,下面这个方法,又叫插头法,指的就是我遍历head的时候,只要过了pre,结点,每遇到一个节点就翻转一次,直到过了right节点为止。
head:1->2->3->4->5->null
遍历...
当遍历到left时:
head:1->2->
下一个结点开始反转
head:1->2->3-> => 1->3->2->
之后每次都会反转:
head:1->3->2->4 => 1->4->3->2->
直到越过right结点
head:1->4->3->2->5->null
结束
我们一共需要三个指针变量,pre,cur,next来进行记录。
-
cur:指向反转区的第一个节点
-
next:指向cur的下一个结点,跟随cur的变化而变化
-
pre:指向待转区的第一个节点left的前一个节点,不变。
head:1->2->3->4->5->null pre = 1,cur = 2; 第一次: next :cur.next = 3, cur.next = next.next = 4, next.next = pre.next = 2, pre.next = next = 3 head:1->3->2->4->5->null; 第二次: next :cur.next = 4, cur.next = next.next = 5, next.next = pre.next = 3, pre.next = next = 4; head:1->4->3->2->5->null;
整理为代码:
var reverseBetween = function(head, left, right) {
// 设置 dummyNode 是这一类问题的一般做法
const dummy_node = new ListNode(-1);
dummy_node.next = head;
// 设置pre(第一个反转节点的前一个节点,并找到它
let pre = dummy_node;
for (let i = 0; i < left - 1; ++i) {
pre = pre.next;
}
// 设置第一个翻转的结点
let cur = pre.next;
// right - left为需要反转的结点的个数
for (let i = 0; i < right - left; ++i) {
// 进行翻转赋值
const next = cur.next;
cur.next = next.next;
next.next = pre.next;
pre.next = next;
}
return dummy_node.next;
};
第三种方法则是两次递归。同样还是分成三段,待转前结点,待转结点,待转后结点。第一次递归到待转结点第一个,然后进行第二次递归,递归到待转结点的最后一个,将待转后结点设为cur,将待转的最后一个设为last结点拼上cur,向前递归,将前一个节点,插入last最后,设为新的再向前递归,直至递归到第一个反转节点left,拼接到pre后。
head:1->2->3->4->5->6->7 ->null ,left=3,right=6;
递归最后一层,last = 6,cur =7
上一层递归,last = 6->5,cur=7
上一层递归,last = 6->5->4, cur=7
上一层递归,last = 6->5->4-3->, cur=7
结束翻转递归,继续遍历递归:head=2,head.next = last,cur=7
继续遍历递归 head=1, head.next = 2->last,cur=7
结束递归
代码实现
var reverseBetween = function (head, left, right) {
if (left == 1) return reverseN(head, right);
// 这里也是递归的方式解决问题,其实可以for循环走到left节点处
// 注意需要对head.next重新赋值,否则链表就断开了。
head.next = reverseBetween(head.next, left - 1, right - 1);
return head;
};
function reverseN(head, n) {
// 需要全局存下N+1个节点
if (n == 1) {
successor = head.next;
return head;
}
let last = reverseN(head.next, n - 1);
head.next.next = head;
// 和全部反转的区别就在这里,全部反转head就是最后一个节点,head.next=null
// 反转前N个节点,头结点的会连上N+1个节点,即head.next=successor
head.next = successor;
return last;
}