Binary Search on Rotated Arrays and Matrices
Plain binary search needs a sorted array. Interviewers rarely give you one. Instead they hand you an array that is almost sorted — rotated at an unknown pivot, or shaped like a mountain, or sorted along rows and columns of a matrix.
The unifying idea is broader than “sorted”:
Binary search works whenever you can look at
midmidand discard half the search space with certainty.
Sortedness is just the most familiar reason you can do that. This page covers the three other reasons that show up constantly.
What you’ll learn
- The “which half is sorted” test that handles rotation.
- Why
lo < hilo < hiwithhi = midhi = midis the right loop shape for minimum/peak-finding, andlo <= hilo <= hiwithmid ± 1mid ± 1for exact search. - Peak finding: binary search with no sortedness at all, just a local gradient.
- The staircase walk for row/column-sorted matrices — , and not a binary search at all.
- Three real LeetCode problems solved in the browser: 33, 153, 162.
The cue
Pattern 1 — rotated array, exact search
In a rotated sorted array, at least one half around midmid is always
properly sorted. Identify which, then check whether the target lies inside
that sorted half: if yes, search it; if no, search the other.
def search(nums, target):
lo, hi = 0, len(nums) - 1
while lo <= hi: # exact search shape
mid = (lo + hi) // 2
if nums[mid] == target:
return mid
if nums[lo] <= nums[mid]: # LEFT half is sorted
if nums[lo] <= target < nums[mid]:
hi = mid - 1 # target is in the sorted left
else:
lo = mid + 1
else: # RIGHT half is sorted
if nums[mid] < target <= nums[hi]:
lo = mid + 1 # target is in the sorted right
else:
hi = mid - 1
return -1
print(search([4, 5, 6, 7, 0, 1, 2], 0)) # 4
print(search([4, 5, 6, 7, 0, 1, 2], 3)) # -1def search(nums, target):
lo, hi = 0, len(nums) - 1
while lo <= hi: # exact search shape
mid = (lo + hi) // 2
if nums[mid] == target:
return mid
if nums[lo] <= nums[mid]: # LEFT half is sorted
if nums[lo] <= target < nums[mid]:
hi = mid - 1 # target is in the sorted left
else:
lo = mid + 1
else: # RIGHT half is sorted
if nums[mid] < target <= nums[hi]:
lo = mid + 1 # target is in the sorted right
else:
hi = mid - 1
return -1
print(search([4, 5, 6, 7, 0, 1, 2], 0)) # 4
print(search([4, 5, 6, 7, 0, 1, 2], 3)) # -1Duplicates break the guarantee. In LC 81, nums[lo] == nums[mid]nums[lo] == nums[mid] no
longer tells you which side is sorted — consider [1,1,1,0,1][1,1,1,0,1]. The fix is
to shrink one step (lo += 1lo += 1) when nums[lo] == nums[mid] == nums[hi]nums[lo] == nums[mid] == nums[hi],
which makes the worst case . That degradation is unavoidable, and
saying so is the expected answer.
Pattern 2 — find the rotation point (minimum)
Here you want a position, not a value match, so use the lo < hilo < hi shape.
Compare nums[mid]nums[mid] against nums[hi]nums[hi]:
def find_min(nums):
lo, hi = 0, len(nums) - 1
while lo < hi: # boundary-finding shape
mid = (lo + hi) // 2
if nums[mid] > nums[hi]:
lo = mid + 1 # minimum is strictly to the right
else:
hi = mid # mid could BE the minimum
return nums[lo]
print(find_min([3, 4, 5, 1, 2])) # 1
print(find_min([11, 13, 15, 17])) # 11 (not rotated at all)def find_min(nums):
lo, hi = 0, len(nums) - 1
while lo < hi: # boundary-finding shape
mid = (lo + hi) // 2
if nums[mid] > nums[hi]:
lo = mid + 1 # minimum is strictly to the right
else:
hi = mid # mid could BE the minimum
return nums[lo]
print(find_min([3, 4, 5, 1, 2])) # 1
print(find_min([11, 13, 15, 17])) # 11 (not rotated at all)Compare with nums[hi]nums[hi], not nums[lo]nums[lo]. Against nums[lo]nums[lo] the
not-rotated case ([11,13,15,17][11,13,15,17]) is ambiguous and needs a special case;
against nums[hi]nums[hi] it falls out correctly with no extra branch.
Pattern 3 — peak finding, with no sortedness at all
LC 162 gives an array where neighbours differ, and asks for any index whose value is greater than both neighbours. The array is not sorted in any sense. Binary search still works:
def find_peak_element(nums):
lo, hi = 0, len(nums) - 1
while lo < hi:
mid = (lo + hi) // 2
if nums[mid] < nums[mid + 1]:
lo = mid + 1 # we are ascending: a peak lies right
else:
hi = mid # descending: mid may be the peak
return lo
print(find_peak_element([1, 2, 1, 3, 5, 6, 4])) # 5 (or 1 -- both are peaks)def find_peak_element(nums):
lo, hi = 0, len(nums) - 1
while lo < hi:
mid = (lo + hi) // 2
if nums[mid] < nums[mid + 1]:
lo = mid + 1 # we are ascending: a peak lies right
else:
hi = mid # descending: mid may be the peak
return lo
print(find_peak_element([1, 2, 1, 3, 5, 6, 4])) # 5 (or 1 -- both are peaks)Why it is guaranteed to find one: if nums[mid] < nums[mid+1]nums[mid] < nums[mid+1], the segment
to the right starts by ascending, and since the array ends (treat the
out-of-bounds neighbours as ), it must turn down somewhere — so a
peak exists in [mid+1, hi][mid+1, hi]. The same argument mirrors on the other side.
The invariant “the current range contains a peak” is preserved, and the
range shrinks every step.
This is the clearest demonstration that binary search is about discarding half with certainty, not about sorted data.
Pattern 4 — sorted matrices are two different problems
| Problem | Structure | Technique | Complexity |
|---|---|---|---|
| 74 Search a 2D Matrix | Fully sorted if flattened row-major | Binary search over m*nm*n, with divmod(mid, cols)divmod(mid, cols) | |
| 240 Search a 2D Matrix II | Rows sorted, columns sorted, but rows do not continue from each other | Staircase walk from the top-right |
def search_matrix_ii(matrix, target):
if not matrix or not matrix[0]:
return False
row, col = 0, len(matrix[0]) - 1 # start top-RIGHT
while row < len(matrix) and col >= 0:
value = matrix[row][col]
if value == target:
return True
if value > target:
col -= 1 # this whole column is too big
else:
row += 1 # this whole row is too small
return Falsedef search_matrix_ii(matrix, target):
if not matrix or not matrix[0]:
return False
row, col = 0, len(matrix[0]) - 1 # start top-RIGHT
while row < len(matrix) and col >= 0:
value = matrix[row][col]
if value == target:
return True
if value > target:
col -= 1 # this whole column is too big
else:
row += 1 # this whole row is too small
return FalseThe variant map
| Variant | The discard test | Canonical problem |
|---|---|---|
| Rotated, exact search | Which half is sorted, then is the target inside it | 33 |
| Rotated with duplicates | Same, plus lo += 1lo += 1 when the test is ambiguous — worst case | 81 · 154 |
| Rotation point | nums[mid] > nums[hi]nums[mid] > nums[hi] means go right | 153 |
| Peak / unimodal | Compare nums[mid]nums[mid] with nums[mid+1]nums[mid+1] | 162 · 852 |
| Fully sorted matrix | Treat as 1D via divmoddivmod | 74 |
| Row/column sorted matrix | Staircase from a corner, | 240 |
Practice — real LeetCode problems
LC 33 — Search in Rotated Sorted Array · Medium
Problem. An ascending array with distinct values was rotated at an
unknown pivot. Given the rotated array and a targettarget, return its index, or
-1-1. You must write an algorithm.
Constraints. 1 <= len(nums) <= 50001 <= len(nums) <= 5000, all values unique,
-10^4 <= nums[i], target <= 10^4-10^4 <= nums[i], target <= 10^4.
Examples. nums = [4,5,6,7,0,1,2], target = 0nums = [4,5,6,7,0,1,2], target = 0 gives 44 ·
target = 3target = 3 gives -1-1 · nums = [1], target = 0nums = [1], target = 0 gives -1-1
Editorial — approach, complexity, follow-ups
A rotation splits the array into two ascending runs. Whatever midmid is, the
side of it that does not contain the rotation point is fully sorted, and
for a sorted range you can test membership with two comparisons. So each
iteration either finds the target or halves the range.
Time . Space .
([3, 1], 1)([3, 1], 1) returning 11 is the case that punishes strict <<: with
lo=0, hi=1lo=0, hi=1, mid=0mid=0, so nums[lo] == nums[mid] == 3nums[lo] == nums[mid] == 3. Only <=<=
classifies the left half (just [3][3]) as sorted, correctly sending the
search right.
A two-pass alternative is equally valid and some find it easier to defend: first find the rotation index with LC 153’s loop, then run an ordinary binary search on the appropriate segment. Two clean passes beat one confusing one.
Follow-ups you should expect: “What if there are duplicates (LC 81)?” —
when nums[lo] == nums[mid] == nums[hi]nums[lo] == nums[mid] == nums[hi] you cannot tell which side is
sorted, so lo += 1lo += 1 and continue; the worst case becomes and that is
provably unavoidable. “Find the rotation count?” — LC 153’s index. “Rotated
descending array?” — mirror every comparison.
LC 153 — Find Minimum in Rotated Sorted Array · Medium
Problem. An ascending array of unique elements was rotated. Return its minimum element in .
Constraints. 1 <= len(nums) <= 50001 <= len(nums) <= 5000, all values unique. The array may
not actually be rotated.
Examples. [3,4,5,1,2][3,4,5,1,2] gives 11 · [4,5,6,7,0,1,2][4,5,6,7,0,1,2] gives 00 ·
[11,13,15,17][11,13,15,17] gives 1111
Editorial — approach, complexity, follow-ups
The minimum is the single point where the array “drops”. Comparing
nums[mid]nums[mid] to nums[hi]nums[hi] locates which side of midmid that drop is on:
nums[mid] > nums[hi]nums[mid] > nums[hi]— the drop is betweenmidmidandhihi, so the minimum is atmid + 1mid + 1or later.nums[mid] <= nums[hi]nums[mid] <= nums[hi]— the rangemid..himid..hiis ascending, so the minimum is atmidmidor earlier.midmidstays in range:hi = midhi = mid.
Time . Space .
Two design choices carry the correctness:
hi = midhi = mid, nevermid - 1mid - 1.midmidis a live candidate; excluding it can skip the answer entirely.- Compare against
nums[hi]nums[hi]. Withnums[lo]nums[lo], the un-rotated array[11,13,15,17][11,13,15,17]givesnums[mid] > nums[lo]nums[mid] > nums[lo], which would pushloloright and miss the minimum at index 0 — you would need an explicit “is it rotated?” pre-check. Againstnums[hi]nums[hi]the case is handled for free.
Follow-ups you should expect: “With duplicates (LC 154)?” — when
nums[mid] == nums[hi]nums[mid] == nums[hi] you cannot decide, so hi -= 1hi -= 1; worst case
. “Return the index instead?” — return lolo. “How many times was it
rotated?” — that is exactly the minimum’s index. “Find the maximum?” —
mirror the comparison, or note it sits just left of the minimum.
LC 162 — Find Peak Element · Medium
Problem. A peak element is strictly greater than its neighbours. Given
an array where nums[i] != nums[i+1]nums[i] != nums[i+1] for all valid ii, return the index of
any peak. Imagine nums[-1]nums[-1] and nums[n]nums[n] are . Must be
.
Constraints. 1 <= len(nums) <= 10001 <= len(nums) <= 1000, adjacent elements always differ.
Examples. [1,2,3,1][1,2,3,1] gives 22 · [1,2,1,3,5,6,4][1,2,1,3,5,6,4] gives 55 or
11 — both are peaks
Editorial — approach, complexity, follow-ups
The invariant is “the range [lo, hi][lo, hi] contains a peak”, and it holds
initially because the out-of-range neighbours are (so the global
maximum is a peak).
Given nums[mid] < nums[mid+1]nums[mid] < nums[mid+1], the range [mid+1, hi][mid+1, hi] starts by ascending.
Either it keeps ascending to hihi — making hihi a peak, since its right
neighbour is — or it turns down somewhere, making that turning
point a peak. Either way a peak exists to the right, so discarding
[lo, mid][lo, mid] is safe. The mirror argument covers the other branch.
Time . Space .
mid + 1mid + 1 is always a valid index: lo < hilo < hi plus floor division gives
mid < himid < hi, so mid + 1 <= himid + 1 <= hi. That is why the loop shape and the
comparison have to agree — with lo <= hilo <= hi you could have mid == himid == hi and
index out of bounds.
Note there is no nums[mid] > nums[mid-1]nums[mid] > nums[mid-1] check anywhere. Verifying a true
peak is unnecessary; maintaining the invariant is enough, and the loop
exits only when lo == hilo == hi, which must then be a peak.
Follow-ups you should expect: “Find all peaks?” — that needs ; a single linear scan, since peaks can be anywhere and halving would skip some. “What if neighbours can be equal?” — the guarantee collapses (consider a long plateau) and the worst case becomes . “2D peak (LC 1901)?” — binary search on columns, taking the row-max within each, giving . “Mountain array (LC 852)?” — same code; the extra promise of exactly one peak changes nothing.
LeetCode problem set
| # | Problem | Difficulty | The twist |
|---|---|---|---|
| 852 | Peak Index in a Mountain Array | Medium | LC 162 with a guarantee of exactly one peak — identical code |
| 33 | Search in Rotated Sorted Array | Medium | Decide which half is sorted, then test membership |
| 81 | Search in Rotated Sorted Array II | Medium | Duplicates make the test ambiguous; lo += 1lo += 1 and accept worst case |
| 153 | Find Minimum in Rotated Sorted Array | Medium | Compare with nums[hi]nums[hi]; hi = midhi = mid, never mid - 1mid - 1 |
| 162 | Find Peak Element | Medium | No sortedness at all — just the local gradient |
| 74 | Search a 2D Matrix | Medium | Fully sorted when flattened: binary search m*nm*n with divmoddivmod |
| 240 | Search a 2D Matrix II | Medium | Staircase from the top-right, — not a binary search |
| 154 | Find Minimum in Rotated Sorted Array II | Hard | 153 with duplicates: hi -= 1hi -= 1 on a tie, worst case |
Interview follow-ups
| They ask | What they’re checking | The answer |
|---|---|---|
| “Why does binary search work without sorted data?” | Depth | All it needs is the ability to discard one half with certainty; sortedness is one way to earn that, a gradient or an invariant is another |
”lo <= hilo <= hi or lo < hilo < hi?” | Whether you pick deliberately | lo <= hilo <= hi with mid ± 1mid ± 1 for exact search; lo < hilo < hi with hi = midhi = mid for boundaries — midmid must stay a candidate |
| “What about duplicates?” | Knowing the limits | The which-half-is-sorted test becomes ambiguous; shrink one step and accept worst case — it is provably unavoidable |
“Why compare with nums[hi]nums[hi] in LC 153?” | Precision | Against nums[lo]nums[lo] the un-rotated array needs a special case; against nums[hi]nums[hi] it works unmodified |
| “Why is LC 240 not ?” | Reading structure | Rows overlap, so there is no single sorted order to halve; each step can only eliminate one row or one column |
| “Prove your loop terminates” | Rigour | lo = mid + 1lo = mid + 1 strictly advances and hi = midhi = mid strictly shrinks given mid < himid < hi, which lo < hilo < hi plus floor division guarantees |
Edge-case checklist
- Single element — every template must handle
len == 1len == 1. - Two elements —
[3,1][3,1]for LC 33 (the<=<=test) and[2,1][2,1]for LC 153. - Not actually rotated —
[11,13,15,17][11,13,15,17]; the reason to compare withnums[hi]nums[hi]. - Rotated by exactly one —
[5,1,2,3,4][5,1,2,3,4]and[2,3,4,5,1][2,3,4,5,1]. - Target absent — must return
-1-1, not the nearest index. - Target at the pivot — both the minimum and the element before it.
- Monotonic input for peak-finding —
[1,2,3,4,5][1,2,3,4,5]peaks at the last index,[5,4,3,2,1][5,4,3,2,1]at the first; both exercise the boundary. - Empty matrix — guard
matrixmatrixandmatrix[0]matrix[0]before indexing. mid + 1mid + 1in range — only safe under thelo < hilo < hishape.
Recap
- Binary search needs only that you can discard half with certainty — not that the data is sorted.
- Rotated exact search: find the sorted half with
nums[lo] <= nums[mid]nums[lo] <= nums[mid](the<=<=matters), then test whether the target lies inside it. - Two loop shapes:
lo <= hilo <= hiwithmid ± 1mid ± 1to find a value;lo < hilo < hiwithhi = midhi = midto find a boundary, minimum, or peak. Never mix them. - Rotation point: compare
nums[mid]nums[mid]withnums[hi]nums[hi], which handles the un-rotated array without a special case. - Peak finding works on unsorted data via the local gradient, and the invariant “this range contains a peak”.
- Duplicates cost you the guarantee and degrade the worst case to — say so rather than pretending otherwise.
- Sorted matrices split in two: fully sorted means ; row/column sorted means an staircase from the top-right.
Next: Binary Search on Answer — searching a space of candidate answers rather than array indices.
If this helped you, consider buying me a coffee ☕
Buy me a coffeeWas this page helpful?
Let us know how we did
