[路飞]_前端算法第九弹-反转链表 II

116 阅读3分钟

「这是我参与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;
}