56. Merge Intervals
Problem Description
You are given an array of intervals where each interval is represented as [start_i, end_i]
. Your task is to merge all overlapping intervals and return an array containing only non-overlapping intervals that cover all the original intervals.
Two intervals overlap if one starts before or when the other ends. For example:
[1, 3]
and[2, 6]
overlap and should be merged into[1, 6]
[1, 4]
and[4, 5]
overlap (they share the endpoint 4) and should be merged into[1, 5]
[1, 2]
and[3, 4]
do not overlap and remain separate
The solution works by first sorting all intervals by their start points. Then it traverses through the sorted intervals, maintaining a current interval [st, ed]
. For each new interval [s, e]
:
- If the current interval's end
ed
is less than the new interval's starts
, they don't overlap, so the current interval is added to the result and a new current interval begins - Otherwise, they overlap, so the current interval is extended by updating
ed
to be the maximum ofed
ande
After processing all intervals, the final current interval is added to the result. This ensures all overlapping intervals are merged into single continuous ranges.
Intuition
The key insight is that if we arrange intervals in a sorted order, overlapping intervals will naturally be positioned next to each other. Think about it visually on a number line - if intervals overlap, they must be adjacent or close to each other when sorted by their starting points.
Once sorted, we can process intervals sequentially and make local decisions. When examining two consecutive intervals, we only need to check if they overlap by comparing the end of the first interval with the start of the second interval. If end1 >= start2
, they overlap and should be merged.
The merging process becomes straightforward: we maintain a "growing" interval that expands as we encounter overlapping intervals. When we find an interval that doesn't overlap with our current growing interval, we know we've found the boundary of a merged group, so we save the current merged interval and start a new one.
Why does sorting work? Without sorting, we'd need to compare every interval with every other interval to find all overlaps, which would be inefficient. Sorting transforms the problem from a global comparison issue to a local one - we only need to look at adjacent intervals. This is because if interval A comes before interval B after sorting (A's start β€ B's start), and they don't overlap, then B cannot overlap with any interval that comes before A either.
The choice to update the end point using max(ed, e)
handles cases where one interval completely contains another. For example, [1, 10]
and [2, 5]
- the second interval is fully contained within the first, so we keep the larger endpoint.
Learn more about Sorting patterns.
Solution Approach
The implementation follows a sorting and one-pass traversal strategy:
Step 1: Sort the intervals
We start by sorting the intervals array using intervals.sort()
. This sorts intervals in ascending order by their first element (start point). After sorting, any intervals that could potentially overlap will be adjacent to each other.
Step 2: Initialize tracking variables
We create an empty result list ans
to store merged intervals. We extract the first interval's start and end points into variables st
and ed
using st, ed = intervals[0]
. These variables will track the current interval being built through merging.
Step 3: Traverse and merge
We iterate through the remaining intervals starting from index 1 using for s, e in intervals[1:]
:
-
Case 1: No overlap - If
ed < s
(current interval's end is less than next interval's start), the intervals don't overlap. We:- Append the completed interval
[st, ed]
to our answer - Start tracking a new interval by setting
st, ed = s, e
- Append the completed interval
-
Case 2: Overlap exists - If
ed >= s
(current interval's end overlaps with or touches next interval's start), we:- Extend the current interval by updating
ed = max(ed, e)
- The
max
function handles cases where the current interval might completely contain the next one
- Extend the current interval by updating
Step 4: Add final interval
After the loop completes, we still have one last merged interval [st, ed]
that hasn't been added to the result. We append it with ans.append([st, ed])
.
Time Complexity: O(n log n)
where n is the number of intervals - dominated by the sorting step. The traversal is O(n)
.
Space Complexity: O(1)
if we don't count the output array, or O(n)
if we do, where n is the number of intervals in the worst case (no overlaps).
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the solution with the input: intervals = [[1,3], [8,10], [2,6], [15,18]]
Step 1: Sort the intervals
After sorting by start points: [[1,3], [2,6], [8,10], [15,18]]
Step 2: Initialize tracking variables
ans = []
(empty result list)st = 1, ed = 3
(from first interval[1,3]
)
Step 3: Process each interval
Processing interval [2,6]:
- Check: Is
ed < s
? Is3 < 2
? No, so intervals overlap - Update:
ed = max(3, 6) = 6
- Current tracking:
st = 1, ed = 6
Processing interval [8,10]:
- Check: Is
ed < s
? Is6 < 8
? Yes, no overlap - Add completed interval
[1,6]
to result:ans = [[1,6]]
- Start new interval:
st = 8, ed = 10
Processing interval [15,18]:
- Check: Is
ed < s
? Is10 < 15
? Yes, no overlap - Add completed interval
[8,10]
to result:ans = [[1,6], [8,10]]
- Start new interval:
st = 15, ed = 18
Step 4: Add final interval
Add the last tracked interval [15,18]
to result: ans = [[1,6], [8,10], [15,18]]
The algorithm successfully merged [1,3]
and [2,6]
into [1,6]
while keeping the non-overlapping intervals [8,10]
and [15,18]
separate.
Solution Implementation
1class Solution:
2 def merge(self, intervals: List[List[int]]) -> List[List[int]]:
3 # Sort intervals by their start time
4 intervals.sort()
5
6 # Initialize result list
7 merged_intervals = []
8
9 # Initialize current interval with the first interval
10 current_start, current_end = intervals[0]
11
12 # Iterate through remaining intervals
13 for interval_start, interval_end in intervals[1:]:
14 # Check if current interval and next interval don't overlap
15 if current_end < interval_start:
16 # No overlap, add current interval to result
17 merged_intervals.append([current_start, current_end])
18 # Update current interval to the new interval
19 current_start, current_end = interval_start, interval_end
20 else:
21 # Intervals overlap, merge them by extending the end time
22 current_end = max(current_end, interval_end)
23
24 # Add the last interval to result
25 merged_intervals.append([current_start, current_end])
26
27 return merged_intervals
28
1class Solution {
2 public int[][] merge(int[][] intervals) {
3 // Sort intervals by their start time in ascending order
4 Arrays.sort(intervals, Comparator.comparingInt(a -> a[0]));
5
6 // Initialize the first interval's start and end points
7 int currentStart = intervals[0][0];
8 int currentEnd = intervals[0][1];
9
10 // List to store merged intervals
11 List<int[]> mergedIntervals = new ArrayList<>();
12
13 // Iterate through all intervals starting from the second one
14 for (int i = 1; i < intervals.length; i++) {
15 int nextStart = intervals[i][0];
16 int nextEnd = intervals[i][1];
17
18 // Check if current interval and next interval overlap
19 if (currentEnd < nextStart) {
20 // No overlap: add current merged interval to result
21 mergedIntervals.add(new int[] {currentStart, currentEnd});
22 // Update current interval to the next one
23 currentStart = nextStart;
24 currentEnd = nextEnd;
25 } else {
26 // Overlap exists: merge by extending the end point if necessary
27 currentEnd = Math.max(currentEnd, nextEnd);
28 }
29 }
30
31 // Add the last merged interval to the result
32 mergedIntervals.add(new int[] {currentStart, currentEnd});
33
34 // Convert list to 2D array and return
35 return mergedIntervals.toArray(new int[mergedIntervals.size()][]);
36 }
37}
38
1class Solution {
2public:
3 vector<vector<int>> merge(vector<vector<int>>& intervals) {
4 // Sort intervals by start time (first element of each interval)
5 sort(intervals.begin(), intervals.end());
6
7 // Initialize variables to track the current merged interval
8 int currentStart = intervals[0][0];
9 int currentEnd = intervals[0][1];
10
11 // Result vector to store merged intervals
12 vector<vector<int>> mergedIntervals;
13
14 // Iterate through all intervals starting from the second one
15 for (int i = 1; i < intervals.size(); ++i) {
16 // Check if current interval overlaps with the next interval
17 if (currentEnd < intervals[i][0]) {
18 // No overlap: save the current merged interval and start a new one
19 mergedIntervals.push_back({currentStart, currentEnd});
20 currentStart = intervals[i][0];
21 currentEnd = intervals[i][1];
22 } else {
23 // Overlap detected: extend the current interval's end if needed
24 currentEnd = max(currentEnd, intervals[i][1]);
25 }
26 }
27
28 // Add the last merged interval to the result
29 mergedIntervals.push_back({currentStart, currentEnd});
30
31 return mergedIntervals;
32 }
33};
34
1/**
2 * Merges overlapping intervals into non-overlapping intervals
3 * @param intervals - Array of intervals where each interval is [start, end]
4 * @returns Array of merged non-overlapping intervals
5 */
6function merge(intervals: number[][]): number[][] {
7 // Sort intervals by their start time in ascending order
8 intervals.sort((a, b) => a[0] - b[0]);
9
10 // Initialize result array to store merged intervals
11 const mergedIntervals: number[][] = [];
12
13 // Initialize current interval with the first interval
14 let [currentStart, currentEnd] = intervals[0];
15
16 // Iterate through remaining intervals starting from index 1
17 for (const [intervalStart, intervalEnd] of intervals.slice(1)) {
18 // Check if current interval and next interval don't overlap
19 if (currentEnd < intervalStart) {
20 // No overlap: add current interval to result and update current interval
21 mergedIntervals.push([currentStart, currentEnd]);
22 [currentStart, currentEnd] = [intervalStart, intervalEnd];
23 } else {
24 // Intervals overlap: extend the current interval's end if necessary
25 currentEnd = Math.max(currentEnd, intervalEnd);
26 }
27 }
28
29 // Add the last processed interval to the result
30 mergedIntervals.push([currentStart, currentEnd]);
31
32 return mergedIntervals;
33}
34
Time and Space Complexity
Time Complexity: O(n Γ log n)
The time complexity is dominated by the sorting operation intervals.sort()
, which takes O(n Γ log n)
time where n
is the number of intervals. After sorting, the code iterates through the intervals once in O(n)
time to merge overlapping intervals. Since O(n Γ log n) + O(n) = O(n Γ log n)
, the overall time complexity is O(n Γ log n)
.
Space Complexity: O(log n)
The space complexity comes from the sorting algorithm. Python's built-in sort()
uses Timsort, which requires O(log n)
space for the recursion stack in the worst case. The output list ans
stores the merged intervals, but this is not counted as extra space since it's the required output. The variables st
and ed
use constant O(1)
space. Therefore, the overall space complexity is O(log n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Forgetting to handle edge cases with single interval or empty input
The current implementation assumes the input has at least one interval. If the input is empty, intervals[0]
will throw an IndexError.
Solution: Add a guard clause at the beginning:
if not intervals: return []
2. Incorrect overlap condition
A frequent mistake is using current_end <= interval_start
instead of current_end < interval_start
for the non-overlap check. This would incorrectly treat touching intervals like [1, 3]
and [3, 5]
as non-overlapping, when they should merge into [1, 5]
.
Solution:
Always use strict inequality current_end < interval_start
to check for non-overlap. Intervals that share an endpoint should be merged.
3. Not using max() when merging intervals
When merging overlapping intervals, simply setting current_end = interval_end
is incorrect. The current interval might completely contain the next interval (e.g., [1, 5]
contains [2, 3]
), so we need to keep the larger end value.
Solution:
Always use current_end = max(current_end, interval_end)
to ensure we keep the furthest-reaching endpoint.
4. Modifying the input array in-place
Sorting the input array with intervals.sort()
modifies the original input. If the caller expects the input to remain unchanged, this creates a side effect.
Solution: Create a copy before sorting if input preservation is required:
intervals = sorted(intervals) # Creates a new sorted list
5. Forgetting to add the last interval
After the loop completes, there's always one final merged interval that hasn't been added to the result. Forgetting merged_intervals.append([current_start, current_end])
after the loop will lose the last group of merged intervals.
Solution: Always remember to append the final interval after the loop, or restructure the logic to handle it within the loop by checking if it's the last iteration.
What are the two properties the problem needs to have for dynamic programming to be applicable? (Select 2)
Recommended Readings
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Donβt Miss This!