skip to content

In a two-pointer scan of two sorted interval lists, why advance the pointer whose interval ends first?

level: middleimportance: should knowfreq 44%

answer

  1. Ask what each interval could still meet
  2. The next window in a list starts after this one ends
  3. One interval is finished with the other list
  4. Retiring it discards no intersection
  5. One retirement per step gives linear time

basics

~20 s

The interval that ends first cannot meet anything further along the other list, because every later interval there begins at or after the current one's end. Discarding it loses no intersection, and each step consumes one interval, giving a linear scan.

solid answer

~40 s

Both lists are sorted by start and internally disjoint, so within one list every later interval begins at or after the previous one ends. Suppose the interval at pointer `i` ends no later than the interval at pointer `j`. Then the next interval in `j`'s list starts at or after `j`'s current end, which is at or after `i`'s end — so `i`'s interval cannot overlap it, or anything beyond it. Its work is finished, and advancing `i` discards nothing. Advancing by *start* instead would be wrong: an interval that begins earlier may still run long and overlap several intervals ahead in the other list. Because every step retires exactly one interval, the scan is `O(n + m)` — the intersection itself is `[max(starts), min(ends))`, emitted whenever that window is non-empty.

code

pseudocode · 11 lines
pseudocode
i = 0
j = 0
while i < length(A) and j < length(B)
    lo = max(A[i].start, B[j].start)
    hi = min(A[i].end, B[j].end)
    if lo < hi
        emit(lo, hi)
    if A[i].end < B[j].end
        i = i + 1
    else
        j = j + 1

go deeper

for a junior

Know the intersection formula: later of the two starts, earlier of the two ends, kept only when that window is non-empty. Be able to say the scan is linear rather than a nested loop.

for a middle

Explain the advance rule and prove it: the interval ending first cannot reach anything further along the other list. Name the precondition that each list is sorted and internally disjoint.

for a senior

Show judgment about input shape — when lopsided list sizes make searching beat scanning, and what you do when a caller hands you a list with internal overlaps rather than assuming it clean.

for a principal

Own the contract at the boundary. Decide whether normalisation is the caller's duty or the service's, and be ready to defend paying for a validation pass against the cost of silently wrong availability results.

## The setting Two ground stations each keep a list of the windows during which a satellite is visible to them. Each list is sorted by start time and **internally disjoint** — one station's windows never overlap each other. The task is to report every window during which the satellite is visible to *both* stations at once. The brute-force answer is to test every window of one list against every window of the other, which is `O(n * m)`. The two-pointer scan does it in `O(n + m)`, and the whole thing turns on one rule: **advance the pointer whose current interval ends first**. ## The two moving parts **Emitting.** For the two intervals currently under the pointers, the shared window is `[max(A[i].start, B[j].start), min(A[i].end, B[j].end))`. It is non-empty — and therefore worth emitting — exactly when that lower bound is strictly less than that upper bound. That is the pairwise overlap condition restated in terms of the two extrema, and it is the only overlap test the scan needs. **Advancing.** Exactly one pointer moves per step, and it is the one whose interval has the smaller end (ties can go either way). ## Why the smaller end is the right one to retire Say `A[i].end <= B[j].end`. Look at what `A[i]` could still possibly meet: - It has already been compared against `B[j]`. - The next window in `B` begins at or after `B[j].end` — that is what *sorted and internally disjoint* buys you — and `B[j].end >= A[i].end`. So `B[j+1].start >= A[i].end`, which means `A[i]` and `B[j+1]` fail the overlap test, and so does every window after it, since starts only grow. `A[i]` therefore has no remaining business anywhere in `B`, and dropping it discards no intersection. The symmetric argument holds when `B[j]` ends first. Since each step retires exactly one interval and never revisits it, the scan makes at most `n + m` steps. ## The wrong rule, and what it costs The tempting alternative is to advance whichever interval *starts* first. That breaks immediately: an early-starting window may be long and still overlap several windows ahead in the other list. Retiring it after one comparison silently drops those intersections. A related mistake is advancing *both* pointers after emitting a result — the same trap, since the interval that outlasts the other still has intersections waiting. ## The precondition is not decoration The safety argument used both properties of each list: sorted by start **and** internally non-overlapping. If one list may contain windows that overlap each other, `B[j+1].start` is no longer bounded below by `B[j].end`, and `B[j+1]` may well still intersect the `A[i]` you just retired. If the input can be ragged, the honest fix is to normalise each list first — coalesce each list into disjoint windows sorted by start — and then run the scan. Saying that precondition out loud is a large part of what the question is testing. ## Cost and alternatives The scan is `O(n + m)` time and `O(1)` auxiliary space beyond the output. Pouring both lists into one collection and sorting is `O((n + m) log(n + m))` and throws away the ordering the inputs already had; it is the right move only when the inputs genuinely arrive unsorted. When the lists are wildly asymmetric — one holds a handful of windows, the other holds millions — the linear scan is no longer the obvious winner: locating each of the few windows by binary search into the large sorted list costs `O(k log N)`, which beats `O(N)` when `k` is small. That is a real judgment call rather than a trick, and it depends on knowing the shape of the data. ## Boundary details worth stating - Under half-open semantics, emit only when the computed lower bound is **strictly** below the upper bound; two windows that merely touch produce an empty result and should be dropped, not emitted as a zero-length window. - Ties in the end comparison are harmless: advancing either pointer is correct, because when both ends are equal both intervals are simultaneously finished with respect to the other list. - The scan generalises to more than two lists only awkwardly; with three or more, the pairwise fold — intersect two, then intersect the result with the third — keeps the same linear character per step and is far easier to defend than a k-way pointer dance.

  • What breaks if one of the two lists contains windows that overlap each other?
    The safety argument collapses. It relies on the next window in a list starting at or after the current one ends; with internal overlaps that bound is gone, so the interval you just retired may still intersect the next one in the other list, and results are silently lost. The fix is to normalise each list into disjoint windows sorted by start before scanning, and to state that precondition wherever the scan is exposed.
  • One list has five windows and the other has two million. Is the linear scan still the right call?
    Probably not. The scan is `O(n + m)`, which means walking all two million entries regardless of how few windows the small list holds. Binary searching the large sorted list for each of the five, then walking forward while overlap persists, costs `O(k log N)` plus the output size. The linear scan wins when the lists are comparable in size; the search-driven approach wins when they are lopsided.
  • Why emit only when the computed lower bound is strictly below the upper bound?
    Because with half-open ranges, equality means the two windows merely touch and share no instant — visibility from one station ends exactly as the other begins. Emitting that as a zero-length window would report a common visibility period that does not exist, and downstream consumers that assume positive duration would then divide by zero or schedule an impossible transmission.

saying these in an interview costs you the question

  • Advances the pointer whose interval starts first
  • Advances both pointers after emitting an intersection
  • Ignores the requirement that each list be internally disjoint
  • Emits zero-length results for windows that merely touch
  • Claims the scan needs a nested loop and is therefore quadratic
  • Merges and re-sorts both lists when they already arrive sorted

context