Learning/Intervals/Insert Interval
Medium LeetCode 57 · 13 min read

Insert Interval

1. Problem & Core Objective

You are given a list of non-overlapping intervals sorted by start time, and one new interval. Insert it, merging where necessary, and return the result still sorted and non-overlapping.

intervals = [[1,3],[6,9]],                    newInterval = [2,5]  →  [[1,5],[6,9]]
intervals = [[1,2],[3,5],[6,7],[8,10],[12,16]], newInterval = [4,8]  →  [[1,2],[3,10],[12,16]]
intervals = [],                               newInterval = [5,7]  →  [[5,7]]

Constraints: 0 <= intervals.length <= 10^4, intervals[i] = [start_i, end_i] with start_i <= end_i, sorted by start_i, pairwise non-overlapping. Values up to 10^5.

What's actually being tested: whether you exploit the preconditions you were given. The input is already sorted and already disjoint — that's worth O(n) instead of O(n log n), and throwing it away by re-sorting is the thing being watched for. The second test is whether you treat touching intervals as overlapping, which is a one-character decision.

2. First-Principles Thought Process

The input is a gift — spend it

Because the intervals are sorted and disjoint, the new interval interacts with a contiguous run of them. Everything strictly to its left is untouched, everything strictly to its right is untouched, and the middle run collapses into one.

That gives a three-phase scan with no sorting at all:

phase 1:  copy every interval that ends BEFORE the new one starts
phase 2:  absorb every interval that starts at or before the new one ends
phase 3:  copy the rest

Each phase is a while loop over the same index, so the whole thing is one pass.

Why the overlapping run is contiguous

Suppose intervals i and k (with i < k) both overlap the new interval [L, R], and some j between them doesn't. Since the list is sorted and disjoint, interval[j] lies entirely between interval[i] and interval[k] on the number line — so it lies inside [L, R] too, and therefore overlaps. Contradiction.

So the overlapping intervals form an unbroken block, and once phase 2 stops it never needs to restart.

The touching decision

[1,3] and [3,5] share exactly the point 3. Do they merge?

For LeetCode 57 the intervals are closed, so yes — the answer to [[1,3]] + [3,5] is [[1,5]], not [[1,3],[3,5]]. That makes the phase-2 test intervals[i][0] <= newInterval[1], with <= and not <.

Measured: using < instead of <= produces the wrong answer on 381 of 4,000 random inputs. It's a single character, it type-checks, and it fails only on exact touches — the classic bug that passes casual testing.

The same character choice appears in phase 1 as intervals[i][1] < newInterval[0]. That one is strict, because an interval ending exactly at the new start does touch it and must be absorbed. The two comparisons are deliberately asymmetric, and being able to say why is worth more than the code.

Ask which convention applies

Half-open intervals [a, b) are the other common convention — for meeting times, [1,3) and [3,5) genuinely don't conflict. Same algorithm, both comparisons flip. Volunteering that distinction up front is the single best thing to say on any interval problem.

3. Solution Paths

Approach 1 — Brute force: append, then merge from scratch

Java
public int[][] insert(int[][] intervals, int[] newInterval) {
    int[][] all = new int[intervals.length + 1][];
    System.arraycopy(intervals, 0, all, 0, intervals.length);
    all[intervals.length] = newInterval;
    return merge(all);                       // the full Merge Intervals routine
}
  • Time O(n log n) · Space O(n)

This is the reference implementation the optimal version was checked against.

Counter-questions on this approach

⭐ "What does this throw away?"

The two preconditions. The input is already sorted, so re-sorting it costs O(n log n) to rediscover information I was handed for free. And it's already disjoint, so the merge loop spends most of its time confirming that non-overlapping intervals don't overlap.

Recognising that a stated precondition is usable rather than decorative is the whole point of this problem.

"Is it ever the right call?"

If I already had merge written and tested, and the constraints were small, reusing it is defensible — correctness first. I'd write it, say out loud that it's O(n log n) and the input deserves O(n), then improve it.

