title: Merge Intervals
slug: merge-intervals
difficulty: medium
leetcode_id: 56
leetcode_url: https://leetcode.com/problems/merge-intervals/
categories:
  - arrays
  - sorting
patterns:
  - intervals

function_signature: "def merge(intervals: list[list[int]]) -> list[list[int]]:"

test_cases:
  visible:
    - input: { intervals: [[1, 3], [2, 6], [8, 10], [15, 18]] }
      expected: [[1, 6], [8, 10], [15, 18]]
    - input: { intervals: [[1, 4], [4, 5]] }
      expected: [[1, 5]]
    - input: { intervals: [[1, 4], [0, 4]] }
      expected: [[0, 4]]
  hidden:
    - input: { intervals: [[1, 4]] }
      expected: [[1, 4]]
    - input: { intervals: [[1, 4], [0, 0]] }
      expected: [[0, 0], [1, 4]]
    - input: { intervals: [[1, 4], [2, 3]] }
      expected: [[1, 4]]

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

constraints: |
  - `1 <= intervals.length <= 10^4`
  - `intervals[i].length == 2`
  - `0 <= start_i <= end_i <= 10^4`

examples:
  - input: "intervals = [[1,3],[2,6],[8,10],[15,18]]"
    output: "[[1,6],[8,10],[15,18]]"
    explanation: "Since intervals [1,3] and [2,6] overlap, merge them into [1,6]."
  - input: "intervals = [[1,4],[4,5]]"
    output: "[[1,5]]"
    explanation: "Intervals [1,4] and [4,5] are considered overlapping because they share endpoint 4."
  - input: "intervals = [[4,7],[1,4]]"
    output: "[[1,7]]"
    explanation: "After sorting by start time, [1,4] comes before [4,7]. Since they share endpoint 4, they merge into [1,7]."

explanation:
  intuition: |
    Imagine you have several meetings scheduled throughout the day, and you want to see which time blocks are actually occupied. Some meetings overlap — if one runs from 1pm to 3pm and another from 2pm to 4pm, you're really busy from 1pm to 4pm continuously.

    The key insight is that **overlapping intervals are easier to detect when they're sorted**. If intervals are sorted by their start times, then two consecutive intervals overlap if and only if the second interval's start is less than or equal to the first interval's end.

    Think of it like this: line up all intervals on a number line, sorted by where they begin. As you scan left to right, each new interval either:
    1. **Overlaps** with the previous one (its start ≤ previous end) — extend the current merged interval
    2. **Doesn't overlap** (its start > previous end) — start a new merged interval

    This mental model transforms a potentially complex problem into a simple linear scan after sorting.

  approach: |
    We solve this using a **Sort and Merge** approach:

    **Step 1: Handle edge cases**

    - If the input has 0 or 1 intervals, return it as-is (nothing to merge)

    &nbsp;

    **Step 2: Sort intervals by start time**

    - Sort the intervals array by the first element (start time) of each interval
    - This ensures that any overlapping intervals will be adjacent after sorting

    &nbsp;

    **Step 3: Initialise the result**

    - Create a `merged` list and add the first interval to it
    - This first interval becomes our "current" interval to potentially extend

    &nbsp;

    **Step 4: Iterate through remaining intervals**

    - For each interval starting from index 1:
    - Get the last interval in our `merged` list (the one we might extend)
    - If the current interval's start ≤ the last merged interval's end → they overlap
      - Update the last merged interval's end to be the maximum of both ends
    - Otherwise → no overlap
      - Append the current interval as a new entry in `merged`

    &nbsp;

    **Step 5: Return the result**

    - Return the `merged` list containing all non-overlapping intervals

  common_pitfalls:
    - title: Forgetting to Sort First
      description: |
        Without sorting, you cannot reliably detect overlaps with a single pass. Consider `[[2,6],[1,3],[8,10]]`:
        - Unsorted: [2,6] and [1,3] are not adjacent, making overlap detection complex
        - Sorted: [1,3] and [2,6] become adjacent, and their overlap is obvious (3 ≥ 2)

        Attempting to merge without sorting requires comparing every pair of intervals, resulting in O(n²) complexity.
      wrong_approach: "Iterate through unsorted intervals"
      correct_approach: "Sort by start time first, then merge in one pass"

    - title: Incorrect Overlap Condition
      description: |
        Two intervals `[a, b]` and `[c, d]` (where `a ≤ c` after sorting) overlap when `c ≤ b`, not `c < b`.

        For example, `[1,4]` and `[4,5]` share the point 4 and should merge to `[1,5]`. Using strict inequality (`c < b`) would incorrectly keep them separate.
      wrong_approach: "Check if current.start < previous.end"
      correct_approach: "Check if current.start <= previous.end"

    - title: Not Taking the Maximum End
      description: |
        When merging overlapping intervals, the new end should be `max(previous.end, current.end)`, not just `current.end`.

        Consider `[1,10]` and `[2,5]`: the second interval is entirely contained within the first. The merged result should be `[1,10]`, not `[1,5]`.
      wrong_approach: "Set merged.end = current.end"
      correct_approach: "Set merged.end = max(merged.end, current.end)"

  key_takeaways:
    - "**Sort first for interval problems**: Sorting by start time makes overlap detection trivial — just compare adjacent intervals"
    - "**The intervals pattern**: This problem introduces the foundational technique used in many interval problems (insert interval, meeting rooms, etc.)"
    - "**In-place vs. new list**: We build a new result list rather than modifying in place, which is cleaner and avoids index shifting issues"
    - "**Edge case awareness**: Remember that touching intervals (sharing an endpoint) count as overlapping"

  time_complexity: "O(n log n). Sorting dominates at O(n log n), and the subsequent merge pass is O(n)."
  space_complexity: "O(n). The `merged` result list can contain up to n intervals if none overlap. Sorting may use O(log n) stack space depending on implementation."

