最近做了两道区间题,题目都不难,对比来看很有意思。两个都是区间合并的,不过两种合并方式,一种是并集,一种是交集。做完懵懵懂懂,有些感悟,写下来发现并没有自己以为的那么清楚,思考过程还是出了不少问题的。先记录下来吧,可能后面写多了就更清楚了。
先看第一道:
合并区间
以数组
intervals
表示若干个区间的集合,其中单个区间为intervals[i] = [starti, endi]
。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。
思路:既然是区间,一定有 starti <= endi
。首先一定会想到排序,优化一定是数据特殊或者问题特殊。排序后,数据就变得特殊了,可以进行下一步的优化。问题来了,怎么排序,是按 starti
还是 endi
?先尝试用 starti 排序,排序后进行遍历,遇到重合就合并,也就是两个区间取并集。
javascript
var merge = function (intervals) {
// 以区间开始值为标准排序
intervals.sort((a, b) => a[0] - b[0])
let result = []
let prev = null
intervals.forEach(item => {
const [start, end] = item
let temp = [...item]
if (prev) {
const [prevStart, prevEnd] = prev
// 重合,合并
// start(i+1) <= endi
if (start <= prevEnd) {
temp[0] = prevStart
// 注意这里,前一个区间可能完全包含当前区间
// 需要取更大的那个区间
temp[1] = Math.max(end, prevEnd)
// 合并完成,需要弹出被合并区间
result.pop()
}
}
result.push(temp)
prev = temp
})
return result
};
能不能使用 endi
排序? 答案是可以,但是遍历顺序需要改变:
ini
var merge = function (intervals) {
intervals.sort((a, b) => a[1] - b[1])
let result = []
let prev = null
let len = intervals.length
// 从后向前遍历
for (let i = len - 1; i >= 0; i--) {
const [start, end] = intervals[i]
let temp = [start, end]
if (prev) {
const [prevStart, prevEnd] = prev
// 重合判断需要修改
if (end >= prevStart) {
temp[0] = Math.min(start, prevStart)
temp[1] = prevEnd
result.pop()
}
}
result.push(temp)
prev = temp
}
return result
};
简单说,这道题就是计算并集。合并时候,一定是相邻集合开始合并。
接下来是另一题:
用最少数量的箭引爆气球
有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组
points
,其中points[i] = [xstart, xend]
表示水平直径在xstart
和xend
之间的气球。你不知道气球的确切 y 坐标。一支弓箭可以沿着 x 轴从不同点 完全垂直 地射出。在坐标
x
处射出一支箭,若有一个气球的直径的开始和结束坐标为x``start
,x``end
, 且满足xstart ≤ x ≤ x``end
,则该气球会被 引爆 。可以射出的弓箭的数量 没有限制 。 弓箭一旦被射出之后,可以无限地前进。给你一个数组
points
,返回引爆所有气球所必须射出的 最小 弓箭数 。
能被一起射爆的,也就是区间重合的气球。和上一题不一样,计算的是交集。题目里说的很复杂,好像和Y轴有关系一样,实际上没什么关系,还是单一维度上区间的计算。
对比合并区间,难点在于能被一根箭引爆的所有区间,必须都有交集。例如 [1, 2],[2, 3],[3, 4],第一个和第三个区间并没有交集,不能视为一组。
如果还是和上一道题一样,以 starti 排序,遍历是否可以?排序后同理,开始值递增,区间长度不定。决定期间重合的变成了上界,有 starti <=start(i+1)
,需要有 endi >= start(i+1)
,两者才能重合。
对于下图所示情况,0 位置,相交区间 [start0, end0]
。1 位置,相交区间 [start1, end1]
。2 位置,相交区间 [start2, end0]
其实无需纠结第二个区间到第三个区间的变化,既然有不重合的,一定会需要多一个。start[2] > start[1],后续区间一定和区间二不重合。对于和区间重合的区间,随你怎么去,反正重合不了。所以边界合并规则就有了,下限一定取最新收缩下边界,上边界取最小,没有重合就新增。
ini
var findMinArrowShots = function (points) {
// 按结尾排序,开始一定小于等于结尾
points.sort((a, b) => a[0] - b[0]);
let ans = 1; // 默认第一个区间必有一次
let limit = points[0][1];
// 开始值永远增加
for (let i = 0; i < points.length; i++) {
const p = points[i];
const [start, end] = p;
// 如果当前开始大于下限,增加
if (start > limit) {
ans++;
// 次数已经增加,直接取最大上限
limit = end;
} else {
// 考虑交集区间
limit = Math.min(end, limit)
}
}
return ans;
};
如果考虑用 endi
排序,决定是否有交集的变成了区间下界。从左到右,上界递增,最小的上界就是最左边的,不重合就增加即可。这种方法比上面要简单,优化在哪里我还要想想,先记录下来吧。
ini
var findMinArrowShots = function (points) {
// 按结尾排序,开始一定小于等于结尾
points.sort((a, b) => a[1] - b[1]);
let ans = 0;
let limit = -Infinity; // 下限默认负无穷,一定会有一次
for (let i = 0; i < points.length; i++) {
const p = points[i];
const [start, end] = p;
// 如果当前开始大于下限,增加
if (start > limit) {
ans++;
// 次数已经增加,直接取最大上限
limit = end;
}
}
return ans;
};
notice: 题目名称是 leetcode 链接,可以点击尝试自己完成一下。