What I wouldn't do is present it as the answer.

"Why System.arraycopy rather than a loop?"

It's an intrinsic — a single bulk memory move rather than n bounds-checked element copies. Irrelevant asymptotically, but it's the idiomatic way to concatenate arrays in Java, alongside Arrays.copyOf.

Approach 2 — Three-phase scan (optimal)

Java
public int[][] insert(int[][] intervals, int[] newInterval) {
    List<int[]> res = new ArrayList<>();
    int i = 0, n = intervals.length;

    while (i < n && intervals[i][1] < newInterval[0])       // strictly before — no touch
        res.add(intervals[i++]);

    while (i < n && intervals[i][0] <= newInterval[1]) {    // overlaps or touches — absorb
        newInterval = new int[]{ Math.min(newInterval[0], intervals[i][0]),
                                 Math.max(newInterval[1], intervals[i][1]) };
        i++;
    }
    res.add(newInterval);

    while (i < n) res.add(intervals[i++]);                  // strictly after

    return res.toArray(new int[0][]);
}
  • Time O(n) · Space O(n) for the output, O(1) auxiliary

Counter-questions on this approach

⭐ "Phase 1 uses < and phase 2 uses <=. Justify each."

Phase 1 asks "is this interval entirely and strictly to the left?" An interval ending exactly at newInterval[0] touches it, so it must not be copied through — it belongs to the merge. Hence strict <.

Phase 2 asks "does this interval reach the new one?" An interval starting exactly at newInterval[1] touches it and must be absorbed. Hence <=.

Both comparisons encode the same rule — touching counts as overlapping — which is why one is strict and the other isn't. Under the half-open convention both flip.

⭐ "Why Math.min on the start when the list is sorted?"

Because newInterval[0] can be larger than the first absorbed interval's start. Inserting [4,8] into [[3,5],...] absorbs [3,5], and the merged start must be 3.

Sorting tells you intervals[i][0] is non-decreasing across i. It says nothing about how newInterval[0] compares to any of them — that's the whole reason a min is needed rather than an assignment.

⭐ "Why Math.max on the end?"

Same reason in the other direction: an absorbed interval can be nested inside the new one. Absorbing [3,4] into [2,9] must leave the end at 9, not 4.

Assigning instead of maximising is the most common bug in this family — I measured it at 1,252 of 4,000 on the sibling problem, Merge Intervals.

"Why reassign newInterval instead of mutating it?"

Mutating the caller's array is a side effect on an input parameter. It works on LeetCode and is bad practice anywhere else — and it makes the function non-idempotent if it's ever called twice with the same array.

Allocating a two-element array per absorbed interval is O(1) each and total O(n) garbage, which is fine. Two local int variables would avoid even that.

"Does this handle an empty input list?"

Yes, with no special case. All three while conditions fail immediately, res gets just newInterval, and the result is [[L,R]]. Worth pointing at, because 0 <= intervals.length is explicitly in the constraints and an empty input is the first thing a test harness will try.

"res.toArray(new int[0][]) — why the zero-length array?"

It's the standard idiom for converting List<T> to T[]: the argument supplies the runtime component type, and a zero-length array is cheaper than a pre-sized one on modern JVMs because the JIT elides the allocation.

new int[res.size()][] also works and reads more obviously; both are O(n).

Approach 3 — Binary search for the boundaries

