LeetCode #56 Medium

Merge Intervals

Given a list of intervals, merge all overlapping ones and return the non-overlapping result.

Constraints
  • 1 <= intervals.length <= 10⁴
  • intervals[i].length == 2
  • 0 <= starti <= endi <= 10⁴
arraysortingintervals
Open on LeetCode ↗
02

Intuition

Merge intervals combines all overlapping intervals in an unsorted list. Comparing every pair is O(n²) and also awkward, because merging two intervals can create a new overlap with a third. Sorting removes that complication entirely: - Sort by start time, and every interval that can merge with the current one appears immediately next to it — so a single left-to-right pass suffices. After sorting, an interval either overlaps the one being built or begins entirely after it. There is no case where a later interval reaches back to merge with an earlier one. Walk the sorted list keeping a current interval. If the next interval starts at or before the current end, extend the current end to max(currentEnd, nextEnd); otherwise the current interval is finished — append it and start a new one. Taking the maximum of the ends is essential. One interval can fully contain another, as with [1, 10] and [2, 3], and blindly assigning the next interval's end would shrink the merged range. The overlap test must be <=, since touching intervals like [1, 3] and [3, 5] merge into [1, 5] for this problem. The final interval must be appended after the loop, since nothing follows to trigger its append. Omitting that step silently drops the last interval — a bug that only shows in the output's length. Sorting dominates the cost at O(n log n), with the merge pass itself O(n).

How to spot this pattern

Almost every interval problem starts with a sort, and the choice of key is the whole puzzle. Sort by start when you're merging or inserting — it guarantees that anything overlapping the current block appears next, so one pass suffices. Sort by end when you're packing the most non-overlapping items in (the classic greedy scheduling move). If you can't decide, ask what the greedy choice is: merging cares about what begins next, scheduling cares about what frees up soonest.

03

Approach

Try it first

Before reading on: price up what the direct approach costs here, then ask what sorting by one endpoint makes obvious that was hidden before. Aim for O(n log n) time and O(n) space.

1

Sort by start time

Sorting places every mergeable interval adjacent to its partner, so one pass suffices and no later interval reaches back to an earlier one.

2

Carry a current interval

Walk the sorted list holding the interval being built. Each new interval either extends it or begins a new one.

3

Extend on overlap

If the next interval starts at or before the current end, merge by extending. Use <= — touching intervals like [1, 3] and [3, 5] merge into [1, 5].

4

Take the maximum end

Extend to max(currentEnd, nextEnd). One interval can contain another, as [1, 10] contains [2, 3], and assigning blindly would shrink the range.

5

Start fresh on a gap

When the next interval begins after the current end, the current one is complete — append it and begin a new current interval.

6

Append the final interval

Add the last interval after the loop ends. Nothing follows to trigger it, and omitting this silently drops one interval from the output.

7

Cost of the approach

Sorting dominates at O(n log n) time, with the merge pass O(n). Space is O(n) for the output, or O(log n) beyond it.

04

Solution & live demo

▶1class Solution:
▶2 def merge(self, intervals):
▶3 intervals.sort(key=lambda x: x[0])
▶4 res = [intervals[0]]
▶5 for start, end in intervals[1:]:
▶6 if start <= res[-1][1]:
▶7 res[-1][1] = max(res[-1][1], end)
▶8 else:
▶9 res.append([start, end])
▶10 return res
05

Common pitfalls

Sorting by end time

✗ Wrong
intervals.sort(key=lambda x: x[1])
✓ Right
intervals.sort(key=lambda x: x[0])

With [[1, 10], [2, 3], [4, 5]], sorting by end gives [2,3], [4,5], [1,10] — the wide interval that swallows both arrives last, so neither earlier block gets merged into it. Sorting by start makes overlaps adjacent.

Extending with end instead of max

✗ Wrong
if start <= res[-1][1]:
    res[-1][1] = end
✓ Right
if start <= res[-1][1]:
    res[-1][1] = max(res[-1][1], end)

A fully-contained interval shrinks the block it lands in: [1, 10] followed by [2, 3] becomes [1, 3], silently dropping everything from 3 to 10. Sorting by start orders the left edges only — it says nothing about which right edge is larger.

Using < and missing exact touches

✗ Wrong
if start < res[-1][1]:
✓ Right
if start <= res[-1][1]:

[1, 4] and [4, 5] share the endpoint 4, and LeetCode counts that as overlapping — the expected answer is [1, 5], not two intervals. Strict < treats touching as disjoint.

06

Edge cases

Touching intervals, e.g. [1,4],[4,5]

start <= last.end is true at equality, so adjacent intervals merge into [1,5].

One interval engulfs another, e.g. [1,10],[2,3]

max(last.end, end) keeps the larger end, so the inner interval is absorbed.

Already disjoint and sorted

No start <= last.end ever holds, so each interval is appended unchanged.

07

Complexity

Time
O(n log n)
Space
O(n)
Dominated by the sort; the sweep is linear.