Patterns/Merge Intervals

Merge Intervals

Top interview pattern
75 min read
Updated June 2026
What you'll learn
  • Apply the sort-then-scan template to merge overlapping intervals in one pass, handling both partial overlap and full containment correctly
  • Select the right sort key based on the problem goal: start time for merging, end time for activity selection, event time for sweep-line counting
  • Count concurrent intervals using a sweep line with proper tie-breaking for meeting-room and maximum-overlap problems
  • Intersect two sorted interval lists using a two-pointer approach that advances past fully-consumed intervals without redundant comparisons

Most interval problems cannot be solved efficiently by checking every pair. The search space is too large and overlapping relationships are scattered across the input. Sorting by start time makes the scan tractable: the algorithm processes intervals left to right and only compares each new interval against the last merged interval, which tracks the farthest end reached by the current overlap group. If an earlier raw interval could have overlapped the current one, its coverage is already absorbed into that last merged interval. One comparison is sufficient. This is what makes a linear scan after sorting correct for five structurally different interval operations: merge, insert, intersect, count concurrency, and select non-overlapping.

Merge Intervals

Step 1/3
Sort by start time
Sort all intervals by their start value. This lets us sweep left-to-right.
intervals.sort((a, b) => a[0] - b[0])
02468101214161820[8,10]1[1,3]2[15,18]3[2,6]4[9,12]5sort bystart
Time
O(n log n)
Space
O(n)
Core Op
Sort + Sweep

What Are Interval Problems?

Five distinct operations appear across interval problems. Each one has a different goal, a different sort order, and a different scan invariant:

  • Merge: Combine overlapping intervals into one (Merge Intervals)
  • Insert: Add a new interval and re-merge (Insert Interval)
  • Intersect: Find where two sets of intervals overlap (Interval List Intersection)
  • Count: Track how many intervals are active at each point (Meeting Rooms)
  • Select: Choose maximum non-overlapping intervals (Non-Overlapping Intervals)

Two intervals overlap when neither ends before the other starts. When intervals are sorted by start time and processed into a merged result, this simplifies to checking whether the current interval's start falls before the end of the last merged interval. That single comparison drives almost every interval algorithm.

Key Insight
Sort Enables Local Comparison
Sorting by start time ensures intervals are processed in order of where they begin. The algorithm maintains a last merged interval that tracks the farthest end of the current overlap group. When the current interval's start falls before that end, it belongs to the same group and extends it. When it does not, no earlier raw interval can overlap the current one either, because the last merged interval has already absorbed the farthest reach of everything processed so far. That is why one comparison against the last merged interval is sufficient.

Building the Intuition

Attempt 1: Check All Pairs

The brute force approach compares every pair of intervals to find overlaps. For n intervals, that's O(n²) comparisons per pass. But merging two intervals can create new overlaps, so the outer while-loop may restart up to O(n) times, making this O(n³) worst case.

Brute Force: O(n³) worst case
1def merge_brute_force(intervals):
2 """Check all pairs - correct but slow."""
3 merged = True
4 while merged:
5 merged = False
6 for i in range(len(intervals)):
7 for j in range(i + 1, len(intervals)):
8 if intervals[i][0] <= intervals[j][1] and intervals[j][0] <= intervals[i][1]:
9 intervals[i] = [min(intervals[i][0], intervals[j][0]),
10 max(intervals[i][1], intervals[j][1])]
11 intervals.pop(j)
12 merged = True
13 break
14 if merged:
15 break
16 return intervals

Attempt 2: What If We Sorted?

Consider these unsorted intervals: [[8,10], [1,3], [15,18], [2,6], [9,12]]. The overlapping pairs are scattered: [1,3] overlaps [2,6], and [8,10] overlaps [9,12]. Finding them requires checking every pair.

Now sort by start time: [[1,3], [2,6], [8,10], [9,12], [15,18]]. Suddenly every overlap is between adjacent intervals. No scattered pairs to hunt for. One left-to-right scan finds them all.

The Sort-Then-Scan Template