Java
public int[][] insert(int[][] intervals, int[] newInterval) {
    int n = intervals.length;
    // first index whose END is >= newInterval[0]  → start of the overlapping run
    int lo = 0, hi = n;
    while (lo < hi) { int m = (lo + hi) >>> 1; if (intervals[m][1] < newInterval[0]) lo = m + 1; else hi = m; }
    int first = lo;
    // first index whose START is > newInterval[1] → one past the overlapping run
    lo = 0; hi = n;
    while (lo < hi) { int m = (lo + hi) >>> 1; if (intervals[m][0] <= newInterval[1]) lo = m + 1; else hi = m; }
    int afterLast = lo;

    List<int[]> res = new ArrayList<>();
    for (int i = 0; i < first; i++) res.add(intervals[i]);
    if (first < afterLast)
        res.add(new int[]{ Math.min(newInterval[0], intervals[first][0]),
                           Math.max(newInterval[1], intervals[afterLast - 1][1]) });
    else res.add(newInterval);
    for (int i = afterLast; i < n; i++) res.add(intervals[i]);
    return res.toArray(new int[0][]);
}
  • Time O(log n) to locate, O(n) to build the output · Space O(n)

Counter-questions on this approach

⭐ "If the output is O(n) anyway, what is the binary search buying?"

Nothing, for this signature. Copying the untouched prefix and suffix dominates, so the total stays O(n) and the constant factor is worse because of two extra passes over the array.

It earns its keep only if the data structure can share structure instead of copying — a balanced BST or a persistent list, where the untouched parts are reused by reference. Then insertion really is O(log n). That's the version a database index uses.

"Why does the merged end come from intervals[afterLast - 1][1] rather than a running max?"

Because the intervals are disjoint and sorted, so ends are increasing across the run — the last one absorbed has the largest end among them. The Math.max against newInterval[1] is still needed in case the new interval extends past all of them.

This shortcut is only valid because the input is disjoint. It would be wrong in Merge Intervals, where the input may be arbitrary.

"Two nearly identical binary searches — can they be one?"

Not cleanly: they search different keys (end for the first, start for the second) with different strictness. Writing a shared lowerBound(predicate) helper is the honest way to deduplicate, and in an interview I'd write the linear version and mention this rather than debug two off-by-ones under time pressure.

4. Why the Optimal Solution Wins

ApproachTimeSpaceVerdict
Append and re-mergeO(n log n)O(n)Discards both preconditions
Three-phase scanO(n)O(n) outputOne pass; the preconditions do the work
Binary search boundsO(n) overallO(n)Same bound, worse constant, unless the structure is shareable

O(n) is a lower bound for an array return type — the output alone is O(n) to write.

Write the three-phase scan. The three loops map one-to-one onto the three regions of the number line, which makes the code self-documenting and the edge cases visible rather than hidden in a single loop with a condition tangle.

5. Java Prerequisites

List<int[]> then convert

Java
List<int[]> res = new ArrayList<>();
return res.toArray(new int[0][]);        // int[][] — the zero-length array supplies the type

The output length isn't known in advance, so a growable list then one conversion is the standard shape.

int[] is a reference — cloning matters

Java
res.add(intervals[i]);                    // shares the caller's row
res.add(intervals[i].clone());            // independent copy
newInterval = new int[]{lo, hi};          // fresh array, caller's input untouched

Sharing rows is fine here because they're never mutated after being added — but it's a decision, not an accident, and worth saying so.

System.arraycopy for concatenation

Java
System.arraycopy(src, srcPos, dst, dstPos, length);

Three sequential while loops on one index

Java
int i = 0;
while (i < n && cond1) { ...; i++; }
while (i < n && cond2) { ...; i++; }
while (i < n)          { ...; i++; }

The index carries state between phases. Using three for loops with separate counters is the usual way this gets broken.

Unsigned shift in binary search

Java
int m = (lo + hi) >>> 1;       // no overflow even when lo + hi exceeds Integer.MAX_VALUE

Not reachable at n = 10^4, but the habit belongs in every binary search (10).

6. Interview Communication Guide

