【LeetCode Hot100 刷题日记 (14/100)】56. 合并区间 —— 数组、排序、区间合并、贪心算法📌

73 阅读6分钟

📌 题目链接:leetcode.cn/problems/merge-intervals/

🔍 难度:中等 | 🏷️ 标签:数组、排序、区间合并、贪心算法

⏱️ 目标时间复杂度:O(n log n)

💾 空间复杂度:O(log n)(额外空间)


✅ 本题是 区间类问题 的经典代表,常用于考察对“重叠判断”、“区间覆盖”和“排序后贪心处理”的理解。
💡 在面试中,这类题目经常出现在 系统设计中的资源调度、日程安排、任务分配 场景中,例如:会议室预订冲突检测、CPU 时间片合并等。


🔍 题目分析

给定一个二维整数数组 intervals,每个元素 [start, end] 表示一个闭区间。要求将所有有重叠的区间进行合并,最终返回一个不重叠且覆盖全部原始区间的最小集合

🧩 关键点解析:

  • 区间是否重叠?
    👉 若两个区间 [a,b][c,d] 满足 b >= c,则它们有交集(包括端点相等的情况)。
  • 合并规则:
    👉 取左端点较小者,右端点较大者,即 [min(a,c), max(b,d)]
  • 注意边界情况:
    • 输入为空
    • 只有一个区间
    • 所有区间都不重叠
    • 所有区间都重叠(如 [[1,2],[2,3],[3,4]]

📊 示例回顾:

输入: [[1,3],[2,6],[8,10],[15,18]]
输出: [[1,6],[8,10],[15,18]]

解释:[1,3][2,6] 重叠 → 合并为 [1,6];其余无重叠。


🧠 核心算法及代码讲解

🔥 核心思想:排序 + 贪心遍历

✅ 策略步骤:

  1. 按左端点升序排序:确保能合并的区间在排序后是连续的。
  2. 逐个扫描区间
    • 如果当前区间与上一个合并后的区间无重叠(即当前左端点 > 上一右端点),直接加入结果。
    • 否则,更新上一区间的右端点为两者最大值(实现“合并”)。

🔄 为什么排序有效?

📌 关键洞察:如果区间按左端点排序,则任意两个可合并的区间必然是相邻或接近的。否则会违反排序顺序,导致矛盾(见官方证明)。

❗ 这种“排序+贪心”的模式广泛应用于:

  • 区间调度问题(如会议安排)
  • 最少射箭次数(LeetCode 435)
  • 无重叠区间(LeetCode 435)

🧮 数学表达:

设当前区间为 [L, R],已合并最后一个区间为 [prev_L, prev_R]

  • R < prev_R:无需合并(已在范围内)
  • L <= prev_R:重叠 → 更新 prev_R = max(prev_R, R)
  • L > prev_R:不重叠 → 新建区间 [L, R]

💡 小技巧:用 !merged.size() 判断是否为空,避免越界访问。


🧩 解题思路(分步详解)

  1. 边界处理

    • 如果输入为空,直接返回空列表。
  2. 排序

    • 使用 sort() 对整个 intervals 按第一个元素(左端点)升序排列。
  3. 初始化结果容器

    • 创建 vector<vector<int>> merged 存储答案。
  4. 遍历每个区间

    • 提取当前区间 [L, R]
    • 判断是否与 merged.back() 重叠:
      • 不重叠:merged.back()[1] < L → 推入新区间
      • 重叠:更新 merged.back()[1] = max(merged.back()[1], R)
  5. 返回结果

🧠 举个例子:

原始:[[1,3],[2,6],[8,10],[15,18]]

排序后仍相同(已有序)

步骤:

  • 加入 [1,3]
  • [2,6]: 2 ≤ 3 → 合并 → [1,6]
  • [8,10]: 8 > 6 → 新增 → [1,6],[8,10]
  • [15,18]: 15 > 10 → 新增 → [1,6],[8,10],[15,18]

📈 算法分析

项目分析
时间复杂度O(n log n) ✅
- 排序占主导:O(n log n)
- 遍历:O(n)
空间复杂度O(log n) ✅
- 排序递归栈空间(快速排序平均情况)
- 结果数组不算额外空间(题目要求输出)
稳定性稳定(只要排序稳定即可)
适用场景所有区间合并类问题

🚫 常见误区:

  • 忽略排序!直接暴力枚举会导致 O(n²) 时间复杂度。
  • 错误判断重叠条件(如写成 L > prev_R 而不是 L <= prev_R)。
  • 忘记处理空输入。

🧱 代码(严格保留原结构 + 行注释)

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

// 主函数:合并区间
class Solution {
public:
    vector<vector<int>> merge(vector<vector<int>>& intervals) {
        // 边界处理:空输入
        if (intervals.size() == 0) {
            return {};
        }

        // 🔥 第一步:按左端点排序(升序)
        sort(intervals.begin(), intervals.end());

        // 🛠️ 初始化结果数组
        vector<vector<int>> merged;

        // 🧩 遍历每一个区间
        for (int i = 0; i < intervals.size(); ++i) {
            int L = intervals[i][0];  // 当前区间的左端点
            int R = intervals[i][1];  // 当前区间的右端点

            // 🤔 判断是否与最后一个已合并区间重叠
            // 如果 merged 为空 或 当前左端点 > 最后一个右端点 → 不重叠
            if (!merged.size() || merged.back()[1] < L) {
                merged.push_back({L, R});  // 添加新区间
            }
            else {
                // ✅ 重叠:更新最后一个区间的右端点
                merged.back()[1] = max(merged.back()[1], R);
            }
        }

        return merged;
    }
};

// 测试主函数
signed main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);

    // 🧪 测试用例 1
    vector<vector<int>> intervals1 = {{1,3},{2,6},{8,10},{15,18}};
    Solution sol;
    auto result1 = sol.merge(intervals1);
    cout << "Test 1: ";
    for (auto& v : result1) {
        cout << "[" << v[0] << "," << v[1] << "] ";
    }
    cout << endl;

    // 🧪 测试用例 2
    vector<vector<int>> intervals2 = {{1,4},{4,5}};
    auto result2 = sol.merge(intervals2);
    cout << "Test 2: ";
    for (auto& v : result2) {
        cout << "[" << v[0] << "," << v[1] << "] ";
    }
    cout << endl;

    // 🧪 测试用例 3
    vector<vector<int>> intervals3 = {{4,7},{1,4}};
    auto result3 = sol.merge(intervals3);
    cout << "Test 3: ";
    for (auto& v : result3) {
        cout << "[" << v[0] << "," << v[1] << "] ";
    }
    cout << endl;

    return 0;
}

