力扣hot100-56.合并区间-排序详解

LeetCode 56. 合并区间:排序详解

1. 这道题到底在问什么

LeetCode 56「合并区间」给出若干区间:

text 复制代码
intervals[i] = [start, end]

要求把所有有重叠的区间合并,最终返回一组互不重叠的区间。

例如:

text 复制代码
intervals = [[1, 3], [2, 6], [8, 10], [15, 18]]

其中:

text 复制代码
[1, 3] 和 [2, 6] 有重叠。

合并后是:

text 复制代码
[1, 6]

另外两个区间不和它重叠,所以最终结果是:

text 复制代码
[[1, 6], [8, 10], [15, 18]]

2. 为什么必须先排序

如果区间是乱序的,很难判断一个区间还会不会与后面的区间重叠。

例如:

text 复制代码
[[8, 10], [1, 3], [2, 6]]

直接从左到右处理时,先看到 [8, 10],之后又看到 [1, 3][2, 6],当前区间顺序没有规律,合并状态会混乱。

因此先按照每个区间的左边界从小到大排序:

text 复制代码
[[1, 3], [2, 6], [8, 10]]

排序后有一个重要性质:

text 复制代码
后面区间的左边界不会比前面更小。

这个性质让我们能够从左到右扫描,只维护一个当前正在合并的区间。

3. 用两个状态理解 Java 写法

Java 版本最清晰的地方,是把事情分成两个状态:

text 复制代码
1. 当前暂存区间:[start, rightmostRightBound]
2. 已经确定的结果区间:res

当前暂存区间

text 复制代码
[start, rightmostRightBound]

表示:

text 复制代码
当前已经合并到什么范围,但它暂时还不能放入最终答案。

因为后面可能还有区间和它重叠,需要继续扩大它的右边界。

已确定结果集合

java 复制代码
List<int[]> res = new ArrayList<>();

res 中保存的是:

text 复制代码
已经确认不可能再和后续区间合并的区间。

区间一旦加入 res,后面就不再修改它。

为什么可以确认?

因为区间已经按左边界排好序。当当前暂存区间的右边界是 rightmostRightBound,下一个区间的左边界是 currentStart,如果:

text 复制代码
currentStart > rightmostRightBound

就说明二者完全不重叠。

而后续区间的左边界只会更大,因此后面也不可能再和当前暂存区间重叠。

所以当前暂存区间可以正式加入 res

4. 每个新区间只有两种情况

排序后,遍历当前区间:

text 复制代码
[currentStart, currentEnd]

与当前暂存区间:

text 复制代码
[start, rightmostRightBound]

比较时只有两种情况。

情况一:不重叠

如果:

text 复制代码
currentStart > rightmostRightBound

例如:

text 复制代码
当前暂存区间:[1, 6]
当前新区间:[8, 10]

因为:

text 复制代码
8 > 6

所以二者不重叠。

此时要做两件事:

text 复制代码
1. [1, 6] 已经确定,加入 res。
2. [8, 10] 成为新的当前暂存区间。

情况二:重叠或端点相接

如果:

text 复制代码
currentStart <= rightmostRightBound

例如:

text 复制代码
当前暂存区间:[1, 3]
当前新区间:[2, 6]

因为:

text 复制代码
2 <= 3

它们重叠,合并后左边界仍然是 1,右边界取两者较大值:

text 复制代码
[1, max(3, 6)] = [1, 6]

只更新右边界:

java 复制代码
rightmostRightBound = Math.max(rightmostRightBound, currentEnd);

为什么左边界不用更新?

因为我们已经按左边界升序排序,当前暂存区间的 start 一定不大于当前新区间的 currentStart

还有一个细节:判断不重叠必须写成:

java 复制代码
currentStart > rightmostRightBound

不能写成 >=

例如:

text 复制代码
[1, 4] 和 [4, 5]

它们在端点 4 相接,题目认为应该合并成:

text 复制代码
[1, 5]

