LeetCode 56:合并区间(贪心算法) —— 题解

👋 欢迎阅读

🎯 欢迎来到「合并区间」题解之旅! 本文将带你从“将重叠区间合并成不重叠区间”这一数组处理问题出发,深入理解排序 + 一次遍历的经典解法,并掌握如何通过按左端点排序、动态维护右端点高效完成合并。

在开始之前,建议你先:

  • 了解题目背景:这是 LeetCode 56 题,给定若干区间 [start, end],要求合并所有重叠的区间,返回一个恰好覆盖所有输入区间的不重叠区间数组。本质上,我们需要先排序让相邻区间有序,再通过贪心合并——若当前区间与上一个合并后的区间有重叠,则更新右端点为二者最大值;否则直接加入结果集。

  • 明确学习目标:掌握排序 + 一次遍历的核心流程——按区间左端点升序排序,维护一个结果列表,依次遍历区间并判断是否与最后一个区间重叠(当前start <= 上一end),重叠则合并(更新 end = max(end, 当前end)),不重叠则加入。理解这种贪心合并策略的正确性(排序后只需考虑相邻区间,局部合并即可得到全局结果),并熟练处理边界情况(如空数组、只有一个区间、完全包含的区间等)。

  • 准备好环境:建议在本地 IDE 或 LeetCode 在线编辑器中打开代码,边看边运行,亲手验证示例(如 intervals = [[1,3],[2,6],[8,10],[15,18]] 输出 [[1,6],[8,10],[15,18]])。

本文将从问题转化、排序合并策略设计、代码实现到复杂度分析,层层递进。即使你对区间处理还不熟悉,我们也会从“按起点排好队,一个个看能不能接上”这一直觉出发,让你轻松抓住核心思想——排序之后,所有需要合并的区间一定是相邻的,所以只需一趟扫描就能完成合并。现在,让我们一起把重叠的区间“粘合”起来,合并成最简洁的结果吧! 📏🔗


一、题目

56. 合并区间 - 力扣(LeetCode)

二、做题思路

1. 问题分析(前置分析)

给定一组区间 [start, end],要求 合并所有重叠的区间,返回一个 不重叠 的区间数组,使其 恰好覆盖输入中的所有区间
核心贪心思想:先按照区间 左端点升序排序,这样所有可能重叠的区间都会相邻。然后 顺序扫描,维护一个当前合并区间,若下一个区间的左端点 ≤ 当前合并区间的右端点,则 合并(更新右端点为两者最大值);否则,断开,将当前合并区间存入结果,并开始新区间。


2. 贪心策略(核心决策规则)

  • 第一步:将 intervals左端点升序排序(若左端点相同,按右端点升序,但本题左端点排序已足够)。

  • 第二步:初始化 当前合并区间 为第一个区间,记录其 leftright

  • 第三步:从第二个区间开始遍历:

    • 若当前区间的 左端点 ≤ 当前合并区间的右端点,说明有重叠,合并right = max(right, currentRight)

    • 否则,说明无重叠,将当前合并区间存入结果,并 更新当前合并区间为当前遍历到的区间

  • 第四步:遍历结束后,将最后一个合并区间加入结果


3. 正确性说明(简单版本)

按左端点排序后,所有可能重叠的区间一定连续排列在一起。因此,顺序扫描时,若当前区间的左端点 ≤ 当前合并区间的右端点,则它们必然重叠,必须合并。若左端点 > 右端点,则由于后续区间的左端点 ≥ 当前区间的左端点,它们也不可能与当前合并区间重叠,因此可以安全断开,开始新区间。这种 “能合并就合并” 的贪心策略不会遗漏任何重叠情况,最终得到的是互不重叠且覆盖完整的区间集合,是全局最优解。


4. 实现细节(边界防护)

  • 先对 intervals 排序,时间复杂度 O(n log n)

  • 使用 leftright 维护当前合并区间的左右端点,初始为第一个区间的端点。

  • 遍历时,比较 a <= right 判断是否重叠,其中 a 是当前区间的左端点。

  • 若不重叠,{left, right} 加入结果,并 更新 left = a, right = b

  • 遍历结束后,务必加入最后一个合并区间(循环内未处理最后一段)。

  • 若输入为空,需要特殊处理(但题目保证 intervals.length >= 1)。


