Skip to content

Intervals

Mark as:

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:

  1. Input is a list of ranges — [start, end] pairs, meetings, bookings, jobs, time slots.
  2. The question is about overlap: merge them, insert into them, remove the fewest, count how many are active at once.
  3. 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]]:

  1. Sort by start: [1,3], [2,6], [8,10], [15,18].
  2. Keep [1,3] as the current group.
  3. [2,6] starts at 2, before the group's end 3: overlap. Extend the group's end to max(3, 6) = 6.
  4. [8,10] starts at 8, after the end 6: a gap. The group is final; start a new one.
  5. [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.

sweepTemplate — go/intervals/intervals.go
// 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:

1slices.SortFunc(sorted, ... a[0] vs b[0])
Why:

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.

2len(out) == 0 || no overlap
Why:

The 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.

3out = append(out, cur)
Why:

Starting a new group. Appending a copy (not the shared slice) is important if you later mutate the end value.

4else { fold cur into the last group }
Why:

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: 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: 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
// 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
// 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
// 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

ApproachTimeSpace
Compare every pairO(n²)O(1)
Sort + one sweepO(n log n)O(n) — output, or the sorted copy
Insert into a sorted listO(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. 1.
    Meeting Rooms
    After ordering by time, only neighbours can clash.
    Easy
  2. 2.
    Merge Intervals
    Medium
  3. 3.
    Insert Interval
    The list is already ordered, so no sort is needed. How many phases are there?
    Medium
  4. 4.
    Non-overlapping Intervals
    Which edge decides which interval to keep?
    Medium
  5. 5.
    Meeting Rooms II
    What is the peak number of things happening at one moment?
    Medium
  6. 6.
    Minimum Number of Arrows to Burst Balloons
    Medium
  7. 7.
    Employee Free Time
    Flatten every schedule, then look for gaps.
    Hard

Which pattern? Drills

Unlabeled problems — pick the pattern, then read why.

Question 1 of 5

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?