DSA sheet · Binary Search · Patterns roadmap

Binary Search Patterns Roadmap

This is a concept video, not a single LeetCode problem. The teacher's main message: binary search is not only "find a number in a sorted array". It is a way of throwing away half of the possibilities at every step, using some rule that tells you which half can't hold the answer. That is what turns O(n) work into O(log n).

She then gives a roadmap of 4 patterns that cover most interview questions on binary search: (1) basic binary search, (2) rotated arrays / lower & upper bound, (3) binary search on the answer, (4) binary search on a 2D matrix. Every later page in this notebook belongs to one of these four. Learn to recognise them here, and the later problems become "which pattern is this?"

Every part below follows the same order (adapted for a concept video):
① what it is → ② when to use it → ③ intuition → ④ building the logic from her examples → ⑤ steps → ⑥ code → ⑦ code line by line → ⑧ dry run → ⑨ complexity & remember

Part 0 · Before starting

Words we will use

Why binary search works at all

Binary search needs one thing: when you stand at mid and look at it, you must be able to say "the answer is surely not on this side". In a sorted array that's easy. If the target is bigger than arr[mid], every item on the left of mid is even smaller, so none of them can be the target. The left half can be thrown away.

More generally, it works whenever there is a yes/no question whose answers look like no no no no yes yes yes (or the opposite) as you move from left to right. That shape is called monotonic: once it switches, it never switches back. A sorted array gives you that shape for the question "is arr[i] >= target?".

low, high, mid, and the safer mid formula

The usual way to find the middle is (low + high) // 2. The teacher prefers mid = low + (high - low) // 2. Both give the same number. The difference is only in how big the numbers get while computing it.

// in Python is "divide and drop the decimal part" (floor division), e.g. 7 // 2 = 3. That's how mid stays a whole index.


Part A · The idea: eliminate half, the template, and the two classic doubts

1What binary search is, in simple words

Most people describe binary search as "searching in a sorted array". The teacher says that description is too narrow. A better description:

Her definitionBinary search is a way to throw away half of the search space at every step, using some logic. Because the space halves each time, the work drops from O(n) to O(log n).

Searching for a value is just the most common use of that idea.

2When to use it: the two signals she asks you to remember

  1. You can decide a side from mid. If you stand at the middle and some rule tells you "go left" or "go right", binary search is possible, even if nothing looks sorted.
  2. The data is sorted, fully or partly. A fully sorted array is the obvious case. But a partly sorted array (for example a sorted array that was rotated, so it is two sorted pieces) is still a binary search candidate.
Doubt: signal 1 doesn't mention "sorted". Can binary search really work without a sorted array?
→ Yes. What it truly needs is the ability to discard one side safely. In "binary search on the answer" (Part D), the array isn't sorted at all. We binary search over a range of possible answers, and the rule "is this answer good enough?" tells us which side to keep.

3Intuition

Think of a guessing game: "I'm thinking of a number from 1 to 100." If you guess 1, 2, 3, … you may need 100 guesses. If you guess 50 and hear "higher", you've removed 50 numbers with one question. Guess 75, hear "lower", and 25 more are gone. Each question cuts what's left in half. That's binary search: always ask about the middle, so that whatever the answer, half the work disappears.

4Building the logic: the steps and the two classic doubts

The teacher lists the basic steps on a small sorted array of 4 items, where index 1 holds 8.

index0123
value381215low = 0, high = 3, mid = 1

Doubt 1 from the video: why low + (high - low) // 2?

The teacher's picture: imagine numbers simply stop existing after 100. Let high = 100 and low = 1.

formulawork donebiggest number made on the wayresult
(low + high) / 21 + 100 = 101, then ÷ 2101 → beyond 100, "doesn't exist" → errorbroken
low + (high - low) / 2100 − 1 = 99, 99 ÷ 2 = 49.5, 1 + 49.5 = 50.5100 (never goes past it)50.5 → 50