5. 返回值(目标映射)

返回 ret,即 合并后的不重叠区间数组

四、代码

class Solution 
{
public:
    vector<vector<int>> merge(vector<vector<int>>& intervals) 
    {
        // 1. 先按区间左端点升序排序,保证后续合并只需顺序扫描
        sort(intervals.begin(), intervals.end());
    int row = intervals.size();
    int col = intervals[0].size();

    vector&lt;vector&lt;int&gt;&gt; ret;   // 存储合并后的区间

    // 初始化当前合并区间的左右端点(第一个区间的左右端点)
    int left = intervals[0][0];
    int right = intervals[0][1];

    // 2. 从第二个区间开始遍历(因为第一个已作为初始合并区间)
    for (int i = 1; i &lt; row; i++) 
    {
        int a = intervals[i][0];   // 当前区间的左端点
        int b = intervals[i][1];   // 当前区间的右端点

        // 如果当前区间的左端点 &lt;= 当前合并区间的右端点,说明有重叠,需要合并
        if (a &lt;= right) 
        {
            // 合并后右端点取两者最大值(左端点因为排序已经确保 a &gt;= left,所以左端点不变)
            right = max(right, b);
        } 
        else 
        {
            // 否则没有重叠,将当前合并区间加入结果集,并开始新的合并区间
            vector&lt;int&gt; value;
            value.push_back(left);
            value.push_back(right);
            ret.push_back(value);

            // 新合并区间初始化为当前区间
            left = a;
            right = b;
        }
    }

    // 3. 将最后一个合并区间加入结果集(循环中最后一段未加入)
    vector&lt;int&gt; value;
    value.push_back(left);
    value.push_back(right);
    ret.push_back(value);

    // 4. 返回合并后的区间列表
    return ret;
}
};

五、流程图

六、正确性说明(详细版)

步骤 1:符号与问题建模
+----------------------------------------------------+
|  输入:区间列表 intervals,每个区间 [start, end]   |
|  目标:合并所有重叠区间,返回不重叠的区间列表      |
+----------------------------------------------------+
                    |
                    v
+----------------------------------------------------+
|  贪心策略:先按左端点升序排序,然后顺序扫描。      |
|  维护当前合并区间 [left, right]。                  |
|  遍历下一区间 [a, b]:                            |
|  - 若 a <= right(重叠),则合并 right = max(right,b)|
|  - 否则(不重叠),将 [left,right] 加入结果,     |
|    新合并区间设为 [a, b]。                         |
|  贪心实质:**排序后,若当前区间与合并区间重叠**,  |
|  则合并它一定是最优的(因为所有后续区间左端点更大,|
|  不会影响已作出的合并决策)。                       |
+----------------------------------------------------+
  • 按左端点升序排序,保证后续区间左端点 ≥ 当前区间左端点

  • 维护一个当前合并区间 [left, right],初始为第一个区间。

  • 贪心选择:每次遇到新区间时,若其左端点 ≤ 当前右端点,说明重叠,必须合并;否则无法再与当前区间重叠,当前区间可封存。


步骤 2:关键性质 —— 贪心选择的正确性(反证法)
+----------------------------------------------------+
|  性质 1:排序后,若当前区间 [a,b] 与合并区间      |
|  [left,right] 满足 a <= right,则二者必然重叠,   |
|  且合并后的区间为 [left, max(right,b)]。           |
|  反证:若不合并,则最终结果中这两个区间将分别存在,|
|  但它们的并集是连续的,合并不会影响其他区间,       |
|  因此不合并只会使区间数增多,不可能更优。           |
+----------------------------------------------------+
                     |
                     v
+----------------------------------------------------+
|  性质 2:若 a > right,则当前区间与合并区间以及    |
|  之前所有已处理区间均不重叠。                       |
|  证明:之前所有区间的右端点 ≤ right(因为合并区间  |
|  右端点是已处理区间右端点的最大值),而 a > right,|
|  所以 a 大于之前所有区间的右端点,**不可能重叠**。  |
|  因此可以安全地将当前合并区间加入结果。             |
+----------------------------------------------------+

