📚关键词:排序、区间合并、贪心思想
📅场景:会议室抢占大战
第 1 章:会议室打架事件现场
"叮铃铃------"
教室响起课代表的怒吼:"谁把小胖和小夫安排在同一节体育课?!"
大雄弱弱地指了指排课表:
css
[ [1,3], [2,6], [8,10], [15,18] ]
"不是我!我只是把所有课程从早到晚排了个序!"
小哆啦推了推眼镜:"小大雄,你虽然先排序了,但你还是太暴力了啊。"
第 2 章:暴力出奇迹(但不够优雅)
小哆啦首先写了最暴力直观的一版思路:
先根据起始时间对所有课程排序,然后从头扫描,看是否重叠,重叠就合并,不重叠就加入结果。
ini
function merge(intervals: number[][]): number[][] {
if (intervals.length <= 1) return intervals;
// 先按起始时间排序
intervals.sort((a, b) => a[0] - b[0]);
let result: number[][] = [];
let prev = intervals[0]; // 当前合并区间起点
for (let i = 1; i < intervals.length; i++) {
let curr = intervals[i];
if (curr[0] <= prev[1]) {
// 有重叠,更新当前区间最大右边界
prev[1] = Math.max(prev[1], curr[1]);
} else {
// 无重叠,保存前一个合并结果
result.push(prev);
prev = curr;
}
}
result.push(prev); // 记得最后一个别漏了
return result;
}
示例运行:
lua
输入: [[1,3],[2,6],[8,10],[15,18]]
输出: [[1,6],[8,10],[15,18]]
🌟 哆啦A梦点评:
这波暴力不简单,它虽然"暴力",但排序后线性合并,时间复杂度 O(n log n) ,空间复杂度 O(n) ,已经是正解级别了。
第 3 章:突然想玩点骚操作 ------ reduce 函数出场
大雄:"老师不是说写代码要更函数式一点吗?"
于是小哆啦又尝试用 reduce
来表达同样的逻辑:
ini
function merge(intervals: number[][]): number[][] {
if (intervals.length <= 1) return intervals;
intervals.sort((a, b) => a[0] - b[0]);
return intervals.reduce((merged: number[][], curr: number[]) => {
const last = merged[merged.length - 1];
if (!last || curr[0] > last[1]) {
// 无重叠,直接添加
merged.push(curr);
} else {
// 重叠了,合并右边界
last[1] = Math.max(last[1], curr[1]);
}
return merged;
}, []);
}
虽然逻辑一样,但结构更紧凑,"函数式写法 + 状态收敛",让胖虎直呼高端。
第 4 章:为什么一定要排序?
静香提问:"为啥一定要先排序?"
小哆啦指着黑板:
- 排序是为了让所有区间按"时间轴"排列,保证当前只需要与上一个比较;
- 否则无序合并会遗漏或错误合并,像
[ [5,10], [1,4] ]
就可能被错过!
排序后的时间线看起来是这样的:
css
[1,3] -> [2,6] -> [8,10] -> [15,18]
重叠检测变得"可预期",不再东奔西撞。
第 5 章:技术复盘 & 面试锦囊
技术点 | 解读 |
---|---|
排序 | O(n log n) ,保证区间线性遍历正确性 |
合并判断 | curr[0] <= prev[1] 是重叠的核心逻辑 |
最终复杂度 | 时间 O(n log n) ,空间最差 O(n) |
可选方案 | 函数式 reduce() vs 普通遍历皆可 |
第 6 章:小哆啦的碎碎念
🧩"合并区间"是很多高级题目的基础,比如:
- 插入新区间
- 统计不被覆盖的区间长度
- 日程管理、资源调度、时间轴图谱
能写好这道题,是走进中高级算法门槛的重要一步。
🍡 最后彩蛋 · 胖虎的吐槽
胖虎抱着排课表走来:"为啥我排的 [ [1,4], [4,5] ] 合并后是 [1,5]?这不是并不重叠嘛?"
哆啦A梦吃着铜锣烧,悠悠答:
"数学上闭区间 [1,4] 和 [4,5] 是连续的哦,合并是对的。"
胖虎:"哼,下次我用半开区间!"