← DSA Fundamentals
Algorithms

Searching

Watch: "Binary Search tutorial (C++ and Python)" — Errichto Algorithms

Searching is the question "is this value here, and if so where," and the entire topic is a lesson in what one extra fact about your data — that it's sorted — buys you. Linear search makes no assumptions and checks every element, one by one, until it either finds the target or runs out of elements: reliable, simple, and O(n). Binary search assumes sorted order and exploits it to eliminate half the remaining candidates on every comparison, giving O(log n). Interviewers care about this because the leap from "check everything" to "check smartly using a known invariant" is the same leap behind database indexes (a B-tree is binary search generalized to disk pages), autocomplete, and version-control bisection (git bisect is literally binary search over commit history to find which commit introduced a bug). The topic also has a second life beyond literal sorted arrays: "binary search on the answer," where you binary-search over a range of possible answers rather than over an array, which is one of the highest-leverage patterns for turning a slow brute-force problem into a fast one.

Deep Dive: Linear Search — the Baseline

Linear search checks 4, 19, 2, then finds 31 at index 3401912231384155≠≠≠found
Target 31: check each element left to right until it matches. 4 comparisons here; a missing target (99) checks all 6.
checked, no matchmatch
Code, line by line
def linear_search(arr, target):
    for i, v in enumerate(arr):
        if v == target:
            return i
    return -1
  1. enumerate gives both the index i and the value v, in order from the front.
  2. if v == target: return i stops at the first match. Best case: it is at index 0, one comparison.
  3. return -1 only runs after every element was checked, which is the worst case: n comparisons. Works on unsorted data, which is its whole advantage.

