题目
给出一个区间的集合,请合并所有重叠的区间。
示例 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:连通块
直觉
如果我们画一个图,区间看成顶点,相交的区间之间连一条无向边,那么图中连通块内的所有区间都可以合并成一个。
算法
根据上面的直觉,我们可以把图用邻接表表示,用两个方向的有向边模拟无向边。然后,为了考虑每个顶点属于哪个连通块,我们从任意一个未被访问的节点出发,遍历相邻点,直到所有顶点都被访问过。为了效率更快,我们将所有访问过的节点记录在 Set 中,可以在常数时间内查询和插入。最后,我们考虑每个连通块,将所有区间合并成一个新的 Interval ,区间左端点 start 是最小的左端点,区间右端点 end 是最大的右端点。
这个算法显然是正确的,因为这是最暴力的方法。我们对两两区间进行比较,所以可以知道他们是否重合。连通块搜索的原理是因为两个区间可能不是直接重合,而是通过第三个区间而间接重合。例如下面的例子:
尽管区间 (1,5) 和 (6, 10) 没有直接重合,但是任意一个和 (4, 7) 合并之后就可以和另一个产生重合。图中有两个连通块,我们得到如下两个合并区间:(1, 10) 和 (15, 20)
class Solution {
private Map<Interval, List<Interval> > graph;
private Map<Integer, List<Interval> > nodesInComp;
private Set<Interval> visited;
// return whether two intervals overlap (inclusive)
private boolean overlap(Interval a, Interval b) {
return a.start <= b.end && b.start <= a.end;
}
// build a graph where an undirected edge between intervals u and v exists
// iff u and v overlap.
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);
}
}
}
}
// merges all of the nodes in this connected component into one interval.
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);
}
// use depth-first search to mark all nodes in the same connected component
// with the same integer.
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);
}
}
}
}
// gets the connected components of the interval overlap graph.
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++;
}
}
}
public List<Interval> merge(List<Interval> intervals) {
buildGraph(intervals);
buildComponents(intervals);
// for each component, merge all intervals into one interval.
List<Interval> merged = new LinkedList<>();
for (int comp = 0; comp < nodesInComp.size(); comp++) {
merged.add(mergeNodes(nodesInComp.get(comp)));
}
return merged;
}
}
复杂度分析
时间复杂度: O ( n 2 ) O(n^2) O(n2)
建图的时间开销 O ( V + E ) = O ( V ) + O ( E ) = O ( n ) + O ( n 2 ) = O ( n 2 ) O(V + E) = O(V) + O(E) = O(n) + O(n^2) = O(n^2) O(V+E)=O(V)+O(E)=O(n)+O(n2)=O(n2),最坏情况所有区间都相互重合,遍历整个图有相同的开销,因为 visited 数组保证了每个节点只会被访问一次。最后每个节点都恰好属于一个连通块,所以合并的时间开销是 O ( V ) = O ( n ) O(V) = O(n) O(V)=O(n)。总和为: O ( n 2 ) + O ( n 2 ) + O ( n ) = O ( n 2 ) O(n^2) + O(n^2) + O(n) = O(n^2) O(n2)+O(n2)+O(n)=O(n2)
空间复杂度: O ( n 2 ) O(n^2) O(n2)
根据之前提到的,最坏情况下每个区间都是相互重合的,所以两两区间都会有一条边,所以内存占用量是输入大小的平方级别。
方法 2:排序
直觉
如果我们按照区间的 start 大小排序,那么在这个排序的列表中可以合并的区间一定是连续的。
算法
首先,我们将列表按上述方式排序。然后,我们将第一个区间插入 merged 数组中,然后按顺序考虑之后的每个区间:如果当前区间的左端点在前一个区间的右端点之后,那么他们不会重合,我们可以直接将这个区间插入 merged 中;否则,他们重合,我们用当前区间的右端点更新前一个区间的右端点 end 如果前者数值比后者大的话。
一个简单的证明:假设算法在某些情况下没能合并两个本应合并的区间,那么说明存在这样的三元组 i,j 和 k 以及区间 ints 满足 i < j < k 并且 (ints[i], ints[k]) 可以合并,而 (ints[i], ints[j]) 和 (ints[j], ints[k]) 不能合并。这说明满足下面的不等式:
$ints[i].end < ints[j].start \ ints[j].end < ints[k].start \ ints[i].end \geq ints[k].start \$
我们联立这些不等式(注意还有一个显然的不等式 i n t s [ j ] . s t a r t ≤ i n t s [ j ] . e n d ints[j].start \leq ints[j].end ints[j].start≤ints[j].end),可以发现冲突:
i n t s [ i ] . e n d < i n t s [ j ] . s t a r t ≤ i n t s [ j ] . e n d < i n t s [ k ] . s t a r t i n t s [ i ] . e n d ≥ i n t s [ k ] . s t a r t \begin{aligned} ints[i].end < ints[j].start \leq ints[j].end < ints[k].start \\ ints[i].end \geq ints[k].start \end{aligned} ints[i].end<ints[j].start≤ints[j].end<ints[k].startints[i].end≥ints[k].start
因此,所有能够合并的区间必然是连续的。
考虑上面的例子,当列表已经排好序,能够合并的区间构成了连通块。
class Solution {
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) {
// if the list of merged intervals is empty or if the current
// interval does not overlap with the previous, simply append it.
if (merged.isEmpty() || merged.getLast().end < interval.start) {
merged.add(interval);
}
// otherwise, there is overlap, so we merge the current and previous
// intervals.
else {
merged.getLast().end = Math.max(merged.getLast().end, interval.end);
}
}
return merged;
}
}
复杂度分析
时间复杂度: O ( n log n ) O(n\log{}n) O(nlogn)
除去 sort 的开销,我们只需要一次线性扫描,所以主要的时间开销是排序的 O(nlgn)O(nlgn)
空间复杂度:O(1) (or O(n))
如果我们可以原地排序 intervals ,就不需要额外的存储空间;否则,我们就需要一个线性大小的空间去存储 intervals 的备份,来完成排序过程。
作者:LeetCode
链接:https://leetcode-cn.com/problems/two-sum/solution/he-bing-qu-jian-by-leetcode/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
感想
观察到每次合并的两个区块都是各自消除一个开头和一个结尾,所以对开头和结束直接各自排序。就可以按规律输出了。
类似题解中的第二个方法。
执行用时 :6 ms, 在所有Java提交中击败了97.79%的用户
内存消耗 :40.8 MB, 在所有Java提交中击败了83.92%的用户
class Solution {
public int[][] merge(int[][] intervals) {
int[] start = new int[intervals.length];
int[] end = new int[intervals.length];
for(int i=0;i<intervals.length;i++){
start[i] = intervals[i][0];
end[i] = intervals[i][1];
}
Arrays.sort(start);
Arrays.sort(end);
int[][] res = new int[intervals.length][2];
int count=0;
int ps=0;
int pe=1;
while(pe<end.length){
while(start[pe]<=end[pe-1]){
pe++;
if(pe>=end.length) break;
}
res[count][0]=start[ps];
res[count][1]=end[pe-1];
count++;
ps=pe;
pe++;
}
if(pe == end.length){
res[count][0]=start[ps];
res[count][1]=end[pe-1];
count++;
}
int[][] result = new int[count][2];
for(int i =0;i<count;i++){
result[i][0] = res[i][0];
result[i][1] = res[i][1];
}
return result;
}
}