前端刷题路-Day57:合并K个升序链表(题号23)

264 阅读1分钟

这是我参与更文挑战的第21天,活动详情查看: 更文挑战

合并K个升序链表(题号23)

题目

给你一个链表数组,每个链表都已经按升序排列。

请你将所有链表合并到一个升序链表中,返回合并后的链表。

示例 1:

输入:lists = [[1,4,5],[1,3,4],[2,6]]
输出:[1,1,2,3,4,4,5,6]
解释:链表数组如下:
[
  1->4->5,
  1->3->4,
  2->6
]
将它们合并到一个有序链表中得到。
1->1->2->3->4->4->5->6

示例 2:

输入:lists = []
输出:[]

示例 3:

输入:lists = [[]]
输出:[]

提示:

  • k == lists.length
  • 0 <= k <= 10^4
  • 0 <= lists[i].length <= 500
  • -10^4 <= lists[i][j] <= 10^4
  • lists[i]升序 排列
  • lists[i].length 的总和不超过 10^4

链接

leetcode-cn.com/problems/me…

解释

这题啊,这题是升级版合并链表。

有时间可以先看看简单题,合并两个有序链表(题号21)。这题是简单版本啦,也可以为困难版本提供思路。

先说说笔者的思路,这其实很简单。

依然是先搞一个新的链表,用来进行存储操作,后序开始遍历数组中的链表,从头开始进行比较,取最小值记录下它的值和index

之后插入新的链表中,同时修改数组中的元素,这样就完事了。

当数组的长度为0时,结束递归,返回新的链表就完事了,非常简单。

官方给出的答案这里其实并不是很推荐,它推荐的三种方法中有两种JavaScript比较适用,这里简单介绍下:

  1. 顺序合并

    顺序合并其实很简单,先写一个方法来合并两个链表。

    之后从头开始循环数组,将数组中的每个链表都与新建链表进行合并,在合并n次后,新建的链表就是数组中所有链表的合并结果了。

  2. 分治合并

    这个思路也比较简单,首先将数组中的链表两两合并,如此本来数组中的n个链表就变成了n /2个链表,之后开始第二次,数组的元素就变成了n / 4,就这样直到最后长度就会变成1,此时返回即可。

以上三种方法(包括笔者的想法),其实对运行效率都差不多,没有本质上的提升,在本文的最后会介绍一种超强的JavaScript解法,内存占用和运行时间都可以达到90%以上。

自己的答案(暴力)

思路上面说过了,先看看代码👇:

var mergeKLists = function(lists) {
  var head = new ListNode(0)
      node = head
  while (lists.length) {
    var active = null
        index = null
    for (let i = 0; i < lists.length; i++) {
      var item = lists[i]
      if (!item) {
        lists.splice(i, 1)
        --i
      } else {
        if (!active) {
          active = item
          index = i
        } else {
          if (item.val < active.val) {
            active = item
            index = i
          }
        }
      }
    }
    if (active) {
      node.next = active
      node = node.next
      active = active.next
      lists[index] = active      
    }
  }
  return head.next
};

代码优点长,主要的内容都在while循环里。

递归中的逻辑主要可以分为两部分,在for循环中,主要是拿到activeindex,还有就是去掉已经为空的链表。

for循环之后,开始给新链表添加元素,添加完成后别忘了修改数组元素,这样就完事了。

最后就可以拿到最后的结果了。

更好的方法(顺序合并)

还是老规矩,先看代码👇:

var mergeKLists = function(lists) {
  function mergeTwoLink(l1, l2) {
    var head = new ListNode(0)
        node = head
    while (l1 && l2) {
      if (l1.val > l2.val) {
        node.next = l2
        l2 = l2.next
      } else {
        node.next = l1
        l1 = l1.next
      }
      node = node.next
    }
    node.next = l1 ? l1 : l2
    return head.next
  }
  var newLink = null
  for (let i = 0; i < lists.length; i++) {
    newLink = mergeTwoLink(newLink, lists[i])    
  }
  return newLink
};

方法比较简单,首先是mergeTwoLink方法,该方法就是简单题的答案,没啥可说的,不记得的可以看上一篇文章。

接下来就更简单了,循环整个数组,取其元素挨个和newLink合并,都合并完了就是最后的结果了。

更好的方法(分治合并)

分治的思路在上面说了,这里两两合并修改数组就需用到双指针了👇:

var mergeKLists = function(lists) {
  if (!lists.length) return null
  function mergeTwoLink(l1, l2) {
    var head = new ListNode(0)
        node = head
    while (l1 && l2) {
      if (l1.val > l2.val) {
        node.next = l2
        l2 = l2.next
      } else {
        node.next = l1
        l1 = l1.next
      }
      node = node.next
    }
    node.next = l1 ? l1 : l2
    return head.next
  }
  var left = 0
      right = lists.length - 1
  while (lists.length !== 1 || left < right) {
    if (left < right) {
      lists[left] = mergeTwoLink(lists[left], lists[right])
      lists.pop()
      left++
      right--
    } else {
      left = 0
      right = lists.length - 1
    }
  }
  return lists[0]
};

这里从数组的头尾开始,先合并头尾的,合并完成后的链表放到left指针的位置,然后pop()掉数组最后一位的元素。

当左指针的位置和右指针重合或者超过右指针时,重制两个指针的位置,从头尾开始再走一次。

每走一次,数组的长度都会减少二分之一左右的长度(数组长度为奇数时会减少的长度是二分之一的长度减一),如果到最后数组长度为1时,就是最后的答案了。

因为减少了。n / 2次的链表合并次数(和顺序合并的方法相比,该方法合并了n次),执行时间和内存占用都减少了不少,属于中上游水平了,但依旧不是最优解法。

更好的方法(排序组合)

这方法是在翻看别人的答案时发现了,感觉巨强,不仅代码量少,而且性能好,笔者认为这可以是最优解了,没有之一。

整体思路并不复杂,分为三步:

  1. 拆解所有节点,放到一个数组中
  2. 对数组中的节点进行排序(从小到大)
  3. 根据排序后的结果进行节点组合

这就完事了,是不是非常简单。

而且这个老哥用reducereduceRight一组合,直接省去了新增一个数组的步骤,一波链式调用,一次解决👇:

var mergeKLists = function(lists) {
  return lists.reduce((p, n) => {
      while (n) {
          p.push(n), n = n.next
      }
      return p
  },[]).sort((a, b) => a.val - b.val).reduceRight((p, n) => (n.next = p, p = n, p), null)
};

按照上面的三部曲拆分出来分别是:

  1. 拆分节点

    lists.reduce((p, n) => {
      while (n) {
          p.push(n), n = n.next
      }
      return p
    },[])
    
  2. 节点排序

    .sort((a, b) => a.val - b.val)
    
  3. 组合节点

    .reduceRight((p, n) => (n.next = p, p = n, p), null)
    

简简单单三部曲,直接搞定,这老哥真的太强了~



PS:想查看往期文章和题目可以点击下面的链接:

这里是按照日期分类的👇

前端刷题路-目录(日期分类)

经过有些朋友的提醒,感觉也应该按照题型分类
这里是按照题型分类的👇

前端刷题路-目录(题型分类)

有兴趣的也可以看看我的个人主页👇

Here is RZ