Leecode热题100:合并区间(数组)

目录

回归本质,拆解"区间"

[1. 什么是区间?](#1. 什么是区间?)

[2. 什么是"重叠"?](#2. 什么是“重叠”?)

[3. 最核心的痛点是什么?](#3. 最核心的痛点是什么?)

第一性原理推导逻辑

[编写 C++ 代码](#编写 C++ 代码)

[1. 排序:建立数轴秩序](#1. 排序:建立数轴秩序)

[2. 线性扫描:局部决策](#2. 线性扫描:局部决策)

深度复盘

为什么排序是第一步?

为什么只比较右边界?

面试回答

第一步:明确物理模型(寻找矛盾点)

第二步:推导合并逻辑(分类讨论)

第三步:总结效率与实现


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

示例 1:

输入:intervals = \[1,3,2,6,8,10,15,18]

输出:\[1,6,8,10,15,18]

解释:区间 1,32,6 重叠, 将它们合并为 1,6.

示例 2:

输入:intervals = \[1,4,4,5]

输出:\[1,5]

解释:区间 1,44,5 可被视为重叠区间。

示例 3:

输入:intervals = \[4,7,1,4]

输出:\[1,7]

解释:区间 1,44,7 可被视为重叠区间。

(来源:Leecode)


回归本质,拆解"区间"

1. 什么是区间?

在数轴上,一个区间 start, end 代表一段连续的范围。

2. 什么是"重叠"?

假设有两个区间 As1, e1 和 Bs2, e2。它们重叠的本质真理是:

其中一个区间的起点,落在另一个区间的范围之内。

3. 最核心的痛点是什么?

如果数组是乱序的(如示例 3:[[4,7], [1,4]]),我们很难判断当前区间该和谁合并。为了判断 4,7 是否能合并,我们需要扫描整个数组去找谁的结尾大于等于 4。

  • 结论无序是效率的敌人。在数轴上处理问题,天然的物理顺序是"从左到右"。

第一性原理推导逻辑

如果我们把所有区间按起点排好序,会发生什么?

一旦按起点排序,对于连续的两个区间 A 和 B,我们能确定 s1 <= s2。此时,重叠的情况被极度简化了:

  • 情况 A:s2 <= e1。说明 B 的起点在 A 的范围内。新的右边界是这两个区间右边界的最大值,即 max(e1, e2)。

  • 情况 B:s2 > e1。说明 B 已经完全超出了 A 的范围。A 已经"定型"了,再往后的区间起点只会比 s2 更大,绝对不可能再和 A 重叠。我们可以把 A 存入结果。


编写 C++ 代码

1. 排序:建立数轴秩序

我们需要让区间按 start 升序排列。

cpp 复制代码
#include <vector>
#include <algorithm>

using namespace std;

vector<vector<int>> merge(vector<vector<int>>& intervals) {
    if (intervals.empty()) return {};

    // 第一步:基于第一性原理的"秩序化"
    // 按区间的起点进行升序排序
    sort(intervals.begin(), intervals.end(), [](const vector<int>& a, const vector<int>& b) {
        return a[0] < b[0];
    });

    vector<vector<int>> merged;

2. 线性扫描:局部决策

我们维护一个"当前正在合并"的区间,拿它和下一个区间比较。

cpp 复制代码
// 先把第一个区间放进去作为"种子"
    merged.push_back(intervals[0]);

    for (int i = 1; i < intervals.size(); ++i) {
        // 取出结果集中最后一个已经合并好的区间(当前的基准)
        int& curr_start = merged.back()[0];
        int& curr_end = merged.back()[1];
        
        // 下一个区间的起点和终点
        int next_start = intervals[i][0];
        int next_end = intervals[i][1];

        // 核心逻辑判断:
        if (next_start <= curr_end) {
            // 情况 A:重叠了,更新当前基准的右边界
            // 为什么取 max?因为可能出现 [[1,10], [2,5]] 这种包含关系
            curr_end = max(curr_end, next_end);
        } else {
            // 情况 B:没重叠,当前的基准彻底完成,开启一个新区间
            merged.push_back(intervals[i]);
        }
    }

    return merged;
}

深度复盘

为什么排序是第一步?

如果不排序,我们需要比较 n² 次(每两个区间都比一下)。排序后,我们只需要比较相邻的区间,时间复杂度从 O(n²) 降到了 O(n log n)(排序的代价)+O(n)(扫描的代价)。

为什么只比较右边界?

因为我们已经按左边界排序了。对于后面进来的区间,它的左边界一定大于或等于当前区间的左边界。所以,右边界成了唯一的变量,决定了这段"连续能量"能否延续。


面试回答

在面试中讲解这道题时,最忌讳直接说"先排序再合并"。面试官想看的是你如何从无序中建立秩序

你可以按照以下三个层级来组织你的表达:


第一步:明确物理模型(寻找矛盾点)

"首先,合并区间的本质是处理数轴上的覆盖问题

如果区间是乱序的,比如 [[4,7], [1,4]],我无法立即知道当前区间 [4,7] 应该和谁合并,除非我扫描全集。这说明**'无序'是导致算法低效的根本原因**。

为了简化问题,我的第一直觉是建立秩序:将所有区间按照左端点(Start)进行升序排列。这样我们就把一个复杂的空间问题,转化为了一个在数轴上从左向右移动的线性问题。"


第二步:推导合并逻辑(分类讨论)

"在排好序的基础上,我们只需要观察相邻 的两个区间。假设当前合并后的区间是 A,下一个待处理的区间是 B

由于我们排过序,B 的起点一定大于等于 A 的起点。那么它们的关系只剩两种可能:

  1. 重叠(Overlapping)B 的起点 <= A 的终点。此时它们可以连成一片。新的终点取 max(A.end, B.end)。之所以取 max,是因为要处理包含关系(比如 [1, 10][2, 5])。

  2. 断开(Disjoint)B 的起点 > A 的终点。这意味着 A 已经完全封闭了,后面不可能再有区间能和 A 合并(因为后面的起点只会更靠后)。我们可以把 A 放入结果集,并把 B 当作下一个待合并的起点。"


第三步:总结效率与实现

"最后,在代码实现上,我可以使用一个结果数组(或 vector)。每次拿输入数组里的新区间与结果数组里的最后一个区间进行对比:

  • 如果重叠,直接原地修改结果数组最后一个元素的 end

  • 如果不重叠,直接 push_back 进去。

这个算法的时间复杂度主要取决于排序,是 O(nlog n);空间复杂度取决于排序所需的栈空间或结果数组,是 O(n)。"

相关推荐
折哥的程序人生 · 物流技术专研32 分钟前
Java面试85题图解版 · 特别篇:2026后端高频面试题复盘(算法底层逻辑+高并发架构设计全解析,附Java实战代码)
java·网络·数据库·算法·面试
想吃火锅10052 小时前
【leetcode】14.最长公共前缀js
算法·leetcode·职场和发展
云絮.3 小时前
数据库操作
数据库·mysql·算法·oracle
小林ixn3 小时前
LeetCode 206. 反转链表(迭代 + 递归详解)
算法·leetcode·链表
凡人叶枫3 小时前
Effective C++ 条款17:以独立语句将 newed 对象置入智能指针
java·linux·开发语言·c++·算法
菜鸟‍5 小时前
LeetCode 1 27 和 704 || 两数之和 移除元素 二分查找
算法·leetcode·职场和发展
退休倒计时6 小时前
【每日一题】LeetCode 142. 环形链表 II TypeScript
算法·leetcode·链表·typescript
popcorn_min6 小时前
Digits 手写数字识别:随机森林多分类 + 像素级特征热力图
算法·随机森林·分类
liulilittle7 小时前
拥塞控制:排水终止的两种决策:OR 与 AND
网络·tcp/ip·计算机网络·算法·信息与通信·tcp·通信
花间相见7 小时前
【LeetCode02】—— 两数之和:哈希表入门经典详解
数据结构·散列表