Mathematically both formulas give 50.5. But the first one has to build 101 first, and in her imaginary world 101 can't exist. In Java/C++ the "limit" is 2³¹ − 1, and going past it turns the number negative, so you get a wrong mid index. The safe form subtracts first (making a smaller number), halves it (smaller still), and only then adds low back. She says to remember this point for interviews.

Doubt: does Python need this?
→ No. Python ints have no fixed maximum, so low + high can't overflow. We still use the safe form so the code reads the same as the Java/C++ version and you keep the habit.

Doubt 2 from the video: while low <= high or while low < high?

She says this is where most coders get confused, and gives a rule of thumb:

loopuse it when…typical updateswhere she says it shows up
while low <= highthe value you want is exactly present and you can recognise it at mid ("found it, return")low = mid + 1 / high = mid - 1basic search, rotated search, sqrt(x), insert position
while low < highyou are searching for a boundary (the first spot where a condition becomes true), not an exact valueone side keeps mid: high = midmostly binary search on the answer
Doubt: why does the boundary style stop at low < high?
→ In the boundary style, mid might itself be the answer, so we keep it (high = mid) instead of skipping it. The search space shrinks until only one candidate is left, which is when low == high. That last one is the answer, so there's nothing left to check and the loop stops. With <= plus high = mid, the loop could get stuck forever on that last item (low = high = mid, nothing changes).
In the exact-value style, every step skips mid (±1), so the space can become empty (low passes high). We must still check the case low == high (one item left), hence <=.

5Steps (the core template)

  1. low = 0, high = n - 1.
  2. While low <= high: compute mid = low + (high - low) // 2.
  3. If arr[mid] == target → return mid.
  4. If arr[mid] < target → the answer is on the right → low = mid + 1.
  5. Else → the answer is on the left → high = mid - 1.
  6. If the loop ends, the target isn't there → return −1 (or False).

6Code (Python)

the core binary search template
def binary_search(arr, target):
    low, high = 0, len(arr) - 1
    while low <= high:
        mid = low + (high - low) // 2   # safe middle
        if arr[mid] == target:
            return mid                  # found it
        elif arr[mid] < target:
            low = mid + 1               # throw away the left half (and mid)
        else:
            high = mid - 1              # throw away the right half (and mid)
    return -1                           # space became empty: not present

7Code line by line

linewhat it means
low, high = 0, len(arr) - 1The search space is the whole array at the start.
while low <= high:While there's at least one index left to look at.
mid = low + (high - low) // 2The middle index of the current space, in the overflow-safe form.
if arr[mid] == target: return midExact hit. Nothing more to do.
elif arr[mid] < target: low = mid + 1mid is too small, so everything before it is too small too. Mid is also not the answer, so start just after it.
else: high = mid - 1mid is too big, so everything after it is too big too. End just before it.
return -1low passed high: no index left, the target isn't in the array.

8Dry run

Her 4-item array, target 12:

steplowhighmidarr[mid]decisionwhat we throw away
103188 < 12 → low = 2indexes 0, 1
223212equal → return 2—
step 1381215mid = 1, 8 < 12
step 2381215mid = 2, found ✓

9Complexity & remember

Each step cuts the search space in half: n → n/2 → n/4 → n/8 → … → 1. How many halvings until 1 item is left? The number k with n / 2k = 1, i.e. k = log₂ n.

nlinear search (worst)binary search (worst, about log₂ n + 1)
16165 (16 → 8 → 4 → 2 → 1)
10⁴10,00014
10⁶1,000,00020
10⁹1,000,000,000 (TLE)30
Remember the ideaBinary search = eliminate half every step. Use it when standing at mid lets you pick a side, or when data is fully/partly sorted. Mid = low + (high - low) // 2. Exact value → <=; boundary/answer → <.

Part B · Pattern 1: basic binary search

1What it is

The plain case. You get a sorted array and one value, e.g. 8. Is 8 in the array (and where)? This is exactly the template from Part A, used directly.

2When to use it

3Intuition

Compare with mid. Smaller → go left. Bigger → go right. Equal → done.

