56. Merge Intervals - Explanation

Problem Link

Description

Given an array of intervals where intervals[i] = [start_i, end_i], merge all overlapping intervals, and return an array of the non-overlapping intervals that cover all the intervals in the input.

You may return the answer in any order.

Note: Intervals are non-overlapping if they have no common point. For example, [1, 2] and [3, 4] are non-overlapping, but [1, 2] and [2, 3] are overlapping.

Example 1:

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

Output: [[1,5],[6,7]]

Example 2:

Input: intervals = [[1,2],[2,3]]

Output: [[1,3]]

Constraints:

  • 1 <= intervals.length <= 1000
  • intervals[i].length == 2
  • 0 <= start <= end <= 1000


Topics

Recommended Time & Space Complexity

You should aim for a solution as good or better than O(nlogn) time and O(n) space, where n is the size of the input array.


Hint 1

Sorting the given intervals in ascending order based on their start values is beneficial, as it helps in identifying overlapping intervals efficiently. How can you determine if two intervals overlap?


Hint 2

If two intervals are sorted in ascending order by their start values, they overlap if the start value of the second interval is less than or equal to the end value of the first interval.


Hint 3

We iterate through the sorted intervals from left to right, starting with the first interval in the output list. From the second interval onward, we compare each interval with the last appended interval. Can you determine the possible cases for this comparison?


Hint 4

The two cases are: if the current interval overlaps with the last appended interval, we update its end value to the maximum of both intervals' end values and continue. Otherwise, we append the current interval and proceed.


Company Tags

Please upgrade to NeetCode Pro to view company tags.



Prerequisites

Before attempting this problem, you should be comfortable with:

  • Sorting - Required to process intervals in order by start time for efficient merging
  • Intervals - Understanding how to represent, compare, and merge overlapping ranges
  • Sweep Line Algorithm - Alternative approach that tracks active intervals using events on a timeline

1. Sorting

Intuition

We are given a list of intervals, and some of them may overlap.
The goal is to merge all overlapping intervals so that the final list contains only non-overlapping intervals, covering the same ranges.

A natural way to approach this is:

  • if intervals are processed in sorted order by start time, then
  • any overlap can only happen with the most recently added interval

So after sorting:

  • we keep track of the last merged interval
  • if the current interval overlaps with it, we merge them
  • otherwise, we start a new interval

Algorithm

  1. Sort all intervals by their start time.
  2. Initialize the result list output with the first interval.
  3. Iterate through each interval (start, end) in the sorted list:
  4. Let lastEnd be the end of the last interval in output.
  5. If the current interval overlaps with the last one (start <= lastEnd):
    • Merge them by updating the end:
      • output[-1][1] = max(lastEnd, end)
  6. Otherwise (no overlap):
    • Append the current interval to output as a new interval
  7. After processing all intervals, return output
class Solution:
    def merge(self, intervals: List[List[int]]) -> List[List[int]]:
        intervals.sort(key=lambda pair: pair[0])
        output = [intervals[0]]

        for start, end in intervals:
            lastEnd = output[-1][1]

            if start <= lastEnd:
                output[-1][1] = max(lastEnd, end)
            else:
                output.append([start, end])
        return output

Time & Space Complexity

  • Time complexity: O(nlogn)O(n \log n)
  • Space complexity:
    • O(1)O(1) or O(n)O(n) space depending on the sorting algorithm.
    • O(n)O(n) for the output list.

2. Sweep Line Algorithm

Intuition

This approach treats each interval as a pair of events on a number line:

  • an interval starts at start
  • an interval ends at end

Instead of merging intervals directly, we track how many intervals are currently active as we move from left to right along the number line.

The key idea:

  • when the number of active intervals goes from 0 → positive, a merged interval starts
  • when it goes from positive → 0, a merged interval ends

By recording how the count changes at each boundary and sweeping through them in order, we can reconstruct all merged intervals.