solutions:
  - approach_name: Sort and Merge
    is_optimal: true
    code: |
      def merge(intervals: list[list[int]]) -> list[list[int]]:
          # Edge case: nothing to merge
          if len(intervals) <= 1:
              return intervals

          # Sort intervals by start time
          intervals.sort(key=lambda x: x[0])

          # Start with the first interval
          merged = [intervals[0]]

          for current in intervals[1:]:
              # Get the last interval in our merged list
              last = merged[-1]

              # Check if current overlaps with last (start <= end means overlap)
              if current[0] <= last[1]:
                  # Extend the last interval's end if needed
                  last[1] = max(last[1], current[1])
              else:
                  # No overlap - add current as a new interval
                  merged.append(current)

          return merged
    explanation: |
      **Time Complexity:** O(n log n) — Dominated by the sorting step.

      **Space Complexity:** O(n) — For the result list; O(log n) for sorting.

      After sorting by start time, we make a single pass through the intervals. Each interval either extends the previous merged interval or starts a new one. The sorting ensures we never miss an overlap.

  - approach_name: Brute Force (Compare All Pairs)
    is_optimal: false
    code: |
      def merge(intervals: list[list[int]]) -> list[list[int]]:
          if not intervals:
              return []

          # Convert to a set of tuples for easier manipulation
          interval_set = set(tuple(i) for i in intervals)

          # Keep merging until no more merges possible
          changed = True
          while changed:
              changed = False
              intervals_list = list(interval_set)

              for i in range(len(intervals_list)):
                  for j in range(i + 1, len(intervals_list)):
                      a, b = intervals_list[i], intervals_list[j]

                      # Check if intervals overlap
                      if a[0] <= b[1] and b[0] <= a[1]:
                          # Merge them
                          merged = (min(a[0], b[0]), max(a[1], b[1]))
                          interval_set.discard(a)
                          interval_set.discard(b)
                          interval_set.add(merged)
                          changed = True
                          break
                  if changed:
                      break

          return [list(i) for i in interval_set]
    explanation: |
      **Time Complexity:** O(n³) — In the worst case, we do n merge operations, each requiring O(n²) comparisons.

      **Space Complexity:** O(n) — For storing the set of intervals.

      This approach repeatedly scans all pairs looking for overlaps. While correct, it's highly inefficient. Each merge might create new overlap opportunities, requiring multiple passes. This illustrates why sorting first is essential — it allows us to find all overlaps in a single O(n) pass.