因此当:

text 复制代码
currentStart == rightmostRightBound

时,仍然要走合并逻辑。

5. 用示例完整模拟

示例:

text 复制代码
intervals = [[1, 3], [2, 6], [8, 10], [15, 18]]

排序后顺序不变。

初始化第一个暂存区间:

text 复制代码
start = 1
rightmostRightBound = 3
res = []

当前暂存区间是:

text 复制代码
[1, 3]

处理 [2, 6]

text 复制代码
2 <= 3,重叠。

更新右边界:

text 复制代码
rightmostRightBound = max(3, 6) = 6

当前暂存区间变成:

text 复制代码
[1, 6]

处理 [8, 10]

text 复制代码
8 > 6,不重叠。

此时 [1, 6] 已经不可能再与后续区间合并:

text 复制代码
res = [[1, 6]]

[8, 10] 设为新的暂存区间:

text 复制代码
start = 8
rightmostRightBound = 10

处理 [15, 18]

text 复制代码
15 > 10,不重叠。

先收集当前暂存区间:

text 复制代码
res = [[1, 6], [8, 10]]

新的暂存区间是:

text 复制代码
[15, 18]

循环结束后,最后这个暂存区间还没有加入结果集,因此补上:

text 复制代码
res = [[1, 6], [8, 10], [15, 18]]

6. 为什么循环结束后还要补一次 res.add

暂存区间在什么情况下会加入 res

答案是:

text 复制代码
遇到下一个不重叠区间时。

最后一个暂存区间后面没有下一个区间,因此没有机会触发"不重叠"逻辑。

例如:

text 复制代码
intervals = [[1, 3], [2, 6]]

处理 [2, 6] 后,暂存区间更新为:

text 复制代码
[1, 6]

然后循环结束。

如果没有最后的:

java 复制代码
res.add(new int[]{start, rightmostRightBound});

[1, 6] 将永远不会加入结果集。

所以循环结束后必须补上最后一个暂存区间。

7. 为什么 Java 用 List<int\[\]>,不能直接用 int\[\]\[\]

每个区间是两个整数:

text 复制代码
[start, end]

在 Java 中,它可以用:

java 复制代码
int[] interval = {start, end};

最终结果是一组区间,因此返回类型是:

java 复制代码
int[][]

但在遍历开始前,我们不知道最终会得到多少个不重叠区间。

例如:

text 复制代码
[[1, 10], [2, 3]]

最终可能只有一个区间:

text 复制代码
[[1, 10]]

而:

text 复制代码
[[1, 2], [5, 6], [9, 10]]

最终可能有三个区间。

Java 的二维数组创建后,第一维长度固定:

java 复制代码
int[][] result = new int[3][2];

创建时必须提前知道结果有多少行。

本题开始时不知道答案数量,因此先使用能动态扩容的:

java 复制代码
List<int[]> res = new ArrayList<>();

每确定一个区间,就加入:

java 复制代码
res.add(new int[]{start, rightmostRightBound});

最后再转成题目要求的二维数组。

8. 为什么 Java 需要转换,C++ 不需要

Java 代码中:

java 复制代码
List<int[]> res = new ArrayList<>();

但方法返回类型要求:

java 复制代码
int[][]

List<int[]>int[][] 是不同的类型,不能直接返回。

因此需要:

java 复制代码
return res.toArray(new int[res.size()][]);

它表示:

text 复制代码
把动态长度的 List<int[]>,转换为长度刚好等于结果数量的 int[][]。

C++ 中:

cpp 复制代码
vector<vector<int>> result;

而方法返回类型也是:

cpp 复制代码
vector<vector<int>>

result 的类型和返回类型完全相同,所以可以直接:

cpp 复制代码
return result;

两种语言的对应关系:

C++ Java 含义
vector<vector<int>> result List<int[]> res 动态保存多个结果区间
result.push_back(interval) res.add(interval) 添加一个区间
result.back() 获取 res 最后一个元素 获取最后一个结果区间
return result res.toArray(...) 按题目返回类型返回结果

