Skip to content

Coding patternsBlind 75

Non-overlapping Intervals

Keep the interval that ends first, because it leaves the most room for the rest. Solved two ways, a longest-chain table in O(n²) and greedy by end time in O(n log n), with a step-through, code in five languages and the Big O worked out from the code.

You are given a list intervals of n intervals [start, end] (n is the length of intervals). Return the fewest intervals you must remove so that the ones left do not overlap.

Here, intervals that only touch do not overlap: [1, 3] and [3, 5] can both stay.

Example 1

Input: intervals = [[1, 4], [2, 3], [3, 6], [5, 7], [6, 8]]

Output: 2

Remove [1, 4] and [5, 7]. The rest, [2, 3], [3, 6] and [6, 8], only touch. No way keeps 4.

intervals[1, 4][2, 3][3, 6][5, 7][6, 8]0123456789
remove
[1, 4][5, 7]

Example 2

Input: intervals = [[1, 5], [1, 5], [1, 5]]

Output: 2

Three copies of the same interval. Only one can stay.

Example 3

Input: intervals = [[0, 2], [2, 4]]

Output: 0

They touch at 2, which is allowed.

  • 1 ≤ n ≤ 100,000
  • −50,000 ≤ start < end ≤ 50,000
Do touching intervals overlap?
No, not here. [1, 3] and [3, 5] may both stay. That is the opposite of Merge Intervals.
Do we return the count or the intervals?
Only the count of intervals to remove.
Is the input sorted?
No. Sorting is our job.
Can intervals repeat?
Yes. Exact copies overlap each other, so all but one must go.

Removing the fewest is the same as keeping the most intervals with no overlap. That is a scheduling question: fit as many meetings as possible into one room. The classic answer is greedy: always keep the interval that ends first. Sorting by end makes that one pass.

Trying every subset of intervals is O(2ⁿ). The table below is the same search with each result stored once.

Greedy by end O(n log n)

Sort by end. Walk the intervals in that order and keep track of last_end, where the last kept interval ends.

  • If an interval starts at or after last_end, it fits. Keep it, and move last_end to its end.
  • If it starts before last_end, it overlaps the last kept one. Remove it, and count it.

Why it is correct: of all intervals that could come next, the one that ends first leaves the most room for the rest. Swapping it in for any other choice never makes things worse. So greedily keeping the earliest end keeps the most intervals, and removes the fewest.

intervals[1, 4][2, 3][3, 6][5, 7][6, 8]0123456789
last_end
−∞
removed
0
  1. intervals.sort(key=lambda interval: interval[1])
  2. removed, last_end = 0, -inf
  3. for start, end in intervals:
  4. if start >= last_end:
  5. last_end = end
  6. else:
  7. removed += 1
  8. return removed

To keep as many as we can, we want each kept interval to end as early as possible. That leaves the most room for the rest.

1 / 8
Solution.javasolution.pySolution.cssolution.gosolution.ts
class Solution {
// Greedy by end: always keep the interval that ends first, because it leaves
// the most room. The sort costs O(n log n), the loop O(n).
public int eraseOverlapIntervals(int[][] intervals) {
java.util.Arrays.sort(intervals, (first, second) -> Integer.compare(first[1], second[1]));
int removed = 0;
long lastEnd = Long.MIN_VALUE;
for (int[] interval : intervals) {
// >=: touching is allowed, so it may start exactly where the last one ends.
if (interval[0] >= lastEnd) lastEnd = interval[1];
// It overlaps a kept interval that ends sooner, so this one goes.
else removed++;
}
return removed;
}
}
Time O(n log n)Space O(1)Here is how we get these from the code, in four steps.
  1. 1 Find the work that repeats

    Two parts repeat. The sort compares intervals many times. Then the loop does one comparison per interval, which is 1 step per run.

    1. intervals.sort(key=lambda interval: interval[1])
    2. removed, last_end = 0, -inf
    3. for start, end in intervals:
    4. if start >= last_end:
    5. last_end = end
    6. else:
    7. removed += 1
    8. return removed
  2. 2 Count the runs on Example 1

    Example 1 has 5 intervals. Sorting 5 items takes about 5 × log₂ 5, so about 12 steps. The loop then runs 5 times.

    12 + 5 = about 17 steps.

    partsortloop
    stepsabout 5 × log 5 ≈ 125
  3. 3 Write the count for any n

    n log₂ n (sort) + n (loop)

    Check it with Example 1: 5 × 2.3 + 5 ≈ 17.

  4. 4 Keep only the part that grows fastest

    Keep the sort. log n keeps growing, so n log n pulls away from the loop’s n. What is left is n log n.

    So the time is O(n log n).

  5. S Space: count the extra memory the same way

    The loop keeps only removed and last_end. Sorting in place needs little extra memory, though some languages use up to O(n) for it. Beyond the sort, the space is O(1).

    removedlast_endsort space

2,000 random lists of 1 to 8 intervals with ends from −4 to 4, so touching ends, nesting and repeats come up often. Longest chain and greedy by end gave the same answer every time.

Largest inputsResult
100,000 intervals (the most allowed), shuffled, each one there twice, so half must go. Greedy by end. Also run with 100,000 that only touch, where none go.0.04 s, and about 1.5 MB of extra memory, all for the sort. The greedy loop itself keeps only a few numbers.
2,000 intervals. Longest chain, about 2 million pairs.0.17 s. At 100,000 intervals it would check 5 billion pairs.