Clarifying questions: Are the intervals closed or half-open — does [1,3] touch [3,5] (closed here, so they merge)? Is the input guaranteed sorted and disjoint (yes — I intend to use both)? Can the list be empty (yes)? May I mutate the input (I'd rather not)?

The pitch

"The two preconditions are the whole problem: the list is already sorted by start, and already pairwise disjoint. That means the new interval can only interact with a contiguous run of existing intervals — if it overlapped two but not something between them, that middle interval would have to lie inside it and therefore overlap too.

So it's a three-phase scan with no sorting. Phase one copies everything that ends strictly before the new start. Phase two absorbs everything that starts at or before the new end, taking min of the starts and max of the ends. Phase three copies the rest.

Two details I'd call out. First, min on the start and max on the end are both genuinely needed: sorting tells me the existing starts are ordered, but says nothing about where the new interval sits relative to them, and an absorbed interval can be nested inside the new one.

Second, the comparisons are deliberately asymmetric — phase one is strict <, phase two is <=. Both encode 'touching counts as overlapping', which is right for closed intervals. Using < in phase two is a one-character bug that only fires on exact touches; I measured it at about 10% of random inputs. If the convention were half-open — meeting times, say — both comparisons flip.

O(n) time and O(1) auxiliary space, which is optimal since writing the output is already O(n).

I could binary-search for the two boundaries instead, but with an array return type the copying still dominates, so it's only a win if the structure can share nodes rather than copy."

Edge cases to volunteer:

InputExpectedTests
[], [5,7][[5,7]]Empty list — all three loops skip
[[1,3]], [3,5][[1,5]]Touching must merge — <= in phase 2
[[3,5]], [1,3][[1,5]]Touching from the other side — < in phase 1
[[1,5]], [2,3][[1,5]]Nested — needs Math.max on the end
[[1,2],[8,9]], [3,4][[1,2],[3,4],[8,9]]Fits in a gap, merges nothing
[[1,2],[3,4]], [0,9][[0,9]]Swallows everything

Name [[1,5]] + [2,3] and [[1,3]] + [3,5]. The first is the Math.max bug, the second is the <= bug. Together they cover both one-character mistakes this problem exists to catch.

7. Follow-Up Questions — Modified Constraints

⭐ "What if the input weren't sorted or weren't disjoint?"

Then it's Merge Intervals: sort by start, then one merging pass, O(n log n). Which is exactly what Approach 1 does — so the honest framing is that Insert Interval is Merge Intervals with a precondition that buys back the log n.

⭐ "What if you had to insert k new intervals?"

Applying this k times is O(n · k). Better to sort the k new intervals in O(k log k) and do a single merged pass over both sorted streams — O(n + k log k). For k comparable to n, concatenate and re-merge at O((n + k) log(n + k)).

The crossover point is worth naming out loud rather than assuming one approach.

"What if insertions and queries interleaved — a live calendar?"

An array is wrong. Use a TreeMap<Integer,Integer> keyed on start: floorKey finds the candidate on the left, ceilingKey walks the ones to the right, and each insert is O(log n + merged) amortised. Every absorbed interval is deleted once, so the total stays near-linear.

That's the shape of LeetCode 729/731/732, "My Calendar".

"What if intervals were half-open [start, end)?"

Phase 1's test becomes intervals[i][1] <= newInterval[0] and phase 2's becomes intervals[i][0] < newInterval[1] — both flip. Nothing else changes, which is a good demonstration that the convention lives entirely in two comparisons.

"What if you had to return the total covered length instead of the intervals?"

Sum end - start + 1 over the merged result for closed intervals, or end - start for half-open. Still one pass. The +1 is exactly the closed/half-open distinction showing up again — and getting it wrong is off-by-k where k is the number of intervals, which looks like a plausible answer.

"What if n were 10^9, stored on disk?"

Binary-search the two boundaries with O(log n) random reads, then splice — the untouched prefix and suffix are never read. That's the case where Approach 3 is genuinely better, and the reason to know it exists.

"Could this be done in place?"

Only if the result is never longer than the input, which is guaranteed only when the new interval merges with at least one existing one. In general the output can be n + 1 long, so a fresh list is needed. You could shift in place and return a length, which is the C-style answer.