4Problems she lists for practice

problemtiny examplehow pattern 1 fits
Binary Search (basic)[-1,0,3,5,9,12], target 9 → 4the template as it is
Search Insert Position[1,3,5,6], target 2 → 1same template, return low when not found
Sqrt(x)x = 8 → 2search over the numbers 1…x/2 instead of an array
Search in Rotated Sorted Array[4,5,6,1,2,3], target 1 → 3listed here too, but it needs the extra "which half is sorted?" check from Part C

5–8Steps, code, line by line, dry run

Identical to Part A, steps 5 to 8. Pattern 1 is the template. The next page (Binary Search Basics) goes through it in full detail.

9Complexity & remember

O(log n) time, O(1) space.

Remember pattern 1Sorted array + exact target → template with <=, mid ± 1, return −1 at the end.

Part C · Pattern 2: rotated sorted arrays, lower & upper bound

1What it is

The teacher groups these together under "lower bound and upper bound". The first example is a rotated sorted array: an array that was sorted, and then some items from the end were moved to the front.

Start from 1 2 3 4 5 6 and rotate it 3 times (each rotation moves the last item to the front):

rotationarraywhat moved
01 2 3 4 5 6—
16 1 2 3 4 56 moved from the end to the front
25 6 1 2 3 45 moved to the front
34 5 6 1 2 34 moved to the front

The result 4 5 6 1 2 3 is "sorted in two parts": 4 5 6 is sorted and 1 2 3 is sorted, both ascending. It's called a k-times rotated sorted array (here k = 3).

2When to use it (her three signals)

  1. The array is a rotated sorted array.
  2. The sorted array contains duplicates.
  3. The question asks for the first occurrence or last occurrence of a value.

3Intuition

A rotated array is not a special kind of binary search. It's normal binary search applied to broken pieces. At each mid, first figure out which half is the properly sorted one. In a sorted half, you can safely ask "is the target inside this range?". Then you know which side to keep.

4Building the logic from her example

Why plain binary search fails

Array 4 5 6 1 2 3, target 1.

index012345
value456123low 0, high 5, mid 2 → 6

Plain binary search compares 1 with 6. 1 is smaller, so it assumes 1 must be on the left and sets high = mid - 1. It then searches 4 5, doesn't find 1, and says "not present". That's wrong: 1 is at index 3, in the right half. The "smaller → go left" rule only holds when the whole array is sorted.

The fix: find the sorted half first

Doubt: why can at least one half always be sorted?
→ A rotated array has only one "break" (the spot where a big number is followed by a small one, 6 → 1 here). Mid splits the array into two halves, and the break can sit in only one of them. The other half has no break, so it's sorted.

What "lower bound" and "upper bound" mean (for the duplicates / first-last signals)

The pattern's name comes from these two helper searches on a sorted array:

index012345
value125559x = 5: lower bound = 2, upper bound = 5

First occurrence of 5 = lower bound = 2. Last occurrence = upper bound − 1 = 4. Count of 5s = 5 − 2 = 3. With duplicates, the basic template might stop at any of the 5s, so to get the first or last one we keep searching after a match. Later pages (First & Last Position, Count Occurrences, Floor & Ceiling) build on this.

5Steps (search in a rotated sorted array, distinct values)

  1. low = 0, high = n - 1, loop while low <= high.
  2. mid as usual. If arr[mid] == target → return mid.
  3. If arr[low] <= arr[mid] (left half sorted): if arr[low] <= target < arr[mid] → high = mid - 1, else low = mid + 1.
  4. Else (right half sorted): if arr[mid] < target <= arr[high] → low = mid + 1, else high = mid - 1.
  5. Loop ends → return −1.

6Code (Python)

pattern 2: search in a rotated sorted array
def search_rotated(arr, target):
    low, high = 0, len(arr) - 1
    while low <= high:
        mid = low + (high - low) // 2
        if arr[mid] == target:
            return mid
        if arr[low] <= arr[mid]:                    # left half is sorted
            if arr[low] <= target < arr[mid]:
                high = mid - 1                      # target is inside the left half
            else:
                low = mid + 1
        else:                                       # right half is sorted
            if arr[mid] < target <= arr[high]:
                low = mid + 1                       # target is inside the right half
            else:
                high = mid - 1
    return -1
