Coding patternsArrays and strings
Intervals
The intervals coding pattern for interviews. Sort by start, merge overlaps, and sweep the line to count meeting rooms, with a step-through, templates in Java, Python, C#, Go and TypeScript, and 10 LeetCode problems.
- Read first
- Two pointers
An interval is a pair [start, end]: a meeting from 9 to 10, a booking from day 3 to day 7, a range
of numbers. Interval problems ask which ones overlap, how to join them, or how many are running at
the same time. With n intervals, comparing every pair is O(n²). Almost every interval problem gets
faster with the same first step: sort.
Once the intervals are sorted by start, an overlap can only happen between neighbors in the list. You walk the list once and keep a small amount of state: the block you are building, or a count of what is open. The sort costs O(n log n), and the walk costs O(n). That’s the whole pattern. The thinking is in choosing what to sort by and what to keep while you walk.
How to spot it
Section titled “How to spot it”- The input is a list of pairs called intervals, meetings, ranges, bookings or segments.
- “Merge”, “overlap”, “conflict”, “free time” or “cover”.
- “How many at the same time”, such as rooms, platforms, cars or CPUs. That’s a sweep line.
- “Remove the fewest” or “keep the most” intervals without overlaps. That’s a greedy choice, sorted by end.
- Points on a line that start and stop something, such as passengers getting on and off a bus.
If the intervals arrive one by one and you must answer after each, sorting once is not enough. Then you need a sorted structure, such as a tree map, or a heap.
Watch it run
Section titled “Watch it run”Merge overlapping intervals. Watch the open block on the right. Each new interval is compared with that one block only. When an interval starts after the block ends, the block is final. Every interval still to come starts even later, so none of them can reach back.
- intervals.sort(key=start)
- merged = []
- for start, end in intervals:
- if merged and start <= merged[-1].end:
- merged[-1].end = max(merged[-1].end, end)
- else:
- merged.append([start, end]) # a gap
- return merged
Merge every group of overlapping intervals. In this order, an overlap can hide anywhere in the list.
Notice [5, 7]. It is compared with the block [1, 6], not with [2, 6] alone. It overlaps
the block, so the block grows to [1, 7]. Also notice max in the update. An interval
can sit fully inside the block, like [2, 3] inside [1, 10]. Then the end must not shrink.
The template
Section titled “The template”Merge intervals, and the fewest meeting rooms with a sweep over starts and ends:
import java.util.ArrayList;import java.util.Arrays;import java.util.List;
public class Intervals { static int[][] merge(int[][] intervals) { // Merge overlapping intervals. After sorting by start, an interval can only overlap the // last merged one: every earlier merged interval ends before that one starts. // O(n log n) for the sort, with n intervals. int[][] sorted = intervals.clone(); Arrays.sort(sorted, (first, second) -> Integer.compare(first[0], second[0])); List<int[]> merged = new ArrayList<>(); for (int[] interval : sorted) { int[] last = merged.isEmpty() ? null : merged.get(merged.size() - 1); if (last != null && interval[0] <= last[1]) { // overlaps (or touches) the last one last[1] = Math.max(last[1], interval[1]); // max: the new one may sit fully inside } else { merged.add(new int[] {interval[0], interval[1]}); } } return merged.toArray(new int[0][]); }
static int minMeetingRooms(int[][] intervals) { // Fewest rooms so that no two meetings share one: the most meetings running at the same // time. Sweep the starts and the ends in time order and count how many are open. // O(n log n). int count = intervals.length; int[] starts = new int[count], ends = new int[count]; for (int i = 0; i < count; i++) { starts[i] = intervals[i][0]; ends[i] = intervals[i][1]; } Arrays.sort(starts); Arrays.sort(ends); int rooms = 0, best = 0, endIndex = 0; for (int start : starts) { // A meeting that ends at or before this start frees its room first. Which meeting ends // doesn't matter, only how many are running. Every meeting has start < end, so any end // counted here belongs to a meeting already started, and endIndex stays in range. while (ends[endIndex] <= start) { rooms--; endIndex++; } rooms++; best = Math.max(best, rooms); } return best; }
public static void main(String[] args) { int[][] in = {{8, 10}, {1, 3}, {15, 18}, {2, 6}, {9, 12}, {5, 7}}; check(Arrays.deepEquals(merge(in), new int[][] {{1, 7}, {8, 12}, {15, 18}})); check(Arrays.deepEquals(merge(new int[][] {{1, 4}, {4, 5}}), new int[][] {{1, 5}})); check(Arrays.deepEquals(merge(new int[][] {{1, 10}, {2, 3}}), new int[][] {{1, 10}})); check(merge(new int[0][]).length == 0); check(minMeetingRooms(new int[][] {{0, 30}, {5, 10}, {15, 20}}) == 2); check(minMeetingRooms(new int[][] {{7, 10}, {2, 4}}) == 1); check(minMeetingRooms(new int[][] {{1, 5}, {2, 6}, {3, 7}, {5, 8}}) == 3); check(minMeetingRooms(new int[0][]) == 0); System.out.println("ok: merge and minMeetingRooms"); }
static void check(boolean ok) { if (!ok) throw new AssertionError(); }}# Merge overlapping intervals. After sorting by start, an interval can only overlap the last merged# one: every earlier merged interval ends before that one starts. O(n log n) for the sort, with n# intervals.def merge(intervals: list[list[int]]) -> list[list[int]]: merged: list[list[int]] = [] for start, end in sorted(intervals, key=lambda interval: interval[0]): if merged and start <= merged[-1][1]: # overlaps (or touches) the last merged interval merged[-1][1] = max(merged[-1][1], end) # max: the new one may sit fully inside else: merged.append([start, end]) return merged
# Fewest rooms so that no two meetings share one: the most meetings running at the same time.# Sweep the starts and the ends in time order and count how many are open. O(n log n).def min_meeting_rooms(intervals: list[list[int]]) -> int: starts = sorted(interval[0] for interval in intervals) ends = sorted(interval[1] for interval in intervals) rooms = best = 0 end_index = 0 for start in starts: # A meeting that ends at or before this start frees its room first. Which meeting # ends doesn't matter, only how many are running. Every meeting has start < end, so # any end counted here belongs to a meeting already started, and end_index stays in range. while ends[end_index] <= start: rooms -= 1 end_index += 1 rooms += 1 best = max(best, rooms) return best
assert merge([[8, 10], [1, 3], [15, 18], [2, 6], [9, 12], [5, 7]]) == [[1, 7], [8, 12], [15, 18]]assert merge([[1, 4], [4, 5]]) == [[1, 5]]assert merge([[1, 10], [2, 3]]) == [[1, 10]]assert merge([]) == []assert min_meeting_rooms([[0, 30], [5, 10], [15, 20]]) == 2assert min_meeting_rooms([[7, 10], [2, 4]]) == 1assert min_meeting_rooms([[1, 5], [2, 6], [3, 7], [5, 8]]) == 3assert min_meeting_rooms([]) == 0print("ok: merge and min_meeting_rooms")using System;using System.Collections.Generic;using System.Linq;
static class Intervals{ public static int[][] Merge(int[][] intervals) { // Merge overlapping intervals. After sorting by start, an interval can only overlap the // last merged one: every earlier merged interval ends before that one starts. // O(n log n) for the sort, with n intervals. var sorted = intervals.OrderBy(interval => interval[0]); var output = new List<int[]>(); foreach (var interval in sorted) { // Overlaps (or touches) the last one? if (output.Count > 0 && interval[0] <= output[^1][1]) { // max: the new one may sit inside output[^1][1] = Math.Max(output[^1][1], interval[1]); } else { output.Add([interval[0], interval[1]]); } } return output.ToArray(); }
public static int MinMeetingRooms(int[][] intervals) { // Fewest rooms so that no two meetings share one: the most meetings running at the same // time. Sweep the starts and the ends in time order and count how many are open. // O(n log n). int[] starts = intervals.Select(interval => interval[0]).Order().ToArray(); int[] ends = intervals.Select(interval => interval[1]).Order().ToArray(); int rooms = 0, best = 0, endIndex = 0; foreach (int start in starts) { // A meeting that ends at or before this start frees its room first. Which meeting ends // doesn't matter, only how many are running. Every meeting has start < end, so any end // counted here belongs to a meeting already started, and endIndex stays in range. while (ends[endIndex] <= start) { rooms--; endIndex++; } rooms++; best = Math.Max(best, rooms); } return best; }
static void Main() { int[][] input = [[8, 10], [1, 3], [15, 18], [2, 6], [9, 12], [5, 7]]; Check(Same(Merge(input), [[1, 7], [8, 12], [15, 18]])); Check(Same(Merge([[1, 4], [4, 5]]), [[1, 5]])); Check(Same(Merge([[1, 10], [2, 3]]), [[1, 10]])); Check(Merge([]).Length == 0); Check(MinMeetingRooms([[0, 30], [5, 10], [15, 20]]) == 2); Check(MinMeetingRooms([[7, 10], [2, 4]]) == 1); Check(MinMeetingRooms([[1, 5], [2, 6], [3, 7], [5, 8]]) == 3); Check(MinMeetingRooms([]) == 0); Console.WriteLine("ok: Merge and MinMeetingRooms"); }
static bool Same(int[][] first, int[][] second) => first.Length == second.Length && first.Zip(second).All(pair => pair.First.SequenceEqual(pair.Second));
static void Check(bool ok) { if (!ok) throw new InvalidOperationException("check failed"); }}package main
import ( "fmt" "reflect" "slices")
// Merge overlapping intervals. After sorting by start, an interval can only overlap the last merged// one: every earlier merged interval ends before that one starts. O(n log n) for the sort, with n// intervals.func merge(intervals [][]int) [][]int { sorted := slices.Clone(intervals) slices.SortFunc(sorted, func(first, second []int) int { return first[0] - second[0] }) merged := [][]int{} for _, interval := range sorted { // Overlaps (or touches) the last one? if len(merged) > 0 && interval[0] <= merged[len(merged)-1][1] { last := merged[len(merged)-1] last[1] = max(last[1], interval[1]) // max: the new one may sit fully inside } else { merged = append(merged, []int{interval[0], interval[1]}) } } return merged}
// Fewest rooms so that no two meetings share one: the most meetings running at the same time.// Sweep the starts and the ends in time order and count how many are open. O(n log n).func minMeetingRooms(intervals [][]int) int { starts := make([]int, len(intervals)) ends := make([]int, len(intervals)) for i, interval := range intervals { starts[i], ends[i] = interval[0], interval[1] } slices.Sort(starts) slices.Sort(ends) rooms, best, endIndex := 0, 0, 0 for _, start := range starts { // A meeting that ends at or before this start frees its room first. Which meeting // ends doesn't matter, only how many are running. Every meeting has start < end, so // any end counted here belongs to a meeting already started, and endIndex stays in range. for ends[endIndex] <= start { rooms-- endIndex++ } rooms++ best = max(best, rooms) } return best}
func main() { in := [][]int{{8, 10}, {1, 3}, {15, 18}, {2, 6}, {9, 12}, {5, 7}} check(reflect.DeepEqual(merge(in), [][]int{{1, 7}, {8, 12}, {15, 18}})) check(reflect.DeepEqual(merge([][]int{{1, 4}, {4, 5}}), [][]int{{1, 5}})) check(reflect.DeepEqual(merge([][]int{{1, 10}, {2, 3}}), [][]int{{1, 10}})) check(len(merge(nil)) == 0) check(minMeetingRooms([][]int{{0, 30}, {5, 10}, {15, 20}}) == 2) check(minMeetingRooms([][]int{{7, 10}, {2, 4}}) == 1) check(minMeetingRooms([][]int{{1, 5}, {2, 6}, {3, 7}, {5, 8}}) == 3) check(minMeetingRooms(nil) == 0) fmt.Println("ok: merge and minMeetingRooms")}
func check(ok bool) { if !ok { panic("check failed") }}function merge(intervals: number[][]): number[][] { // Merge overlapping intervals. After sorting by start, an interval can only overlap the last // merged one: every earlier merged interval ends before that one starts. O(n log n) for the // sort, with n intervals. const sorted = [...intervals].sort((first, second) => first[0] - second[0]); const merged: number[][] = []; for (const [start, end] of sorted) { const last = merged[merged.length - 1]; if (last && start <= last[1]) { // overlaps (or touches) the last merged interval last[1] = Math.max(last[1], end); // max: the new one may sit fully inside } else { merged.push([start, end]); } } return merged;}
function minMeetingRooms(intervals: number[][]): number { // Fewest rooms so that no two meetings share one: the most meetings running at the same time. // Sweep the starts and the ends in time order and count how many are open. O(n log n). const starts = intervals.map((interval) => interval[0]).sort((first, second) => first - second); const ends = intervals.map((interval) => interval[1]).sort((first, second) => first - second); let rooms = 0; let best = 0; let endIndex = 0; for (const start of starts) { // A meeting that ends at or before this start frees its room first. Which meeting ends // doesn't matter, only how many are running. Every meeting has start < end, so any end // counted here belongs to a meeting already started, and endIndex stays in range. while (ends[endIndex] <= start) { rooms--; endIndex++; } rooms++; best = Math.max(best, rooms); } return best;}
const check = (ok: boolean) => { if (!ok) throw new Error("check failed");};const same = (first: number[][], second: number[][]) => JSON.stringify(first) === JSON.stringify(second);const meetings = [[8, 10], [1, 3], [15, 18], [2, 6], [9, 12], [5, 7]];check(same(merge(meetings), [[1, 7], [8, 12], [15, 18]]));check(same(merge([[1, 4], [4, 5]]), [[1, 5]]));check(same(merge([[1, 10], [2, 3]]), [[1, 10]]));check(merge([]).length === 0);check(minMeetingRooms([[0, 30], [5, 10], [15, 20]]) === 2);check(minMeetingRooms([[7, 10], [2, 4]]) === 1);check(minMeetingRooms([[1, 5], [2, 6], [3, 7], [5, 8]]) === 3);check(minMeetingRooms([]) === 0);console.log("ok: merge and minMeetingRooms");The meeting rooms sweep uses two sorted lists, one of starts and one of ends. It doesn’t need to know which end belongs to which meeting. It only needs to know how many meetings have ended before the next one starts. When an end and a start are at the same time, the end goes first. The room is free at 10 for a meeting that ends at 10.
Variations
Section titled “Variations”Insert an interval
Section titled “Insert an interval”The list is already sorted and has no overlaps. A new interval arrives. Walk the list in three parts. First copy every interval that ends before the new one starts. Then merge every interval that overlaps the new one, by taking the smallest start and the largest end. Then copy the rest. That’s O(n), with no sort.
Keep the most, remove the fewest
Section titled “Keep the most, remove the fewest”Non-overlapping Intervals asks for the fewest removals so the rest don’t overlap. Sort by end, not by start. Walk the list and keep an interval if it starts at or after the end of the last one you kept. Otherwise remove it. Keeping the interval that ends first is always safe. Any answer that keeps a different one can swap it for this one, and nothing else gets worse. That argument is explained on the greedy page. Minimum Number of Arrows is the same problem in a different story.
Sweep line with events
Section titled “Sweep line with events”Turn each interval into two events: (start, +1) and (end, -1). Sort the events by time. Put
-1 before +1 at the same time if touching doesn’t count as overlap. Then walk the events and
keep a running sum. The highest sum is the most intervals open at once. This works for any “how
many at once” question. Car Pooling is the same idea, with +passengers and -passengers.
Meeting rooms with a heap
Section titled “Meeting rooms with a heap”Sort meetings by start. Keep a min-heap of end times, one per room in use. For each meeting, if the earliest end is at or before its start, that room is free, so pop it. Then push this meeting’s end. The heap’s largest size is the answer. It’s the same O(n log n), and it also tells you which room each meeting uses.
Two sorted lists
Section titled “Two sorted lists”Interval List Intersections gives two sorted lists. Use two pointers.
The overlap of two intervals is [max(starts), min(ends)], if that is not empty. Then move the
pointer whose interval ends first. It can’t overlap anything else in the other list.
Complexity
Section titled “Complexity”| Time | Extra memory | |
|---|---|---|
| Merge intervals | O(n log n) | O(n) for output |
| Insert into a sorted list | O(n) | O(n) for output |
| Most non-overlapping (by end) | O(n log n) | O(1) beyond sort |
| Meeting rooms, sweep or heap | O(n log n) | O(n) |
| Check whether any two overlap | O(n log n) | O(1) beyond sort |
I’ll sort by start. Then an interval can only overlap the block I’m building, because every earlier block ends before that one starts. So one pass is enough. It’s O(n log n) for the sort and O(n) for the walk.
Interview questions
Section titled “Interview questions”Interviewer asksWhy compare only with the last merged interval and not with all of them?
The list is sorted by start. Every block before the last one ended before the last block began. The current interval starts at or after the last block’s start. So it starts after every earlier block ended, and can’t overlap any of them. Only the last block is still open.
Interviewer asksFor the most non-overlapping intervals, why sort by end and not by start?
Sorting by start would keep [1, 100] first, and it blocks everything after it. Sorting by end
keeps the interval that frees the line the soonest. Any best answer can swap its first interval
for the earliest-ending one and stay valid. Repeat that argument, and the greedy choice is always
as good as the best answer.
Interviewer asksThe intervals arrive one at a time, and you must answer after each. What changes?
Sorting once no longer works. Keep the merged blocks in a sorted map from start to end. For a new interval, find the block just before it and the blocks after it that it overlaps, merge them and remove the old ones. Each insert is O(log n) plus the blocks it removes. Every block is removed once at most.
Practice
Section titled “Practice”10 problems on LeetCode: 2 easy, 7 medium, 1 hard
- Summary Ranges(opens LeetCode in a new tab)Easy
A run of consecutive numbers is one interval; a gap closes it.
- Meeting Rooms(opens LeetCode in a new tab)Easy
After sorting by start, only neighbors can clash.
- Merge Intervals(opens LeetCode in a new tab)Medium
Sort by start; each interval either extends the last block or opens a new one.
- Insert Interval(opens LeetCode in a new tab)Medium
Three parts: intervals before the new one, those that overlap it, and those after.
- Non-overlapping Intervals(opens LeetCode in a new tab)Medium
Sort by end and keep the interval that frees up the line soonest.
- Minimum Number of Arrows to Burst Balloons(opens LeetCode in a new tab)Medium
Shoot at the earliest end; it hits every balloon that has started by then.
- Interval List Intersections(opens LeetCode in a new tab)Medium
Two pointers: the overlap is max of the starts to min of the ends.
- Meeting Rooms II(opens LeetCode in a new tab)Medium
Count how many meetings are open at once with a sweep over starts and ends.
- Car Pooling(opens LeetCode in a new tab)Medium
Riders get on and off at points on a line: sweep and keep a running count.
- Minimum Interval to Include Each Query(opens LeetCode in a new tab)Hard
Sort queries too, and keep the open intervals in a heap by size.
Ticks are saved in this browser only. Clear your site data and they're gone.
Five of these are solved step by step on this site, as part of the Blind 75: Merge Intervals, Insert Interval, Non-overlapping Intervals, Meeting Rooms and Meeting Rooms II.
- Use it when the input is a list of
[start, end]pairs and the question is about overlap, merging or how many run at once. - Sort by start to merge or insert. Sort by end to keep the most.
- Sweep line: open and close events, a running count, and the highest count wins.
- Cost: O(n log n) for the sort, then one O(n) pass.
- Watch for: whether touching counts as overlap, and
maxwhen one interval sits inside another.
The designbeyondcode digest, every Saturday
New deep dives, interview tips and site news in one short email. Free. Leave with one click.
We never sell or share your email. We only use it to send this newsletter.