Interval Problem Template
1def solve_interval_problem(intervals):
2 # Step 1: Sort (by start, by end, or both; problem-specific)
3 intervals.sort(key=lambda x: x[0]) # Usually by start
4
5 # Step 2: Sweep left to right with an invariant
6 result = []
7 for interval in intervals:
8 if should_merge(interval, result):
9 merge(interval, result)
10 else:
11 result.append(interval)
12
13 return result
Common Mistake
Omitting Sort Is the Most Common Bug
Pre-sorted test cases may obscure the need for an explicit sort step. Your code passes the sample input, then fails on real input where overlaps are scattered. Always sort explicitly, even if the input "looks" sorted.
ProblemSort ByWhy
Merge IntervalsStart timeGroups overlapping intervals together
Insert IntervalAlready sortedThree contiguous phases emerge
Non-Overlapping IntervalsEnd timeEarliest end leaves most room (activity selection)
Meeting Rooms IIEvents by timeCounts concurrent meetings by processing start/end events

Why start time for some problems and end time for others? Merging needs intervals ordered by where they begin, while selection problems need them ordered by where they end (to leave maximum room). Sort by Start vs End covers the full reasoning.

Interview Tip
Always mention sorting first
The first step for any interval problem is determining the sort order. The sort order (start vs end) dictates the sweep invariant for the rest of the algorithm.

The Three Interval Relationships

Every pair of intervals has exactly one of three relationships. Understanding these is the foundation of all interval problems.

Three Interval Relationships

0123456789101112[1, 5][3, 8]

Every pair of intervals has one of three relationships. Understanding these is the foundation of all interval problems.

The relationship depends on how the start of one interval compares to the end of another.

RelationshipCondition (sorted by start)Visual
Overlapcurrent[0] < last[1]Bars share a region
Adjacentcurrent[0] == last[1]Bars touch at a single point
Disjointcurrent[0] > last[1]Gap between bars

Deriving the Overlap Formula

Instead of memorizing when intervals overlap, derive it from when they don't. Two intervals [a,b] and [c,d] are disjoint when one ends before the other starts:

Deriving the Overlap Condition
1# Two intervals [a,b] and [c,d] do NOT overlap when:
2# b <= c (first ends before second starts)
3# OR d <= a (second ends before first starts)
4
5# Flip the condition. They overlap when:
6# NOT (b <= c OR d <= a)
7# = b > c AND d > a
8# = a < d AND c < b
9
10# When sorted by start (a <= c guaranteed), simplifies to:
11# c < b → current[0] < last[1]

The full formula a < d AND c < b works for any two intervals regardless of order. But when the intervals are sorted by start time, we know a <= c, so the first condition is automatically satisfied (as long as intervals have positive length). Only the second condition needs checking: current[0] < last[1].

