LeetCode 力扣 56. 合併區間

題目描述(中等難度)

給定一個列表,將有重疊部分的合併。例如[ [ 1 3 ] [ 2 6 ] ] 合併成 [ 1 6 ] 。java

解法一

常規的思想,將大問題化解成小問題去解決。node

假設給了一個大小爲 n 的列表,而後咱們假設 n - 1 個元素的列表已經完成了所有合併,咱們如今要解決的就是剩下的 1 個,怎麼加到已經合併完的 n -1 個元素中。算法

這樣的話分下邊幾種狀況, 咱們把每一個範圍叫作一個節點,節點包括左端點和右端點。segmentfault

  1. 以下圖,新加入的節點左端點和右端點,分別在兩個節點之間。這樣,咱們只要刪除這兩個節點,而且使用左邊節點的左端點,右邊的節點的右端點做爲一個新節點插入便可。也就是刪除 [ 1 6 ] 和 [ 8 12 ] ,加入 [ 1 12 ] 到合併好的列表中。

  2. 以下圖,新加入的節點只有左端點在以前的一個節點以內,這樣的話將這個節點刪除,使用刪除的節點的左端點,新加入的節點的右端點,做爲新的節點插入便可。也就是刪除 [ 1 6 ],加入 [ 1 7 ] 到合併好的列表中。

  3. 以下圖,新加入的節點只有右端點在以前的一個節點以內,這樣的話將這個節點刪除,使用刪除的節點的右端點,新加入的節點的左端點,做爲新的節點插入便可。也就是刪除 [ 8 12 ],加入 [ 7 12 ] 到合併好的列表中。

  4. 以下圖,新加入的節點的左端點和右端點在以前的一個節點以內,這樣的話新加入的節點捨棄就能夠了。

  5. 以下圖,新加入的節點沒有在任何一個節點以內,那麼將它直接做爲新的節點加入到合併好的節點以內就能夠了。

  6. 以下圖,還有一種狀況,就是新加入的節點兩個端點,將以前的節點囊括其中,這種的話,咱們只須要將囊括的節點刪除,把新節點加入便可。把 [ 8 12 ] 刪除,將 7 13 加入便可。而且,新加入的節點可能會囊括多箇舊節點,好比新加入的節點是 [ 1 100 ],那麼下邊的三個節點就都包括了,就須要都刪除掉。

以上就是全部的狀況了,能夠開始寫代碼了。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
相關文章
相關標籤/搜索