LeetCode 热题 HOT 100(普通数组)56. 合并区间

88 阅读4分钟

题目描述

56. 合并区间

以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。

 

示例 1:

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

示例 2:

输入: intervals = [[1,4],[4,5]]
输出: [[1,5]]
解释: 区间 [1,4] 和 [4,5] 可被视为重叠区间。

 

提示:

  • 1 <= intervals.length <= 104
  • intervals[i].length == 2
  • 0 <= starti <= endi <= 104

解题思路

将重叠的区间合并成一个区间,最终返回一个不重叠的区间数组。解题思路如下:

  1. 首先按照区间的起始位置对所有区间进行排序
  2. 初始化一个结果数组,将第一个区间加入结果数组
  3. 遍历剩余的区间,判断当前区间是否与结果数组中的最后一个区间重叠:
  • 如果重叠则合并两个区间(更新解结果数组中最后一个区间的结束位置)
  • 如果不重叠,则将当前区间添加到结果数组中

代码实现

func merge(intervals [][]int) [][]int {
    sort.Slice(intervals, func(i, j int) bool {
        return intervals[i][0] < intervals[j][0]
    })
    ans := make([][]int, 0)
    ans = append(ans, intervals[0])

    for _, inteval := range intervals {
        if interval[0] > ans[len(ans)-1][1] {
            ans = append(ans, interval)
        } else {
            ans[len(ans)-1][1] = max(ans[len(ans)-1][1], interval[1])
        }
    }
    return ans
}
  1. 将问题线性化(通过 sort.Slice 自定义排序规则实现)
  2. 将第一个区间加入到结果数组中作为第一个参考区间
  3. 处理其他所有区间,对于每个新的区间,判断该区间的起始位置是否比最新的参考区间的结束位置更大
  4. 如果是,则说明这是一个新的区间,加入到结果数组中作为新的参考区间
  5. 否则,合并新的区间和最后一个参考区间

时空复杂度分析

只需要遍历一次切片,因此 for 循环部分时间复杂度为 O(n)。

但是需要对整个切片进行排序,最优时间复杂度为 O(nlogn)。

综上,时间复杂度为 O(nlogn)。

最坏情况下,所有区间都是单独的区间,此时空间复杂度为 O(n)。

自己 solve 1

type interval struct {
    start int
    end int
}

type mySlice []interval

func (ms mySlice) Len() int {
    return len(ms)
}

func (ms mySlice) Swap(i, j int) {
    ms[i], ms[j] = ms[j], ms[i]
}

func (ms mySlice) Less(i, j int) bool {
    return ms[i].start < ms[j].start
}

func merge(intervals [][]int) [][]int {
    var mi mySlice
    for _, intervalSlice := range intervals {
        mi = append(mi, interval{
            start: intervalSlice[0],
            end: intervalSlice[1],
        })
    } 
    sort.Sort(mi)
    
    ans := make([][]int, 0)
    start, end := mi[0].start, mi[0].end
    for i := 1; i < mi.Len(); i++ {
        if mi[i].start > end {
            ans = append(ans, []int{start, end})
            start, end = mi[i].start, mi[i].end
        } else {
            end = max(end, mi[i].end)
        }
    }
    ans = append(ans, []int{start, end})
    return ans
}

自己 solve 2

type mySlice []int

func merge(intervals [][]int) [][]int {
    var ms []mySlice
    for _, interval := range intervals {
        ms = append(ms, mySlice(interval))
    } 
    sort.Slice(ms, func(i, j int) bool {
        return ms[i][0] < ms[j][0]
    })
    
    ans := make([][]int, 0)
    start, end := ms[0][0], ms[0][1]
    for i := 1; i < len(ms); i++ {
        if ms[i][0] > end {
            ans = append(ans, []int{start, end})
            start, end = ms[i][0], ms[i][1]
        } else {
            end = max(end, ms[i][1])
        }
    }
    ans = append(ans, []int{start, end})
    return ans
}

关键数组变化表

步骤操作intervalsresult说明
1输入[[1,3],[2,6],[8,10],[15,18]]-初始输入
2排序[[1,3],[2,6],[8,10],[15,18]]-已按起始位置排序,无需变化
3初始化-[[1,3]]将第一个区间加入结果数组
4处理区间[2,6]-[[1,6]]与[1,3]重叠,合并为[1,6]
5处理区间[8,10]-[[1,6],[8,10]]与[1,6]不重叠,直接添加
6处理区间[15,18]-[[1,6],[8,10],[15,18]]与[8,10]不重叠,直接添加
7返回结果-[[1,6],[8,10],[15,18]]最终结果

考察知识点

这道题主要考察以下知识点:

  1. 区间操作:理解和处理区间的重叠关系,判断两个区间是否重叠的条件。

  2. 贪心算法:通过先排序,然后线性处理的方式来解决问题,体现了贪心策略。

  3. 排序应用:按照区间起始位置排序是解决此类问题的关键前置步骤。

  4. 数组操作:对结果数组进行动态添加和更新操作。

  5. 边界条件处理:考虑边界情况,如区间端点的相等情况(如示例2中的[1,4]和[4,5])。

  6. 时间复杂度分析

    • 排序: O(n log n)
    • 遍历合并: O(n)
    • 整体: O(n log n)
  7. 空间复杂度分析:需要 O(n) 的空间来存储结果。

  8. 思维能力:需要思考如何通过排序将问题简化,使得可以用线性的方式解决。

这道题是区间问题的典型代表,掌握这类问题的解决方案对于解决其他区间相关问题(如区间调度、会议室安排等)都有很大帮助。核心思想是通过排序将问题线性化,然后进行扫描处理。