DSA Field Notes
Course map / 05 Binary Search
NeetCode · DSA for Beginners · Lessons 14-15

Binary Search Field Notes

Search a sorted array, then search a range of possible answers. Every diagram shows where L, mid and R stand and which half was thrown away. Read the notes, then close them and write all three templates from memory.

0Why binary search exists

Analogy

Looking up "python" in a paper dictionary. You do not start at page 1. You open in the middle, see the letter M, and know "python" must be after it. Now the left half of the book is dead to you. Open the middle of the right half, decide again, and repeat. Every open throws away half of the pages that are left, until you land on the word or run out of pages.

Binary search is an efficient way to search for an element in a sorted array. You are given an array and a target (the value you look for). The word sorted is the whole trick: because the values are in order, one comparison at the middle tells you which half the target can be in, and the other half can be dropped without looking at it.

NeetCode calls the set of positions the target could still be in the search space. It starts as the whole array and is cut in half at every step. Two variations show up in interviews:

  1. Search Array (lesson 14): a sorted array and a target. Decide whether the target is in the array, and where.
  2. Search Range (lesson 15): no array. You get a range of numbers and some way to tell "too big / too small / correct", and you search the range itself.
target word: "python" (its first letter is P) ① A B C D E F G H I J K L M N O P Q R S T U V W X Y Z ① open in the middle: M. "python" comes after M → throw away A-M, keep N-Z (26 → 13 letters) ② A ... M (gone) N O P Q R S T U V W X Y Z ② open in the middle of what is left: T. "python" comes before T → throw away T-Z, keep N-S (13 → 6) ③ A ... M (gone) N O P Q R S T ... Z (gone) ③ open in the middle of N-S: P. Found the P pages after 3 opens instead of walking 15 letters.
The search space shrinks by half on every open. Orange is the page you open (the mid). Faded letters are pages you will never touch again. This only works because a dictionary is sorted; in a pile of unsorted pages you would have to read every one.
Keep in mind

Binary search needs two things: the data is sorted (or at least ordered by the question you ask), and you can jump to the middle in O(1). Arrays give you both. A linked list gives you neither, so binary search on a linked list is pointless.

1Search an array (lesson 14)

Three markers: L, mid, R

Set two pointers. L is the left-most index (0) and R is the right-most index (len(arr) - 1). Together they mark the current search space; at the start that is the whole array. NeetCode also calls them low and high, or left and right. Then compute the middle index:

mid = (L + R) // 2, using integer division (throw away the fraction). mid is the index that splits the current subarray into two halves.

Compare arr[mid] with the target. There are exactly three outcomes:

  • target > arr[mid]: every value at or left of mid is too small (the array is sorted). Search the right half: L = mid + 1.
  • target < arr[mid]: every value at or right of mid is too big. Search the left half: R = mid - 1.
  • target == arr[mid]: found. Return mid.

Each step throws away half of what is left. That is the whole algorithm.

arr = [1, 3, 3, 4, 5, 6, 7, 8]      # NeetCode's array in the code block (note the two 3s)

def binarySearch(arr, target):
    L, R = 0, len(arr) - 1          # search space = whole array

    while L <= R:                    # at least one index is still possible
        mid = (L + R) // 2

        if target > arr[mid]:
            L = mid + 1              # drop mid and everything left of it
        elif target < arr[mid]:
            R = mid - 1              # drop mid and everything right of it
        else:
            return mid               # found
    return -1                        # L crossed R: not in the array

Case 1: the target exists. arr = [1,2,3,4,5,6,7,8], target = 5

The walkthrough in the video uses the array [1,2,3,4,5,6,7,8] (no duplicates). Follow L, mid and R for three loop turns.

