题目
给出一个区间的集合,请合并所有重叠的区间。
示例 1:
输入: [[1,3],[2,6],[8,10],[15,18]]
输出: [[1,6],[8,10],[15,18]]
解释: 区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
示例 2:
输入: [[1,4],[4,5]]
输出: [[1,5]]
解释: 区间 [1,4] 和 [4,5] 可被视为重叠区间。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/merge-intervals
解题
思路
将数组内元素按照区间起始位置排序,这样区间就变成顺序的,方便比较有没有重叠,将题目中示例1图示如下图:
判断重叠的方式就可以只看前一个区间的尾intervals[i-1][1]和后一个区间的头是否重叠intervals[i][0]。若重叠将两个区间合并保存,再与下一个区间相比较。
算法思想
其实这道题使用的是贪心算法思想。
贪心算法(又称贪婪算法):是指在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的是在某种意义上的局部最优解。
贪心算法的关键是贪心策略的选择,选择的贪心策略必须具备无后效性,即某个状态以前的过程不会影响以后的状态,只与当前状态有关。
贪心策略适用的前提是:局部最优策略能导致产生全局最优解。
贪心算法有两个基本要素: 贪心选择和最优子结构。
-
贪心选择: 是指 所求问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来达到。这是贪心算法可行的第一个基本要素,也是贪心算法与动态规划算法的主要区别。贪心选择是采用从顶向下、以迭代的方法做出相继选择,每做一次贪心选择就将所求问题简化为一个规模更小的子问题。
对于一个具体问题,要确定它是否具有贪心选择的性质,我们必须证明每一步所作的贪心选择最终能得到问题的最优解。通常可以首先证明问题的一个整体最优解,是从贪心选择开始的,而且作了贪心选择后,原问题简化为一个规模更小的类似子问题。然后,用数学归纳法证明,通过每一步贪心选择,最终可得到问题的一个整体最优解。 -
最优子结构: 当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质。运用贪心策略在每一次转化时都取得了最优解。问题的最优子结构性质是该问题可用贪心算法或动态规划算法求解的关键特征。贪心算法的每一次操作都对结果产生直接影响,而动态规划则不是。
贪心算法的特性:
- 随着算法的进行,将积累起其它两个集合:一个包含已经被考虑过并被选出的候选对象,另一个包含已经被考虑过但被丢弃的候选对象。
- 有一个函数来检查一个候选对象的集合是否提供了问题的解答。该函数不考虑此时的解决方法是否最优。
- 还有一个函数检查一个候选对象的集合是否是可行的,也即是否可能往该集合上添加更多的候选对象以获得一个解。和上一个函数一样,此时不考虑解决方法的最优性。
- 选择函数可以指出哪一个剩余的候选对象最有希望构成问题的解。 最后,目标函数给出解的值。
- 为了解决问题,需要寻找一个构成解的候选对象集合,它可以优化目标函数,贪婪算法一步一步的进行。起初,算法选出的候选对象的集合为空。接下来的每一步中,根据选择函数,算法从剩余候选对象中选出最有希望构成解的对象。如果集合中加上该对象后不可行,那么该对象就被丢弃并不再考虑;否则就加到集合里。每一次都扩充集合,并检查该集合是否构成解。如果贪婪算法正确工作,那么找到的第一个解通常是最优的。
这道题的贪心策略是:在右端点的选择中,如果产生交集,总是将右端点的数值更新成为最大的,这样就可以合并更多的区间。
本人胡言乱语:贪心和动态规划最大的区别可能在于,贪心是从子结构的选择一点点扩充到全部(数学归纳法),俗称正着来;动态规划是从最后一个的选择是由前一个选择递推而来,是正着嵌套,不是倒着推。
java实现
class Solution {
public int[][] merge(int[][] intervals) {
int n = intervals.length;
//用动态列表存储结果,因为并不知道结果有几个区间所以不好用静态数组
ArrayList<int[]> reslist = new ArrayList<>();
//判断特殊情况
if(n<=1){
return intervals;
}
// 先按照区间起始位置排序
Arrays.sort(intervals, (v1, v2) -> v1[0] - v2[0]);
for(int i = 1; i < n; i++){
//依次判断是否重叠
if(intervals[i][0]<=intervals[i-1][1]){
intervals[i][0] = intervals[i-1][0];
intervals[i][1] = Math.max(intervals[i-1][1],intervals[i][1]);
}else{
reslist.add(intervals[i-1]);
}
//将最后一个区间添加进去
if(i == n-1){
reslist.add(intervals[i]);
}
}
//将动态数组转换为静态数组返回结果
int[][] res = new int[reslist.size()][2];
for(int i = 0; i < reslist.size(); i++){
res[i] = reslist.get(i);
}
return res;
}
}
优化
参考力扣甜姨题解:吃🐳!🤷♀️竟然一眼秒懂合并区间!
没有使用动态数组,在静态数组内完成。代码更为简洁。主要是Arrays.sort()和Arrays.copyOf()这两个方法的使用。
Arrays.sort(int[] a) 默认升序排序,降序可采用Collection.sort()匿名内部类。如下所示代码。
Arrays.sort(int[] a, int fromIndex, int toIndex)
//降序,可用Comparator()匿名内部类
Arrays.sort(a2, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2.compareTo(o1);
}
});
Arrays的copyOf()方法传回的数组是新的数组对象,改变传回数组中的元素值,不会影响原来的数组。
copyOf()的第二个自变量指定要建立的新数组长度,如果新数组的长度超过原数组的长度,则保留数组默认值。
class Solution {
public int[][] merge(int[][] intervals) {
// 先按照区间起始位置排序
Arrays.sort(intervals, (v1, v2) -> v1[0] - v2[0]);
// 遍历区间
int[][] res = new int[intervals.length][2];
int idx = -1;
for (int[] interval: intervals) {
// 如果结果数组是空的,或者当前区间的起始位置 > 结果数组中最后区间的终止位置,
// 则不合并,直接将当前区间加入结果数组。
if (idx == -1 || interval[0] > res[idx][1]) {
res[++idx] = interval;
} else {
// 反之将当前区间合并至结果数组的最后区间
res[idx][1] = Math.max(res[idx][1], interval[1]);
}
}
return Arrays.copyOf(res, idx + 1);
}
}
参考:
[1] 贪心算法百度百科
[2] Arrays.copyOf() 用法 - Ruixin1993
[3] 浅谈Arrays.sort()原理 - Duuuhs