pattern 2: lower bound and upper bound
def lower_bound(arr, x):          # first index with arr[i] >= x
    low, high = 0, len(arr)       # high = n means "past the end"
    while low < high:             # boundary search
        mid = low + (high - low) // 2
        if arr[mid] >= x:
            high = mid            # mid could be the answer, keep it
        else:
            low = mid + 1
    return low

def upper_bound(arr, x):          # first index with arr[i] > x
    low, high = 0, len(arr)
    while low < high:
        mid = low + (high - low) // 2
        if arr[mid] > x:
            high = mid
        else:
            low = mid + 1
    return low

7Code line by line

linewhat it means
if arr[low] <= arr[mid]:mid belongs to the left sorted piece, so low…mid is in order.
if arr[low] <= target < arr[mid]:The target fits inside that sorted range, so it can only be on the left. (< arr[mid] because equal was already handled.)
else: (right half sorted)The break is on the left, so mid…high is in order. Same check, mirrored.
high = len(arr)In lower/upper bound the answer can be "past the end" (no value is big enough), so n must be a possible answer.
if arr[mid] >= x: high = midmid qualifies, but an earlier index might too. Keep mid and look left.
else: low = mid + 1mid doesn't qualify, and nothing before it can either.

8Dry run: her example with the fix

4 5 6 1 2 3, target 1:

steplowhighmidarr[mid]which half is sorted?decisionthrown away
105264 ≤ 6 → left1 not in [4, 6) → low = 34 5 6
235421 ≤ 2 → left1 in [1, 2) → high = 32 3
33331—equal → return 3—
step 1456123
step 2456123
step 3456123found ✓

9Complexity & remember

Still O(log n) time, O(1) space: each step does a constant amount of extra checking and still halves the space. (With many duplicates in a rotated array, arr[low] == arr[mid] can hide which half is sorted. That case is handled later in its own problem.)

Remember pattern 2Rotated / duplicates / first-or-last occurrence. At mid, first ask which side is valid (sorted), then ask if the target lies in it. Lower bound = first ≥ x, upper bound = first > x.

Part D · Pattern 3: binary search on the answer (Koko)

LeetCode 875 (solved fully on its own page later)

1What it is

The teacher calls this very, very important for interviews, and the tricky one. Here you do not binary search over the given array. You binary search over the range of possible answers. You pick a candidate answer (say 5), test it, and the result tells you whether to try bigger or smaller candidates next, so you jump around the range (2? 7?) instead of trying every value in order.

2When to use it

Practice problems she names: Allocate Books (Allocate Minimum Pages), Koko Eating Bananas, Split Array Largest Sum.

3Intuition with her Koko example

A monkey named Koko has piles of bananas: piles = [6, 5, 3, 4] (each number = bananas in that pile). She must finish all of them within h = 8 hours. She eats at a fixed speed of k bananas per hour, and in one hour she works on only one pile. If a pile has fewer than k left, she finishes it and waits for the rest of that hour. Find the smallest speed k that lets her finish in time.

Hours for one pile at speed k = pile ÷ k rounded up, because a partial hour still costs a whole hour.

4Building the logic from her example

speed kpile 6pile 5pile 3pile 4total hours≤ 8?
46/4 = 1.5 → 25/4 = 1.25 → 23/4 → 14/4 → 16yes
36/3 → 25/3 = 1.67 → 23/3 → 14/3 = 1.33 → 27yes
26/2 → 35/2 = 2.5 → 33/2 = 1.5 → 24/2 → 210no

She starts with speed 4: 6 hours, which is within 8, so it's valid. But the question wants the minimum speed, so she tries 3: 7 hours, still valid. Then she tries 2. Working it out gives 10 hours, too slow. So the answer is 3.