① L mid R 1 2 3 4 5 6 7 8 01234567 mid = (0+7)//2 = 3 arr[3] = 4 < 5 → L = 4 ② L mid R 1 2 3 4 5 6 7 8 01234567 mid = (4+7)//2 = 5 arr[5] = 6 > 5 → R = 4 ③ L = mid = R 1 2 3 4 5 6 7 8 01234567 mid = (4+4)//2 = 4 arr[4] = 5 = target → return 4
Orange is the cell being tested; faded cells are out of the search space for good. After turn ① the search space is indices 4-7. After turn ② it is only index 4. Note that mid itself is dropped each time: the value at mid was compared and was not the target, so there is no reason to keep it.
TurnLRL <= R?mid = (L+R)//2arr[mid]compare to 5action
107yes345 > 4L = mid + 1 = 4
247yes565 < 6R = mid - 1 = 4
344yes45equalreturn 4

Case 2: the target does not exist. Same array, target = 9

9 is bigger than everything, so every comparison says "go right" and L keeps moving up. In the end L walks off the right end of the array, out of bounds. In general the search space is used up when L > R; the loop condition L <= R becomes false and we return -1.

① L mid R 1 2 3 4 5 6 7 8 01234567 mid = 3, arr[3] = 4 < 9 → L = 4 ② L mid R 1 2 3 4 5 6 7 8 01234567 mid = 5, arr[5] = 6 < 9 → L = 6 ③ L = mid R 1 2 3 4 5 6 7 8 01234567 mid = 6, arr[6] = 7 < 9 → L = 7 ④ L = mid = R 1 2 3 4 5 6 7 8 01234567 mid = 7, arr[7] = 8 < 9 → L = 8 ⑤ R L 1 2 3 4 5 6 7 8 ? 012345678 (out of bounds) L = 8 > R = 7 loop ends, return -1 4 comparisons for n = 8, which is floor(log2 8) + 1. The search space never gets a 5th chance.
The loop ends when L crosses R. In turn ④ the search space is a single cell (index 7). It is not the target, so L = mid + 1 = 8. Now nothing is left between L and R (the dashed cell does not exist), L <= R is false, and the function returns -1. The "out of bounds" pointer is not a bug; it is the signal that the array was exhausted.
TurnLRL <= R?midarr[mid]compare to 9action
107yes349 > 4L = 4
247yes569 > 6L = 6
367yes679 > 7L = 7
477yes789 > 8L = 8
587no---return -1

Why L <= R and not L < R

Look at turn 3 of case 1 and turn 4 of case 2. Both have L == R: the search space is exactly one cell, and that cell has not been checked yet. With while L < R the loop would skip it. A target that sits in the last remaining cell would be reported as "not found". The smallest example: binarySearch([7], 7) returns -1 with L < R, because the loop body never runs.

Rule: when R = len(arr) - 1 (R is a real index that may hold the answer), the loop is while L <= R. Section 3 shows the other convention, where R = len(arr) and the loop is while L < R. Both are correct; mixing them is the number one binary search bug.

The overflow note: mid = L + (R - L) // 2

NeetCode mentions a better formula for the middle: L + (R - L) // 2. It gives the same number as (L + R) // 2, but it never computes L + R. In languages with fixed-size integers (Java, C++, Go) L + R can exceed the largest 32-bit int (about 2.1 billion) and wrap around to a negative number. This exact bug sat in Java's own library binary search for years; a Google Research blog post famously wrote it up. Python integers grow as large as needed, so (L + R) // 2 cannot overflow here. Interviewers still like to see the safe form, and it costs nothing, so use it.

# same value, no L + R anywhere
mid = L + (R - L) // 2

# Python ints do not overflow; this would be a bug in Java with 32-bit ints
big = 2**31 - 1
(big + big) // 2            # 2147483647 in Python, negative garbage in Java

Time and space: why log n

The same idea as merge sort's split: divide the size in half until you reach size 1. If you start with n cells and halve x times, you have n / 2^x left. The search ends when that equals 1:

1 = n / 2^x  →  2^x = n  →  x = log2(n).

So the number of comparisons is about log2(n), and time is O(log n). The iterative version keeps only L, R and mid, so space is O(1). (A recursive version costs O(log n) stack frames.)