9. Java 代码完整注释

java 复制代码
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

class Solution {
    public int[][] merge(int[][] intervals) {
        // 题目约束通常保证 intervals 非空。
        // 保留判断后,独立调用时也能正常处理空数组。
        if (intervals.length == 0) {
            return new int[0][];
        }

        // 按区间左边界从小到大排序。
        Arrays.sort(intervals, (x, y) -> Integer.compare(x[0], y[0]));

        // 保存已经确定、不再可能和后续区间合并的结果区间。
        List<int[]> res = new ArrayList<>();

        // [start, rightmostRightBound] 是当前正在合并的暂存区间。
        int start = intervals[0][0];
        int rightmostRightBound = intervals[0][1];

        // 从第二个区间开始,依次与当前暂存区间比较。
        for (int i = 1; i < intervals.length; i++) {
            int currentStart = intervals[i][0];
            int currentEnd = intervals[i][1];

            // 当前区间在暂存区间右侧,且没有重叠。
            if (currentStart > rightmostRightBound) {
                // 暂存区间已经确定,收集到结果中。
                res.add(new int[]{start, rightmostRightBound});

                // 当前区间成为新的暂存区间。
                start = currentStart;
                rightmostRightBound = currentEnd;
            } else {
                // 两个区间重叠或端点相接。
                // 左边界无需更新,只扩展到更靠右的右边界。
                rightmostRightBound = Math.max(rightmostRightBound, currentEnd);
            }
        }

        // 最后一个暂存区间没有后续区间触发收集,因此循环结束后补上。
        res.add(new int[]{start, rightmostRightBound});

        // List<int[]> 转为题目要求的 int[][]。
        return res.toArray(new int[res.size()][]);
    }
}

10. 复杂度分析

时间复杂度:

text 复制代码
O(n log n)

排序需要 O(n log n),后续遍历需要 O(n),整体由排序主导。

空间复杂度:

text 复制代码
O(n)

res 最坏情况下会保存 n 个不重叠区间。

如果不计算返回结果占用的空间,合并过程本身只使用了几个普通变量;不过 Java 的对象数组排序实现可能还会使用额外辅助空间,因此不宜简单断言为 O(log n)

11. 总结

这道题的核心是:

text 复制代码
先按左边界排序,再从左到右维护一个正在合并的区间。

Java 写法中有两个清晰状态:

text 复制代码
[start, rightmostRightBound]:当前暂存、仍可能继续扩展的合并区间。
res:已经彻底确定、不会再变化的结果区间。

处理一个新区间时:

text 复制代码
重叠:更新当前暂存区间的右边界。
不重叠:收集当前暂存区间,把新区间作为新的暂存区间。

最后别忘了:

text 复制代码
循环结束后,把最后一个暂存区间加入 res。

可以把整题记成一句话:

text 复制代码
排序后,能合并就扩右边界;不能合并就收集旧区间,并从新区间重新开始。
相关推荐
卡提西亚2 小时前
leetcode-1438. 绝对差不超过限制的最长连续子数组
算法·leetcode·职场和发展
Java面试题总结3 小时前
LeetCode 93.复原IP地址
算法·leetcode·职场和发展·.net
从零开始的代码生活_3 小时前
C++ 多态详解:虚函数、动态绑定、抽象类与虚表原理
开发语言·c++·后端·学习·算法
yuannl103 小时前
图的存储方式
数据结构
泷寂4 小时前
最小生成树 (MST基础)
算法
Daniel_1234 小时前
数组——总结篇
算法
不懒不懒4 小时前
【针对路面识别数据集,结合三轴加速度标准化数据及多路面识别需求,以下是算法选择与处理方案】
算法
Reart4 小时前
Leetcode 121. 买卖股票的最佳时机(717)
后端·算法
会编程的小孩5 小时前
初识数据类型以及变量定义
数据结构·算法