Skip to content

435. Non-overlapping Intervals (Medium)

Problem

Given an array of intervals, return the minimum number of intervals to remove so the remainder are non-overlapping. (Intervals touching at endpoints are not considered overlapping.)

Example

  • intervals = [[1,2],[2,3],[3,4],[1,3]]1 (remove [1,3])
  • intervals = [[1,2],[1,2],[1,2]]2
  • intervals = [[1,2],[2,3]]0

LeetCode 435 · Link · Medium

Try it yourself

idle

Starter code: this editor begins with intentional TODOs. Fill the function, run the embedded tests, then compare your solution with the worked approaches below.

Click Run Python to execute. First run downloads Python (~10 MB, cached after that).

Approach 1: DP, LIS-like

Sort by start; find the longest chain of non-overlapping intervals; answer = n minus longest.

def erase_overlap_intervals(intervals):
intervals.sort(key=lambda x: x[0]) # L1: O(n log n)
n = len(intervals) # L2: O(1)
dp = [1] * n # L3: O(n)
for i in range(1, n): # L4: outer loop, n-1 iterations
for j in range(i): # L5: inner loop, up to i iterations
if intervals[j][1] <= intervals[i][0]: # L6: O(1) overlap check
dp[i] = max(dp[i], dp[j] + 1) # L7: O(1) update
return n - max(dp, default=0) # L8: O(n) scan dp

Where the time goes, line by line

Variables: n = len(intervals).

LinePer-call costTimes executedContribution
L1 (sort)O(nlogn)O(n log n)1O(nlogn)O(n log n)
L2, L3 (init)O(n)O(n)1O(n)O(n)
L4 (outer loop)O(1)O(1)n-1O(n)O(n)
L5 (inner loop)O(1)O(1)O(n)O(n) per outer iterO(n2)O(n²) ← dominates
L6, L7 (body)O(1)O(1)O(n2)O(n²) totalO(n2)O(n²)
L8 (max scan)O(n)O(n)1O(n)O(n)

The nested loops are the classic O(n2)O(n²) LIS pattern. L5 runs 0+1+2+…+(n-1) = n(n-1)/2 times total.

Complexity

  • Time: O(n2)O(n²), driven by L5 (the nested inner loop).
  • Space: O(n)O(n) for the dp array.

Approach 2: Greedy, sort by end, keep earliest ends

Classic interval-scheduling: sort by end; greedily pick the interval with the smallest end that doesn’t conflict with the previous pick.

def erase_overlap_intervals(intervals):
intervals.sort(key=lambda x: x[1]) # L1: O(n log n)
count = 0 # L2: O(1)
end = float('-inf') # L3: O(1)
for s, e in intervals: # L4: outer loop, n iterations
if s >= end: # L5: O(1) non-overlap check
end = e # L6: O(1) keep this interval
else:
count += 1 # L7: O(1) remove this interval
return count # L8: O(1)

Where the time goes, line by line

Variables: n = len(intervals).

LinePer-call costTimes executedContribution
L1 (sort)O(nlogn)O(n log n)1O(nlogn)O(n log n) ← dominates
L2, L3 (init)O(1)O(1)1O(1)O(1)
L4 (loop)O(1)O(1)nO(n)O(n)
L5-L7 (body)O(1)O(1)nO(n)O(n)
L8 (return)O(1)O(1)1O(1)O(1)

After the sort, a single linear pass does all the work. Each interval is examined once, and the decision (keep or remove) is made in O(1)O(1) by comparing its start to the running end variable.

Complexity

  • Time: O(nlogn)O(n log n), driven by L1 (sort).
  • Space: O(1)O(1) extra; only two scalar variables beyond the (sorted) input.

Why “sort by end” is the right greedy

Keeping the interval with the smallest end frees the most room for subsequent intervals, any optimal solution can be rewritten to include it (exchange argument). This is the interval scheduling maximization template.

Try this approach:

idle
Click Run Python to execute. First run downloads Python (~10 MB, cached after that).

Approach 3: Sort by start, remove on conflict (equivalent)

Sort by start; when two overlap, remove the one with the larger end (since it blocks more future intervals).

def erase_overlap_intervals(intervals):
intervals.sort(key=lambda x: x[0]) # L1: O(n log n)
count = 0 # L2: O(1)
prev_end = float('-inf') # L3: O(1)
for s, e in intervals: # L4: outer loop, n iterations
if s >= prev_end: # L5: O(1) non-overlap check
prev_end = e # L6: O(1) accept interval
else:
count += 1 # L7: O(1) remove this interval
prev_end = min(prev_end, e) # L8: O(1) keep the smaller end
return count # L9: O(1)

Where the time goes, line by line

Variables: n = len(intervals).

LinePer-call costTimes executedContribution
L1 (sort)O(nlogn)O(n log n)1O(nlogn)O(n log n) ← dominates
L2, L3 (init)O(1)O(1)1O(1)O(1)
L4 (loop)O(1)O(1)nO(n)O(n)
L5-L8 (body)O(1)O(1)nO(n)O(n)
L9 (return)O(1)O(1)1O(1)O(1)

Identical cost structure to Approach 2. The difference is the sort key (start vs. end) and the tie-breaking rule on conflict (keep the smaller end explicitly via min). Both approaches are O(nlogn)O(n log n) dominated by the sort.

Complexity

  • Time: O(nlogn)O(n log n), driven by L1 (sort).
  • Space: O(1)O(1) extra.

Try this approach:

idle
Click Run Python to execute. First run downloads Python (~10 MB, cached after that).

Summary

ApproachTimeSpace
LIS DPO(n2)O(n²)O(n)O(n)
Sort by end, greedyO(nlogn)O(n log n)O(1)O(1)
Sort by start, remove on conflictO(nlogn)O(n log n)O(1)O(1)

Interval scheduling is a canonical greedy. The proof-by-exchange argument is worth knowing.

Test cases

# Quick smoke tests, paste into a REPL or save as test_435.py and run.
# Uses the canonical implementation (Approach 2: greedy sort by end).
def erase_overlap_intervals(intervals):
intervals.sort(key=lambda x: x[1])
count = 0
end = float('-inf')
for s, e in intervals:
if s >= end:
end = e
else:
count += 1
return count
def _run_tests():
assert erase_overlap_intervals([[1,2],[2,3],[3,4],[1,3]]) == 1 # example 1
assert erase_overlap_intervals([[1,2],[1,2],[1,2]]) == 2 # example 2
assert erase_overlap_intervals([[1,2],[2,3]]) == 0 # example 3: touching only
assert erase_overlap_intervals([[1,5]]) == 0 # single interval
assert erase_overlap_intervals([]) == 0 # empty
assert erase_overlap_intervals([[1,100],[2,3],[4,5],[6,7]]) == 1 # one giant overlaps many
print("all tests pass")
if __name__ == "__main__":
_run_tests()
  • Greedy Exchange Arguments, proof tactics for showing that a greedy choice can be swapped into an optimal solution without making it worse.
  • Intervals, range-boundary tactics for overlap, containment, scheduling, and sweep-line problems.
  • Merge Intervals, sorted-boundary tactics for combining overlapping ranges and maintaining the current covered span.
  • Sorting as Preprocessing, order-first tactics that pay O(n log n) so adjacency, monotonic movement, or greedy choice becomes visible.