n = 8 start: 8 cells could hold the target n / 2 = 4 step ①: one compare drops 4 n / 4 = 2 step ②: one compare drops 2 n / 8 = 1 step ③: size 1, last compare decides 1 = n / 2^x → 2^x = n → x = log2(n) (n = 8 → x = 3)
Every comparison halves what is left, so the number of comparisons is the number of halvings. Doubling n adds only one more step. That is why binary search on a billion sorted items takes about 30 comparisons.
n (size of search space)log2(n), rounded upLinear scan worst case
838
1,024101,024
1,000,000201,000,000
1,000,000,000301,000,000,000
# the recursive shape, to see the merge-sort-style halving directly
def binary_search_rec(arr, target, L, R):
    if L > R:
        return -1                                   # search space is empty
    mid = L + (R - L) // 2
    if arr[mid] == target:
        return mid
    if target > arr[mid]:
        return binary_search_rec(arr, target, mid + 1, R)
    return binary_search_rec(arr, target, L, mid - 1)
About the two 3s

NeetCode's code block uses arr = [1, 3, 3, 4, 5, 6, 7, 8] while the walkthrough uses [1,2,3,4,5,6,7,8]. Binary search still works with duplicates; it just returns some index holding the target, not necessarily the first one. binarySearch(arr, 3) here returns 1 (mid lands on index 3, then 1). If you need the first or last copy, use the boundary template in section 3.

2Search a range (lesson 15)

Analogy

Pick a number from 1 to 100 and ask a friend to guess it. After each guess you say one of three things: correct, too small, or too big. A smart friend guesses 50 first, then 25 or 75, and needs at most 7 guesses. There is no array anywhere in this game, but it is binary search.

This is the second variation. You are not given an array. You are given a range of numbers and a way to check a guess. At its core it is still binary search: as long as there is some way to tell whether a guess is too big, too small, or correct, you can adjust the search space in the same way. Often the comparison is done by a predefined function or a math equation instead of arr[mid].

The black box: isCorrect(n)

NeetCode's example is a function isCorrect(n) that returns 1 if n is too big, -1 if too small, and 0 if correct. In the lesson it is hardcoded with 10 as the secret number. In a real problem you do not see the inside; you are given the function as an API (a method you can call but not read) and you treat it as a black box inside your own binary search.

# Return 1 if n is too big, -1 if too small, 0 if correct
def isCorrect(n):
    if n > 10:
        return 1
    elif n < 10:
        return -1
    else:
        return 0

Binary search over the range

Compare with the array version line by line. L and R became low and high, and they are values, not indices. arr[mid] became a call to the black box. Everything else is identical, including the low <= high loop and the mid ± 1 moves.

# Binary search on some range of values
def binarySearch(low, high):

    while low <= high:
        mid = (low + high) // 2

        if isCorrect(mid) > 0:      # mid is too big  → answer is below mid
            high = mid - 1
        elif isCorrect(mid) < 0:    # mid is too small → answer is above mid
            low = mid + 1
        else:
            return mid              # correct
    return -1

binarySearch(1, 100)   # → 10 after 6 guesses

NeetCode warns that these questions can be confusing and subtle: the hard part is figuring out how to modify the typical binary search so that the black box replaces the array comparison. Notice one small trap in the code above: isCorrect(mid) is called twice per turn. That is fine for a cheap function, but if the check is expensive, call it once and store the result.

guesses 1-3 on the 1..100 line (bar = still possible) 1 100 guess 50 ① 50 is too big → high = 49 1 49 guess 25 ② 25 is too big → high = 24 1 24 guess 12 ③ 12 is too big → high = 11 zoom in: last 3 guesses inside 1..11 (9× bigger) 1 11 guess 6 ④ 6 is too small → low = 7 7 11 guess 9 ⑤ 9 is too small → low = 10 10 11 guess 10 ⑥ correct → return 10
Same shrinking search space, no array. "Too big" moves high down to mid - 1; "too small" moves low up to mid + 1. The three answers from the friend play the role of arr[mid] > target, <, and ==.
Guesslowhighmid = (low+high)//2isCorrect(mid)meaningaction
11100501too bighigh = 49
2149251too bighigh = 24
3124121too bighigh = 11
41116-1too smalllow = 7
57119-1too smalllow = 10
61011100correctreturn 10

