--- title: "算法面試套路|合併區間(Merge Intervals)" description: "Patterns for Coding Questions: Merge Intervals." image: https://i.imgur.com/7jiNtkx.png author: Hsins tags: LeetCode, Coding Interview robots: breaks: GA: disqus: --- # 算法面試套路|合併區間(Merge Intervals) <p style="text-align: center"> <img src="https://i.imgur.com/7jiNtkx.png" height=200/> </p> > 本篇內容主要為 [Grokking the Coding Interview: Patterns for Coding Questions](https://www.educative.io/courses/grokking-the-coding-interview) 的翻譯與整理,行有餘力建議可以購買該專欄課程閱讀並在上面進行練習。 ## 重點整理 - 合併區間(Merge Intervals) - 策略:給定兩個區間,彼此之間共有六種不同的交互模式 - 題型: - 涉及間隔(intervals)的問題 - 找尋重疊部分、合併重疊間隔 ## 題目匯總 - `0056` Merge Intervals - `0057` Insert Interval - `0252` Meeting Rooms - `0253` Meeting Rooms II - `0435` Non-overlapping Intervals - `0729` My Calendar I - `0759` Employee Free Time - `0986` Interval List Intersections - `1094` Car Pooling - `1288` Remove Covered Intervals ## [例題] Merge Intervals ### 問題 給定一個區間陣列,合併所有重疊的區間,並返回這些區間所組成的陣列。比如 `[[1,4], [2,5], [7,9]]` 合併後會得到 `[[1,5], [7,9]]`: <p style="text-align: center"> <img src="https://i.imgur.com/Rt7imGE.png" /> </p> **Exapmles** ``` Intervals: [[6,7], [2,4], [5,9]] Output: [[2,4], [5,9]], Since the intervals [6,7] and [5,9] overlap, we merged them into one [5,9]. Intervals: [[1,4], [2,6], [3,5]] Output: [[1,6]], Since all the given intervals overlap, we merged them into one. ``` ### 題解 考慮兩個區間 `a` 和 `b` 滿足 `a.start <= b.start`,一共會有四種交互情況: <table> <tr> <th>狀況</th> <th>圖示</th> </tr> <tr> <td> 1. 彼此不重疊 2. 部分的 `b` 與 `a` 重疊 3. 所有的 `b` 在 `a` 裡面 4. 所有的 `a` 在 `b` 裡面,且起點相同 </td> <td> <p style="text-align: center"> <img src="https://i.imgur.com/4pna3R1.png" /> </p> </td> </tr> </table> 我們的目標是要合併重複區間,依據上述 (2), (3), (4) 的重疊情況,合併的方式如下: <p style="text-align: center"> <img src="https://i.imgur.com/gbPKMwJ.png" /> </p> ``` c.start = a.start // 左側固定 c.end = max(a.end, b.end) // 右側取大 ``` 因此我們的演算法策略為: 1. 先將所有的區間進行排序,確保 `a.start <= b.start` 2. 如果發生「重疊」(即 `b.start <= a.end`)的狀況,需要合併為新的區間 `c` 3. 反覆上述動作,如果下一個區間與得到的區間 `c` 有重疊的狀況 ### 代碼 #### C++ ``` cpp class MergeIntervals { public: static vector<Interval> mergeIntervals(vector<Interval> &intervals) { if (intervals.size() < 2) { return intervals; } sort(intervals.begin(), intervals.end(), [](const Interval &x, const Interval &y) { return x.start < y.start; }); vector<Interval> mergedIntervals; vector<Interval>::const_iterator intervalItr = intervals.begin(); Interval interval = *intervalItr++; int start = interval.start; int end = interval.end; while (intervalItr != intervals.end()) { interval = *intervalItr++; if (interval.start <= end) { end = max(interval.end, end); } else { mergedIntervals.push_back({ start, end }); start = interval.start; end = interval.end; } } mergedIntervals.push_back({ start, end }); return mergedIntervals; } } ``` #### Java ``` java class MergeIntervals { public static List<Interval> merge(List<Interval> intervals) { if (intervals.size() < 2) { return intervals; } // sort the intervals by start index Collections.sort(intervals, (a, b) -> Integer.compare(a.start, b.start)); List<Interval> mergedIntervals = new LinkedList<Interval>(); Iterator<Interval> intervalItr = intervals.iterator(); Interval interval = intervalItr.next(); int start = interval.start; int end = interval.end; while (intervalItr.hasNext()) { interval = intervalItr.next(); if (interval.start <= end) { // overlapping intervals, adjust the end end = Math.max(interval.end, end); } else { // non-overlapping interval, add the previous interval and reset mergedIntervals.add(new Interval(start, end)); start = interval.start; end = interval.end; } } // add the last interval mergedIntervals.add(new Interval(start, end)); return mergedIntervals; } } ``` #### JavaScript ``` javascript const mergeIntervals = (intervals) => { if (intervals.length < 2) { return intervals; } intervals.sort((a, b) => a.start - b.start); const mergedIntervals = []; let start = intervals[0].start; let end = intervals[0].end; for (let i = 1; i < intervals.length; ++i) { const interval = intervals[i]; if (interval.start <= end) { end = Math.max(interval.end, end); } else { mergedIntervals.push(new Interval(start, end)); start = interval.start; end = interval.end; } } mergedIntervals.push(new Interval(start, end)); return mergedIntervals; } ``` #### Python ``` python def merge_intervals(intervals): if len(intervals) < 2: return intervals intervals.sort(key=lambda x: x.start) mergedIntervals = [] start = intervals[0].start end = intervals[0].end for i in range(1, len(intervals)): interval = intervals[i] if interval.start <= end: end = max(interval.end, end) else: mergedIntervals.append(Interval(start, end)) start = interval.start end = interval.end mergedIntervals.append(Interval(start, end)) return mergedIntervals ``` ### 分析 - 時間複雜度:$\mathcal{O}(n\log{n})$,其中遍歷需 $\mathcal{O}(n)$ 而排序需 $\mathcal{O}(n\log{n})$,又 $\mathcal{O}(n\log{n}) > \mathcal{O}(n)$ - 空間複雜度:$\mathcal{O}(n)$,需要 $\mathcal{O}(n)$ 進行排序且需要 $\mathcal{O}(n)$ 用來儲存返回的區間陣列 ## [例題] Insert Interval ### 問題 給定一個由不重疊區間所組成的陣列,並且該陣列已經根據起始位置進行排序,將一個額外給定的區間插入正確的位置,並合併重疊區間使返回的區間陣列中為彼此獨立的區間。 **Examples** ``` Input: Intervals=[[1,3], [5,7], [8,12]], New Interval=[4,6] Output: [[1,3], [4,7], [8,12]], After insertion, since [4,6] overlaps with [5,7], we merged them into one [4,7]. Input: Intervals=[[1,3], [5,7], [8,12]], New Interval=[4,10] Output: [[1,3], [4,12]], After insertion, since [4,10] overlaps with [5,7] & [8,12], we merged them into [4,12]. Input: Intervals=[[2,3],[5,7]], New Interval=[1,4] Output: [[1,4], [5,7]], After insertion, since [1,4] overlaps with [2,3], we merged them into one [1,4]. ``` ### 題解 倘若給定陣列並未排序,我們可以簡單地將新的區間添加至陣列中,並呼叫 [Merge Intervals](#例題-Merge-Intervals) 中的 `merge()` 函數來處理。但既然題目已經說給定的陣列 **有序**,我們需要想看看是否能夠有比 $\mathcal{O}(n\log{n})$ 更好的解決方法。 當我們要在一個已經排序的區間陣列中插入一個新的區間時,<mark>首先需要找到可以放置新區間的正確位置</mark>;換句話說,我們需要跳過在新區間開始前之前結束的所有區間,亦即遍歷區間陣列,並使用以下條件跳過這些不滿足條件的區間: ``` intervals[i].end < newInterval.start ``` 找到正確的位置後,可以遵循類似於 [Merge Intervals](#例題-Merge-Intervals) 的做法來插入或合併新的區間。在不失一般性的情況下將新區間稱為 `a`,而滿足上述條件的第一個區間稱為 `b`,則共有以下五種可能性: <table> <tr> <th>狀況</th> <th>圖示</th> </tr> <tr> <td> 1. 彼此不重疊,直接插入 `a` 2. 部分的 `b` 與 `a` 重疊,合併為 `c(a.start, b.end)` 3. 所有的 `b` 在 `a` 裡面,合併為 `c(a.start, a.end)` 4. 部分的 `a` 與 `b` 重疊,合併為 `c(b.start, a.end)` 5. 所有的 `a` 在 `b` 裡面,合併為 `c(b.start, b.end)` </td> <td> <p style="text-align: center"> <img src="https://i.imgur.com/Y1VXsyq.png" /> </p> </td> </tr> </table> 要處理上述 (2), (3), (4) 和 (5) 的重疊狀況,只需要執行以下操作: ``` c.start = min(a.start, b.start) // 左側取小 c.end = max(a.end, b.end) // 右側取大 ``` 因此我們的演算法策略為: 1. 遍歷陣列跳過滿足 `intervals[i].end < newInterval.start` 的區間 2. 找到不滿足條件的區間,判斷左右邊界並合併得到新區間 `c` 3. 反覆上述動作,如果下一個區間與得到的區間 `c` 有重疊的狀況 ### 代碼 #### C++ ``` cpp class InsertInterval { public: static vector<Interval> insert(const vector<Interval> &intervals, Interval newInterval) { if (intervals.empty()) { return vector<Interval> { newInterval }; } vector<Interval> mergedIntervals; int idx = 0; while (idx < intervals.size() && intervals[idx].end < newInterval.start) { mergedIntervals.push_back(intervals[idx]); idx++; } while (idx < intervals.size() && intervals[idx].start <= newInterval.end) { newInterval.start = min(intervals[idx].start, newInterval.start); newInterval.end = max(intervals[idx].end, newInterval.end); idx++; } mergedIntervals.push_back(newInterval); while (idx < intervals.size()) { mergedIntervals.push_back(intervals[idx]); idx++; } return mergedIntervals; } } ``` #### Java ``` java class InsertInterval { public static List<Interval> insert(List<Interval> intervals, Interval newInterval) { if (intervals == null || intervals.isEmpty()) { return Arrays.asList(newInterval); } List<Interval> mergedIntervals = new ArrayList<>(); int i = 0; // skip (and add to output) all intervals that come before the newInterval while (i < intervals.size() && intervals.get(i).end < newInterval.start) { mergedIntervals.add(intervals.get(i++)); } // merge all intervals that overlap with newInterval while (i < intervals.size() && intervals.get(i).start <= newInterval.end) { newInterval.start = Math.min(intervals.get(i).start, newInterval.start); newInterval.end = Math.max(intervals.get(i).end, newInterval.end); i++; } // insert the newInterval mergedIntervals.add(newInterval); // add all the remaining intervals to the output while (i < intervals.size()) { mergedIntervals.add(intervals.get(i++)); } return mergedIntervals; } } ``` #### JavaScript ``` javascript const insertInterval = (intervals, newInterval) { let idx = 0; let merged = []; while (idx < intervals.length && intervals[idx].end < newInterval.start) { merged.push(intervals[idx]); idx += 1; } while (idx < intervals.length && intervals[idx].start <= newInterval.end) { newInterval.start = Math.min(intervals[idx].start, newInterval.start); newInterval.end = Math.max(intervals[idx].end, newInterval.end); idx += 1; } merged.push(newInterval); while (idx < intervals.length) { merged.push(intervals[idx]); i += 1; } return merged; } ``` #### Python ``` python def insert_interval(intervals, new_interval): merged = [] i = 0 start = 0 end = 1 while i < len(intervals) and intervals[i][end] < new_interval[start]: merged.append(intervals[i]) i += 1 while i < len(intervals) and intervals[i][start] <= new_interval[end]: new_interval[start] = min(intervals[i][start], new_interval[start]) new_interval[end] = max(intervals[i][end], new_interval[end]) i += 1 merged.append(new_interval) while i < len(intervals): merged.append(intervals[i]) i += 1 return merged ``` ### 分析 - 時間複雜度:$\mathcal{O}(n)$ - 空間複雜度:$\mathcal{O}(n)$ ## [例題] Intervals Intersection ### 問題 給定兩個區間陣列,找出兩個陣列的交集部分,每一個陣列中的區間彼此獨立且已經依據起始位置進行排序。 **Examples**: ``` Input: arr1=[[1, 3], [5, 6], [7, 9]], arr2=[[2, 3], [5, 7]] Output: [2, 3], [5, 6], [7, 7]. Input: arr1=[[1, 3], [5, 7], [9, 12]], arr2=[[5, 10]] Output: [5, 7], [9, 10]. ``` ### 題解 這個問題可以採用 **合併區間(Merge Intervals)** 模式求解,正如我們在 [Insert Interval](#例題-Insert-Interval) 中所討論的,在兩個間隔 `a` 和 `b` 之間有五種交互的狀況;仔細觀察不難發現,每<mark>當兩個區間重疊時,其中一個區間的開始位置會位於另外一個區間中</mark>,這個事實可以幫助我們確認兩個區間是否重疊。 <p style="text-align: center"> <img src="https://i.imgur.com/SGjdmS4.png" /> </p> 此時,兩個重疊區間交集的部分,可以透過以下方式判斷其區間: ``` start = max(a.start, b.start) // 左側取大 end = min(a.end, b.end) // 右側取小 ``` 因此我們只需要遍歷兩個陣列,同時查看是否有區間重疊,重疊的部分計算其交集並插入結果陣列中,直到結束。 ### 代碼 #### C++ ``` cpp class Intersection { public: static vector<Interval> merge(const vector<Interval> &arr1, const vector<Interval> &arr2) { vector<Interval> result; int i = 0; int j = 0; while (i < arr1.size() && j < arr2.size()) { if ((arr1[i].start >= arr2[j].start) && arr1[i].start <= arr2[j].end) || (arr2[j].start >= arr1[i].start) && arr2[j].start <= arr1[i].end)) { result.push_back({max(arr1[i].start, arr2[j].start), min(arr1[i].end, arr2[j].end)}); } if (arr1[i].end < arr2[j].end) { i++; } else { j++; } } return result; } }; ``` #### Java ``` java class IntervalsIntersection { public static Interval[] merge(Interval[] arr1, Interval[] arr2) { List<Interval> result = new ArrayList<Interval>(); int i = 0; int j = 0; while (i < arr1.length && j < arr2.length) { if ((arr1[i].start >= arr2[j].start && arr1[i].start <= arr2[j].end) || (arr2[j].start >= arr1[i].start && arr2[j].start <= arr1.[i].end)) { result.add(new Interval(Math.max(arr1[i].start, arr2[j].start), Max.min(arr1[i].end, arr2[j].end))); } } if (arr1[i].end < arr2[j].end) { i++; } else { j++; } return result.toArray(new Interval[result.size()]); } } ``` #### JavaScript ``` javascript const IntersectionIntervals = (intervals_a, intervals_b) => { let results = []; let i = 0; let j = 0; while (i < intervals_a.length && j < intervals_b.length) { if ((intervals_a[i].start >= intervals_b[j].start && intervals_a[i].start <= intervals_b[j].end) || (intervals_b[j].start >= intervals_a[i].start && intervals_b[j].start <= intervals_a[i].end)) { const left = Math.max(intervals_a[i].start, intervals_b[j].start); const right = Math.min(intervals_a[i].end, intervals_b[j].end); result.push(newInterval(left, right)); } if (intervals_a[i].end < intervals_b[j].end) { i += 1; } else { j += 1; } } return result; } ``` #### Python ``` python def intersection(intervals_a, intervals_b): result = [] i, j, start, end = 0, 0, 0, 1 while i < len(intervals_a) and j < len(intervals_b): a_overlaps_b = intervals_a[i][start] >= intervals_b[j][start] and intervals_a[i][start] <= intervals_b[j][end] b_overlaps_a = intervals_b[j][start] >= intervals_a[i][start] and intervals_b[j][start] <= intervals_a[i][end] if (a_overlaps_b or b_overlaps_a): result.append([max(intervals_a[i][start], intervals_b[j][start]), min(intervals_a[i][end], intervals_b[j][end])]) if intervals_a[i][end] < intervals_b[j][end]: i += 1 else: j += 1 return result ``` ### 分析 - 時間複雜度:$\mathcal{O}(n+m)$ - 空間複雜度:$\mathcal{O}(n+m)$,若不計返回陣列所需的空間則為 $\mathcal{O}(1)$ ## [例題] Conflicting Appointments 給定一組陣列,其中有 $N$ 個約會行程(以區間表示時間範圍),撰寫一支程式判斷一個人是否可以出席所有的約會行程。 ### 問題 **Examples** ``` Input: [[1,4], [2,5], [7,9]] Output: false. Since [1,4] and [2,5] overlap, a person cannot attend both of these appointments. Input: [[6,7], [2,4], [8,12]] Output: true. None of the appointments overlap, therefore a person can attend all of them. Input: [[4,5], [2,3], [3,6]] Output: false. Since [4,5] and [3,6] overlap, a person cannot attend both of these appointments. ``` ### 題解 這個問題可以採用 **合併區間(Merge Intervals)** 模式求解,我們需要做的是先將約會行程依據起始時間進行排序,再逐個確認這些區間是否存在重疊的狀況。 > 1. 注意判斷的邊界需不需要包含 > 2. 存在重疊的條件 ### 代碼 #### C++ ``` cpp class ConflictingAppointments { public: static bool canAttendAllAppointments(vector<Interval>& intervals) { sort(intervals.begin(), intervals.end(), [](const Interval& x, const Interval& y) { return x.start < y.start; }); for (int i = 1; i < intervals.size(); i++) { if (intervals[i].start < intervals[i - 1].end) { return false; } } return true; } } ``` #### Java ``` java class ConflictingAppointments { public static boolean canAttendAllAppointments(Interval[] intervals) { Arrays.sort(intervals, (a, b) -> Integer.compare(a.start, b.start)); for (int i = 1; i < intervals.length; i++) { if (intervals[i].start < intervals[i - 1].end) { return false; } } return true; } } ``` #### JavaScript ``` javascript const canAttendAllAppointments = (intervals) => { intervals.sort((a, b) => a.start - b.start); for (let i = 1; i < intervals.length; i++) { if (intervals[i].start < intervals[i - 1].end) { return false; } } return true; } ``` #### Python ``` python def can_attend_all_appointments(intervals): intervals.sort(key=lambda x: x[0]) start, end = 0, 1 for i in range(1, len(intervals)): if intervals[i][start] < intervals[i-1][end]: return False return True ``` ### 分析 - 時間複雜度:$\mathcal{O}(n\log{n})$ - 空間複雜度:$\mathcal{O}(n)$ 排序所需空間 ## 參考資料 - [Merge Overlapping Intervals | GeeksforGeeks](https://www.geeksforgeeks.org/merging-intervals/) - [Coding Patterns: Merge Intervals | emre.me](https://emre.me/coding-patterns/merge-intervals/#similar-leetcode-problems) <!-- Widget: License --> {%hackmd @Hsins/widget-license %}