【力扣算法】56-合并区间

题目

给出一个区间的集合,请合并所有重叠的区间。

示例 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 是最大的右端点。

这个算法显然是正确的,因为这是最暴力的方法。我们对两两区间进行比较,所以可以知道他们是否重合。连通块搜索的原理是因为两个区间可能不是直接重合,而是通过第三个区间而间接重合。例如下面的例子:

56-1.png

尽管区间 (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].startints[j].end),可以发现冲突:

i n t s [ i ] . e n d &lt; i n t s [ j ] . s t a r t ≤ i n t s [ j ] . e n d &lt; 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 &lt; ints[j].start \leq ints[j].end &lt; ints[k].start \\ ints[i].end \geq ints[k].start \end{aligned} ints[i].end<ints[j].startints[j].end<ints[k].startints[i].endints[k].start

因此,所有能够合并的区间必然是连续的。

56-2.png

考虑上面的例子,当列表已经排好序,能够合并的区间构成了连通块。

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;
    }
}
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值