Time and space

O(log n), where n is the size of the search space (100 here, so at most 7 guesses), not the size of any array. No extra memory is used: O(1) space.

Keep in mind

The range version is the one that shows up disguised. "Find the first bad version", "guess the number", "minimum speed to finish in time", "square root of x": none of them hand you an array, all of them hand you a range plus a yes/no or too-big/too-small check. When you see a range and a check, think binary search.

3The three templates

Almost every binary search problem is one of three shapes. Memorize the three as separate templates and do not mix their loop conditions.

Template 1: exact match (while L <= R)

The lesson's version, tidied. Use it when you want one index holding the target and -1 otherwise. R is a real index. Both pointers skip past mid.

def exact_match(nums, target):
    L, R = 0, len(nums) - 1
    while L <= R:
        mid = L + (R - L) // 2
        if nums[mid] == target:
            return mid
        if nums[mid] < target:
            L = mid + 1
        else:
            R = mid - 1
    return -1

Template 2: boundary, also called lower bound or "first True" (while L < R)

Many problems do not ask "is the target here?" but "where does the answer start?": the first index with value ≥ target, the first bad version, the first speed that is fast enough. Picture the array as answers to a yes/no question. Because the data is sorted, the answers look like F F F T T T: all False, then all True. The word for that is monotonic (it never goes back from True to False). The boundary template finds the first True.

Three things change compared to template 1:

  • R = len(nums), one past the end. That lets "no True at all" be a valid answer (len(nums)).
  • When mid is True, do R = mid, not mid - 1. mid might be the first True, so keep it.
  • Loop while L < R. The loop ends when they meet, and the meeting point is the answer. There is no return -1 inside.
def lower_bound(nums, target):
    """First index i with nums[i] >= target. Returns len(nums) if none."""
    L, R = 0, len(nums)            # R is one past the end
    while L < R:
        mid = L + (R - L) // 2
        if nums[mid] < target:
            L = mid + 1            # mid is too small: answer is to the right
        else:
            R = mid                # mid could be the answer: keep it
    return L                       # L == R here

def upper_bound(nums, target):
    """First index i with nums[i] > target."""
    L, R = 0, len(nums)
    while L < R:
        mid = L + (R - L) // 2
        if nums[mid] <= target:
            L = mid + 1
        else:
            R = mid
    return L

# the same template with any yes/no check
def first_true(n, is_true):
    """Smallest i in [0, n) with is_true(i) True, given F..F T..T. Returns n if none."""
    L, R = 0, n
    while L < R:
        mid = L + (R - L) // 2
        if is_true(mid):
            R = mid
        else:
            L = mid + 1
    return L
① L mid R = len F F F T T T 0123456 mid = 3 is True → R = mid = 3 (keep it) ② L mid R F F F T T T 0123456 mid = 1 is False → L = mid + 1 = 2 ③ L = mid R F F F T T T 0123456 mid = 2 is False → L = mid + 1 = 3 ④ L = R F F F T T T 0123456 L == R → loop stops → return 3, the first True
The invariant: everything left of L is False, everything from R on is True. Each turn moves one of them without breaking that rule. When they meet, the meeting index is the boundary. Note turn ①: mid was True, so R moved onto it, not past it. With R = mid - 1 you would lose the answer.
TurnLRL < R?midis_true(mid)action
106yes3TrueR = mid = 3
203yes1FalseL = mid + 1 = 2
323yes2FalseL = mid + 1 = 3
433no--return 3
Why this loop cannot spin forever

In the boundary template, mid = L + (R - L) // 2 rounds down, so mid < R always. R = mid therefore always shrinks the space, and L = mid + 1 always grows L. If you ever write L = mid in a while L < R loop, then with L = 2, R = 3 you get mid = 2, L = 2 again, forever. That is the classic infinite loop. (The fix for a "last True" search is to round up: mid = L + (R - L + 1) // 2.)

Template 3: binary search on the answer

