MediumLeetCode 986Intervals — two pointers

Interval List Intersections (LeetCode 986)

Problem 6 of 10 in Merge Intervals — Practice Problems

The problem

You get two lists of closed intervals, A and B. Each list is sorted and its intervals do not overlap each other. Return every interval where the two lists overlap.

A = [[0, 2], [5, 10], [13, 23], [24, 25]]
B = [[1, 5], [8, 12], [15, 24], [25, 26]]

Output: [[1, 2], [5, 5], [8, 10], [15, 23], [24, 24], [25, 25]]

For example [0, 2] and [1, 5] share [1, 2], and [5, 10] and [1, 5] share just the point [5, 5].

How to think about it

Start with brute force. Compare every interval in A with every interval in B and keep the non-empty overlaps. That is O(m·n), and it ignores that both lists are sorted.

How to intersect two intervals. The overlap of [a1, a2] and [b1, b2] starts at the later start and ends at the earlier end: [max(a1, b1), min(a2, b2)]. It is real only if lo <= hi.

The key insight: advance the one that ends first. Look at the current pair A[i], B[j]. Whichever ends earlier cannot overlap anything further in the other list, because every later interval there starts even later. So it is finished; move its pointer. The other interval might still overlap the next one, so keep it. Each step moves one pointer, so the walk is linear.

Which pattern is this? Intervals with two pointers, like merging two sorted lists. The tell is “two sorted interval lists”.

Approach

  1. Set i = j = 0 and res = [].
  2. While both pointers are in range, compute lo = max(A[i][0], B[j][0]) and hi = min(A[i][1], B[j][1]).
  3. If lo <= hi, append [lo, hi].
  4. If A[i] ends before B[j], advance i; otherwise advance j.
  5. Return res.

Solution

def interval_intersection(A, B):
    i = j = 0; res = []
    while i < len(A) and j < len(B):
        lo = max(A[i][0], B[j][0]); hi = min(A[i][1], B[j][1])   # candidate overlap
        if lo <= hi: res.append([lo, hi])                       # non-empty: keep it
        if A[i][1] < B[j][1]: i += 1                            # A[i] ends first: done with it
        else: j += 1
    return res
# interval_intersection([[0,2],[5,10],[13,23],[24,25]], [[1,5],[8,12],[15,24],[25,26]])
#   -> [[1,2],[5,5],[8,10],[15,23],[24,24],[25,25]]

Dry run

A[i] B[j] lo, hi Added Advance
[0, 2] [1, 5] 1, 2 [1, 2] i
[5, 10] [1, 5] 5, 5 [5, 5] j
[5, 10] [8, 12] 8, 10 [8, 10] i
[13, 23] [8, 12] 13, 12 none j
[13, 23] [15, 24] 15, 23 [15, 23] i
[24, 25] [15, 24] 24, 24 [24, 24] j
[24, 25] [25, 26] 25, 25 [25, 25] i

i runs off the end of A, so the loop stops with six intersections.

Complexity

  • Time: O(m + n). Every iteration advances one pointer.
  • Space: O(1) extra. Only the output list grows (it has at most m + n entries).

Edge cases and pitfalls

  • Either list empty returns [].
  • Single-point overlaps like [5, 5] count because intervals are closed; use lo <= hi, not lo < hi.
  • Equal ends: when A[i][1] == B[j][1] the code advances j. Either choice is fine, since both are finished and the other will be advanced next round.
  • Advancing by start instead of end skips overlaps: an interval that starts early but ends late can still overlap the next interval in the other list.