Algorithm

  1. Create a map mp to store boundary changes:
    • for each interval [start, end]:
      • increment mp[start] (interval starts)
      • decrement mp[end] (interval ends)
  2. Initialize:
    • have = 0 to track how many intervals are currently active
    • interval = [] to build the current merged interval
    • res = [] to store the final merged intervals
  3. Iterate through all keys in mp in sorted order:
  4. For each position i:
    • If interval is empty, this position may mark the start of a new merged interval:
      • set interval = [i]
    • Update the active count:
      • have += mp[i]
    • If have == 0:
      • all intervals have closed at this point
      • append i as the end of the current merged interval
      • add interval to res
      • reset interval to empty
  5. After processing all events, return res
class Solution:
    def merge(self, intervals: List[List[int]]) -> List[List[int]]:
        mp = defaultdict(int)
        for start, end in intervals:
            mp[start] += 1
            mp[end] -= 1

        res = []
        interval = []
        have = 0
        for i in sorted(mp):
            if not interval:
                interval.append(i)
            have += mp[i]
            if have == 0:
                interval.append(i)
                res.append(interval)
                interval = []
        return res

Time & Space Complexity

  • Time complexity: O(nlogn)O(n \log n)
  • Space complexity: O(n)O(n)

3. Greedy

Intuition

We want to merge all overlapping intervals so the result contains only non-overlapping ranges.

This solution uses a greedy idea with an auxiliary array:

  • For every possible start point start, we record the farthest end of any interval that begins at start
  • Then we scan from left to right, maintaining the farthest point we must still cover (have)

While scanning:

  • if we see an interval starting at position i, we may need to extend our current merged interval’s end to include it
  • once our scan index reaches the farthest required end, we can safely close the current merged interval

So the scan behaves like:

  • “start a merged interval when we first see coverage”
  • “keep extending its end while overlaps exist”
  • “close it when we finish the coverage”

Algorithm

  1. Find the maximum start value among all intervals (max_val).
  2. Create an array mp of size max_val + 1:
    • mp[start] will store the farthest (end + 1) among intervals that start at start
  3. For each interval [start, end]:
    • update mp[start] = max(mp[start], end + 1)
  4. Initialize:
    • res as an empty list for merged intervals
    • interval_start = -1 to mark the start of the current merged interval
    • have = -1 to track the farthest end of the current merged interval
  5. Scan i from 0 to len(mp) - 1:
    • If mp[i] != 0, it means some interval starts at i:
      • if we are not already in a merged interval, set interval_start = i
      • update have = max(have, mp[i] - 1) (convert back to real end)
    • If have == i, it means we reached the end of the current merged interval:
      • append [interval_start, have] to res
      • reset interval_start and have
  6. After the scan, if a merged interval is still open, append it to res.
  7. Return res.
class Solution:
    def merge(self, intervals: List[List[int]]) -> List[List[int]]:
        max_val = max(interval[0] for interval in intervals)

        mp = [0] * (max_val + 1)
        for start, end in intervals:
            mp[start] = max(end + 1, mp[start])

        res = []
        have = -1
        interval_start = -1
        for i in range(len(mp)):
            if mp[i] != 0:
                if interval_start == -1:
                    interval_start = i
                have = max(mp[i] - 1, have)
            if have == i:
                res.append([interval_start, have])
                have = -1
                interval_start = -1

        if interval_start != -1:
            res.append([interval_start, have])

        return res

Time & Space Complexity

  • Time complexity: O(n+m)O(n + m)
  • Space complexity: O(n)O(n)

Where nn is the length of the array and mm is the maximum start value among all the intervals.


Common Pitfalls

Using Strict Inequality for Overlap Check

Two intervals overlap when current.start <= last.end, not <. Intervals [1, 3] and [3, 5] are considered overlapping and should merge to [1, 5].

Forgetting to Update the End When Merging

When merging overlapping intervals, the new end should be max(last.end, current.end), not just current.end. An earlier interval might extend further than a later-starting one.

Not Sorting Before Processing

The sorting approach assumes intervals are processed in order of start time. Without sorting, you cannot guarantee that overlapping intervals are adjacent, leading to missed merges.