This is lesson 15 in interview clothing. The question asks for a number (a speed, a capacity, a number of days), and checking whether a given number works is easy. Then:

  1. Write the check, can(x), that returns True if answer x is good enough. This is your isCorrect.
  2. Make sure it is monotonic: if x works, every bigger x works too (or every smaller, depending on the problem). Then the answers look like F F F T T T over the range.
  3. Set L and R to the smallest and largest possible answers.
  4. Run the boundary template to find the first True. That is the minimum answer.

Example, Koko Eating Bananas (LeetCode 875): piles [3, 6, 7, 11], h = 8 hours. Koko eats k bananas per hour, one pile per hour, and finishes a pile of size p in ceil(p / k) hours. Find the smallest k that finishes in time.

def min_eating_speed(piles, h):
    def can_finish(k):                       # the "isCorrect" black box
        hours = 0
        for p in piles:
            hours += (p + k - 1) // k        # ceil(p / k) with integers
        return hours <= h

    L, R = 1, max(piles)                     # answer space: speeds 1..max pile
    while L < R:
        mid = L + (R - L) // 2
        if can_finish(mid):
            R = mid                          # mid works: try slower
        else:
            L = mid + 1                      # too slow: need faster
    return L

min_eating_speed([3, 6, 7, 11], 8)   # → 4
first True = answer hours needed 27 15 10 8 8 6 5 5 5 5 4 FFF TTTTTTTT ≤ 8 hours? k=1234567891011 speed k nobody builds this row; binary search only calls can_finish at mid = 6, 3, 5, 4 and never looks at the rest
Search the answers, not the input. The piles are not sorted and never need to be. The thing that is sorted is the row of yes/no answers over the speeds, F F F T T T T T T T T, and that is where binary search runs. Hours needed only go down as speed goes up, so the row is monotonic.
TurnLRmidhours at speed midcan_finish(mid)action
111161+1+2+2 = 6TrueR = 6
21631+2+3+4 = 10FalseL = 4
34651+2+2+3 = 8TrueR = 5
44541+2+2+3 = 8TrueR = 4
544---return 4

The mirror image is "largest x that still works" (last True). Sqrt(x) (LeetCode 69) wants the largest k with k * k <= x. With the L <= R template, R ends up sitting on the last value that passed:

def my_sqrt(x):
    L, R = 0, x
    while L <= R:
        mid = L + (R - L) // 2
        if mid * mid <= x:
            L = mid + 1                       # mid fits: look for a bigger one
        else:
            R = mid - 1
    return R                                  # R stops on the last mid that fit

my_sqrt(8)    # → 2   (2*2 = 4 ≤ 8, 3*3 = 9 > 8)

Python's bisect module

Templates 1 and 2 are built into the standard library. bisect_left is exactly lower_bound; bisect_right is exactly upper_bound; insort finds the spot with binary search and inserts there (the insert itself shifts elements, so it is O(n)). In an interview, say "I would use bisect in production" and then still write the loop by hand, because that is what is being tested.

import bisect

a = [1, 2, 2, 2, 5]
bisect.bisect_left(a, 2)     # 1   first index with a[i] >= 2  (= lower_bound)
bisect.bisect_right(a, 2)    # 4   first index with a[i] > 2   (= upper_bound)
bisect.bisect_left(a, 3)     # 4   where 3 would go
bisect.bisect_left(a, 9)     # 5   == len(a): bigger than everything
bisect.insort(a, 3)          # a is now [1, 2, 2, 2, 3, 5]; O(log n) to find, O(n) to shift

# "is target in the sorted list?" with bisect (exact match, template 1)
nums = [1, 2, 3, 4, 5, 6, 7, 8]
i = bisect.bisect_left(nums, 5)
found = i < len(nums) and nums[i] == 5    # True; both checks are needed

4Moves that solve most binary search problems

The lessons teach two variations. Interviews dress them up. These three moves undress them.

Move 1: "Is it monotonic? Then binary search."

