title: Insert Interval
slug: insert-interval
difficulty: medium
leetcode_id: 57
leetcode_url: https://leetcode.com/problems/insert-interval/
categories:
  - arrays
patterns:
  - intervals

function_signature: "def insert(intervals: list[list[int]], newInterval: list[int]) -> list[list[int]]:"

test_cases:
  visible:
    - input: { intervals: [[1, 3], [6, 9]], newInterval: [2, 5] }
      expected: [[1, 5], [6, 9]]
    - input: { intervals: [[1, 2], [3, 5], [6, 7], [8, 10], [12, 16]], newInterval: [4, 8] }
      expected: [[1, 2], [3, 10], [12, 16]]
    - input: { intervals: [], newInterval: [5, 7] }
      expected: [[5, 7]]
  hidden:
    - input: { intervals: [[1, 5]], newInterval: [2, 3] }
      expected: [[1, 5]]
    - input: { intervals: [[1, 5]], newInterval: [6, 8] }
      expected: [[1, 5], [6, 8]]
    - input: { intervals: [[3, 5], [12, 15]], newInterval: [6, 6] }
      expected: [[3, 5], [6, 6], [12, 15]]

description: |
  You are given an array of non-overlapping intervals `intervals` where `intervals[i] = [start_i, end_i]` represent the start and the end of the i<sup>th</sup> interval and `intervals` is sorted in ascending order by `start_i`. You are also given an interval `newInterval = [start, end]` that represents the start and end of another interval.

  Insert `newInterval` into `intervals` such that `intervals` is still sorted in ascending order by `start_i` and `intervals` still does not have any overlapping intervals (merge overlapping intervals if necessary).

  Return `intervals` *after the insertion*.

  **Note** that you don't need to modify `intervals` in-place. You can make a new array and return it.

constraints: |
  - `0 <= intervals.length <= 10^4`
  - `intervals[i].length == 2`
  - `0 <= start_i <= end_i <= 10^5`
  - `intervals` is sorted by `start_i` in **ascending** order
  - `newInterval.length == 2`
  - `0 <= start <= end <= 10^5`

examples:
  - input: "intervals = [[1,3],[6,9]], newInterval = [2,5]"
    output: "[[1,5],[6,9]]"
    explanation: "The new interval [2,5] overlaps with [1,3], so they merge into [1,5]. The interval [6,9] doesn't overlap and remains unchanged."
  - input: "intervals = [[1,2],[3,5],[6,7],[8,10],[12,16]], newInterval = [4,8]"
    output: "[[1,2],[3,10],[12,16]]"
    explanation: "The new interval [4,8] overlaps with [3,5], [6,7], and [8,10]. These all merge into [3,10]. Intervals [1,2] and [12,16] don't overlap."
  - input: "intervals = [], newInterval = [5,7]"
    output: "[[5,7]]"
    explanation: "When there are no existing intervals, simply return the new interval."

