LEETCODE 056Medium

合并区间

按左端点排序后,结果集里的区间彼此隔开,新区间够不着最后一个就更够不着前面的——所以只需和最后一个比较。

问题拆解

给一堆区间,把所有互相重叠的合并成一个,返回互不重叠的结果。麻烦在于重叠关系是传递的:[1,3][2,6] 重叠,[2,6] 又和 [5,8] 重叠,三个要并成一个。如果对每一对区间都去检查重叠,是 O(n²) 的比较,还要处理合并后再次触发合并的连锁反应。

先按左端点排序,局面就完全不同了:逐个扫描时,能和当前区间重叠的区间左端点都不比它大,也就是说,它的“合并对象”只可能出现在已经处理过的部分里。更进一步,只可能是结果集里最后那一个。

排序保证了重叠只发生在相邻的扫描顺序之间:新区间若与结果集里更早的区间重叠,那个区间和最后一个区间之间就不可能断开——矛盾。所以每一步只需回头看一眼。

排序后与结果集尾部合并

严格论证一下“只看最后一个”为什么够:结果集里的区间互不重叠且按左端点递增,所以最后一个区间的左端点大于前面所有区间的右端点。而新区间 cur 的左端点不小于最后一个的左端点(排序保证),因此 cur 也够不着前面任何区间。判断就简化成一条:cur[0] <= last[1] 则重叠,把 last[1] 扩到两者较大值;否则 cur 自成一段。

public int[][] merge(int[][] intervals) {
    Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0]));
    List<int[]> merged = new ArrayList<>();
    for (int[] cur : intervals) {
        int last = merged.size() - 1;
        if (!merged.isEmpty() && cur[0] <= merged.get(last)[1]) {
            // 有重叠,扩展右端点;注意 cur 可能被 last 完全包住
            merged.get(last)[1] = Math.max(merged.get(last)[1], cur[1]);
        } else {
            merged.add(cur);
        }
    }
    return merged.toArray(new int[0][]);
}
def merge(intervals: list[list[int]]) -> list[list[int]]:
    intervals.sort(key=lambda iv: iv[0])
    merged = []
    for cur in intervals:
        if merged and cur[0] <= merged[-1][1]:
            # 有重叠,扩展右端点;注意 cur 可能被完全包住
            merged[-1][1] = max(merged[-1][1], cur[1])
        else:
            merged.append(cur)
    return merged
func merge(intervals [][]int) [][]int {
    slices.SortFunc(intervals, func(a, b []int) int { return a[0] - b[0] })
    merged := [][]int{}
    for _, cur := range intervals {
        n := len(merged)
        if n > 0 && cur[0] <= merged[n-1][1] {
            // 有重叠,扩展右端点;注意 cur 可能被完全包住
            merged[n-1][1] = max(merged[n-1][1], cur[1])
        } else {
            merged = append(merged, cur)
        }
    }
    return merged
}
pub fn merge(mut intervals: Vec<Vec<i32>>) -> Vec<Vec<i32>> {
    intervals.sort_by_key(|iv| iv[0]);
    let mut merged: Vec<Vec<i32>> = Vec::new();
    for cur in intervals {
        match merged.last_mut() {
            // 有重叠,扩展右端点;注意 cur 可能被完全包住
            Some(last) if cur[0] <= last[1] => last[1] = last[1].max(cur[1]),
            _ => merged.push(cur),
        }
    }
    merged
}

最容易错的一行是 max(last[1], cur[1])。排序只按左端点,cur 的右端点未必更大——比如 [1,10] 后面跟着 [2,3],若直接写 last[1] = cur[1],区间会被错误地缩短。另外重叠判断用 <= 而不是 <:题目里 [1,4][4,5] 这种端点相触的算重叠,要并成 [1,5]

复杂度

指标 复杂度 原因
时间 O(n log n) 排序占主导,之后线性扫描一遍
空间 O(log n) 排序的栈开销;结果集不算额外空间

可以迁移的模式

  • 区间问题先按左端点(或右端点)排序,把二维的重叠关系降成线性扫描;
  • 排序后维护一个“当前正在生长的区间”,新元素要么并入它、要么让它定稿;
  • 合并时对两个端点分别取极值,别默认后来者的右端点更大。

“排序 + 与结果尾部比较”这套流程,在插入区间(57)、会议室(253)等区间题里都是同一副骨架。