Intervals
One-liner: put the ranges in order, then walk through them once, comparing each one only with the last one you kept.
The analogy
Think of a calendar. If someone hands you a pile of meeting cards in random order, overlaps are hard to spot — you would compare every card with every other card. Sort the cards by start time first, and an overlap can only ever happen between neighbours: if a meeting does not collide with the one before it, it cannot collide with anything earlier. Sorting turns a pairwise problem into a single pass.
Recognition signals
Reach for the intervals pattern when you see:
- Input is a list of ranges —
[start, end]pairs, meetings, bookings, jobs, time slots. - The question is about overlap: merge them, insert into them, remove the fewest, count how many are active at once.
- Order in the input is arbitrary, but order by time would make the answer obvious.
Step-by-step walkthrough
Merge [[8,10], [1,3], [2,6], [15,18]]:
- Sort by start:
[1,3], [2,6], [8,10], [15,18]. - Keep
[1,3]as the current group. [2,6]starts at 2, before the group's end 3: overlap. Extend the group's end tomax(3, 6) = 6.[8,10]starts at 8, after the end 6: a gap. The group is final; start a new one.[15,18]starts after 10: another new group.
Result: [1,6], [8,10], [15,18]. The max in step 3 matters — a long interval can swallow later short ones.
Code template
Sort first, then sweep. Only the three marked spots change per problem.
// Template: sort by start, then sweep once, comparing each interval to the last one kept.
func sweepTemplate(intervals [][]int) [][]int {
sorted := slices.Clone(intervals) // never reorder the caller's slice
slices.SortFunc(sorted, func(a, b []int) int { return cmp.Compare(a[0], b[0]) })
var out [][]int
for _, cur := range sorted {
if len(out) == 0 || false /* 1. cur does NOT overlap out's last */ {
out = append(out, cur) // 2. start a new group
} else {
// 3. cur overlaps: fold it into the last group (extend the end, drop it, ...)
}
}
return out
}Why each part exists:
slices.SortFunc(sorted, ... a[0] vs b[0])Sorting by start is what makes "compare with the last one only" valid. Some problems (non-overlapping) sort by end instead — ask which edge the greedy choice depends on. Clone first so you do not reorder the caller's data.
len(out) == 0 || no overlapThe first interval always starts a group. After that, a gap with the last group means the last group is complete and a new one begins.
out = append(out, cur)Starting a new group. Appending a copy (not the shared slice) is important if you later mutate the end value.
else { fold cur into the last group }This is where problems differ: merge extends the end with max, "erase" drops the interval, "rooms" bumps a counter. Always use max for the end — never assume the new interval ends later.
The real solution
// Merge: merge all overlapping intervals.
func Merge(intervals [][]int) [][]int {
sorted := slices.Clone(intervals)
slices.SortFunc(sorted, func(a, b []int) int { return cmp.Compare(a[0], b[0]) })
out := [][]int{}
for _, cur := range sorted {
last := len(out) - 1
if last < 0 || out[last][1] < cur[0] { // gap: no overlap
out = append(out, []int{cur[0], cur[1]})
} else { // overlap (touching counts): extend the end
out[last][1] = max(out[last][1], cur[1])
}
}
return out
}The check out[last][1] < cur[0] means "the last group ends strictly before this one starts". Touching intervals fall into the else and get merged.
Variations
Insert into an already sorted list — no sort needed. Walk three phases: everything entirely before, everything overlapping (absorb it), everything after:
// Insert: insert newInterval into sorted, non-overlapping intervals and merge.
// No sorting needed: three phases — before, overlapping, after.
func Insert(intervals [][]int, newInterval []int) [][]int {
out := [][]int{}
i, n := 0, len(intervals)
for i < n && intervals[i][1] < newInterval[0] { // entirely before
out = append(out, intervals[i])
i++
}
merged := []int{newInterval[0], newInterval[1]}
for i < n && intervals[i][0] <= merged[1] { // overlaps: absorb
merged[0] = min(merged[0], intervals[i][0])
merged[1] = max(merged[1], intervals[i][1])
i++
}
out = append(out, merged)
for ; i < n; i++ { // entirely after
out = append(out, intervals[i])
}
return out
}Remove the fewest intervals — a greedy choice. Among overlapping intervals, keep the one that ends earliest, since it leaves the most room for the rest. That is why this one sorts by end:
// EraseOverlapIntervals: fewest intervals to remove so the rest do not overlap.
// Greedy: sort by END, keep every interval that starts at or after the last kept end.
func EraseOverlapIntervals(intervals [][]int) int {
sorted := slices.Clone(intervals)
slices.SortFunc(sorted, func(a, b []int) int { return cmp.Compare(a[1], b[1]) })
kept, end := 0, 0
for i, iv := range sorted {
if i == 0 || iv[0] >= end {
kept++
end = iv[1]
}
}
return len(sorted) - kept
}How many at once? (minimum rooms) — sort starts and ends separately and sweep. Each start adds a room; each end that has already happened frees one. The peak is the answer:
// MinMeetingRooms: minimum rooms so no two overlapping meetings share one.
// Sweep line: sort starts and ends separately; a start before the next end needs a new room.
func MinMeetingRooms(intervals [][]int) int {
n := len(intervals)
starts, ends := make([]int, n), make([]int, n)
for i, iv := range intervals {
starts[i], ends[i] = iv[0], iv[1]
}
slices.Sort(starts)
slices.Sort(ends)
rooms, best, e := 0, 0, 0
for _, s := range starts {
for e < n && ends[e] <= s { // meetings that ended by s free their room
rooms--
e++
}
rooms++
best = max(best, rooms)
}
return best
}Any overlap at all? The simplest check, comparing each neighbour after sorting:
// CanAttendMeetings: true if no two meetings overlap.
func CanAttendMeetings(intervals [][]int) bool {
sorted := slices.Clone(intervals)
slices.SortFunc(sorted, func(a, b []int) int { return cmp.Compare(a[0], b[0]) })
for i := 1; i < len(sorted); i++ {
if sorted[i][0] < sorted[i-1][1] {
return false
}
}
return true
}Complexity
| Approach | Time | Space |
|---|---|---|
| Compare every pair | O(n²) | O(1) |
| Sort + one sweep | O(n log n) | O(n) — output, or the sorted copy |
| Insert into a sorted list | O(n) | O(n) |
The sort dominates; the sweep itself is linear. Insert skips the sort because the input is already ordered.
Common mistakes
Practice ladder
Ordered Easy → Hard. For each, decide first: sort by start or end, and does touching count?
- 1.Meeting RoomsEasyAfter ordering by time, only neighbours can clash.
- 2.Merge IntervalsMedium
- 3.Insert IntervalMediumThe list is already ordered, so no sort is needed. How many phases are there?
- 4.Non-overlapping IntervalsMediumWhich edge decides which interval to keep?
- 5.Meeting Rooms IIMediumWhat is the peak number of things happening at one moment?
- 6.Minimum Number of Arrows to Burst BalloonsMedium
- 7.Employee Free TimeHardFlatten every schedule, then look for gaps.
Which pattern? Drills
Unlabeled problems — pick the pattern, then read why.
Given a list of [start, end] ranges in no particular order, combine every group of overlapping ranges and return the disjoint ranges that remain.
Which pattern?