Linear search's cost is exactly the position of the target: best case O(1) (it's the first element), worst case O(n) (it's last, or absent — you must exhaust every element to be sure). There is no cleverness possible here in general, because with no ordering guarantee, any unexamined element could be the target; skipping one is a correctness bug, not an optimization. That is precisely why it is still the right tool for unsorted data or a one-off scan of a small collection — the cost of *sorting first* (at best O(n log n)) is not worth paying if you're only going to search once, or if n is small enough that O(n) and O(log n) are practically indistinguishable.

The subtlety worth stating precisely in an interview: linear search's O(n) is worst-case and average-case both scale linearly (average case is n/2 comparisons for a present target, uniformly distributed — still Θ(n) asymptotically, just with a smaller constant).

Deep Dive: Binary Search — Halving the Search Space

Binary search for 27 in 16 sorted values: 3 steps1031527394115136157178199211023112512271329143115lohimidstep 1135791113151719212325272931lohimidstep 2135791113151719212325272931lohimidstep 3
Target 27. Step 1: middle is 15, too small, so the whole left half is dropped. Step 2: 23, still too small. Step 3: 27, found. 3 comparisons instead of linear search's 14.
mid, not a matchmid, matchstill in range

Binary search is O(log n) because each comparison doesn't just rule out one element — it rules out an entire half of the remaining candidates at once, by using the sortedness guarantee: if arr[mid] < target, every index at or before mid is also less than target (sortedness propagates that fact without checking them individually), so the whole left half is provably not the answer. Halving n repeatedly until you reach 1 takes log₂(n) steps — that's the definition of a logarithm. This precondition is not optional: run binary search on unsorted data and the halving logic is simply wrong, because "arr[mid] < target" no longer tells you anything reliable about the discarded half.

Best case is O(1) (target is the very first mid checked); worst and average case are both O(log n) — there's no pathological input that makes binary search worse than log n on a sorted array, which is part of why it's such a dependable building block.

Deep Dive: The Low/Mid/High Update Logic, and Its Classic Bug

Correct version, line by line
def binary_search(arr, target):
    lo, hi = 0, len(arr) - 1
    while lo <= hi:
        mid = (lo + hi) // 2
        if arr[mid] == target:
            return mid
        elif arr[mid] < target:
            lo = mid + 1
        else:
            hi = mid - 1
    return -1
  1. lo, hi = 0, len(arr) - 1: the target, if present, is somewhere in arr[lo..hi] inclusive. Every line below keeps that promise.
  2. while lo <= hi: keep going while the range holds at least one element. With < a one-element range is skipped and a present target can be reported missing.
  3. mid = (lo + hi) // 2: look at the middle. In Java/C++ write lo + (hi - lo) // 2 so lo + hi cannot overflow.
  4. Equal: done, return the index.
  5. arr[mid] < target: the target is right of mid. mid itself is already ruled out, so the new range starts at mid + 1.
  6. Otherwise the target is left of mid: hi = mid - 1, again excluding the checked mid.
  7. The range is empty (lo > hi): the target is not in the array.
With hi = mid the range stops shrinking and the loop never ends135lomidhi3 > 0 → hi = mid = 1135lo/midhi1 > 0 → hi = mid = 0135lo/hi/mid1 > 0 → hi = mid = 0 (no change!)135lo/hi/mid… same state forever
The bug: hi = mid instead of hi = mid - 1, searching [1, 3, 5] for 0. Once lo == hi == mid, setting hi = mid changes nothing and lo <= hi stays true forever.
midstuck

The three classic off-by-one bugs are: using lo < hi instead of lo <= hi (misses the case where the search space has exactly one element left, incorrectly reporting "not found" for a valid single-element range); forgetting the +1/-1 when narrowing (lo = mid or hi = mid instead of mid + 1/mid - 1), which can loop forever because the already-checked mid stays inside the range; and integer overflow on mid = (lo + hi) // 2 in languages with fixed-width integers when lo + hi exceeds the max int (Python doesn't have this problem, but Java/C++ interviewers will ask about it — the fix is mid = lo + (hi - lo) // 2).

The invariant to hold in your head while coding this: at every iteration, if the target exists, it is guaranteed to be somewhere in arr[lo..hi] inclusive. Every update must preserve that invariant exactly — narrowing too aggressively (skipping mid itself when it's still a valid candidate) or too conservatively (leaving an already-ruled-out index in range) breaks it.

Deep Dive: Binary Search on the Answer

Capacities 10 to 55: 10 to 14 fail, 15 and above work; five probes find 15101520304055too smallcan ship in ≤ 5 days12345answer = 15
Every capacity from 10 to 55 is a possible answer. Below 15 it fails, from 15 up it works, and that single flip is what binary search finds. Probes in order: 32 yes, 21 yes, 15 yes, 12 no, 14 no, so the answer is 15 after 5 probes.
can't finish in 5 dayscan finish
Code, line by line
def can_ship(weights, capacity, days):
    days_needed, load = 1, 0
    for w in weights:
        if load + w > capacity:
            days_needed += 1
            load = 0
        load += w
    return days_needed <= days

def min_capacity(weights, days):
    lo, hi = max(weights), sum(weights)
    while lo < hi:
        mid = (lo + hi) // 2
        if can_ship(weights, mid, days):
            hi = mid
        else:
            lo = mid + 1
    return lo
  1. can_ship is the yes/no test. It loads packages in order and starts a new day whenever the next package would overflow the ship.
  2. It answers True if the packages fit in the allowed number of days. Bigger capacity never needs more days, so the answers go no, no, …, yes, yes: sorted, even though no array is.
  3. lo = max(weights): the ship must carry the heaviest single package. hi = sum(weights): everything in one day always works.
  4. while lo < hi (not <=): we are looking for a boundary, and when lo == hi it has been found.
  5. If mid works, the answer is mid or smaller, so hi = mid (keep mid, it might be the answer).
  6. If it fails, the answer is bigger than mid: lo = mid + 1. Unlike the bug above this can't loop forever, because mid is always less than hi here.

"Binary search on the answer" means: instead of searching for a value's position inside a sorted array, you search over the *space of possible answers* (here, every integer capacity from max(weights) to sum(weights)) for the boundary where a yes/no predicate flips. You recognize a problem needs this pattern when it asks for a minimum (or maximum) value satisfying some feasibility condition, and you can write a "given this candidate value, is it feasible?" check that runs in reasonable time and is provably monotonic — once true, always true for larger values (or the symmetric case for smaller). The array itself never has to be sorted, or even exist; what's sorted, implicitly, is the true/false outcome of the predicate across the answer range.

This turns an O(n · feasibility-check-cost) brute force (try every capacity from lo to hi one at a time) into O(log(range) · feasibility-check-cost) — the same halving logic as classic binary search, just applied to a different axis.

Key Pointers

How It Works

  1. Linear search: start at index 0, compare each element to the target in order, return the index on a match, return "not found" after the last index.
  2. Binary search setup: confirm (or sort) that the array is sorted; set lo = 0, hi = len(arr) - 1.
  3. Loop while lo <= hi: compute mid = (lo + hi) // 2.
  4. If arr[mid] == target, return mid — done.
  5. If arr[mid] < target, the target (if present) must be to the right, so set lo = mid + 1.
  6. Otherwise the target must be to the left, so set hi = mid - 1.
  7. If the loop exits without a match (lo > hi), the target is not in the array.
  8. For binary search on the answer: define lo/hi as the plausible bounds of the answer itself, write a monotonic is_feasible(candidate) predicate, and binary-search over the candidate range instead of over array indices, narrowing toward the smallest (or largest) feasible value.

Try It Yourself

Linear vs binary search on the same sorted array, counting actual comparisons, plus a binary-search-on-the-answer example (minimum ship capacity).

What the code does
def linear_search(arr, target):
    steps = 0
    for i, v in enumerate(arr):
        steps += 1
        if v == target:
            return i, steps
    return -1, steps

def binary_search(arr, target):
    lo, hi = 0, len(arr) - 1
    steps = 0
    while lo <= hi:
        steps += 1
        mid = (lo + hi) // 2
        if arr[mid] == target:
            return mid, steps
        elif arr[mid] < target:
            lo = mid + 1
        else:
            hi = mid - 1
    return -1, steps

arr = [1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31]
print("array:", arr, "n =", len(arr))
for target in (27, 4):
    li, ls = linear_search(arr, target)
    bi, bs = binary_search(arr, target)
    print(f"target={target:>3}  linear-> index={li:>2} steps={ls:>2}   binary-> index={bi:>2} steps={bs}")

# binary search on the answer: smallest capacity to ship packages within `days` days
def can_ship(weights, capacity, days):
    days_needed = 1
    load = 0
    for w in weights:
        if load + w > capacity:
            days_needed += 1
            load = 0
        load += w
    return days_needed <= days

def min_capacity(weights, days):
    lo, hi = max(weights), sum(weights)
    steps = 0
    while lo < hi:
        steps += 1
        mid = (lo + hi) // 2
        if can_ship(weights, mid, days):
            hi = mid
        else:
            lo = mid + 1
    return lo, steps

weights = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
cap, steps = min_capacity(weights, 5)
print(f"min capacity to ship {weights} in 5 days = {cap} (found in {steps} probes)")

# Output:
# array: [1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31] n = 16
# target= 27  linear-> index=13 steps=14   binary-> index=13 steps=3
# target=  4  linear-> index=-1 steps=16   binary-> index=-1 steps=4
# min capacity to ship [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] in 5 days = 15 (found in 5 probes)
  1. linear_search and binary_search both return (index, steps) so the two can be compared on the same 16-element sorted array.
  2. For target 27, linear takes 14 steps and binary takes 3. For 4 (missing), linear checks all 16 and binary gives up after 4.
  3. can_ship and min_capacity are the binary-search-on-the-answer code from above, run on weights 1 to 10 with 5 days. It prints 15, found in 5 probes.

Common Mistakes & Interview Traps

Interview Q&A

Q: Why does binary search require sorted data?

A: Every step relies on inferring that an entire half of the array is definitively too small or too large based on a single comparison at mid. That inference is only valid if the ordering guarantees every element to one side is uniformly less than (or greater than) arr[mid] — on unsorted data arr[mid] < target tells you nothing about the other elements, so the halving logic can eliminate the actual target.

Q: Walk through why binary search is O(log n) — what's actually being cut in half each step?

A: The size of the remaining candidate range, hi - lo + 1, is exactly halved (rounding) on every iteration, because one comparison rules out either everything at or below mid or everything at or above mid. Halving a quantity repeatedly until it reaches 1 takes log₂(n) halvings by definition — that's why n=16 takes at most 4 comparisons and n=1,000,000 takes at most 20.

Q: What's the classic off-by-one bug in binary search and how do you avoid it?

A: The most common is narrowing the range with lo = mid or hi = mid instead of mid + 1/mid - 1 — since arr[mid] was already checked and ruled out, leaving it inside the next range can cause lo and hi to stop converging, looping forever. Avoid it by writing the loop invariant explicitly ("if the target exists, it's in arr[lo..hi] inclusive") and making sure every branch preserves it exactly, excluding the just-checked mid.

Q: What is "binary search on the answer," and how do you recognize a problem that needs it?

A: It's applying the halving idea to a range of candidate answers rather than array indices — you binary-search for the boundary where a monotonic "is this candidate feasible?" predicate flips from false to true. You recognize it from phrasing like "find the minimum/maximum value such that X holds," combined with being able to write an efficient feasibility check that's monotonic across the candidate range.

Q: When would linear search actually be the better choice over binary search?

A: When the data is unsorted and will only be searched once or a few times — sorting costs O(n log n) up front, so paying that just to enable O(log n) searches loses to a single O(n) linear scan unless you'll reuse the sorted order many times. It's also the right (only) choice when there's no total order to sort by, or the dataset is small enough that the constant-factor overhead of binary search's branching isn't worth it.

Follow-up: How many searches does it take before sorting-then-binary-searching beats repeated linear search?

A: Roughly, once total cost O(n log n) + k·O(log n) (sort once, then k binary searches) drops below k·O(n) (k linear searches), which happens once k is large enough relative to log n — for most real n this crossover happens at single or low double-digit numbers of repeated searches, so "will this data be searched more than a handful of times" is the practical question to ask.

Follow-up: How would binary search change if the array had duplicate values and you needed the *first* occurrence of the target?

A: Instead of returning immediately on arr[mid] == target, record the match and keep searching the left half (hi = mid - 1) to see if an earlier occurrence exists, only stopping when the range is exhausted. This is the standard "leftmost binary search" variant and is a common follow-up precisely because naive early-return code gives an arbitrary occurrence, not the first.

Follow-up: Can binary search be applied to a rotated sorted array, like [15,18,2,3,6,9]?

A: Yes — at each step, at least one half (lo..mid or mid..hi) is still guaranteed to be normally sorted even though the whole array isn't; you check which half is sorted by comparing its endpoints, then decide whether the target could lie in that sorted half or must be in the other, and recurse the same halving logic accordingly.

Trade-offs

Linear search is the right default whenever data is unsorted and will be searched rarely, whenever the collection is small enough that asymptotic differences don't matter in practice, or whenever maintaining sorted order would cost more than it saves (frequent insertions into the middle of a sorted array are themselves O(n) each, which can erase binary search's advantage if updates vastly outnumber lookups). Binary search wins decisively when the same sorted (or naturally monotonic) dataset is queried repeatedly — the O(n log n) sort is a one-time cost amortized across many O(log n) lookups. Neither is universally right for dynamic data with frequent inserts and searches; that's usually a sign to reach for a different structure entirely — a hash table for O(1) average lookup with no ordering, or a balanced BST / B-tree for ordered data that also needs efficient insertion, both covered on their own pages in this section.

PracticeSolve related problems in the 150 →