阅读 58

一道算法面试题引发的思考~

前言

“算法”的中文最早出现在中国汉代的数学名著《周髀算经》中。《周髀算经》卷上有:“数之法出于圆方。圆出于方,方出于矩。矩出于九九八十一”。意思是: 算数的方法都出于对圆、对方的计算,其中圆出于方(圆形面积=外接正方形x圆周率/4),方出于矩(正方形源自两边相等的矩),矩的计算出于九九八十一 (长乘宽面积的计算依自九九乘法表)。追溯回去,在春秋战国时代,《九九乘法歌诀》已经开始流行起来。

书归正传,上午的时候一个老哥提出来一道题:

输入:intervals = [[1,3],[2,6],[8,10],[15,18]]
输出:[[1,6],[8,10],[15,18]]
解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
复制代码

看完这道题的描述后对题目有个了一个大概的理解,区间重叠的需要合并,这样的话第一印象就是先要排序,然后判断交集的差值进行排序。

复杂度分析

时间复杂度:O(nlogn)O(nlogn)O(n\log n)O(nlogn),其中 nnnn 为区间的数量。除去排序的开销,我们只需要一次线性扫描,所以主要的时间开销是排序的 O(nlogn)O(nlogn)O(n\log n)O(nlogn)

空间复杂度:O(logn)O(logn)O(\log n)O(logn),其中 nnnn 为区间的数量。这里计算的是存储答案之外,使用的额外空间。O(logn)O(logn)O(\log n)O(logn) 即为排序所需要的空间复杂度。

思路

如果我们按照区间的左端点排序,那么在排完序的列表中,可以合并的区间一定是连续的。如下图所示,标记为蓝色、黄色和绿色的区间分别可以合并成一个大区间,它们在排完序的列表中是连续的:

算法

我们用数组 merged 存储最终的答案。

首先,我们将列表中的区间按照左端点升序排序。然后我们将第一个区间加入 merged 数组中,并按顺序依次考虑之后的每个区间:

如果当前区间的左端点在数组 merged 中最后一个区间的右端点之后,那么它们不会重合,我们可以直接将这个区间加入数组 merged 的末尾;

否则,它们重合,我们需要用当前区间的右端点更新数组 merged 中最后一个区间的右端点,将其置为二者的较大值。

正确性证明

上述算法的正确性可以用反证法来证明:在排完序后的数组中,两个本应合并的区间没能被合并,那么说明存在这样的三元组 (i,j,k)(i,j,k)(i, j, k)(i,j,k) 以及数组中的三个区间a[i],a[j],a[k]a[i],a[j],a[k]a[i], a[j], a[k]a[i],a[j],a[k]满足i<j<ki<j<ki < j < ki<j<k并且(a[i],a[k])(a[i],a[k])(a[i], a[k])(a[i],a[k])可以合并,但(a[i],a[j])(a[i],a[j])(a[i], a[j])(a[i],a[j])(a[j],a[k])(a[j],a[k])(a[j], a[k])(a[j],a[k])不能合并。这说明它们满足下面的不等式:

a[i].end<a[j].start(a[i] 和 a[j] 不能合并)a[i].end<a[j].start(a[i] 和 a[j] 不能合并)

a[j].end<a[k].start(a[j] 和 a[k] 不能合并)a[j].end<a[k].start(a[j] 和 a[k] 不能合并)

a[i].enda[k].start(a[i] 和 a[k] 可以合并)a[i].end≥a[k].start(a[i] 和 a[k] 可以合并)

我们联立这些不等式(注意还有一个显然的不等式a[j].starta[j].enda[j].starta[j].end)a[j].start \leq a[j].enda[j].start≤a[j].end),可以得到:

a[i].end<a[j].starta[j].end<a[k].starta[i].end < a[j].start \leq a[j].end < a[k].start

产生了矛盾!这说明假设是不成立的。因此,所有能够合并的区间都必然是连续的。

我的代码

针对这道问题菜鸡的我写了2个小时写出来一版答案

var merge = function (intervals) {
  intervals.sort((a, b) => a[0] - b[0])
  for (let i = 0; i < intervals.length; i++) {
    if (intervals[i + 1] == undefined) {
      break;
    }
    if (isRangeIn(intervals[i][0], intervals[i][1], intervals[i + 1][0], intervals[i + 1][1])) {
      intervals.splice(i, 2, [Math.min.apply(null, [...intervals[i], ...intervals[i + 1]]), Math.max.apply(null, [...intervals[i], ...intervals[i + 1]])])
      i--;
    }
  }
  return intervals;
};
// 判断一个数字是否在一个区间
function isRangeIn(numFirst, numLast, minNum, maxNum) {
  if ((numFirst <= minNum && numLast >= maxNum) || (numFirst >= minNum && numFirst <= maxNum) || (numLast >= minNum && numLast <= maxNum)) {
    return true;
  }
  return false;
}
复制代码

代码的阅读性,简洁程度都是最低分,在力扣的评分也是惨不忍睹。

image.png

大神的代码

var merge = function(intervals) {
    intervals.sort((a, b) => a[0] - b[0]);
    let res = [];
    let idx = -1;
    for (let interval of intervals) {
        if (idx == -1 || interval[0] > res[idx][1]) {
            res.push(interval);
            idx++;
        } else {
            res[idx][1] = Math.max(res[idx][1], interval[1]);
        }
    }
    return res;
};
复制代码

看完大佬的代码,才明白自己跟人家的差别有多大,考虑到要先排序,后面合并思路是一样的,但大佬几行代码就搞定了,我啰啰嗦嗦写一堆,向大佬致敬。

内容来源

力扣算法题

文章分类
前端
文章标签