explanation:
  intuition: |
    Imagine you have a timeline with several non-overlapping time blocks already scheduled, and you need to add a new meeting. Some existing blocks might overlap with your new meeting and need to be combined into one larger block.

    The key insight is that the intervals are **already sorted** by start time. This means we can process them in order, and any interval that overlaps with our new interval must be *consecutive* in the list. There can't be a non-overlapping interval sandwiched between two overlapping ones.

    Think of it as walking through a sorted list of events: first, we encounter events that end before our new event starts (no overlap, keep them). Then we hit events that overlap with ours (merge them all together). Finally, we see events that start after our merged event ends (no overlap, keep them too).

    This three-phase approach lets us solve the problem in a single pass through the intervals.

  approach: |
    We solve this using a **Single Pass with Three Phases**:

    **Step 1: Initialise result list**

    - `result`: Empty list to store our final intervals

    &nbsp;

    **Step 2: Add all intervals that come before the new interval**

    - Iterate through intervals while `intervals[i].end < newInterval.start`
    - These intervals end before our new interval starts, so no overlap
    - Add each of these directly to `result`

    &nbsp;

    **Step 3: Merge all overlapping intervals**

    - Continue iterating while `intervals[i].start <= newInterval.end`
    - These intervals overlap with our new interval (they start before it ends)
    - Expand `newInterval` to encompass each overlapping interval:
      - `newInterval.start = min(newInterval.start, intervals[i].start)`
      - `newInterval.end = max(newInterval.end, intervals[i].end)`
    - After processing all overlaps, add the merged `newInterval` to `result`

    &nbsp;

    **Step 4: Add all remaining intervals**

    - Any remaining intervals start after our merged interval ends
    - Add each of these directly to `result`

    &nbsp;

    **Step 5: Return the result**

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

  common_pitfalls:
    - title: Forgetting to Handle Edge Cases
      description: |
        The new interval might need to be inserted at the very beginning (before all existing intervals), at the very end (after all existing intervals), or the input might be empty.

        For example, with `intervals = [[3,5],[6,9]]` and `newInterval = [1,2]`, the new interval comes before everything. With `newInterval = [10,12]`, it comes after everything.

        The three-phase approach handles these naturally: if no intervals are "before," phase 2 starts immediately. If no intervals overlap, we just add the new interval. If no intervals are "after," we're done after phase 3.
      wrong_approach: "Assuming the new interval always overlaps with something"
      correct_approach: "Handle all three phases even if some are empty"

    - title: Incorrect Overlap Detection
      description: |
        Two intervals `[a, b]` and `[c, d]` overlap if and only if `a <= d` AND `c <= b`. A common mistake is checking only one condition.

        For example, `[1, 5]` and `[3, 7]` overlap because `1 <= 7` AND `3 <= 5`.

        In our algorithm, we use: "no overlap before" means `end < newStart`, and "overlap" means `start <= newEnd`. These conditions partition all intervals correctly.
      wrong_approach: "Checking only if starts overlap or only if ends overlap"
      correct_approach: "Check both conditions: interval.end >= new.start AND interval.start <= new.end"

    - title: Mutating the New Interval Incorrectly
      description: |
        When merging, you must expand `newInterval` using both `min` for the start and `max` for the end. A common bug is only updating one bound.

        For example, merging `[4, 8]` with `[3, 5]` should give `[3, 8]`, not `[4, 8]` or `[3, 5]`.
      wrong_approach: "Only updating end or only updating start during merge"
      correct_approach: "Always update: start = min(start, interval.start), end = max(end, interval.end)"

  key_takeaways:
    - "**Intervals pattern**: When intervals are sorted, overlapping intervals are always consecutive, enabling single-pass solutions"
    - "**Three-phase structure**: Before, during, and after overlap is a common pattern for interval insertion and merging problems"
    - "**Overlap condition**: Two intervals `[a,b]` and `[c,d]` overlap if and only if `max(a,c) <= min(b,d)`"
    - "**Foundation for harder problems**: This technique extends to Merge Intervals, Meeting Rooms, and interval scheduling problems"

  time_complexity: "O(n). We traverse the list of intervals exactly once, performing constant-time operations for each interval."
  space_complexity: "O(n). We create a new result list that stores all intervals. In the worst case (no merging), this contains n+1 intervals."

solutions:
  - approach_name: Single Pass with Three Phases
    is_optimal: true
    code: |
      def insert(intervals: list[list[int]], newInterval: list[int]) -> list[list[int]]:
          result = []
          i = 0
          n = len(intervals)

          # Phase 1: Add all intervals that end before newInterval starts
          while i < n and intervals[i][1] < newInterval[0]:
              result.append(intervals[i])
              i += 1

          # Phase 2: Merge all overlapping intervals with newInterval
          while i < n and intervals[i][0] <= newInterval[1]:
              # Expand newInterval to include the overlapping interval
              newInterval[0] = min(newInterval[0], intervals[i][0])
              newInterval[1] = max(newInterval[1], intervals[i][1])
              i += 1

          # Add the merged interval
          result.append(newInterval)

          # Phase 3: Add all intervals that start after newInterval ends
          while i < n:
              result.append(intervals[i])
              i += 1

          return result
    explanation: |
      **Time Complexity:** O(n) — Single pass through all intervals.

      **Space Complexity:** O(n) — Result list stores up to n+1 intervals.

      We process intervals in three phases: (1) add non-overlapping intervals before, (2) merge all overlapping intervals into one, (3) add non-overlapping intervals after. The sorted property guarantees overlapping intervals are consecutive.

  - approach_name: Binary Search Optimisation
    is_optimal: false
    code: |
      import bisect

      def insert(intervals: list[list[int]], newInterval: list[int]) -> list[list[int]]:
          if not intervals:
              return [newInterval]

          # Find where overlaps might start and end using binary search
          starts = [interval[0] for interval in intervals]
          ends = [interval[1] for interval in intervals]

          # Find first interval that might overlap (ends >= newInterval start)
          left = bisect.bisect_left(ends, newInterval[0])

          # Find last interval that might overlap (starts <= newInterval end)
          right = bisect.bisect_right(starts, newInterval[1])

          # If there are overlapping intervals, merge them
          if left < right:
              newInterval[0] = min(newInterval[0], intervals[left][0])
              newInterval[1] = max(newInterval[1], intervals[right - 1][1])

          # Build result: before + merged + after
          return intervals[:left] + [newInterval] + intervals[right:]
    explanation: |
      **Time Complexity:** O(n) — While binary search is O(log n), slicing creates new lists in O(n).

      **Space Complexity:** O(n) — Creating lists for starts, ends, and the result.

      This approach uses binary search to find the range of overlapping intervals quickly. While binary search itself is O(log n), the overall complexity remains O(n) due to list slicing. This approach is more elegant but not faster in practice. It's included to show how binary search can identify overlap boundaries.