Before you write any code, ask one question about the problem: is there a yes/no test that is all False then all True (or all True then all False) across some range? If yes, the boundary lives there, and binary search finds it in O(log n). The range can be array indices, a range of numbers, or version numbers. The test can be nums[i] >= target, isBadVersion(i), or "can I finish with speed k". If the answers flip back and forth (F T F T), binary search is wrong and you need a scan.

First Bad Version (LeetCode 278) is the cleanest example: versions 1..n, a black-box isBadVersion(v), and once a version is bad every later one is bad. That is F F F T T T over 1..n.

def first_bad_version(n, isBadVersion):
    L, R = 1, n                     # answer is guaranteed to exist, so R = n is enough
    while L < R:
        mid = L + (R - L) // 2
        if isBadVersion(mid):
            R = mid                 # mid is bad: it could be the first one
        else:
            L = mid + 1             # mid is good: first bad is after it
    return L

The monotonic test can be less obvious. In Find Minimum in Rotated Sorted Array (LeetCode 153), [4,5,6,7,0,1,2] is not sorted, but the test nums[mid] > nums[R] reads T T T T F F F, and the minimum is the first False.

def find_min_rotated(nums):
    L, R = 0, len(nums) - 1
    while L < R:
        mid = L + (R - L) // 2
        if nums[mid] > nums[R]:
            L = mid + 1                       # mid is in the big part: min is to the right
        else:
            R = mid                           # mid could be the min
    return nums[L]

find_min_rotated([4, 5, 6, 7, 0, 1, 2])   # → 0

Move 2: Search on the answer space

When the question asks "what is the minimum X such that..." or "the maximum X such that...", and X is a number with a clear smallest and largest possible value, do not search the input. Search X.

  1. Name the answer variable and its range: L = smallest possible, R = largest possible. For Koko: 1 to max(piles). For "ship packages within D days": max(weights) to sum(weights).
  2. Write can(x): a plain loop that says whether answer x is good enough. Usually O(n).
  3. Check the direction: does can go from False to True as x grows (minimize) or True to False (maximize)?
  4. Run the boundary template. Total time: O(n log(range)).

The template is min_eating_speed in section 3. Copy it and change only can, L, and R.

Move 3: The boundary template for first and last position

Two boundary searches give you the whole run of a value. Find First and Last Position (LeetCode 34): the first index of target is lower_bound(target); the last index is upper_bound(target) - 1. Search Insert Position (LeetCode 35) is lower_bound by itself.

def search_range(nums, target):
    lo = lower_bound(nums, target)              # first i with nums[i] >= target
    if lo == len(nums) or nums[lo] != target:
        return [-1, -1]                        # target is not there at all
    return [lo, upper_bound(nums, target) - 1]  # last i with nums[i] == target

search_range([5, 7, 7, 8, 8, 10], 8)   # → [3, 4]

def search_insert(nums, target):            # LeetCode 35
    return lower_bound(nums, target)

One more disguise: a 2D matrix whose rows are sorted and each row starts after the previous row ends (LeetCode 74) is just a sorted 1D array folded into rows. Binary search on the index 0 .. rows*cols - 1 and translate with row = mid // cols, col = mid % cols.

