LeetCode for JavaScrpt:反转链表+K 个一组翻转链表

106 阅读4分钟

反转链表

这道题基本属于各个大厂出题频次最高的算法题了。

反转链表: Given the head of a singly linked list, reverse the list, and return the reversed list.

example:

image.png

Input: head = [1,2,3,4,5]
Output: [5,4,3,2,1]

思路

对于链表这种数据结构,主要是注意左右指针。 一般有三类题:

  1. 常规题,就是对链表的指针进行移动,也称为穿针引线
  2. 利用链表头部的特性,设置虚拟节点;(参考上一篇文章的大数相加)
  3. 不在指针上做文章,比如修改链表节点值等。

反转链表就属于第一类,我们需要对原链表的指针进行穿针引线反向来得到要的结果。

迭代

解法:针对上面的example的图来说,我们只需要新建的一个链表存下反转的结果,然后让原链表的2指向1,然后让3指向2,为了不让原链表的指针关系被破坏,就多增加几个变量来存储就行了。

一般来说,链表的题绝大部分都是这样,需要变量去存储元素,自己画一下移动的图会清晰很多。

/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var reverseList = function (head) {
  let prev = null,
    curr =  head;

  while (curr) {
    const next = curr.next;

    curr.next = prev;
    prev = curr;
    curr = next;
  }
  return prev;
};

考个小问题,head此时的值是多少呢?

答案:此时的head的值应该为第一个链表的值。curr = headhead的引用赋给了currcurrcurr.next = prev = null这一步就将head的next置为null,然后curr = next使得curr的引用不再和head相同,所以head就一直保持着链表第一个值的结果。

递归

对于链表这种需要一直从头迭代到尾的结构,也是天然可以用递归计算的,就把链表想象成不会分叉的二叉树一样,所以对于这道题还可以用递归写法:

假设链表的结构是 L1 -> L2 -> Lk -> L(k+1) -> ... -> Ln,假设我们已经反转了L(k+1)之后的链表,此时链表的结构是L1 -> L2 -> Lk -> L(k+1) <- ... <- Ln,接下来如果要继续反转链表的元素的话,我们需要将L(k+1)的next变为Lk,依次类推Lk的next变为L2,要注意L1的next需要为null。

var reverseList = function (head) {
  if (head == null || head.next == nullreturn head;
  
  // 假设head为L1的话,此时head.next为L2
  const tmp = reverseList(head.next);
  // 将L2的next设置为L1
  head.next.next = head;
  // L1的next为null
  head.next = null;
  
  return tmp;
};

此外还需要注意一下链表的定义,虽然这个在题目中是给到的,但是面试的时候也是有可能会被问到链表的定义:

function ListNode(val, next) {
    this.val = (val===undefined ? 0 : val);
    this.next = (next===undefined ? null : next);
}

class ListNode {
    constructor(val, next) {
        this.val = (val===undefined ? 0 : val);
        this.next = (next===undefined ? null : next);
    }
}

K 个一组翻转链表

K 个一组翻转链表属于一道hard题目,但是是前端常考的高频题。这里放在一起的原因是这道题就是一个复杂版本的反转链表,只要能搞明白反转链表,这道题也很好做。

题目

给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表。

k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。

你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。

examples:

image.png

输入: head = [1,2,3,4,5], k = 2
输出: [2,1,4,3,5]

思路

这道题实际就是将反转链表变成了分几次去反转链表这种情况。

比如: 1 -> 2 -> 3 -> 4 -> 5按照两个一组去反转的话,2 -> 1 -> (3 -> 4) -> 5,假设 1 -> 2 我们已经反转了,然后在反转3 -> 4的时候是需要把3前一个节点和4的后一点节点存起来,在(3,4)反转之后,将存起来的节点和反转链表的头尾再连接上就行了。然后等3和4已经反转完毕之后,继续下去发现剩下的5其实是不够2个的,所以直接返回就行了;

明白思路之后,会发现在反转1和2的时候,1前面是没有元素的,所以我们要给一个虚拟的节点来保证逻辑正确运行。

解题:

  1. 首先是需要写一个反转链表的函数,可以根据当前链表头尾(变量命名为头->head和尾->tail)反转,然后返回新的头尾(head,tail);
  2. 保存当前反转链表前一个的节点(也就是head节点的前一个,命名为left)和后一个的节点(tail节点的后一个命名为right)。
  3. 因为题目需要K个一组,所以此时还需要两个指针去做遍历保证K个一组,p1表示链表的起始位,p2表示链表的终止位,p1和p2之间就是K个一组了。

p1/p2
0 -> head -> ... -> tail -> ...

从最开始的步骤开启:p1和p2在虚拟节点,需要反转head节点到tail节点部分,怎么找到tail呢,就是靠p2节点去遍历k次,然后就可以调用反转链表函数reverseGroup(head,p2)。

接着我们按照步骤来写就行了:

/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} head
 * @param {number} k
 * @return {ListNode}
 */
function reverseGroup(head, tail) {
  let prev = tail.next;
  let cur = head;
  while (prev !== tail) {
    let next = cur.next;

    cur.next = prev;
    prev = cur;
    cur = next;
  }
  return [tail, head];
}
var reverseKGroup = function (head, k) {
  let left = new ListNode(0);
  left.next = head;
  let p1 = left;
  while (head) {
    let p2 = p1;
    for (let i = 0; i < k; ++i) {
      p2 = p2.next;
      if (!p2) {
        return left.next;
      }
    }
    let right = p2.next;

    [head, tail] = reverseGroup(head, p2);
    p1.next = head;
    tail.next = right;
    p1 = tail;
    head = tail.next;
  }
  return left.next;
};