Search Tech Journey

Find topics, journeys and posts

back to blog
algorithmsintermediate 60m read

L14 · Binary Search II — Rotated & 2D

Binary search when the array is no longer sorted end-to-end: decide which half is still sorted, then discard the other. Same trick flattens a 2D matrix into one index space.

🧩DSAPhase 1 · Core patterns· Session 014 of 130 60 min

🎯 Binary search on a rotated sorted array by deciding which half is sorted, find the rotation minimum, and search a row-sorted matrix by treating it as one flat sorted array.

Series: LeetCode — From Basics to Interview-Ready · Session 14 / 65 · Phase 1 · Core patterns

Watch first

What this pattern IS

L13 assumed the array was sorted from front to back. Two very common twists break that assumption but still leave enough order to binary search:

  1. Rotated sorted array. A sorted array cut at some pivot and reattached: [4, 5, 6, 7, 0, 1, 2]. It's no longer globally sorted, so plain binary search fails. But here's the key fact: at least one half around any midpoint is still perfectly sorted. If you can tell which half is sorted, you can check whether the target lies inside that sorted half's range — if yes, search it; if no, search the other half. You still halve the space each step, so it's O(log n).

  2. Row-and-column sorted matrix (each row sorted, first of each row > last of previous). This is just a sorted 1D array folded into a grid. Treat indices 0 .. rows*cols − 1 as one flat sorted array and convert mid to (mid // cols, mid % cols). One binary search, O(log(rows·cols)).

The unifying move: find the remaining monotone structure and search within it. Rotation hides the order in one half; the matrix hides it in a fold. Both are recovered by a single comparison at the midpoint.

A cut deck of sorted cards
🌍 Real world
Someone cuts a sorted deck and puts the bottom part on top. Look at any card in the middle: comparing it to the top card tells you which side is still in perfect order. You search the ordered side if your card belongs there, otherwise the messy side.
💻 Code world
if nums[lo] <= nums[mid]: left half sorted; else right half sorted.

First principles

From first principles
Start with the question
Why is at least one half of a rotated sorted array always sorted, and how does that let you binary search?
  1. 1
    A rotated sorted array is two sorted runs: a higher run then a lower run (e.g. [4,5,6,7] then [0,1,2]).
    forced by · Rotation moves a suffix to the front, leaving exactly one 'drop' point where order breaks.
  2. 2
    Any midpoint splits the array into [lo..mid] and [mid..hi]; the single drop point can lie in only ONE of them.
    forced by · There is exactly one break, so at most one half contains it — the other half is fully sorted.
  3. 3
    Compare nums[lo] to nums[mid]: if nums[lo] <= nums[mid], the LEFT half is sorted; otherwise the RIGHT half is sorted.
    forced by · A sorted run has its start ≤ its end; the break shows up as a start > end.
  4. 4
    In the sorted half, a range check (is target between its ends?) decides in O(1) whether to keep or discard it.
    forced by · Membership in a sorted range is a two-sided comparison; if target isn't in the sorted half, it must be in the other.
  5. 5
    Each step discards half, preserving O(log n).
    forced by · Exactly one half is eliminated per iteration, same as ordinary binary search.
⇒ Therefore
Identify the sorted half, range-check the target against it, keep or discard — one comparison recovers the binary search.

The templates

Template A — search in a rotated sorted array (no duplicates)

def search_rotated(nums, target):
    lo, hi = 0, len(nums) - 1          # closed interval here (we compare against nums[hi])
    while lo <= hi:
        mid = lo + (hi - lo) // 2
        if nums[mid] == target:
            return mid
        if nums[lo] <= nums[mid]:      # LEFT half [lo..mid] is sorted
            if nums[lo] <= target < nums[mid]:
                hi = mid - 1           # target is in the sorted left half
            else:
                lo = mid + 1           # otherwise search the right
        else:                          # RIGHT half [mid..hi] is sorted
            if nums[mid] < target <= nums[hi]:
                lo = mid + 1           # target is in the sorted right half
            else:
                hi = mid - 1
    return -1

Template B — find the minimum (pivot) of a rotated sorted array

def find_min(nums):
    lo, hi = 0, len(nums) - 1
    while lo < hi:                     # half-open on the answer: converge to the min
        mid = lo + (hi - lo) // 2
        if nums[mid] > nums[hi]:       # min must be to the RIGHT of mid
            lo = mid + 1
        else:                          # nums[mid] <= nums[hi]: min is at mid or LEFT
            hi = mid
    return nums[lo]                    # lo == hi == index of the minimum

Comparing to nums[hi] (not nums[lo]) is the correct pivot compass — it avoids an ambiguous case when the array isn't rotated.

Template C — search a row+col sorted matrix as one flat array

def search_matrix(matrix, target):
    if not matrix or not matrix[0]:
        return False
    rows, cols = len(matrix), len(matrix[0])
    lo, hi = 0, rows * cols            # flat index space, half-open
    while lo < hi:
        mid = lo + (hi - lo) // 2
        val = matrix[mid // cols][mid % cols]   # unflatten the 1D index to (row, col)
        if val == target:
            return True
        if val < target:
            lo = mid + 1
        else:
            hi = mid
    return False

Mental model

Mental modelOne half is honest
A rotated array is two sorted ramps. At any midpoint, one side is a clean ramp. Ask 'does my target live on the clean ramp?' — if yes, go there; if no, the answer is on the other ramp. A sorted matrix is a single ramp folded into rows: unfold with //cols and %cols.
  • Rotated: compare nums[lo] vs nums[mid] to find the sorted half.
  • Range-check target against the sorted half's ends; keep or discard.
  • Find-min: compare nums[mid] vs nums[hi]; min is where the ramp restarts.
  • Matrix: flat index mid -> (mid // cols, mid % cols), then plain binary search.
🔔 Fires when you see
'rotated sorted array', 'find the minimum / pivot', 'search a matrix sorted row-wise where each row starts above the previous'.

Memory / mnemonic

Two compasses and one fold — that's the whole session.

  • Search rotated → the compass is "left-start vs mid." nums[lo] <= nums[mid]left ramp is honest. Peg: "if the start is below the middle, the left is settled." Then range-check and keep/toss.
  • Find min → the compass is "mid vs right." nums[mid] > nums[hi] ⇒ the drop is to the right ⇒ go right. Peg: "bigger than the tail? the valley's ahead."
  • Matrix → the fold: "divide by cols for the row, mod cols for the col." Say "slash-cols, mod-cols" while typing mid // cols, mid % cols.

The recall trap to avoid: in find-min, always compare to nums[hi], never nums[lo]"the tail tells the truth."

The trap

Common misconception
✗ What most people think
For find-minimum in a rotated array you should compare nums[mid] to nums[lo].
✓ What is actually true
Compare nums[mid] to nums[hi]. Comparing to nums[lo] mishandles the non-rotated (already sorted) case and can pick the wrong half.
Why the myth is so sticky
If nums[mid] > nums[hi], the rotation break is strictly to the right, so the minimum is right of mid. Comparing to nums[lo] gives no clean signal when the current window is already sorted (nums[lo] <= nums[mid] is true both when rotated far and not at all).
Prove it to yourself
Run find-min on an UN-rotated array like [1,2,3,4,5]. The nums[hi] version returns 1 correctly; a naive nums[lo] comparison can walk the wrong direction.

What interviewers actually ask

Rotated-array search is a classic follow-up to plain binary search and a frequent Amazon/Microsoft/Meta question.

  • Search in Rotated Sorted Array (33) · Medium · Amazon, Microsoft, Meta — probes: can you identify the sorted half and range-check? Follow-up: what if duplicates are allowed (81)?
  • Search in Rotated Sorted Array II (81) · Medium · Amazon — probes: duplicates break the nums[lo] <= nums[mid] test; handle nums[lo] == nums[mid] by shrinking lo. Follow-up: worst-case O(n) — why?
  • Find Minimum in Rotated Sorted Array (153) · Medium · Amazon, Microsoft — probes: the pivot compass (compare to nums[hi]). Follow-up: with duplicates (154), worst-case O(n).
  • Search a 2D Matrix (74) · Medium · Amazon, Microsoft — probes: flatten to one index space vs a staircase walk. Follow-up: the row+col-sorted-only variant (240), where flattening doesn't apply.
  • Search a 2D Matrix II (240) · Medium · Amazon — probes: staircase from top-right (O(m+n)) when rows aren't globally chained. Follow-up: why binary search per row is worse than the staircase.
  • Peak Index in a Mountain Array (852) · Medium · Amazon, Google — probes: binary search on a non-sorted-but-monotone-then-monotone shape. Follow-up: find a peak in an arbitrary array (162).

Universal probe: "the array isn't fully sorted — where's the structure you can still exploit?" Name the sorted half or the fold.

Worked solutions

1. Search in Rotated Sorted Array (33)

Plain words: a sorted array was rotated at an unknown pivot; find the index of target, or −1. No duplicates.

Brute force: linear scan, O(n) — ignores the structure. Insight: at each midpoint one half is sorted; range-check the target against that half to decide where to go.

def search(nums, target):
    lo, hi = 0, len(nums) - 1
    while lo <= hi:
        mid = lo + (hi - lo) // 2
        if nums[mid] == target:
            return mid
        if nums[lo] <= nums[mid]:              # left half sorted
            if nums[lo] <= target < nums[mid]: # target inside sorted left
                hi = mid - 1
            else:
                lo = mid + 1
        else:                                  # right half sorted
            if nums[mid] < target <= nums[hi]: # target inside sorted right
                lo = mid + 1
            else:
                hi = mid - 1
    return -1

Complexity: O(log n) time, O(1) memory. Follow-up — "with duplicates" (LC81): when nums[lo] == nums[mid] you can't tell which half is sorted, so do lo += 1 and retry; this degrades to O(n) in the worst case (e.g. all equal).

Try itReturn True/False for whether target EXISTS, handling duplicates (LC81).
def searchDup(nums, target):
    lo, hi = 0, len(nums) - 1
    while lo <= hi:
        mid = lo + (hi - lo) // 2
        if nums[mid] == target:
            return True
        if nums[lo] == nums[mid] == nums[hi]:  # ambiguous -> shrink
            lo += 1
            hi -= 1
        elif nums[lo] <= nums[mid]:
            if nums[lo] <= target < nums[mid]:
                hi = mid - 1
            else:
                lo = mid + 1
        else:
            if nums[mid] < target <= nums[hi]:
                lo = mid + 1
            else:
                hi = mid - 1
    return False
💡 Hint · Add a branch: if nums[lo] == nums[mid] == nums[hi], shrink both ends by one.

2. Find Minimum in Rotated Sorted Array (153)

Plain words: a sorted array rotated at an unknown pivot; return the smallest element. No duplicates.

Insight: the minimum is the point where the ramp restarts. Compare nums[mid] to nums[hi]: if nums[mid] > nums[hi], the drop is to the right, so the min is right of mid; otherwise it's mid or left.

def findMin(nums):
    lo, hi = 0, len(nums) - 1
    while lo < hi:
        mid = lo + (hi - lo) // 2
        if nums[mid] > nums[hi]:      # minimum is strictly to the right
            lo = mid + 1
        else:                         # minimum is mid or to the left
            hi = mid
    return nums[lo]

Complexity: O(log n) time, O(1) memory. Follow-up — "with duplicates" (LC154): when nums[mid] == nums[hi], you can't decide, so do hi -= 1; worst case O(n) when many equal values hide the pivot.

3. Search a 2D Matrix (74)

Plain words: an m×n matrix where each row is sorted and the first element of each row is greater than the last of the previous row; return whether target is present.

Insight: the matrix is a single sorted sequence folded into rows. Binary search the flat index range [0, m*n) and convert mid back to (row, col) with divmod.

def searchMatrix(matrix, target):
    if not matrix or not matrix[0]:
        return False
    rows, cols = len(matrix), len(matrix[0])
    lo, hi = 0, rows * cols
    while lo < hi:
        mid = lo + (hi - lo) // 2
        r, c = divmod(mid, cols)      # unflatten
        val = matrix[r][c]
        if val == target:
            return True
        if val < target:
            lo = mid + 1
        else:
            hi = mid
    return False

Complexity: O(log(m·n)) time, O(1) memory. Follow-up — "rows and columns individually sorted but NOT globally chained" (LC240): flattening no longer works; walk a staircase from the top-right, moving left when too big and down when too small, for O(m + n).

Tradeoff

The tradeoff
Search a 2D matrix for a target.
Flatten + binary search
+ you gain O(log(m·n)); the fewest comparisons.
− you pay Requires the STRONG property: each row's first > previous row's last.
pick when LC74-style matrices where the grid is one folded sorted array.
Staircase from top-right
+ you gain O(m + n); works with the WEAKER property (rows and cols each sorted only).
− you pay Linear, not logarithmic; more comparisons on large square matrices.
pick when LC240-style matrices where rows aren't globally chained.
Binary search each row
+ you gain Simple to write.
− you pay O(m log n) — worse than the staircase for the weak property.
pick when Rarely; only if m is tiny.
What a senior engineer actually does
Use the flatten+binary-search when rows are globally chained (74); use the top-right staircase for the weaker row/col-sorted matrices (240). Per-row binary search is almost never the best choice.

Where this shows up

Rotated-array search models lookups in circular buffers and log-structured storage where a sorted sequence wraps around a ring — you still binary search by first locating the wrap point. The 2D flatten trick is the same index arithmetic (row = i // width, col = i % width) used to address a pixel buffer or a flattened tensor in memory, where a logically 2D structure is stored as one contiguous sorted-or-indexable array.

Checkpoint

You can now…
  • Identify which half of a rotated array is sorted with one comparison.
  • Range-check the target against the sorted half to decide the next window.
  • Find the rotation minimum by comparing nums[mid] to nums[hi].
  • Handle duplicates by shrinking the boundary and explain the O(n) worst case.
  • Flatten a globally-chained matrix and binary search its index space.
  • Choose the top-right staircase for weaker row/col-sorted matrices.

Quiz

Recall check · click to reveal
★ = stretch question

Practice queue

+7d / +21d mark spaced-repetition anchors.