Introduction to the Merge Intervals Pattern
The Merge Intervals pattern is an essential algorithmic technique for dealing with overlapping intervals, scheduling problems, or contiguous blocks of time/space. It frequently appears in top tech interviews because it tests your ability to visualize sorting and process elements linearly based on conditional overlaps.
The core idea is simple but incredibly powerful: If you sort a list of intervals by their starting points, any intervals that overlap must be adjacent to each other in the sorted list. This simple realization reduces an $O(n^2)$ pairing problem down to $O(n \log n)$ time due to the sorting step, followed by a clean $O(n)$ linear scan.
When Should You Think About Merge Intervals?
Consider the Merge Intervals pattern when:
- You are given a list of intervals, an array of pairs, or start and end times.
- The problem asks you to merge overlapping intervals, find the intersection, or determine if any intervals overlap.
- You need to find free time blocks or schedule meetings without conflicts.
- Keywords in the prompt include "overlapping", "mutually exclusive", "contiguous", or "time slots".
Core Concept of Merge Intervals
An "interval" is generally represented as a pair of numbers [start, end]. Two intervals a and b (where a comes before b in a sorted list) can interact in exactly one of three ways:
-
Mutually Exclusive (No Overlap):
a.end < b.startExample:[1, 3]and[4, 6]. -
Partial Overlap:
a.end >= b.startbuta.end < b.endExample:[1, 4]and[3, 6]. Here, they merge into[1, 6]. -
Complete Overlap (Subsumed):
a.end >= b.endExample:[1, 6]and[2, 4]. The second interval is completely inside the first. They merge into[1, 6].
By sorting the intervals based on the start value, we guarantee that a.start <= b.start. Therefore, to check for an overlap, we only need to compare a.end with b.start.
Example: Merge Overlapping Intervals
Given an array of intervals where intervals[i] = [starti, endi], merge all overlapping intervals, and return an array of the non-overlapping intervals that cover all the intervals in the input.
Brute Force Approach
A naive approach would be to compare every interval against every other interval. If they overlap, merge them and restart the checking process until no more merges can occur.
Complexity:
- Time Complexity: $O(n^2)$, or worse if we repeatedly iterate after merging.
- Space Complexity: $O(n)$ to store the result.
Optimized with the Merge Intervals Pattern
We first sort the intervals by their start times. Then, we iterate through the sorted intervals, building our result list by checking if the current interval overlaps with the last interval added to our result.
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
class Solution {
public int[][] merge(int[][] intervals) {
if (intervals == null || intervals.length <= 1) {
return intervals;
}
// Step 1: Sort intervals by starting value
Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0]));
List<int[]> merged = new ArrayList<>();
// Step 2: Initialize the first interval
int[] currentInterval = intervals[0];
merged.add(currentInterval);
// Step 3: Iterate through the remaining intervals
for (int[] interval : intervals) {
int currentEnd = currentInterval[1];
int nextStart = interval[0];
int nextEnd = interval[1];
// If overlapping, merge them by updating the end of the current interval
if (currentEnd >= nextStart) {
currentInterval[1] = Math.max(currentEnd, nextEnd);
} else {
// If not overlapping, add the new interval to the list
currentInterval = interval;
merged.add(currentInterval);
}
}
// Convert the List back to an array
return merged.toArray(new int[merged.size()][]);
}
}
Complexity:
- Time Complexity: $O(n \log n)$ due to the sorting step. The linear scan takes $O(n)$, making the total time dominated by sorting.
- Space Complexity: $O(n)$ or $O(\log n)$ depending on the sorting algorithm implementation in Java (TimSort/Dual-Pivot Quicksort), plus $O(n)$ for the resulting list.
Dry Run: Merge Overlapping Intervals
Input: intervals = [[1, 3], [2, 6], [8, 10], [15, 18]]
| Step | Current interval |
currentInterval (in merged) |
Action | merged List State |
|---|---|---|---|---|
| Initialize | - | [1, 3] |
Add first to list | [[1, 3]] |
interval = [1, 3] |
[1, 3] |
[1, 3] |
Overlaps (3 >= 1). Max end is 3. |
[[1, 3]] |
interval = [2, 6] |
[2, 6] |
[1, 3] |
Overlaps (3 >= 2). Update end to max(3, 6) = 6. |
[[1, 6]] |
interval = [8, 10] |
[8, 10] |
[1, 6] |
No overlap (6 < 8). Add new. |
[[1, 6], [8, 10]] |
interval = [15, 18] |
[15, 18] |
[8, 10] |
No overlap (10 < 15). Add new. |
[[1, 6], [8, 10], [15, 18]] |
Result: [[1, 6], [8, 10], [15, 18]]
Reusable Template for Merge Intervals
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public int[][] mergeIntervalsTemplate(int[][] intervals) {
// 1. Handle edge cases
if (intervals.length < 2) return intervals;
// 2. Sort by the starting point
Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0]));
List<int[]> result = new ArrayList<>();
// 3. Initialize tracking variables
int start = intervals[0][0];
int end = intervals[0][1];
for (int i = 1; i < intervals.length; i++) {
int nextStart = intervals[i][0];
int nextEnd = intervals[i][1];
// 4. Overlap condition
if (end >= nextStart) {
// Merge intervals
end = Math.max(end, nextEnd);
} else {
// No overlap, finalize current and start new
result.add(new int[]{start, end});
start = nextStart;
end = nextEnd;
}
}
// 5. Don't forget to add the last processed interval
result.add(new int[]{start, end});
return result.toArray(new int[result.size()][]);
}
Advanced Variation: Insert Interval
Sometimes the intervals are already sorted, and you just need to insert a new interval and merge if necessary. Because the array is already sorted, you can achieve this in strict $O(n)$ time.
Problem: Insert newInterval into intervals (which are sorted by start times) such that the resulting intervals remain sorted and non-overlapping.
class Solution {
public int[][] insert(int[][] intervals, int[] newInterval) {
List<int[]> result = new ArrayList<>();
int i = 0;
int n = intervals.length;
// Step 1: Add all intervals that come completely BEFORE the new interval
while (i < n && intervals[i][1] < newInterval[0]) {
result.add(intervals[i]);
i++;
}
// Step 2: Merge all intervals that overlap with the new interval
while (i < n && intervals[i][0] <= newInterval[1]) {
newInterval[0] = Math.min(newInterval[0], intervals[i][0]);
newInterval[1] = Math.max(newInterval[1], intervals[i][1]);
i++;
}
result.add(newInterval); // Add the merged new interval
// Step 3: Add the remaining intervals
while (i < n) {
result.add(intervals[i]);
i++;
}
return result.toArray(new int[result.size()][]);
}
}
How to Recognize Merge Intervals in Interviews
Look for these clues:
- Data Shape: Arrays of arrays, lists of pairs, or objects with
startandendproperties. - Concepts: "Time slots", "meetings", "schedules", "segments", "ranges".
- The Request: "Find the overlap", "merge ranges", "determine minimum rooms required", "insert a new schedule".
- Optimization Path: A naive nested loop solution can almost always be optimized to $O(n \log n)$ by sorting first.
Common Mistakes
Mistake 1: Forgetting to Sort
You cannot linearly merge intervals if they are not sorted by start time. Overlapping intervals might be at opposite ends of the array, breaking the linear scan logic. Always sort first unless explicitly told the input is sorted.
Mistake 2: Updating the Wrong End Value
When merging [1, 5] and [2, 4], the merged interval is [1, 5], not [1, 4]. Always use Math.max(currentEnd, nextEnd) to extend the boundary correctly.
Mistake 3: Forgetting the Last Interval
When using a tracking variable approach (maintaining start and end variables instead of modifying the last element in the list directly), beginners often forget to add the final interval to the result list after the loop finishes.
Practice Problems for This Pattern
- Merge Intervals (LeetCode 56)
- Insert Interval (LeetCode 57)
- Non-overlapping Intervals (LeetCode 435)
- Meeting Rooms (LeetCode 252)
- Meeting Rooms II (LeetCode 253) - Often involves sorting start and end times separately, or using a Min-Priority Queue along with the Merge Interval logic.
- Interval List Intersections (LeetCode 986)
Interview Script You Can Reuse
"Since the problem involves contiguous ranges and finding overlaps, the most efficient approach is the Merge Intervals pattern. First, I'll sort the intervals by their starting points. This ensures that any intervals that overlap are adjacent in the array. Then, I can iterate through the array in a single pass. If the current interval's start time is less than or equal to the previous interval's end time, an overlap exists, and I'll merge them by taking the maximum end time. If they don't overlap, I'll append the previous interval to my result set and move on. The time complexity will be bounded by the sorting step at O(n log n), and space complexity will be O(n) to store the result."
Final Takeaways
- Merge Intervals is the go-to pattern for overlapping subarrays and scheduling.
- The primary step is Sorting by start time to guarantee adjacency of overlaps.
- Overlaps are identified by checking
previous.end >= current.start. - It reduces $O(n^2)$ pairing to $O(n \log n)$ total time.