📝 输出示例:

Test 1: [1,6] [8,10] [15,18] 
Test 2: [1,5] 
Test 3: [1,7] 

💡 面试拓展 & 进阶思考

🎯 1. 如何优化空间?

  • 原地修改:若允许破坏输入,可以使用双指针从后往前压缩,但需注意顺序。
  • 减少拷贝:使用 move() 或引用传递避免深拷贝。

🎯 2. 多维区间如何处理?

  • 三维区间?→ 按某一维度排序,再逐层合并。
  • 区间树 / 线段树:适用于频繁查询和插入的动态场景。

🎯 3. 类似题目推荐:

题号题名核心技巧
435无重叠区间贪心选右端点小的
452用最少数量的箭引爆气球区间重叠判定
561数组拆分分组贪心
757设置交替位位运算 + 贪心

🌟 本期完结,下期见!🔥

👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!

💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪


📣 下一期预告:LeetCode 热题 100 第189题 —— 轮转数组(中等)

🔹 题目:给定一个数组,将其向右轮转 k 步,其中 k 是非负整数。例如:[1,2,3,4,5,6,7], k=3[5,6,7,1,2,3,4]

🔹 核心思路:使用 三次反转法(Reverse Three Times),时间复杂度 O(n),空间复杂度 O(1)

🔹 考点:数组操作、原地修改、数学规律、旋转对称性

🔹 难度:中等,但非常经典,是许多数组旋转问题的基础!

💡 提示:不要用额外数组存储!掌握 reverse 函数的应用!

🔄 公式:
reverse(arr, 0, n-k)
reverse(arr, n-k, n)
reverse(arr, 0, n)
(整体反转三次)

📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!


✅ 本系列将持续更新至 LeetCode Hot100 全部题目,每一篇都是你面试路上的“知识锚点”。
🚀 一起坚持,直到 Offer 到手!🎯