Notice what she was doing: the "hours needed" part is just a simple function, and the real search was over speeds, values she picked from a range, not items from the array. That's the whole pattern.

Doubt: why is this a binary search and not just "try 1, 2, 3, …"?
→ Because the yes/no answer is monotonic: a faster speed never needs more hours. So the speeds line up as no no yes yes yes … (1 no, 2 no, 3 yes, 4 yes, …). We want the first yes. If mid says yes, the answer is mid or smaller → keep the left (including mid). If mid says no, everything smaller is also no → keep the right of mid.
Doubt: what is the answer range?
→ The smallest sensible speed is 1. The largest you'd ever need is max(piles): at that speed every pile takes exactly 1 hour, and going faster can't help. So we search k in [1, max(piles)].

5Steps

  1. Write hours(k) = sum of ⌈pile / k⌉ over all piles.
  2. low = 1, high = max(piles).
  3. While low < high (boundary search): mid; if hours(mid) <= h → high = mid, else low = mid + 1.
  4. Return low (the first speed that works).

6Code (Python)

brute force: try every speed from 1 up
class SolutionBrute:
    def minEatingSpeed(self, piles, h):
        k = 1
        while True:
            total = sum((p + k - 1) // k for p in piles)   # ceil(p / k)
            if total <= h:
                return k                                  # first speed that works
            k += 1
pattern 3: binary search on the answer
class Solution:
    def minEatingSpeed(self, piles, h):
        def hours(k):
            return sum((p + k - 1) // k for p in piles)   # ceil(p / k)

        low, high = 1, max(piles)        # the answer range
        while low < high:                # boundary search
            mid = low + (high - low) // 2
            if hours(mid) <= h:          # mid works: answer is mid or smaller
                high = mid
            else:                        # mid too slow: answer is bigger
                low = mid + 1
        return low

7Code line by line

linewhat it means
(p + k - 1) // kRounds p / k up using only whole numbers. e.g. p = 5, k = 4: (5 + 3) // 4 = 2 ✓.
low, high = 1, max(piles)The range of possible speeds, not array indexes.
while low < high:Boundary style: stop when one candidate is left.
if hours(mid) <= h: high = midmid is fast enough. It might be the minimum, so keep it, and look for something smaller.
else: low = mid + 1mid is too slow, and so is everything below it.
return lowlow == high: the smallest speed that works.

8Dry run

piles = [6, 5, 3, 4], h = 8, range [1, 6]:

steplowhighmidhours(mid)check (≤ 8?)decisionthrown away
11637yeshigh = 3speeds 4, 5, 6
213210nolow = 3speeds 1, 2
333———loop endsanswer 3 ✓

9Complexity & remember

Remember pattern 3Search over the answer range, not the array. Write check(answer). Monotonic yes/no → find the first yes (or last yes). "Minimum speed / pages / capacity" is the classic smell.

Part E · Pattern 4: binary search on a 2D matrix

1What it is

You get a matrix (a grid of rows and columns) that is sorted along each row and down each column. In her example, each row also starts after the previous row ends. Find a target, say 9.

         col0 col1 col2 col3
  row0     2    3    5    6
  row1     7   9   10   11
  row2    12   15   17   18

2When to use it

When the input is a sorted 2D grid. She says this pattern has several variations (e.g. rows sorted but rows overlapping), which come later with their own problems.

3Intuition

How the column tells us the row (her reasoning)

Because the matrix is sorted, the first item of a row (7) is bigger than the last item of the row above (6). So compare the target with the last item of each row:

So we binary search the last column for the first row whose last item is ≥ target. (You could use the first column instead, with the matching condition.) Then we binary search that row.

4Building the logic: why log r + log c = log(r·c)

Searching the column costs log r (r = number of rows). Searching one row costs log c (c = number of columns). Total log r + log c = log(r · c).

She shows this isn't magic: "open up" the rows and lay them end to end:

index01234567891011
value235679101112151718

That's just one sorted array of size r·c = 12. Binary search on it costs log(r·c), the same thing. A flat index k maps back to row = k // c, col = k % c (here 5 → row 1, col 1 ✓).

5Steps

  1. Binary search rows 0…r−1 for the first row whose last item is ≥ target. If none, return False.
  2. Binary search that row for the target.

6Code (Python)

pattern 4: column first, then the row
def search_matrix(matrix, target):
    if not matrix or not matrix[0]:
        return False
    r, c = len(matrix), len(matrix[0])

    # 1) binary search on the last column: first row whose end is >= target
    low, high = 0, r - 1
    row = -1
    while low <= high:
        mid = low + (high - low) // 2
        if matrix[mid][c - 1] >= target:
            row = mid                 # this row could hold it; try an earlier one
            high = mid - 1
        else:
            low = mid + 1
    if row == -1:
        return False                  # target is bigger than everything

    # 2) normal binary search inside that row
    low, high = 0, c - 1
    while low <= high:
        mid = low + (high - low) // 2
        if matrix[row][mid] == target:
            return True
        elif matrix[row][mid] < target:
            low = mid + 1
        else:
            high = mid - 1
    return False

7Code line by line

linewhat it means
matrix[mid][c - 1] >= targetRow mid ends at or after the target, so the target could be in it. Remember it, then check whether an earlier row also qualifies.
else: low = mid + 1Row mid ends before the target, so this row and all rows above it are too small.
if row == -1: return FalseNo row ends at or after the target: it's bigger than every value.
second loopThe plain template from Part A, on one row.

8Dry run

Target 9.

stagelowhighmidvalue looked atdecision
column021row 1 ends 1111 ≥ 9 → row = 1, high = 0
column000row 0 ends 66 < 9 → low = 1 → loop ends, row = 1
row 10319equal → True

9Complexity & remember

Remember pattern 4Sorted grid → binary search the column to pick the row, then the row. Or treat it as one flat sorted array of size r·c. Same cost: log(r·c).

Part F · Revision page

patternsignalswhat we search overkey move at midlooppractice
1 · basicfully sorted, exact targetarray indexescompare with target<=Binary Search, Insert Position, Sqrt(x)
2 · rotated / boundsrotated, duplicates, first/last occurrencearray indexesfirst find the sorted (valid) half<= (search) / < (bounds)Rotated array, First & Last, Count
3 · on answer"minimum/maximum X such that possible"a range of answerscheck(mid) yes/no<Koko, Allocate Books, Split Array
4 · 2D matrixsorted gridcolumn, then rowcompare with row ends, then values<=Search a 2D Matrix (I, II)
If you remember only 5 lines 1. Binary search = eliminate half the search space each step: O(n) → O(log n).
2. Use it if standing at mid tells you a side, or the data is fully/partly sorted.
3. mid = low + (high - low) // 2 (overflow-safe in Java/C++; Python doesn't overflow).
4. Exact value → while low <= high. Boundary / answer search → while low < high.
5. Four patterns: basic · rotated & bounds · on answer · 2D matrix.
Mistakes to avoid ✗ thinking binary search only means "find x in a sorted array"
✗ using plain "smaller → go left" on a rotated array
✗ binary searching the input array in an "on answer" problem (search the answer range)
✗ high = mid together with while low <= high (can loop forever)
✗ forgetting to round up when a partial hour counts as a full hour
test it yourself (paste under the functions above)
print(binary_search([3, 8, 12, 15], 12))           # 2
print(search_rotated([4, 5, 6, 1, 2, 3], 1))       # 3
print(lower_bound([1, 2, 5, 5, 5, 9], 5))          # 2
print(upper_bound([1, 2, 5, 5, 5, 9], 5))          # 5
print(Solution().minEatingSpeed([6, 5, 3, 4], 8))  # 3
m = [[2, 3, 5, 6], [7, 9, 10, 11], [12, 15, 17, 18]]
print(search_matrix(m, 9), search_matrix(m, 8))    # True False

Based on this video: How to identify Binary Search patterns