Common Mistake
<= vs < changes the answer
Merge Intervals uses <= (merge adjacent intervals). Meeting Rooms uses < (a meeting ending at t=10 doesn't conflict with one starting at t=10). This single character changes the entire solution. Always clarify which semantics the problem uses.

Merge Intervals

Given a collection of intervals, merge all overlapping intervals. This is the pattern's namesake problem.

Why sort by start? Sorting by start time groups overlapping intervals together. If [1,3] and [2,6] overlap, sorting ensures [1,3] comes first, so when we reach [2,6] we see it overlaps with the last merged interval and extend it. Without sorting, [2,6] might appear before [1,3] and we'd miss the merge.

Merge Intervals Walkthrough

02468101214161820INPUT[1,3][2,6][8,10][9,12][15,18]MERGED

Input: [[1,3],[2,6],[8,10],[9,12],[15,18]]. Already sorted by start time.

Step 1 of every merge problem: sort by start. Here, input is already sorted.

Merge Intervals: O(n log n)
1def merge(intervals):
2 intervals.sort(key=lambda x: x[0])
3 merged = [intervals[0]]
4
5 for current in intervals[1:]:
6 last = merged[-1]
7 if current[0] <= last[1]: # Overlap or adjacent
8 # KEY: use max, not assignment
9 merged[-1] = [last[0], max(last[1], current[1])]
10 else:
11 merged.append(current)
12
13 return merged
14
15# [[1,3],[2,6],[8,10],[9,12],[15,18]]
16# → [[1,6],[8,12],[15,18]]

Two details matter here:

  1. Sort by start time so overlapping intervals end up next to each other.
  2. Use max(last[1], current[1]) to merge. Direct assignment (last[1] = current[1]) fails for contained intervals like [1,10] and [2,5].
Common Mistake
The contained interval bug
If merged[-1] = [1,10] and current = [2,5], direct assignment gives [1,5], shrinking the interval. max(10, 5) = 10 correctly keeps [1,10]. This is the most common bug in merge implementations.
Interview Tip
Write the max() call first
Write max(last[1], current[1]) and verify it handles containment. The max() call is the difference between correct merging and the most common bug in this problem.

Insert Interval

Given a sorted, non-overlapping list of intervals and a new interval, insert it and merge if necessary. The input is already sorted, so no sorting needed. This problem is O(n).

Insert Interval: Three Phases

024681012141618[1,2][3,5][6,7][8,10][12,16]new [4,8]

Insert [4,8] into [[1,2],[3,5],[6,7],[8,10],[12,16]]. Three phases, one pass.

The input is already sorted. The algorithm splits into three contiguous regions.

The algorithm splits into three contiguous phases:

Insert Interval: O(n)
1def insert(intervals, new_interval):
2 result = []
3 i = 0
4 n = len(intervals)
5
6 # Phase 1: Add all intervals that end before new_interval starts
7 while i < n and intervals[i][1] < new_interval[0]:
8 result.append(intervals[i])
9 i += 1
10
11 # Phase 2: Merge all overlapping intervals with new_interval
12 while i < n and intervals[i][0] <= new_interval[1]:
13 new_interval = [
14 min(new_interval[0], intervals[i][0]),
15 max(new_interval[1], intervals[i][1])
16 ]
17 i += 1
18 result.append(new_interval)
19
20 # Phase 3: Add all remaining intervals
21 while i < n:
22 result.append(intervals[i])
23 i += 1
24
25 return result

Why min/max in Phase 2? The new interval might start before the current interval (use min to capture the earlier start) and might end after it (use max to capture the later end). Both are needed because the new interval can extend in either direction relative to the intervals it overlaps.

Common Mistake
Off-by-one in phase boundaries
Phase 1 uses strict < (intervals[i][1] < new_interval[0]) while Phase 2 uses <= (intervals[i][0] <= new_interval[1]). Swapping these operators breaks the algorithm: you'll either miss a merge or include a non-overlapping interval.
Key Insight
Three phases because sorted input
Because the input is sorted, intervals naturally partition into three contiguous groups relative to new_interval: all-before, overlapping, all-after. That's why it runs in O(n) without re-sorting.

Insert Interval runs in O(n) because the input is already sorted. No sorting step needed. A single linear scan walks through three contiguous phases (before, merge, after), touching each interval exactly once.

Interval List Intersection

Given two sorted lists of intervals, find all intersections. This is the two-pointer variation of interval problems.

Interval Intersection: Two Pointers

02468101214161820222426A[0,2]i[5,10][13,23]B[1,5]j[8,12][15,24]

Two sorted interval lists. Find all intersections using two pointers i and j.

Both lists are sorted by start time. We scan both simultaneously.

Interval List Intersection: O(m + n)
1def interval_intersection(first_list, second_list):
2 result = []
3 i, j = 0, 0
4
5 while i < len(first_list) and j < len(second_list):
6 a_start, a_end = first_list[i]
7 b_start, b_end = second_list[j]
8
9 # Intersection exists when max(starts) <= min(ends)
10 lo = max(a_start, b_start)
11 hi = min(a_end, b_end)
12
13 if lo <= hi:
14 result.append([lo, hi])
15
16 # Advance the pointer whose interval ends first
17 if a_end < b_end:
18 i += 1
19 else:
20 j += 1
21
22 return result

The intersection formula: The intersection of [a,b] and [c,d] is [max(a,c), min(b,d)], and it exists only when max(a,c) <= min(b,d). This formula generalizes to rectangle and box intersection in higher dimensions.

Advance the pointer whose interval ends first. That interval can't intersect with anything later in the other list. The other interval might still overlap with the next entry in the first list.

Interview Tip
State the formula upfront
The intersection of [a,b] and [c,d] is [max(a,c), min(b,d)], valid when max(a,c) <= min(b,d). This formula generalizes to any rectangle/box intersection in higher dimensions.

Meeting Rooms: Counting Concurrency

Meeting Rooms II asks: what is the minimum number of conference rooms needed to hold all meetings? This introduces the sweep line technique, which works differently from merge-and-scan.

Meeting Rooms: Sweep Line

051015202530[0,30][5,10][15,20]

Meetings: [0,30], [5,10], [15,20]. How many rooms do we need?

The sweep line approach: decompose into events, sort, scan.

Merging intervals won't work here because it loses overlap count. Merging [0,30] and [5,10] produces [0,30], which looks like one room. But both meetings run simultaneously from time 5 to 10, so you need two rooms. The sweep line tracks when each meeting starts and ends independently, keeping concurrency visible.

The sweep line decomposes each interval into two point events, then scans them in order:

Meeting Rooms II: Sweep Line O(n log n)
1def min_meeting_rooms(intervals):
2 events = []
3 for start, end in intervals:
4 events.append((start, 1)) # +1 at start
5 events.append((end, -1)) # -1 at end
6
7 # Sort by time. Tie-break: end (-1) before start (+1)
8 events.sort(key=lambda x: (x[0], x[1]))
9
10 max_rooms = 0
11 current = 0
12 for _, delta in events:
13 current += delta
14 max_rooms = max(max_rooms, current)
15
16 return max_rooms

Tie-breaking matters. When a meeting ends and another starts at the same time, the ending event must be processed first. Otherwise you'd count an extra room that isn't needed. The sort key (time, delta) achieves this because -1 < +1.

Alternative: Min-Heap Approach

Sort meetings by start time. Maintain a min-heap of end times, where each entry represents a room and when it becomes free. For each meeting, compare its start time against the heap's minimum (the earliest ending meeting currently in progress). If that earliest meeting ends before this one starts, pop it off the heap. That room is now freed. Then push the new meeting's end time onto the heap. If the earliest meeting hasn't ended yet, the new meeting needs its own room, so just push without popping. The heap size at any point is the number of rooms currently in use.

Meeting Rooms II: Min-Heap O(n log n)
1import heapq
2
3def min_meeting_rooms_heap(intervals):
4 if not intervals:
5 return 0
6
7 intervals.sort(key=lambda x: x[0]) # Sort by start
8
9 # Min-heap of end times (each entry = one room's current end)
10 rooms = [intervals[0][1]]
11
12 for start, end in intervals[1:]:
13 if start >= rooms[0]:
14 # Room is free: reuse it (update end time)
15 heapq.heapreplace(rooms, end)
16 else:
17 # No room free: allocate new room
18 heapq.heappush(rooms, end)
19
20 return len(rooms)
ApproachIntuitionWhen to Use
Sweep LineDecompose into events, count with +1/-1When you only need the count (simpler code)
Min-HeapTrack actual room state with end timesWhen you need to know which room each meeting uses
Interview Tip
Know both approaches
The heap approach tracks actual room state: each heap entry represents a specific room with its current end time. This lets you answer "which room does meeting X go in?" directly, which sweep line cannot. Use sweep line when you only need the count; use the heap when you need room assignments.
Common Mistake
Meeting Rooms I vs II
Meeting Rooms I only asks "can one person attend all meetings?" Sort by start time and check if any consecutive pair overlaps. Sweep line is overkill here. Meeting Rooms II asks for the minimum number of rooms, and that's where sweep line applies.

Non-Overlapping Intervals

Given a collection of intervals, find the minimum number of intervals to remove so that the rest are non-overlapping. This is activity selection in disguise: maximizing kept = minimizing removed.

Non-Overlapping Intervals: Sort by End

0123456[1,2][2,3][3,4][1,3]

Intervals: [[1,2],[2,3],[3,4],[1,3]]. Find minimum removals for no overlaps.

This is activity selection in disguise: maximize kept = minimize removed.

Non-Overlapping Intervals: O(n log n)
1def erase_overlap_intervals(intervals):
2 # Sort by END time (critical for correctness)
3 intervals.sort(key=lambda x: x[1])
4
5 kept = 0
6 last_end = float('-inf')
7
8 for start, end in intervals:
9 if start >= last_end: # No overlap
10 kept += 1
11 last_end = end
12 # else: skip (remove) this interval
13
14 return len(intervals) - kept

Why sort by end, not start? This is the same exchange argument from activity selection. An interval that ends earlier is compatible with everything a later-ending one is compatible with, plus potentially more. Sorting by start time can greedily keep an interval that blocks multiple future ones.

Concrete Failure of Sort-by-Start

Sort-by-start fails on this input
1intervals = [[1, 10], [2, 3], [4, 5], [6, 7]]
2
3# Sort by start: [[1,10], [2,3], [4,5], [6,7]]
4# Greedy keeps [1,10], blocks everything else
5# Result: 1 interval kept, 3 removed
6
7# Sort by end: [[2,3], [4,5], [6,7], [1,10]]
8# Greedy keeps [2,3], then [4,5], then [6,7]
9# Skips [1,10] because it overlaps with [2,3]
10# Result: 3 intervals kept, 1 removed <-- OPTIMAL

Sorting by start greedily picked [1,10] because it started earliest. But [1,10] is a "bully" interval that blocks three smaller ones. Sorting by end avoids this: [1,10] ends last, so it's considered last and correctly rejected.

Interview Tip
Frame it as activity selection
This is structurally identical to the activity selection problem: maximize non-overlapping intervals, then subtract from total to get removals. Sorting by end time guarantees greedy optimality.

Advanced Variations

Remove Covered Intervals

An interval [a,b] is covered by [c,d] if c <= a and b <= d. Given a list of intervals, return the number of remaining intervals after removing all covered ones.

The key insight is the sort order: sort by start ASC, and for ties, by end DESC. This ensures that when two intervals share the same start, the longer one comes first and establishes itself as the "covering" interval.

Remove Covered Intervals: O(n log n)
1def remove_covered_intervals(intervals):
2 # Sort by start ASC, then end DESC for ties
3 intervals.sort(key=lambda x: (x[0], -x[1]))
4
5 count = 0
6 max_end = 0
7
8 for _, end in intervals:
9 if end > max_end:
10 count += 1 # Not covered
11 max_end = end
12 # else: covered by a previous interval (skip)
13
14 return count

The invariant: after processing each interval, max_end equals the furthest-reaching end of any non-covered interval seen so far. An interval [start, end] is covered iff end <= max_end. The sort ensures that for any two intervals with the same start, the longer one (larger end) comes first and establishes max_end before the shorter one is checked.

Common Mistake
Wrong tie-break direction
If you sort ties by end ASC instead of DESC, shorter intervals come first and aren't recognized as covered. For example, with [[1,4],[1,2]], sorting by end ASC gives [[1,2],[1,4]]. The algorithm counts [1,2] as not covered, then [1,4] as not covered. But [1,2] is covered by [1,4]. Descending end sort fixes this: [[1,4],[1,2]] correctly identifies [1,2] as covered.

Minimum Number of Arrows to Burst Balloons

Each balloon is an interval on the x-axis. An arrow shot at position x bursts all balloons where start <= x <= end. Find the minimum number of arrows to burst all balloons.

This is activity selection in disguise: min arrows = number of non-overlapping groups. Each arrow handles one group of overlapping balloons. The code is structurally identical to Non-Overlapping Intervals.

Minimum Arrows: O(n log n)
1def find_min_arrow_shots(points):
2 if not points:
3 return 0
4
5 # Sort by end (same as activity selection)
6 points.sort(key=lambda x: x[1])
7
8 arrows = 1
9 arrow_pos = points[0][1] # Shoot at end of first balloon
10
11 for start, end in points[1:]:
12 if start > arrow_pos:
13 # New group: need another arrow
14 arrows += 1
15 arrow_pos = end
16
17 return arrows
Key Insight
Merge, Insert, Intersection: One Template
Remove Covered, Non-Overlapping, and Min Arrows are all the same skeleton: sort, scan with a running boundary, count transitions. The differences are only in what you're counting (covered vs overlapping vs groups) and the sort order (start+end vs end-only).

Sort by Start vs End: When It Matters

Sorting by start vs end is the most common source of bugs in interval problems. The wrong choice produces code that passes some test cases but fails on others.

Sort ByUse WhenProblems
Start timeYou need to process intervals left-to-right and merge/compare neighborsMerge Intervals, Meeting Rooms I, Interval Intersection
End timeYou need to maximize non-overlapping selections (activity selection)Non-Overlapping Intervals, Min Arrows to Burst Balloons
Events (sweep line)You need to count concurrent intervals at each point in timeMeeting Rooms II, Min Platforms, My Calendar

Concrete Walkthrough

Consider intervals [[1,10], [2,3], [3,5], [5,7], [8,9]] for the "maximum non-overlapping" problem:

Sort-by-start vs sort-by-end trace
1intervals = [[1,10], [2,3], [3,5], [5,7], [8,9]]
2
3# === Sort by START: [[1,10], [2,3], [3,5], [5,7], [8,9]] ===
4# Keep [1,10] (last_end = 10)
5# Skip [2,3] (2 < 10, overlaps)
6# Skip [3,5] (3 < 10, overlaps)
7# Skip [5,7] (5 < 10, overlaps)
8# Skip [8,9] (8 < 10, overlaps)
9# Result: 1 kept, 4 removed <-- WRONG (suboptimal)
10
11# === Sort by END: [[2,3], [3,5], [5,7], [8,9], [1,10]] ===
12# Keep [2,3] (last_end = 3)
13# Keep [3,5] (3 >= 3, no overlap, last_end = 5)
14# Keep [5,7] (5 >= 5, no overlap, last_end = 7)
15# Keep [8,9] (8 >= 7, no overlap, last_end = 9)
16# Skip [1,10] (1 < 9, overlaps)
17# Result: 4 kept, 1 removed <-- OPTIMAL

Sort-by-start greedily committed to the long interval [1,10], which blocked everything else. Sort-by-end tries short intervals first, leaving room for more.

Key Insight
Start-Time vs End-Time Sort Order
Template: sort intervals, scan left-to-right, track a boundary value (max_end, last_end, etc.), count when boundary changes. Combining intervals: sort by start. Selecting maximum non-overlapping: sort by end. Counting concurrency: decompose into events.
Interview Tip
Sort by start time is the default
You can merge with either sort order, but sort-by-start is standard because the merge logic is simpler: when sorted by start, the overlap check is just current[0] <= last[1]. With sort-by-end, you'd need to check both endpoints.

Debugging Interval Problems

Check for empty input

1def merge(intervals):
2 if not intervals:
3 return []
4 # ... rest of algorithm

Forgetting this guard causes IndexError when accessing intervals[0].

Single interval returns unchanged

1# One interval is trivial - merge returns it unchanged
2assert merge([[5, 10]]) == [[5, 10]]

Identical intervals: behavior differs by problem

1# [[1,5],[1,5]]: overlap completely
2merge([[1,5],[1,5]]) # → [[1,5]]
3meeting_rooms([[1,5],[1,5]]) # → 2 rooms needed
4non_overlapping([[1,5],[1,5]]) # → remove 1

Contained intervals: use max, not assignment

1# [1,10] contains [2,5]: merge result is [1,10], NOT [1,5]
2merged[-1] = [last[0], max(last[1], current[1])]
3# max(10, 5) = 10 ✓

Touching intervals: clarify the overlap definition

Common Mistake
Adjacent intervals: does [1,5] overlap with [5,8]?
It depends on the problem. Merge Intervals: yes (use <=, they merge to [1,8]). Meeting Rooms: no (the first meeting ends, the next starts, so one room suffices). Always check whether the problem treats touching as overlapping.

Integer overflow in Java and C++

1// WRONG: can overflow with large coordinates
2if (a[0] + a[1] > b[0] + b[1]) ...
3
4// SAFE: compare directly
5if (a[1] > b[1]) ...

Python doesn't overflow, but Java and C++ do. When interval endpoints are near Integer.MAX_VALUE, arithmetic on them can wrap around. Compare endpoints directly instead of computing sums or differences.

Mutating the input in-place

Bug: mutating the input list
1# WRONG: sorts in-place, modifies caller's data
2def merge(intervals):
3 intervals.sort(key=lambda x: x[0]) # Mutates!
4 ...
5
6# SAFE: sort a copy
7def merge(intervals):
8 intervals = sorted(intervals, key=lambda x: x[0])
9 ...

Complexity at a Glance

ProblemTimeSpaceWhy
Merge IntervalsO(n log n)O(n)Sort dominates; output is O(n) merged intervals
Insert IntervalO(n)O(n)Already sorted; single scan builds output
Interval IntersectionO(m+n)O(m+n)Two pointers; output can be O(m+n)
Meeting Rooms IO(n log n)O(1)Sort + single bool check, no output array
Meeting Rooms IIO(n log n)O(n)Sort + sweep line or heap
Non-OverlappingO(n log n)O(1)Sort + counter, no output array
Remove CoveredO(n log n)O(1)Sort + counter, no output array
Min ArrowsO(n log n)O(1)Sort + counter, no output array
Key Insight
The universal interval complexity rule
Every interval problem is O(n log n) unless the input is pre-sorted (Insert Interval is O(n)). The bottleneck is always the sort. Space is O(1) if you only need a count, O(n) if you build output.

Identifying Interval Problems

Use these signal phrases to recognize interval problems instantly:

Signal PhraseProblemTechnique
"merge overlapping"Merge IntervalsSort by start, scan and merge
"insert into sorted intervals"Insert IntervalThree-phase scan (before/merge/after)
"minimum rooms" or "minimum platforms"Meeting Rooms IISweep line with +1/-1 events
"can attend all meetings"Meeting Rooms ISort by start, check consecutive overlaps
"remove minimum to avoid overlaps"Non-Overlapping IntervalsSort by end, activity selection
"intersection of two lists"Interval IntersectionTwo pointers, advance earlier-ending
"minimum arrows" or "minimum points"Burst BalloonsSort by end, greedy coverage
"remove covered intervals"Remove CoveredSort by start ASC, end DESC; track max end

Practice Problems

Problems are grouped by technique, starting with core problems and progressing to variations.

Core Merge & Insert

ProblemDifficultyApproach
Merge IntervalsMediumSort by start, merge with max(ends). Using max() prevents shrinking when a contained interval like [2,5] appears inside [1,10].
Insert IntervalMediumThree contiguous phases, O(n) without re-sorting
Interval List IntersectionMediumTwo pointers, advance the earlier-ending pointer

Scheduling & Rooms

ProblemDifficultyApproach
Meeting RoomsEasySort by start, check consecutive overlap
Meeting Rooms IIMediumSweep line or min-heap for concurrency count. Merging intervals would lose overlap information, which is why sweep line is necessary.
Employee Free TimeHardMerge all employee schedules, find gaps

Activity Selection

ProblemDifficultyApproach
Non-Overlapping IntervalsMediumSort by end, greedy selection, count removals. Sorting by end ensures each kept interval blocks the fewest future options (activity selection).
Minimum Number of Arrows to Burst BalloonsMediumSort by end, one arrow per non-overlapping group
Remove Covered IntervalsMediumSort by start (desc end for ties), count non-covered

Reference Templates

Five compact shapes. Each covers one interval operation. One context sentence precedes each template to describe when to reach for it.

Use when intervals may overlap and the output should combine connected ranges.

Template 1: Merge Overlapping Intervals
1def merge(intervals):
2 intervals.sort(key=lambda x: x[0])
3 merged = [intervals[0]]
4 for start, end in intervals[1:]:
5 if start <= merged[-1][1]:
6 merged[-1][1] = max(merged[-1][1], end)
7 else:
8 merged.append([start, end])
9 return merged

Use when the input is already sorted and non-overlapping, so re-sorting would be wasteful.

Template 2: Insert into Sorted Intervals
1def insert(intervals, new_interval):
2 result = []
3 i = 0
4 while i < len(intervals) and intervals[i][1] < new_interval[0]:
5 result.append(intervals[i]); i += 1
6 while i < len(intervals) and intervals[i][0] <= new_interval[1]:
7 new_interval[0] = min(new_interval[0], intervals[i][0])
8 new_interval[1] = max(new_interval[1], intervals[i][1])
9 i += 1
10 result.append(new_interval)
11 while i < len(intervals):
12 result.append(intervals[i]); i += 1
13 return result

Use when two sorted interval lists must be compared without checking every pair.

Template 3: Intersect Two Sorted Lists
1def interval_intersection(A, B):
2 result = []
3 i = j = 0
4 while i < len(A) and j < len(B):
5 lo = max(A[i][0], B[j][0])
6 hi = min(A[i][1], B[j][1])
7 if lo <= hi:
8 result.append([lo, hi])
9 if A[i][1] < B[j][1]:
10 i += 1
11 else:
12 j += 1
13 return result

Use when overlap count matters and merging would destroy the overlap information.

Template 4: Count Concurrent Intervals (Sweep Line)
1def min_meeting_rooms(intervals):
2 events = []
3 for s, e in intervals:
4 events.append((s, 1))
5 events.append((e, -1))
6 events.sort(key=lambda x: (x[0], x[1])) # -1 sorts before +1
7 count = max_count = 0
8 for _, delta in events:
9 count += delta
10 max_count = max(max_count, count)
11 return max_count

Use when the goal is to keep the maximum compatible set or find the minimum removals.

Template 5: Select Maximum Non-Overlapping
1def erase_overlap_intervals(intervals):
2 intervals.sort(key=lambda x: x[1])
3 kept = 0
4 last_end = float('-inf')
5 for start, end in intervals:
6 if start >= last_end:
7 kept += 1
8 last_end = end
9 return len(intervals) - kept

Pattern Recap

Merging overlapping intervals

Sort by start time so that any interval overlapping the current one is guaranteed to be adjacent. A single comparison against the last merged interval is sufficient. When an overlap is found, extend the end using max rather than direct assignment to handle the containment case where the incoming interval ends before the current merged one does.

Inserting into a sorted list

Because the input is already sorted, intervals partition naturally into three contiguous groups relative to the new interval: those that end before it starts, those that overlap with it, and those that start after it ends. A single linear scan handles all three phases without re-sorting, keeping the algorithm at O(n).

Intersecting two sorted lists

Two pointers advance through the lists together. After each comparison, the interval that ends first is fully consumed: every remaining interval in the other list starts later, so this interval cannot contribute any new intersections. Advancing the pointer on the earlier-ending side is the only safe move.

Counting concurrent intervals

The sweep line decomposes each interval into two point events and processes them in time order. When a meeting ends at the same time another begins, processing the end event first frees a room before counting the new arrival, which reflects the correct physical situation. Without that tie-breaking, the count at a shared boundary will be one higher than the actual number of rooms needed.

Selecting maximum non-overlapping intervals

Sort by end time and greedily keep each interval that does not conflict with the last kept one. The exchange argument proves that an interval ending earlier is compatible with at least as many future intervals as any later-ending alternative. Keeping the earliest-ending compatible interval never makes the final count worse.

Rebuilding the Pattern

When an interval problem feels unclear, start by identifying which of the five operations it needs. Merge asks you to combine overlapping intervals. Insert gives you a pre-sorted list and one new interval. Intersection finds where two sorted lists of intervals share coverage. Counting problems ask how many intervals are active simultaneously. Selection problems ask you to keep as many non-overlapping intervals as possible, or equivalently to remove as few as possible.

Once the operation is clear, the sort order follows from the goal. If the problem involves combining intervals or comparing neighbors, sort by start time. Start-time order guarantees that any interval overlapping the current one is immediately adjacent, which is the property that makes a one-pass scan correct. If the problem involves selecting a maximum compatible subset, sort by end time. End-time order implements the activity selection argument: the interval that ends earliest blocks the fewest future intervals and should always be preferred over any later-ending alternative that is currently compatible.

The sweep line approach for counting concurrency requires a different argument. Instead of sorting intervals, you sort events. Each interval produces two events: a start event adds one to the concurrent count, and an end event subtracts one. When a start and an end share the same timestamp, the tie-breaking rule matters. Processing end events before start events at the same time reflects a room being freed before a new meeting claims it. Without this ordering, the count at a shared boundary will be one higher than the actual number of rooms needed.

The activity selection argument for sort-by-end is worth reconstructing from scratch when it feels uncertain. Suppose you have two intervals that are both currently compatible with the last kept interval, and one ends earlier than the other. Any future interval that is compatible with the later-ending one is also compatible with the earlier-ending one, because the earlier-ending one creates strictly less obstruction. The later-ending one may block future intervals that the earlier-ending one would not. Choosing the earlier-ending interval can never be strictly worse, so it is always safe to prefer it.

Insert Interval is the one operation that does not require sorting because the input is already sorted. The three contiguous phases appear because sorted order means intervals cannot interleave: all intervals that end before the new one are grouped at the front, all intervals that overlap with the new one are grouped in the middle, and all intervals that start after the new one are grouped at the end. A single pointer moving from left to right crosses each boundary exactly once.

Check Your Understanding

Use these questions to check whether you can reason through the pattern without looking at the template.

Take Assessment

15 questions covering merge logic, sweep line, sort-by-start vs end, correctness reasoning, and advanced variations.

Practice this pattern

Apply the guide to complete interview problems with explanations and code.

Learn Intervals in a guided sequence

The Interview Course connects this pattern to its prerequisites, worked lessons, and progressively harder problems.

Explore the course