11. How would you solve Merge Intervals?
Merge overlapping intervals and explain your approach, edge cases, and complexity.
I would first sort the intervals by their start value. Then I keep a merged list starting with the first interval. For each remaining interval, I compare its start with the end of the last merged interval. If they overlap, I extend the last end to the larger end. Otherwise, I append a new interval. Sorting makes possible overlaps adjacent. The total time is O(n log n), and the merged output uses O(n) auxiliary space.
See the Code while reading this explanation.
The input is a list of closed intervals. Each interval has a start and an end. The goal is to combine ranges that overlap and return ranges that no longer overlap. I first sort the intervals by their start values. This puts possible overlaps next to each other. I then build a merged result from left to right. I only need to compare the current interval with the last interval already in the result. If they overlap, I extend that result interval. Otherwise, I add a new one.
- Should touching closed intervals such as [1,4] and [4,5] be merged?
- Is it acceptable to sort the input intervals by their start value?
The input is an array of closed intervals. For the example, the input is [[1,3],[2,6],[8,10],[15,18]]. The required output is a new set of non-overlapping intervals that covers exactly the same ranges. The expected result is [[1,6],[8,10],[15,18]]. Because the intervals are closed, touching intervals also overlap under the condition current.start <= lastMerged.end.
Sort the intervals by their start value. The example is already in sorted order: [[1,3],[2,6],[8,10],[15,18]]. If the input is empty, return an empty array. Otherwise, put a copy of the first interval, [1,3], into the merged list. Start processing at index 1. The invariant is that the merged list contains the correctly merged, non-overlapping intervals for the part of the input already processed.
At index 1, the current interval is [2,6]. The merged list before the step is [[1,3]]. Check 2 <= 3. This is true, so the intervals overlap. Update the end to max(3,6) = 6. The merged list becomes [[1,6]].
At index 2, the current interval is [8,10]. The state before the step is [[1,6]]. Check 8 <= 6. This is false, so there is no overlap. Append [8,10]. The state becomes [[1,6],[8,10]].
At index 3, the current interval is [15,18]. The state before the step is [[1,6],[8,10]]. Compare it with the last merged interval [8,10]. Check 15 <= 10. This is false. Append [15,18]. The final state is [[1,6],[8,10],[15,18]].
Sorting places possible overlaps next to each other. After earlier intervals have already been merged, the current interval cannot need to merge with an older result interval without also overlapping the last merged interval. So each current interval only needs one comparison with the last merged interval. We either extend that interval or append a new non-overlapping interval.
The invariant is that the processed prefix is represented by correctly merged, non-overlapping intervals. If current.start <= lastMerged.end, merging preserves the same covered range by extending the end to the maximum endpoint. If current.start > lastMerged.end, the current interval cannot overlap the previous merged ranges, so appending it preserves non-overlap. Therefore, the final result covers exactly the same ranges as the original input.
The Java code handles an empty input first. It sorts with Integer.compare on the start values. It stores merged intervals in an ArrayList<int[]>. During the loop, it reads the current interval and the last merged interval. On overlap, it updates the last end with Math.max. Otherwise, it adds a new interval. Finally, it converts the list to int[][]. Sorting costs O(n log n), the sweep costs O(n), and the merged output uses O(n) auxiliary space.
The key idea is to sort intervals by their start value before merging. After sorting, intervals that may overlap are next to each other. Keep a list called merged and compare each current interval only with the last interval in that list. If current[0] <= lastMerged[1], the ranges overlap, so set lastMerged[1] to Math.max(lastMerged[1], current[1]). Otherwise, append the current interval as a new merged block. The central invariant is that after each processed interval, merged contains the correctly merged, non-overlapping representation of the processed prefix.
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class Main {
public static int[][] merge(int[][] intervals) {
// Handle the empty-input edge case before reading the first interval.
if (intervals == null || intervals.length == 0) {
return new int[0][];
}
// Sort by start value so intervals that can overlap are adjacent.
Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0]));
// This list stores the correctly merged processed prefix.
List<int[]> merged = new ArrayList<>();
// Seed the result with a copy of the first sorted interval.
merged.add(new int[] { intervals[0][0], intervals[0][1] });
// Process every remaining interval from left to right.
for (int i = 1; i < intervals.length; i++) {
int[] current = intervals[i];
int[] lastMerged = merged.get(merged.size() - 1);
// Closed intervals overlap when the current start is at or before
// the end of the last merged interval.
if (current[0] <= lastMerged[1]) {
// Keep the earlier start and extend the right boundary when needed.
lastMerged[1] = Math.max(lastMerged[1], current[1]);
} else {
// No overlap exists, so start a new merged interval.
merged.add(new int[] { current[0], current[1] });
}
}
// Convert the merged list to the required int[][] result.
return merged.toArray(new int[merged.size()][]);
}
public static void main(String[] args) {
// Use the exact example shown in the approved diagram.
int[][] intervals = { { 1, 3 }, { 2, 6 }, { 8, 10 }, { 15, 18 } };
// Run the merge algorithm on the diagram's example.
int[][] result = merge(intervals);
// Expected output: [[1, 6], [8, 10], [15, 18]]
System.out.println(Arrays.deepToString(result));
}
}Let n be the number of intervals. Sorting the intervals by start value takes O(n log n) time. After sorting, the algorithm makes one left-to-right sweep, which takes O(n) time. Sorting therefore dominates, so the total time is O(n log n). The merged result can contain up to n intervals, so the illustrated solution uses O(n) auxiliary space for the merged output.
This pattern is useful whenever overlapping time or numeric ranges must be combined. Examples include combining booking windows, calendar ranges, reserved time periods, covered numeric ranges, or other intervals before later processing.
This problem tests whether you recognize the sort-and-sweep interval pattern. The interviewer can see whether you choose the correct sorting key, maintain a useful invariant, apply the overlap condition correctly, and update interval boundaries without losing coverage. It also checks whether you handle cases such as touching or contained intervals, write clear Java collection code, and include the sorting cost when explaining time complexity.
A common mistake is trying to merge before sorting by start value. Then possible overlaps may not be adjacent. Another mistake is using current[0] < lastMerged[1] instead of current[0] <= lastMerged[1]. For closed intervals, [1,4] and [4,5] touch and must merge. Candidates may also compare the current interval with every earlier interval instead of only the last merged interval. Another mistake is mishandling fully contained intervals such as [1,10] and [2,3]. Finally, the complexity explanation must include the O(n log n) sorting cost.
State the invariant before coding: after processing each interval, the merged list is already correct and non-overlapping for the processed prefix. Then the merge-or-append decision is easy to explain.









