算法:合并区间和无重叠区间

255 阅读3分钟

LeetCode 56. 给出一个区间的集合,请合并所有重叠的区间。

示例

// 示例1
输入: [[1,3], [2,6], [8,10], [15,18]]
输出: [[1,6], [8,10], [15,18]]
解释: 区间 [1,3] 和 [2,6] 重叠,将它们合并为 [1,6]。

// 示例2
输入: [[1,4], [4,5]]
输出: [[1,5]]
解释: 区间 [1,4] 和 [4,5] 可被视为重叠区间。

解法思路

假设有区间 a 和 b,区间 a 的起始时间要早于 b 的起始时间。那么它们之间有如下 3 种可能会出现的情况。

  1. 两个区间没有任何重叠的部分,因此区间不会发生融合。
  2. 两个区间有重叠。
    1. 新区间的起始时间是 a 的起始时间,这个不变。
    2. 终止时间为a终止时间和b终止时间的最大值。

javaScript 代码实现

/**
 * @param {number[][]} intervals
 * @return {number[][]}
 */
var merge = function(intervals) {
    // 排序: 按从小到大
    intervals.sort((a, b) => a[0] - b[0])
    // 前一个区间,后一个区间, 存储结果
    let previous, current, result = [];
    for (let i = 0; i < intervals.length; i++) {
        current = intervals[i]
        // 是第一个区间或者当前区间和前一个区间没有重叠,将当前区间加入到结果中
        if (!previous || current[0] > previous[1]) {
            // 将当前区间赋值给前一个区间
            previous = current
            result.push(current)
        } else {
        // 否则的话,就是两个区间发生了重叠
            // 更新前一个区间的结束时间
            previous[1] = Math.max(previous[1], current[1])
        }
    }
    return result;
};

LeetCode 435:给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。

注意:

  1. 可以认为区间的终点总是大于它的起点。
  2. 区间 [1,2][2,3] 的边界相互“接触”,但没有相互重叠。

示例

示例 1:
输入: [ [1,2], [2,3], [3,4], [1,3] ]
输出: 1
解释: 移除 [1,3] 后,剩下的区间没有重叠。

示例 2:
输入: [ [1,2], [1,2], [1,2] ]
输出: 2
解释: 你需要移除两个 [1,2] 来使剩下的区间没有重叠。

示例 3:
输入: [ [1,2], [2,3] ]
输出: 0
解释: 你不需要移除任何区间,因为它们已经是无重叠的了。

暴力解法

解法思路

  1. 如果当前区间和前一个区间没有发生重叠,则尝试保留当前区间,表明此处不需要删除操作。
  2. 题目要求最少的删除个数,只有在这样的情况下,才不需要做任何删除操作。
  3. 在这种情况下,虽然两个区间没有重叠,但是也要考虑尝试删除当前区间的情况。
  4. 对比哪种情况所需要删除的区间最少。

javaScript 代码实现

/**
 * @param {number[][]} intervals
 * @return {number}
 */
var eraseOverlapIntervals = function(intervals) {
    // 排序:从小到大
    intervals.sort((a, b) => a[0] - b[0])
    // 递归处理每个区间
    return eraseOverlapIntervalsRe(-1, 0, intervals)
};

function eraseOverlapIntervalsRe(prev, cur, intervals) {
    // 已经处理完所有区间,表明不需要删除的区间,直接返回
    if (cur === intervals.length) {
        return 0
    }
    let take = Number.MAX_SAFE_INTEGER, notake;
    // 只有当prev, curr没有发生重叠的时候,才可以选择保留当前的区间curr
    if (prev === -1 || intervals[prev][1] <= intervals[cur][0]) {
        take = eraseOverlapIntervalsRe(cur, cur + 1, intervals)
    }
    // 其他情况,删除当前区间, 继续看删除之后有什么结果
    notake = eraseOverlapIntervalsRe(prev, cur + 1, intervals) + 1

    return Math.min(take, notake)
}

贪婪法

解法思路

要尽可能少得删除区间,那么当遇到了重叠的时候,应该把区间跨度大,即结束比较晚的那个区间删除。因为如果不删除它,它会和剩下的其他区间发生重叠的可能性非常大。

实现代码

/**
 * @param {number[][]} intervals
 * @return {number}
 */
var eraseOverlapIntervals = function(intervals) {
    if (intervals.length === 0) return 0
    // 排序:从小到大
    intervals.sort((a, b) => a[0] - b[0])
    // 用一个变量 end 记录当前的最小结束时间点
    // 用count记录到目前为止删除了多少区间
    let end = intervals[0][1], count = 0;

    // 从第二个区间开始遍历,判断当前区间和前一个区间的结束时间
    for (let i = 1; i < intervals.length; i++) {
        // 当前区间和前一个区间有重叠,即当前区间的起始时间小区前一个时间的结束时间,end记录下两个结束时间的最小值,把结束时间晚的区间删除,计数加1。
        if (intervals[i][0] < end) {
            end = Math.min(end, intervals[i][1]);
            count++;
        } else {
            end = intervals[i][1]
        }
    }
    // 如果没有发生重叠,根据贪婪法,更新 end 变量为当前区间的结束时间
    return count
};