Skip to content

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 midmid and 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 < hi with hi = midhi = mid is the right loop shape for minimum/peak-finding, and lo <= hilo <= hi with mid ± 1mid ± 1 for exact search.
  • Peak finding: binary search with no sortedness at all, just a local gradient.
  • The staircase walk for row/column-sorted matrices — O(m+n)O(m + n), and not a binary search at all.
  • Three real LeetCode problems solved in the browser: 33, 153, 162.

The cue

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.

search_rotated.py
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))   # -1
search_rotated.py
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))   # -1

Duplicates 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 O(n)O(n). 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]:

find_min_rotated.py
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)
find_min_rotated.py
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:

find_peak.py
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)
find_peak.py
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 -\infty), 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

ProblemStructureTechniqueComplexity
74 Search a 2D MatrixFully sorted if flattened row-majorBinary search over m*nm*n, with divmod(mid, cols)divmod(mid, cols)O(logmn)O(\log mn)
240 Search a 2D Matrix IIRows sorted, columns sorted, but rows do not continue from each otherStaircase walk from the top-rightO(m+n)O(m + n)
staircase_search_240.py
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 False
staircase_search_240.py
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 False

The variant map

VariantThe discard testCanonical problem
Rotated, exact searchWhich half is sorted, then is the target inside it33
Rotated with duplicatesSame, plus lo += 1lo += 1 when the test is ambiguous — O(n)O(n) worst case81 · 154
Rotation pointnums[mid] > nums[hi]nums[mid] > nums[hi] means go right153
Peak / unimodalCompare nums[mid]nums[mid] with nums[mid+1]nums[mid+1]162 · 852
Fully sorted matrixTreat as 1D via divmoddivmod74
Row/column sorted matrixStaircase from a corner, O(m+n)O(m+n)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 O(logn)O(\log n) 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 O(logn)O(\log n). Space O(1)O(1).

([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 O(logn)O(\log n) 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 O(n)O(n) 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 O(logn)O(\log n).

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 between midmid and hihi, so the minimum is at mid + 1mid + 1 or later.
  • nums[mid] <= nums[hi]nums[mid] <= nums[hi] — the range mid..himid..hi is ascending, so the minimum is at midmid or earlier. midmid stays in range: hi = midhi = mid.

Time O(logn)O(\log n). Space O(1)O(1).

Two design choices carry the correctness:

  • hi = midhi = mid, never mid - 1mid - 1. midmid is a live candidate; excluding it can skip the answer entirely.
  • Compare against nums[hi]nums[hi]. With nums[lo]nums[lo], the un-rotated array [11,13,15,17][11,13,15,17] gives nums[mid] > nums[lo]nums[mid] > nums[lo], which would push lolo right and miss the minimum at index 0 — you would need an explicit “is it rotated?” pre-check. Against nums[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 O(n)O(n). “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 -\infty. Must be O(logn)O(\log n).

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 -\infty (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 -\infty — 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 O(logn)O(\log n). Space O(1)O(1).

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 O(n)O(n); 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 O(n)O(n). “2D peak (LC 1901)?” — binary search on columns, taking the row-max within each, giving O(mlogn)O(m \log n). “Mountain array (LC 852)?” — same code; the extra promise of exactly one peak changes nothing.

LeetCode problem set

#ProblemDifficultyThe twist
852Peak Index in a Mountain ArrayMediumLC 162 with a guarantee of exactly one peak — identical code
33Search in Rotated Sorted ArrayMediumDecide which half is sorted, then test membership
81Search in Rotated Sorted Array IIMediumDuplicates make the test ambiguous; lo += 1lo += 1 and accept O(n)O(n) worst case
153Find Minimum in Rotated Sorted ArrayMediumCompare with nums[hi]nums[hi]; hi = midhi = mid, never mid - 1mid - 1
162Find Peak ElementMediumNo sortedness at all — just the local gradient
74Search a 2D MatrixMediumFully sorted when flattened: binary search m*nm*n with divmoddivmod
240Search a 2D Matrix IIMediumStaircase from the top-right, O(m+n)O(m+n) — not a binary search
154Find Minimum in Rotated Sorted Array IIHard153 with duplicates: hi -= 1hi -= 1 on a tie, O(n)O(n) worst case

Interview follow-ups

They askWhat they’re checkingThe answer
“Why does binary search work without sorted data?”DepthAll 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 deliberatelylo <= 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 limitsThe which-half-is-sorted test becomes ambiguous; shrink one step and accept O(n)O(n) worst case — it is provably unavoidable
“Why compare with nums[hi]nums[hi] in LC 153?”PrecisionAgainst 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 O(logmn)O(\log mn)?”Reading structureRows overlap, so there is no single sorted order to halve; each step can only eliminate one row or one column
“Prove your loop terminates”Rigourlo = 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 with nums[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 -\infty boundary.
  • Empty matrix — guard matrixmatrix and matrix[0]matrix[0] before indexing.
  • mid + 1mid + 1 in range — only safe under the lo < hilo < hi shape.

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 <= hi with mid ± 1mid ± 1 to find a value; lo < hilo < hi with hi = midhi = mid to find a boundary, minimum, or peak. Never mix them.
  • Rotation point: compare nums[mid]nums[mid] with nums[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 O(n)O(n) — say so rather than pretending otherwise.
  • Sorted matrices split in two: fully sorted means O(logmn)O(\log mn); row/column sorted means an O(m+n)O(m+n) 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 coffee

Was this page helpful?

Let us know how we did