给定一个列表,将有重叠部分的合并。例如[ [ 1 3 ] [ 2 6 ] ] 合并成 [ 1 6 ] 。java
常规的思想,将大问题化解成小问题去解决。node
假设给了一个大小为 n 的列表,而后咱们假设 n - 1 个元素的列表已经完成了所有合并,咱们如今要解决的就是剩下的 1 个,怎么加到已经合并完的 n -1 个元素中。算法
这样的话分下边几种状况, 咱们把每一个范围叫作一个节点,节点包括左端点和右端点。segmentfault
以上就是全部的状况了,能够开始写代码了。ide
public class Interval { int start; int end; Interval() { start = 0; end = 0; } Interval(int s, int e) { start = s; end = e; } } public List<Interval> merge(List<Interval> intervals) { List<Interval> ans = new ArrayList<>(); if (intervals.size() == 0) return ans; //将第一个节点加入,做为合并好的节点列表 ans.add(new Interval(intervals.get(0).start, intervals.get(0).end)); //遍历其余的每个节点 for (int i = 1; i < intervals.size(); i++) { Interval start = null; Interval end = null; //新加入节点的左端点 int i_start = intervals.get(i).start; //新加入节点的右端点 int i_end = intervals.get(i).end; int size = ans.size(); //状况 6,保存囊括的节点,便于删除 List<Interval> in = new ArrayList<>(); //遍历合并好的每个节点 for (int j = 0; j < size; j++) { //找到左端点在哪一个节点内 if (i_start >= ans.get(j).start && i_start <= ans.get(j).end) { start = ans.get(j); } //找到右端点在哪一个节点内 if (i_end >= ans.get(j).start && i_end <= ans.get(j).end) { end = ans.get(j); } //判断新加入的节点是否囊括当前旧节点,对应状况 6 if (i_start < ans.get(j).start && i_end >ans.get(j).end) { in.add(ans.get(j)); } } //删除囊括的节点 if (in.size() != 0) { for (int index = 0; index < in.size(); index++) { ans.remove(in.get(index)); } } //equals 函数做用是在 start 和 end 有且只有一个 null,或者 start 和 end 是同一个节点返回 true,至关于状况 2 3 4 中的一种 if (equals(start, end)) { //若是 start 和 end 都不等于 null 就表明状况 4 // start 等于 null 的话至关于状况 3 int s = start == null ? i_start : start.start; // end 等于 null 的话至关于状况 2 int e = end == null ? i_end : end.end; ans.add(new Interval(s, e)); // start 和 end 不是同一个节点,至关于状况 1 } else if (start!= null && end!=null) { ans.add(new Interval(start.start, end.end)); // start 和 end 都为 null,至关于状况 5 和 状况 6 ,加入新节点 }else if (start == null) { ans.add(new Interval(i_start, i_end)); } //将旧节点删除 if (start != null) { ans.remove(start); } if (end != null) { ans.remove(end); } } return ans; } private boolean equals(Interval start, Interval end) { if (start == null && end == null) { return false; } if (start == null || end == null) { return true; } if (start.start == end.start && start.end == end.end) { return true; } return false; }
时间复杂度:O(n²)。函数
空间复杂度:O (n),用来存储结果。优化
参考这里的解法二。ui
在解法一中,咱们每次对于新加入的节点,都用一个 for 循环去遍历已经合并好的列表。若是咱们把以前的列表,按照左端点进行从小到大排序了。这样的话,每次添加新节点的话,咱们只须要和合并好的列表最后一个节点对比就能够了。spa
排好序后咱们只须要把新加入的节点和最后一个节点比较就够了。3d
状况 1,若是新加入的节点的左端点大于合并好的节点列表的最后一个节点的右端点,那么咱们只须要把新节点直接加入就能够了。
状况 2 ,若是新加入的节点的左端点不大于合并好的节点列表的最后一个节点的右端点,那么只须要判断新加入的节点的右端点和最后一个节点的右端点哪一个大,而后更新最后一个节点的右端点就能够了。
private class IntervalComparator implements Comparator<Interval> { @Override public int compare(Interval a, Interval b) { return a.start < b.start ? -1 : a.start == b.start ? 0 : 1; } } public List<Interval> merge(List<Interval> intervals) { Collections.sort(intervals, new IntervalComparator()); LinkedList<Interval> merged = new LinkedList<Interval>(); for (Interval interval : intervals) { //最开始是空的,直接加入 //而后对应状况 1,新加入的节点的左端点大于最后一个节点的右端点 if (merged.isEmpty() || merged.getLast().end < interval.start) { merged.add(interval); } //对于状况 2 ,更新最后一个节点的右端点 else { merged.getLast().end = Math.max(merged.getLast().end, interval.end); } } return merged; }
时间复杂度:O(n log(n)),排序算法。
空间复杂度:O(n),存储结果。另外排序算法也可能须要。
参考这里的解法 1。
刷这么多题,第一次利用图去解决问题,这里分享下做者的思路。
若是每一个节点若是有重叠部分,就用一条边相连。
咱们用一个 HashMap,用邻接表的结构来实现图,相似于下边的样子。
图存起来之后,能够发现,最后有几个连通图,最后合并后的列表就有几个。咱们须要把每一个连通图保存起来,而后在每一个连通图中找最小和最大的端点做为一个节点加入到合并后的列表中就能够了。最后,咱们把每一个连通图就转换成下边的图了。
class Solution { private Map<Interval, List<Interval> > graph; //存储图 private Map<Integer, List<Interval> > nodesInComp; ///存储每一个有向图 private Set<Interval> visited; //主函数 public List<Interval> merge(List<Interval> intervals) { buildGraph(intervals); //创建图 buildComponents(intervals); //单独保存每一个有向图 List<Interval> merged = new LinkedList<>(); //遍历每一个有向图,将有向图中最小最大的节点加入到列表中 for (int comp = 0; comp < nodesInComp.size(); comp++) { merged.add(mergeNodes(nodesInComp.get(comp))); } return merged; } // 判断两个节点是否有重叠部分 private boolean overlap(Interval a, Interval b) { return a.start <= b.end && b.start <= a.end; } //利用邻接表创建图 private void buildGraph(List<Interval> intervals) { graph = new HashMap<>(); for (Interval interval : intervals) { graph.put(interval, new LinkedList<>()); } for (Interval interval1 : intervals) { for (Interval interval2 : intervals) { if (overlap(interval1, interval2)) { graph.get(interval1).add(interval2); graph.get(interval2).add(interval1); } } } } // 将每一个链接图单独存起来 private void buildComponents(List<Interval> intervals) { nodesInComp = new HashMap(); visited = new HashSet(); int compNumber = 0; for (Interval interval : intervals) { if (!visited.contains(interval)) { markComponentDFS(interval, compNumber); compNumber++; } } } //利用深度优先遍历去找到全部互相相连的边 private void markComponentDFS(Interval start, int compNumber) { Stack<Interval> stack = new Stack<>(); stack.add(start); while (!stack.isEmpty()) { Interval node = stack.pop(); if (!visited.contains(node)) { visited.add(node); if (nodesInComp.get(compNumber) == null) { nodesInComp.put(compNumber, new LinkedList<>()); } nodesInComp.get(compNumber).add(node); for (Interval child : graph.get(node)) { stack.add(child); } } } } // 找出每一个有向图中最小和最大的端点 private Interval mergeNodes(List<Interval> nodes) { int minStart = nodes.get(0).start; for (Interval node : nodes) { minStart = Math.min(minStart, node.start); } int maxEnd = nodes.get(0).end; for (Interval node : nodes) { maxEnd= Math.max(maxEnd, node.end); } return new Interval(minStart, maxEnd); } }
时间复杂度:
空间复杂度:O(n²),最坏的状况,每一个节点都互相重合,这样每一个都与其余节点相连,就会是 n² 的空间存储图。
惋惜的是,这种解法在 leetcode 会遇到超时错误。
开始的时候,使用最经常使用的思路,将大问题化解为小问题,而后用递归或者直接用迭代实现。解法二中,先对列表进行排序,从而优化了时间复杂度,也不是第一次看到了。解法三中,利用图解决问题很新颖,是我刷题第一次遇到的,又多了一种解题思路。
更多详细通俗题解详见 leetcode.wang 。