def search_matrix(matrix, target):
    rows, cols = len(matrix), len(matrix[0])
    L, R = 0, rows * cols - 1
    while L <= R:
        mid = L + (R - L) // 2
        val = matrix[mid // cols][mid % cols]  # unfold the flat index
        if val == target:
            return True
        if val < target:
            L = mid + 1
        else:
            R = mid - 1
    return False
Keep in mind

Pick the template before you type. "Is it there?" → template 1, L <= R, return inside the loop. "Where does it start / smallest x that works?" → template 2, L < R, return L after the loop. "Largest x that works?" → template 1 shape and return R, or template 2 with mid rounded up.

5Big-O summary

In every row, n is the size of the search space: the array length, or the number of possible answers in the range.

TaskBinary searchLinear scanExtra space
Find a target in a sorted arrayO(log n)O(n)O(1)
Find a target in an unsorted arraynot possible*O(n)-
Sort first, then binary search onceO(n log n)O(n)depends on sort
Sort once, then search k timesO(n log n + k log n)O(k · n)depends on sort
Guess a number in a range of size nO(log n)O(n)O(1)
Lower bound / first TrueO(log n)O(n)O(1)
Binary search on the answer, check costs O(n)O(n log(range))O(n · range)O(1)
Recursive binary searchO(log n)-O(log n) stack
bisect.bisect_leftO(log n)-O(1)
bisect.insortO(n)**-O(1)

* Binary search on unsorted data gives wrong answers, not slow answers. Sorting costs O(n log n), so it pays off only when you will search many times. ** insort finds the position in O(log n), but inserting into a Python list shifts everything after it, which is O(n).

6Mistakes to watch for

while L < R with R = len - 1

The last remaining cell is never checked. binarySearch([7], 7) returns -1. If R is a real index, the loop is while L <= R.

L, R = 0, len(arr) - 1
while L < R:        # BUG: skips the case L == R
Mixing the two templates

R = len(arr) with while L <= R reads arr[len(arr)] and crashes with IndexError. R = len(arr) - 1 with R = mid and L < R can miss the answer at the last index. Choose one template and keep all three of its parts together.

L = mid in a while L < R loop

When R = L + 1, mid rounds down to L, and L = mid changes nothing. Infinite loop. In the boundary template the moves are R = mid and L = mid + 1, never L = mid.

Forgetting the array must be sorted

Binary search on unsorted data returns confident garbage: it may say -1 for a value that is there. If the input is not sorted and you may sort it, sort first (O(n log n)). If you may not, the answer is a linear scan.

mid = (L + R) / 2 in Python 3

A single slash gives a float: (0 + 7) / 2 is 3.5, and arr[3.5] raises TypeError. Use //. NeetCode's slides write / and mean integer division.

Returning L when you needed R (or the reverse)

After the L <= R loop ends without a match, L is the insert position (first bigger value) and R = L - 1 is the last smaller value. For "largest k with k*k <= x" you want R. Trace a two-element example on paper before you decide.

Trusting bisect_left's index without checking it

bisect_left(nums, 9) on [1..8] returns 8, a valid-looking number that is out of bounds. Always check i < len(nums) and nums[i] == target before you call it "found".

Searching the input instead of the answer

Koko's piles are unsorted and sorting them does not help. The monotonic thing is the speed. When the question says "minimum X such that", set L and R to the smallest and largest X and write can(X).

Calling the black box twice per turn

NeetCode's range code calls isCorrect(mid) in both the if and the elif. If the check is an O(n) loop, that doubles the work. Store it once: r = isCorrect(mid).

Interview one-liners

  • "The array is sorted, so one comparison at the middle tells me which half to drop. That halves the search space each step: O(log n) time, O(1) space."
  • "Halving n down to 1 takes x steps where n / 2^x = 1, so x = log2 n."
  • "I keep L and R as the inclusive bounds of the search space and loop while L <= R; when L crosses R the target is not there."
  • "I compute mid as L + (R - L) // 2 to avoid overflow in fixed-width languages; Python ints do not overflow, but it is a habit."
  • "This asks for the first index where a condition becomes true, so I use the boundary form: R = len, while L < R, R = mid on true, L = mid + 1 on false, return L."
  • "The check is monotonic in X, so I binary search on the answer: L and R are the smallest and largest X, and can(mid) replaces the array comparison."
  • "In production I would reach for bisect.bisect_left, which is the same lower-bound loop."

7Practice, in this order

Solve these on LeetCode with the notes closed. Before each one, say which template you will use and why, then write it.

#ProblemMove it trainsLevel
704Binary Searchtemplate 1, exact match, the lesson's codeEasy
35Search Insert Positiontemplate 2, lower boundEasy
374Guess Number Higher or Lowersearch a range with a black-box API (lesson 15)Easy
278First Bad Versionfirst True over versions 1..nEasy
69Sqrt(x)last True over a numeric rangeEasy
74Search a 2D Matrixflat index math, template 1Medium
875Koko Eating Bananasbinary search on the answer, minimizeMedium
1011Capacity To Ship Packages Within D Daysbinary search on the answer, minimize capacityMedium
153Find Minimum in Rotated Sorted Arrayhidden monotonic test: nums[mid] > nums[R]Medium
33Search in Rotated Sorted Arraydecide which half is sorted, then template 1Medium
34Find First and Last Position of Element in Sorted Arraylower bound + upper boundMedium

Your next step today: LeetCode 704. Write it from memory with L <= R, then test it on [1,2,3,4,5,6,7,8] with targets 5 and 9 and compare with the two trace tables above.

8Self-check

Answer out loud before opening. If you cannot answer in one breath, reread that section.

Q1What single property of the input makes binary search possible, and why?

The array is sorted. Sorted means one comparison with the middle value tells you which side every other value is on, so you can throw away half without looking at it.

Q2What are the three outcomes of comparing arr[mid] with the target, and what do you do for each?

Target greater: L = mid + 1 (search right). Target smaller: R = mid - 1 (search left). Equal: return mid.

Q3Trace it: arr = [1,2,3,4,5,6,7,8], target = 5. Give L, R, mid and arr[mid] for each loop turn.

Turn 1: L=0, R=7, mid=3, arr[3]=4, 5 > 4 so L=4. Turn 2: L=4, R=7, mid=5, arr[5]=6, 5 < 6 so R=4. Turn 3: L=4, R=4, mid=4, arr[4]=5, found, return 4.

Q4Trace it: same array, target = 9. What are L and R when the loop stops, and what is returned?

L moves 0 → 4 → 6 → 7 → 8 while R stays 7. The loop stops at L=8, R=7 because L <= R is false. Return -1. L is out of bounds; that is expected.

Q5Why is the loop condition L <= R and not L < R in the lesson's code?

When L == R the search space still holds one unchecked cell. L < R would stop before checking it, so a target in that cell would be reported missing. Example: binarySearch([7], 7).

Q6Why does NeetCode prefer L + (R - L) // 2 over (L + R) // 2? Does it matter in Python?

In languages with fixed-size integers, L + R can overflow when both are large. L + (R - L) // 2 never adds two large numbers. Python integers cannot overflow, so both are correct there, but interviewers expect the safe form.

Q7Derive the time complexity. Start from "the search space halves each step".

After x steps the size is n / 2^x. The search ends at size 1: 1 = n / 2^x, so 2^x = n and x = log2(n). Time O(log n). Iterative space O(1).

Q8In the "guess 1 to 100" version, what replaces arr[mid], and what do low and high hold?

A black-box check such as isCorrect(mid) that says too big (1), too small (-1), or correct (0). low and high are values in the range, not array indices. "Too big" does high = mid - 1; "too small" does low = mid + 1.

Q9Trace it: secret number 10, range 1..100. List the guesses.

50 (too big, high=49), 25 (too big, high=24), 12 (too big, high=11), 6 (too small, low=7), 9 (too small, low=10), 10 (correct). Six guesses; log2(100) is about 6.6.

Q10What three things change between the exact-match template and the boundary (lower bound) template?

R = len(nums) instead of len(nums) - 1; on a True/match-or-bigger result do R = mid instead of mid - 1; loop while L < R and return L after the loop instead of returning inside it.

Q11Why can L = mid inside a while L < R loop hang forever, and what is the fix?

When R = L + 1, mid rounds down to L, so L = mid does not move. The fix is L = mid + 1 (first-True search) or rounding mid up with L + (R - L + 1) // 2 (last-True search).

Q12Koko has piles [3,6,7,11] and 8 hours. What is the search space, what is the check, and what is the answer?

Search space: speeds 1 to 11 (max pile). Check: sum(ceil(p / k)) <= 8, which is monotonic (F F F T T T T T T T T). First True is k = 4, so the answer is 4.

Q13Which bisect function equals lower bound, which equals upper bound, and what extra check do you need after bisect_left to say "found"?

bisect_left is lower bound (first index with value ≥ target). bisect_right is upper bound (first index with value > target). After i = bisect_left(a, t), "found" means i < len(a) and a[i] == t.