Binary Search & Intervals
Two ideas that both start with "sort it". Binary search throws away half of a sorted (or merely monotone) search space per probe, turning a million candidates into twenty comparisons. Interval problems sort by start time so that every overlap becomes a neighbour, and then a single sweep resolves them. Drills 33โ39 cover both, from the five-line lower bound to the partition trick behind the median of two sorted arrays.
Order Lets You Skip Work
Binary search is not really about sorted arrays. It is about a yes/no question whose answers, read left to right, look like no, no, no, yes, yes, yes. "Is nums[i] >= target?" on a sorted array has that shape. So does "is nums[i] > nums[i + 1]?" when looking for a peak, and "is nums[i] <= nums[last]?" on a rotated array. Once the question is monotone, probing the middle tells you which half cannot contain the boundary, and you discard it: n โ n/2 โ n/4 โ โฆ โ 1 in โlogโ nโ steps.
Interval problems use order differently. A set of ranges [start, end] can overlap in tangled ways, but once they are sorted by start, an interval can only overlap the ones immediately before it in that order. Sorting costs O(n log n); the sweep that follows is O(n).
Lower bound
First index where the predicate turns true. Drill 33.
Neighbour compare
No global sort needed โ only a local slope or a comparison with the end. Drills 34, 35.
Search in 2D
Staircase walk, or flatten to one index. Drill 36.
Search a partition
Binary search over how many items go left. Drill 37.
Sort + sweep
Merge touching ranges in one pass. Drill 38.
Overlap / rooms
Adjacent check, or a min-heap of end times. Drill 39.
How These Problems Think
mid tells you which side of the boundary mid is on โ the other half can be discarded without looking at itmid rounds, and how bounds move- "Must run in O(log n)" is the interviewer telling you to binary search โ even if the array is not sorted in the usual sense (Drills 34, 35).
- "Sorted" plus "find" or "insert" โ lower bound. Memorise it once; use it everywhere.
- "Intervals", "meetings", "bookings", "ranges" โ sort by start (or sweep start/end events) before anything else.
Three Templates That Never Off-By-One
There are many correct ways to write binary search and far more incorrect ones. The cure is to commit to a small number of templates, each with a stated invariant, and never mix their pieces.
n if none). Invariant: the answer is always in [lo, hi], and every index left of lo is too small.lo when they meet.| Template | Loop | Bounds | Use for |
|---|---|---|---|
| Exact match | lo <= hi | lo = mid + 1 / hi = mid - 1 | "Is x present?" โ returns inside the loop |
| Lower bound (T1) | lo < hi, hi = n | lo = mid + 1 / hi = mid | Insert position, first โฅ, count โค |
| Converge (T2) | lo < hi, hi = n โ 1 | lo = mid + 1 / hi = mid | Peak, rotated minimum โ answer always exists |
Search Insert Position
Given a sorted array of distinct integers and a target, return the index of the target if present; otherwise return the index where it would be inserted to keep the array sorted. Must run in O(log n).
Brute force โ optimal. Scanning left to right until nums[i] >= target is O(n). Because the predicate nums[i] >= target is false-then-true on a sorted array, binary search finds the switch point in O(log n). Using the half-open range [0, n) makes "insert at the end" (answer n) fall out naturally.
| step | lo | hi | mid | nums[mid] | decision |
|---|---|---|---|---|---|
| 1 | 0 | 4 | 2 | 5 | 5 < 7 โ lo = 3 |
| 2 | 3 | 4 | 3 | 6 | 6 < 7 โ lo = 4 |
| โ | 4 | 4 | โ | โ | lo == hi โ return 4 |
| Approach | Time | Space | Note |
|---|---|---|---|
| Linear scan | O(n) | O(1) | Ignores sortedness |
| Lower bound | O(log n) | O(1) | Same as Arrays.binarySearch's insertion point |
int mid = (lo + hi) / 2overflows whenlo + hi > Integer.MAX_VALUE. Uselo + (hi - lo) / 2(or(lo + hi) >>> 1).- Starting with
hi = n - 1in the half-open template โ target 7 would return 3 instead of 4. Arrays.binarySearchreturns-(insertionPoint) - 1when not found, not the insertion point itself.- With duplicates, this template returns the first equal index โ the exact-match template may return any of them.
Find Peak Element
A peak is an element strictly greater than its neighbours; positions outside the array count as โโ. Adjacent elements are never equal. Return the index of any peak in O(log n).
nums[mid] vs nums[mid + 1] rules out half the array.
Brute force โ optimal. A linear scan for the first i with nums[i] > nums[i + 1] is O(n). The array is not sorted, but the predicate "slope goes down at i" has a guaranteed true somewhere in every uphill-ending range, which is all Template 2 needs. The array need not be monotone overall โ we are not finding the maximum, only a peak.
| Approach | Time | Space | Note |
|---|---|---|---|
| Linear scan for first descent | O(n) | O(1) | Correct, too slow for the stated bound |
| Binary search on slope | O(log n) | O(1) | Needs adjacent elements to differ |
- Using
lo <= hiโ whenlo == hi == n - 1,nums[mid + 1]is out of bounds. Withlo < hiand a rounded-down mid,mid + 1 <= hialways. - Comparing with
nums[mid - 1]as well and branching three ways โ more code, more bounds checks, no benefit. - Assuming the result is the global maximum. It is a peak; [1, 2, 1, 3, 5, 6, 4] has two.
- If equal neighbours were allowed, the argument breaks: [1, 1, 1, 2, 1, 1] cannot be searched in O(log n) in the worst case.
Find Minimum in Rotated Sorted Array
A sorted array of distinct values was rotated at an unknown pivot, e.g. [0,1,2,4,5,6,7] became [4,5,6,7,0,1,2]. Return the minimum element in O(log n).
nums[mid] with nums[hi]: if it is bigger, mid is on the left ramp and the drop (the minimum) is strictly to its right; otherwise mid is on the right ramp and the minimum is at mid or to its left. The predicate "nums[i] <= nums[last]" is false-then-true, and the minimum is its first true.
Brute force โ optimal. A linear scan for the drop (or simply the min) is O(n). Comparing mid with hi โ not with lo โ gives a clean two-way split with no special case for the un-rotated array, because in a sorted array nums[mid] <= nums[hi] always and hi marches left to index 0.
| step | lo | hi | mid | nums[mid] vs nums[hi] | decision |
|---|---|---|---|---|---|
| 1 | 0 | 6 | 3 | 7 > 2 | left ramp โ lo = 4 |
| 2 | 4 | 6 | 5 | 1 โค 2 | right ramp โ hi = 5 |
| 3 | 4 | 5 | 4 | 0 โค 1 | right ramp โ hi = 4 |
| โ | 4 | 4 | โ | โ | return nums[4] = 0 |
| Approach | Time | Space | Note |
|---|---|---|---|
| Linear min | O(n) | O(1) | Ignores structure |
| Binary search vs nums[hi] | O(log n) | O(1) | Distinct values required |
| Same with duplicates (LC 154) | O(log n) avg, O(n) worst | O(1) | On a tie, hi-- |
- Comparing with
nums[lo]instead ofnums[hi]: for an un-rotated arraynums[mid] > nums[lo]would push you right, away from the minimum. - Writing
hi = mid - 1in the "right ramp" branch โmiditself may be the minimum (step 3 above). - With duplicates (
[3,3,1,3]),nums[mid] == nums[hi]tells you nothing; this code is only correct for distinct values.
Search a 2D Sorted Matrix
An m ร n matrix has every row sorted left to right and every column sorted top to bottom. Decide whether target is in it. (Variant LC 74 is stricter: each row also starts after the previous row ends, so the whole matrix is one sorted list.)
Brute force โ optimal. Scanning every cell is O(mยทn). Binary searching each row is O(m log n). The staircase walk is O(m + n), which is optimal for this matrix shape. For the LC 74 shape, treat index k in [0, mยทn) as cell (k / n, k % n) and run one ordinary binary search: O(log(mยทn)). The top-left corner does not work as a start โ both neighbours are larger, so there is no decision.
| Approach | Time | Space | Note |
|---|---|---|---|
| Scan every cell | O(m ยท n) | O(1) | Ignores both orderings |
| Binary search each row | O(m log n) | O(1) | Uses only the row ordering |
| Staircase from top-right | O(m + n) | O(1) | Rows and columns sorted (LC 240) |
| Flattened binary search | O(log(m ยท n)) | O(1) | Only when rows chain (LC 74) |
- Starting at the top-left (or bottom-right): both moves increase (or decrease) the value, so there is no rule for which way to go.
- Using the flattened search on an LC 240 matrix โ row 1 (2, 5, 8 โฆ) is not entirely greater than row 0, so the flat order is not sorted.
- Index math with columns:
row = k / cols,col = k % colsโ dividing byrowsis wrong for non-square matrices. - Not guarding
matrix.length == 0 || matrix[0].length == 0before readingmatrix[0].length.
Median of Two Sorted Arrays
Given sorted arrays a (length m) and b (length n), return the median of all m + n values. The required complexity is O(log(m + n)) โ merging is not allowed.
i come from a; then j = half โ i must come from b. The split is correct exactly when a[iโ1] โค b[j] and b[jโ1] โค a[i] โ every left element โค every right element. If a[iโ1] is too big, take fewer from a; if b[jโ1] is too big, take more. That is a monotone yes/no over i, so binary search it.
Brute force โ optimal. Merging both arrays and picking the middle is O(m + n) time and space; a two-pointer walk to the middle drops the space to O(1) but is still O(m + n). Binary searching i over the shorter array gives O(log min(m, n)). Searching the shorter array also guarantees j stays within [0, n]. Sentinels (Integer.MIN_VALUE / MAX_VALUE) stand in for "nothing on this side" when i or j is 0 or at the end.
| Approach | Time | Space | Note |
|---|---|---|---|
| Merge, take middle | O(m + n) | O(m + n) | Simplest, violates the bound |
| Two-pointer walk to the middle | O(m + n) | O(1) | Merge without storing |
| Binary search on partition | O(log min(m, n)) | O(1) | Swap so a is shorter |
- Not searching the shorter array: with
alonger,j = half โ ican go negative and index out of bounds. - Averaging with
(x + y) / 2.0onints: if both are nearInteger.MAX_VALUEthe sum overflows before the division. Widen tolongfirst. - Falling out of the loop with
return 0.0hides bad input; throwing makes an unsorted input fail loudly. half = (m + n) / 2instead of(m + n + 1) / 2puts the extra element on the right, and then the odd-case answer ismin(aRight, bRight), notmax(aLeft, bLeft). Pick one convention and keep it.
Merge Intervals
Given an array of intervals [start, end] in any order, merge all overlapping intervals and return the non-overlapping intervals that cover exactly the same points. Intervals that touch ([1,4] and [4,5]) count as overlapping.
Brute force โ optimal. Repeatedly scanning all pairs and merging any two that overlap until nothing changes is O(nยฒ) per pass and up to O(nยณ) overall. Sorting first costs O(n log n) and makes the merge a single O(n) sweep โ Template 3.
| next | current before | test | action | output so far |
|---|---|---|---|---|
| [1,3] | [1,3] | 1 โค 3 | end = max(3, 3) โ the first interval meets itself, harmless | [1,3] |
| [2,6] | [1,3] | 2 โค 3 | end = max(3, 6) = 6 | [1,6] |
| [8,10] | [1,6] | 8 > 6 | start new block | [1,6] [8,10] |
| [15,18] | [8,10] | 15 > 10 | start new block | [1,6] [8,10] [15,18] |
| Approach | Time | Space | Note |
|---|---|---|---|
| Pairwise merge until stable | O(nยณ) | O(n) | Each pass O(nยฒ) |
| Sort + sweep | O(n log n) | O(n) | Output list; sort itself uses O(log n) |
current[1] = next[1]instead ofMath.max(current[1], next[1])โ a nested interval like [2,3] inside [1,4] would shrink the block (Template 3 diagram).- Using
<instead of<=for the overlap test โ [1,4] and [4,5] would not merge. - Comparator
(a, b) -> a[0] - b[0]overflows for large negative/positive starts.Integer.compareis always safe. - Mutating the caller's arrays: extending
result.get(last)[1]whenlastis the caller'sint[]rewrites their input. Clone the arrays you store.
Meeting Rooms (and the Rooms-Needed Follow-Up)
Difficulty
Easy ยท follow-up Medium
Pattern
sort by start min-heap
Given meeting times [start, end), decide whether one person can attend all of them. A meeting ending at 10 and one starting at 10 do not conflict. The natural follow-up (LC 253) asks for the minimum number of rooms so that every meeting can be held.
i < j overlap, then start[i+1] โค start[j] < end[i], so meeting i also overlaps its neighbour i + 1. Checking each meeting against the one just before it is therefore enough for the yes/no question. Counting rooms needs more state, because a long earlier meeting may still be running: keep a min-heap of the end times of meetings in progress โ its top says whether any room has freed up.
Brute force โ optimal. Comparing every pair is O(nยฒ). For the yes/no question, sorting and checking adjacent pairs is O(n log n): if any two meetings overlap, then in sorted order some adjacent pair overlaps. For the room count, push each meeting's end time onto a min-heap; before pushing, if the earliest-ending meeting has finished (peek() <= start), reuse its room by polling. The heap size at the end is the peak number of simultaneous meetings.
| meeting | heap before | peek โค start? | action | heap after |
|---|---|---|---|---|
| [0,30] | [] | โ | push 30 | [30] |
| [5,10] | [30] | 30 โค 5? no | push 10 (new room) | [10, 30] |
| [15,20] | [10, 30] | 10 โค 15? yes | poll 10, push 20 (reuse) | [20, 30] |
| โ | โ | โ | answer = heap size | 2 |
| Approach | Time | Space | Note |
|---|---|---|---|
| All pairs overlap test | O(nยฒ) | O(1) | Answers both questions slowly |
| Sort + adjacent check (LC 252) | O(n log n) | O(log n) | Sort's stack; not O(1) as often claimed |
| Sort + min-heap (LC 253) | O(n log n) | O(n) | Heap holds active meetings |
| Two sorted arrays of starts/ends | O(n log n) | O(n) | Event sweep, no heap |
- Comparator
(a, b) -> a[0] - b[0]โ overflows for extreme values and sorts wrongly. UseInteger.compare(a[0], b[0]). <=instead of<in the conflict test: back-to-back meetings [5,10] and [10,15] do not conflict.- In the rooms version, sorting by end time instead of start time โ the heap logic assumes meetings arrive in start order.
- Claiming O(1) space:
Arrays.sorton objects (int[][]) is a merge sort (TimSort) that may allocate up to n/2 references.
All Seven Drills at a Glance
| # | Drill | Pattern | Time | Space | Difficulty |
|---|---|---|---|---|---|
| 33 | Search Insert Position | Lower bound, half-open | O(log n) | O(1) | Easy |
| 34 | Find Peak Element | Converge on slope | O(log n) | O(1) | Medium |
| 35 | Find Minimum in Rotated Array | Converge, compare with hi | O(log n) | O(1) | Medium |
| 36 | Search a 2D Sorted Matrix | Staircase from top-right | O(m + n) | O(1) | Medium |
| 37 | Median of Two Sorted Arrays | Binary search on partition | O(log min(m, n)) | O(1) | Hard |
| 38 | Merge Intervals | Sort by start + sweep | O(n log n) | O(n) | Medium |
| 39 | Meeting Rooms | Sort + adjacent check (heap for rooms) | O(n log n) | O(log n) / O(n) | Easy |
- What is the yes/no question, and is it false-then-true over the index?
- Is the range
[lo, hi)or[lo, hi], and what does the loop return when it ends? - Which update keeps
midas a candidate โ and doesmidround the right way for it?
- Next page: 05 ยท Linked Lists โ Drills 40โ50, pointer surgery without indices.
- Previous page: 03 ยท Strings & Sliding Window.
- Theory: Binary Search, Sorting, Heap, Greedy.
- More problems: LeetCode binary-search set and LeetCode intervals set โ try Search in Rotated Sorted Array (LC 33) and Insert Interval (LC 57) next.
โ 03 Strings & Sliding Window All drills 05 Linked Lists โ