问题描述
小明正在设计一台打点计数器,该计数器可以接受多个递增的数字范围,并对这些范围内的每个唯一数字打点。如果多个范围之间有重叠,计数器将合并这些范围并只对每个唯一数字打一次点。小明需要你帮助他计算,在给定的多组数字范围内,计数器会打多少个点。
例如,给定三个数字范围 [1, 4], [7, 10], 和 [3, 5],计数器首先将这些范围合并,变成 [1, 5] 和 [7, 10],然后计算这两个范围内共有多少个唯一数字,即从 1 到 5 有 5 个数字,从 7 到 10 有 4 个数字,共打 9 个点。
问题分析
很容易得出,这道题是想让我们合并区间, 然后给出区间的大小。那么剩下的问题就是怎么合并区间了。我首先想到的是比较区间的边界值,然后根据题目的思路去模拟一遍合并区间,但我发现这样很难操作,因为最后得出的区间数量是不确定的,中间会有一些割裂。这时我从结果出发思考问题发现,无论最后合并成什么样子,他涉及到的数字都是题目给出条件已有的,也就是说,我们只需要统计出现过的区间范围有哪些数字即可,不用考虑是否重复出现。
解决思路
遍历题目给出的区间,将涉及到的数字记录下来,最后只需要返回出现过的数字有多少个即可。 代码:
const map = new Map()
inputArray.forEach( val =>{
for(let i = val[0]; i <= val[1]; i++) {
map.set(i, 1);
}
})
return map.size;
时间复杂度: O(n^2),同时还用到了map存储所有的数字
优化
上面的方法虽然简洁,但在处理大量数据时可能会效率较低。这时我们回到合并区间来优化这个算法,从而减少不必要的存储和计算。
刚刚提到了很难去合并区间,因为题目给出的区间是乱序的,我们很难找到不同区间之间的联系,这时我们应该考虑怎么去提高他们的关联性,除了继续挖掘题目条件,我们还有一个办法就是去对原条件进行一个排序。
- 排序:首先将输入的区间按照起始点进行排序,这样可以方便后续的区间合并操作。
- 合并区间:遍历排序后的区间,如果当前区间的结束点大于等于下一个区间的起始点,则合并这两个区间;否则,将当前区间加入合并后的区间列表,并更新当前区间为下一个区间。
- 计算合并后的区间长度:遍历合并后的区间列表,计算每个区间的长度,并累加得到最终的打点数。
// 1. 按照区间的起始点进行排序
inputArray.sort((a, b) => a[0] - b[0]);
// 2. 合并区间
const mergedIntervals = [];
let currentInterval = inputArray[0];
for (let i = 1; i < inputArray.length; i++) {
const nextInterval = inputArray[i];
// 如果当前区间的结束点大于等于下一个区间的起始点,则合并区间
if (currentInterval[1] >= nextInterval[0]) {
currentInterval[1] = Math.max(currentInterval[1], nextInterval[1]);
} else {
// 否则,将当前区间加入合并后的区间列表,并更新当前区间
mergedIntervals.push(currentInterval);
currentInterval = nextInterval;
}
}
// 将最后一个区间加入合并后的区间列表
mergedIntervals.push(currentInterval);
// 3. 计算合并后的区间长度之和
let count = 0;
for (const interval of mergedIntervals) {
count += interval[1] - interval[0] + 1;
}
优化后的时间复杂度: O(n log n),排序算法的事件复杂度是n log n。
总结
一开始由于觉得模拟比较麻烦,没有深入去思考。突然出现的另一个思路非常的简单,代码量也少,但是性能上还存在着不足。这时返过去时候又有了新的模拟思路,做出了进一步的优化。这和我们平时的工作也是相近的道理,我们应该首先保证完成的前提下,才去考虑进一步的优化。之前我总在工作前期做大量的思考和衡量,导致自己总是在做一些无用功,加上项目排期的紧张,自己的思维只会受限。于是在保证一定质量的前提下去完成目标才是首要目的。