Array · 23 of 36
Merge Intervals
Playdates on a calendar overlap, so you join every group of overlapping playdates into one long block of busy time.
- Simple way
- time O(n^2)
room O(n) - Clever way
- time O(n log n)
room O(n)
The problem, simply
You get a list of time ranges. Each range is a pair [start, end]. Some ranges overlap, and some only touch at one end, like [1, 4] and [4, 5]. Join every bunch of ranges that overlap or touch into one range.
Give back the joined ranges. None of them may overlap each other. Together they must cover exactly the same times as the input.
A story to remember it
Picture it
Five playdates, not in order. 1 to 3 and 2 to 6 overlap, and 8 to 10 and 9 to 12 overlap. 15 to 18 is alone.
Examples
A normal case
- In
[[1, 3], [2, 6], [8, 10], [15, 18]]- Out
[[1, 6], [8, 10], [15, 18]]
1 to 3 and 2 to 6 overlap, so they become 1 to 6. The others stand alone.
Ranges that only touch
- In
[[1, 4], [4, 5]]- Out
[[1, 5]]
They share the time 4, so they join. Use <= when you compare, not <.
One range swallows others
- In
[[1, 10], [2, 3], [4, 5]]- Out
[[1, 10]]
2 to 3 and 4 to 5 sit inside 1 to 10. The end must stay 10. If you set the end to the last range's end, you get 1 to 5, which is wrong.
Unsorted input
- In
[[2, 6], [1, 3], [8, 10], [9, 12], [15, 18]]- Out
[[1, 6], [8, 12], [15, 18]]
Sort first. Then neighbours in the sorted order are the only ones that can join.
One range
- In
[[5, 7]]- Out
[[5, 7]]
Nothing to join.
Way 1: the simple way
Keep a list of finished groups. Take the ranges one at a time. Compare the new range with every group. Each group it overlaps gets swallowed: the new range grows to cover it and the group is removed. Then the grown range goes into the list.
- For each range, set
startandendfrom it. - Walk every group in the list. If the group overlaps
[start, end], growstartandendto cover it, and remove the group. - Add
[start, end]to the list.
class Solution {
public int[][] merge(int[][] intervals) {
List<int[]> groups = new ArrayList<>();
for (int[] in : intervals) {
int start = in[0], end = in[1];
Iterator<int[]> it = groups.iterator();
while (it.hasNext()) {
int[] g = it.next();
if (g[0] <= end && start <= g[1]) { // they overlap or touch
start = Math.min(start, g[0]);
end = Math.max(end, g[1]);
it.remove(); // swallow that group
}
}
groups.add(new int[] { start, end });
}
return groups.toArray(new int[0][]);
}
}How long it takes: each new range is compared with every group so far, O(n^2). Extra room: the list of groups, O(n). Why does one walk over the groups catch everything? A range that grows can only reach a group that touches something it already swallowed, and the groups never touch each other.
Way 2: the clever way
Sort first, then one walk is enough.
- Sort the ranges by start time.
- Put the first range in the answer. It is the current block.
- For each next range: if its start is at most the current block's end, they overlap. Stretch the block's end to
max(block end, range end). - Otherwise the block is finished. Add this range to the answer as the new current block.
After sorting, a range can only overlap the block right before it. Any earlier block already ended before that block started. The thin line in the picture marks where the current block ends.
Sort, then sweep left to right
01 / 08
The notes arrive in a messy order.
- looking now
- done
- answer
- path
- just added
class Solution {
public int[][] merge(int[][] intervals) {
if (intervals.length == 0) return new int[0][];
Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0])); // by start time
List<int[]> out = new ArrayList<>();
int[] block = intervals[0].clone();
out.add(block);
for (int i = 1; i < intervals.length; i++) {
int[] in = intervals[i];
if (in[0] <= block[1]) {
block[1] = Math.max(block[1], in[1]); // overlaps: stretch the block
} else {
block = in.clone(); // a gap: start a new block
out.add(block);
}
}
return out.toArray(new int[0][]);
}
}How long it takes: sorting is O(n log n) and the walk is O(n), so O(n log n) in total. Extra room: the answer list, O(n), plus whatever the sort uses.
Watch out for
- Use
<=for the overlap test if touching ranges should join. Ask the interviewer; some versions treat[1, 4]and[4, 5]as separate. - Sort with
Integer.compare(a[0], b[0]), nota[0] - b[0]. Subtraction can overflow for very large or very negative values. - Forgetting the empty input. Return an empty array before reading
intervals[0].
What interviewers ask next
- Insert one new range into a sorted list of non-overlapping ranges. Copy the ranges that end before it, join every range that overlaps it, then copy the rest. That is
O(n)with no sort. - How many meeting rooms are needed? Sort the starts and the ends separately and sweep, or use a min heap of end times. The most meetings running at once is the answer.
- Remove the fewest ranges so none overlap. Sort by end time and greedily keep each range that starts after the last kept one ends.
- What if the ranges arrive one by one in a stream? Keep them in a sorted map (a
TreeMapkeyed by start) and join with the neighbours on each insert.
Remember this
Grown-up words: the sticky notes are intervals, joining them is merging overlapping intervals, the current block is the last interval in the output, and walking once after sorting is a sweep line.