详细论证

  • 性质1(重叠必须合并):当 a <= right 时,当前区间 [a,b] 与合并区间 [left,right] 有交集(因为 a <= rightleft <= a)。如果不合并,则两个区间在最终结果中会分开存在,但它们实际上覆盖了一个连续范围,合并后不会影响任何其他区间(因为其他区间要么左端点更大,要么已被处理)。因此合并是安全的,且能使区间数更少,必然更优。

  • 性质2(不重叠则安全封存):当 a > right 时,由于所有已处理区间的右端点都不超过 rightright 是已处理区间的右端点最大值),而 a 大于 right,所以当前区间的左端点大于所有已处理区间的右端点。这意味着当前区间与之前所有区间都无重叠。因此可以放心地将 [left,right] 加入结果,并开始新的合并区间。


步骤 3:归纳证明 —— 扫描过程得到全局最优
+----------------------------------------------------+
|  初始:排序后,合并区间为第一个区间。              |
|  归纳假设:处理完前 i 个区间后,已加入结果的区间   |
|  互不重叠且覆盖了前 i 个区间的并集,当前合并区间   |
|  是剩余未封存区间的并集。                          |
+----------------------------------------------------+
                     |
                     v
+----------------------------------------------------+
|  处理第 i+1 个区间 [a,b]:                        |
|  - 若 a <= right,由性质1,合并,right 更新。      |
|  - 否则,由性质2,将当前合并区间封存,开始新区间。 |
|  归纳假设保持。                                    |
+----------------------------------------------------+
                     |
                     v
+----------------------------------------------------+
|  遍历结束后,将最后的合并区间加入结果。            |
|  最终结果覆盖所有区间,且互不重叠,即为最优解。    |
+----------------------------------------------------+

详细论证

  • 归纳基础:处理前,结果为空,合并区间为第一个区间,显然覆盖了已处理的1个区间

  • 归纳步骤:假设已处理前 i 个区间,结果中的区间互不重叠且覆盖了前 i 个区间的并集,当前合并区间 [left,right] 是前 i 个区间中尚未封存部分的并集(即最右侧可能继续扩展的区间)。考虑第 i+1 个区间 [a,b](排序后 a >= left):

    • a <= right,说明新区间与当前合并区间重叠,合并后右端点变为 max(right,b),新合并区间覆盖了前 i+1 个区间的并集,且结果中的区间仍互不重叠(因为封存部分未变)。

    • a > right,由性质2,新区间与当前合并区间及之前所有区间均不重叠。因此将当前合并区间封存到结果中,新的合并区间设为 [a,b]。结果中的区间仍互不重叠,且覆盖了前 i+1 个区间的并集。

  • 终止:遍历所有区间后,将最后一个合并区间加入结果。最终结果满足互不重叠且覆盖所有区间,贪心解即为最优解(因为每次合并决策都是强制或最优的,没有其他选择能减少区间总数)

🎯 闭幕

动态规划学习路径图

🎉 恭喜你完成了「合并区间」问题的学习!

为了巩固知识并进一步拓展,建议你:

🚀 动手实践
在 LeetCode 上提交代码,尝试不同的测试用例。

💡 深入思考

  • 本题采用 排序 + 贪心合并 策略:先按区间左端点升序排序,然后依次判断当前区间是否与已合并区间重叠,若重叠则扩展右端点,否则保存并开启新区间。为什么按左端点排序后,只需要和“当前合并区间”比较,而无需与更早的区间比较?

  • 代码中判断重叠的条件是 a <= right,这表示当前区间左端点 ≤ 已合并区间右端点。如果 a == right 也算重叠吗? 示例 2 中 [1,4][4,5] 合并为 [1,5],说明相等情况确实需要合并,代码中的 <= 是正确的。

📚 延伸挑战

  • 如果区间是 左闭右开 [start, end),合并逻辑需要改变吗?

如果你觉得本文对你有所帮助,欢迎:

👍 点赞 / 收藏
👤 关注作者,获取更多题解
💬 留言交流你的疑问或优化思路

祝你在 算法之路 上越走越稳,早日攻克每一道难题!下次见 🚀✨

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值