Binary Search: Complete Deep Dive (Iterative & Recursive)

By DSA Prep Team · March 10, 2026 · 14 min read · Algorithms

Binary Search is the most important search algorithm in all of DSA. Not because LC #704 is hard — it's Easy — but because the binary search pattern appears everywhere: in rotated arrays, in 2D matrices, in invisible search spaces (Koko Eating Bananas, Find Peak Element), and in almost every hard array/sorting problem.

If your binary search has ever had an off-by-one bug — infinite loop, skipping the target, or returning the wrong boundary — this guide will fix it permanently. We'll break down the exact mechanics of every boundary decision, show you why lo + (hi - lo) // 2 instead of (lo + hi) // 2, trace through every case fully, and map out the 6 variants that show up in actual interviews.

Table of Contents

Problem Statement

LeetCode #704 — Binary Search

Given an array of integers nums sorted in ascending order, and an integer target, write a function to search target in nums. If target exists, return its index. Otherwise, return -1.

Constraints: 1 <= nums.length <= 10^4, all values in nums are unique, nums is sorted in ascending order.

Examples
Input:  nums = [-1, 0, 3, 5, 9, 12],  target = 9   →  Output: 4
Input:  nums = [-1, 0, 3, 5, 9, 12],  target = 2   →  Output: -1
Input:  nums = [5],                    target = 5   →  Output: 0
Input:  nums = [5],                    target = -5  →  Output: -1

The Core Intuition: Halving the Search Space

The key property that makes binary search possible is that the array is sorted. This gives us a superpower: at every step, by looking at just the middle element, we can determine which half of the array the target cannot be in — and discard that entire half.

Linear search checks every element: O(n) comparisons for n elements. Binary search halves the remaining elements at every step:

Why O(log n)?
n = 1,000,000 elements

Linear search (worst case):  1,000,000 comparisons
Binary search (worst case):  log₂(1,000,000) ≈ 20 comparisons

After each step:
  Step 1: 1,000,000 elements → 500,000
  Step 2: 500,000 → 250,000
  Step 3: 250,000 → 125,000
  ...
  Step 20: 2 → 1

This is why binary search is one of the most powerful tools in DSA — it reduces a million-element problem to 20 comparisons. The catch: the array must be sorted (or have some monotonic property you can exploit).

The Mid Formula and Why It Matters

The midpoint formula seems trivial but has a real-world bug hiding in it:

Two Ways to Compute Mid
# ❌ Naive formula — works in Python, but dangerous in Java/C++
mid = (lo + hi) // 2

# ✅ Safe formula — works in all languages
mid = lo + (hi - lo) // 2

# In Java/C++, if lo = 1,000,000,000 and hi = 1,500,000,000:
# lo + hi = 2,500,000,000  →  OVERFLOW (exceeds 32-bit int max of ~2.1 billion)
# lo + (hi - lo) = 1,000,000,000 + 500,000,000 = 1,500,000,000  ✅ safe

Python integers have arbitrary precision so overflow doesn't apply there — but in Java and C++, using (lo + hi) / 2 when both values are large 32-bit integers causes integer overflow, producing a negative midpoint and completely wrong behavior. Always use lo + (hi - lo) / 2 as a habit regardless of language.

💡 Java Bitshift Alternative

In Java, you'll sometimes see mid = (lo + hi) >>> 1 (unsigned right shift). This avoids overflow too, but lo + (hi - lo) / 2 is clearer and works identically. Use whichever your team prefers, but always be able to explain why the naive formula can overflow.

The Three-Case Decision Logic

At each iteration, we compute mid and compare nums[mid] to target. There are exactly three cases:

  1. nums[mid] == target — Found it. Return mid.
  2. nums[mid] < target — The target is to the right of mid. We can safely discard everything from lo to mid (inclusive). Set lo = mid + 1.
  3. nums[mid] > target — The target is to the left of mid. We can safely discard everything from mid to hi (inclusive). Set hi = mid - 1.

⚠️ Why lo = mid + 1 and hi = mid - 1 (not mid)?

We already checked nums[mid] and it wasn't the target. So mid itself is no longer a candidate. If we set lo = mid or hi = mid, we'd re-examine the same index on the next iteration — which with a 1-element or 2-element search space causes an infinite loop. The +1 and -1 guarantee we always make progress.

Boundary Analysis: Why lo <= hi and Not lo < hi

This is the most common source of off-by-one bugs in binary search. The loop condition should be while lo <= hi. Here's why:

Consider a 1-element array: nums = [5], target = 5. We start with lo = 0, hi = 0. The only element is at index 0. If our loop condition were lo < hi, the condition 0 < 0 is immediately false — we'd never enter the loop and return -1 incorrectly. With lo <= hi, 0 <= 0 is true, we enter the loop, check nums[0] == 5, and correctly return 0.

Boundary Proof — 1-Element Array
nums = [5],  target = 5
lo = 0,  hi = 0

With "while lo < hi":   0 < 0  → FALSE → skip loop → return -1  ❌
With "while lo <= hi":  0 <= 0 → TRUE  → enter loop → check nums[0]=5=target → return 0  ✅

The loop terminates naturally when lo > hi — which happens when the search space is completely exhausted (every element has been considered and ruled out). At that point, the target genuinely doesn't exist in the array, and we return -1.

Full Step-by-Step Trace

Trace 1: Target Found

nums = [-1, 0, 3, 5, 9, 12], target = 9
Indices:   0   1   2   3   4   5
Values:  [-1,  0,  3,  5,  9, 12]

Initial:  lo = 0,  hi = 5

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Iteration 1:
  lo=0, hi=5 → lo <= hi ✅ → enter loop
  mid = 0 + (5-0)//2 = 2
  nums[2] = 3

  3 < 9  →  target is to the right
  lo = mid + 1 = 3

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Iteration 2:
  lo=3, hi=5 → 3 <= 5 ✅ → enter loop
  mid = 3 + (5-3)//2 = 4
  nums[4] = 9

  9 == 9  →  FOUND!
  return 4  ✅

Total comparisons: 2  (out of 6 elements)

Trace 2: Target Not Found

nums = [-1, 0, 3, 5, 9, 12], target = 2
Initial:  lo = 0,  hi = 5

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Iteration 1:
  mid = 0 + (5-0)//2 = 2
  nums[2] = 3
  3 > 2  →  target is to the left
  hi = mid - 1 = 1

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Iteration 2:
  lo=0, hi=1 → 0 <= 1 ✅
  mid = 0 + (1-0)//2 = 0
  nums[0] = -1
  -1 < 2  →  target is to the right
  lo = mid + 1 = 1

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Iteration 3:
  lo=1, hi=1 → 1 <= 1 ✅
  mid = 1 + (1-1)//2 = 1
  nums[1] = 0
  0 < 2  →  target is to the right
  lo = mid + 1 = 2

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Check: lo=2, hi=1 → 2 <= 1 ✗ → exit loop
return -1  ✅

Target 2 does not exist in the array.

Iterative Code

Python 3
def search(nums: list[int], target: int) -> int:
    lo, hi = 0, len(nums) - 1

    while lo <= hi:
        mid = lo + (hi - lo) // 2  # overflow-safe mid

        if nums[mid] == target:
            return mid             # found: return index
        elif nums[mid] < target:
            lo = mid + 1           # target is to the right — discard left half
        else:
            hi = mid - 1           # target is to the left — discard right half

    return -1  # search space exhausted: target not in array
Java
class Solution {
    public int search(int[] nums, int target) {
        int lo = 0, hi = nums.length - 1;

        while (lo <= hi) {
            int mid = lo + (hi - lo) / 2; // overflow-safe

            if (nums[mid] == target) {
                return mid;           // found
            } else if (nums[mid] < target) {
                lo = mid + 1;         // discard left half
            } else {
                hi = mid - 1;         // discard right half
            }
        }

        return -1; // not found
    }
}
JavaScript
var search = function(nums, target) {
    let lo = 0, hi = nums.length - 1;

    while (lo <= hi) {
        const mid = lo + Math.floor((hi - lo) / 2); // overflow-safe

        if (nums[mid] === target) {
            return mid;        // found
        } else if (nums[mid] < target) {
            lo = mid + 1;      // discard left half
        } else {
            hi = mid - 1;      // discard right half
        }
    }

    return -1; // not found
};

Recursive Solution

The recursive approach passes lo and hi as parameters instead of maintaining them as variables. It's functionally identical to the iterative version but uses the call stack to track state.

Base cases:

Recursive cases:

The recursive solution has O(log n) space complexity due to the call stack — the same number of frames as iterations in the iterative version. For very large arrays (n = 10^7+), prefer iterative to avoid stack overflow risk.

Recursive Code

Python 3
def search(nums: list[int], target: int) -> int:
    def binary_search(lo: int, hi: int) -> int:
        # Base case: search space exhausted
        if lo > hi:
            return -1

        mid = lo + (hi - lo) // 2

        if nums[mid] == target:
            return mid                          # found
        elif nums[mid] < target:
            return binary_search(mid + 1, hi)   # search right half
        else:
            return binary_search(lo, mid - 1)   # search left half

    return binary_search(0, len(nums) - 1)
Java
class Solution {
    public int search(int[] nums, int target) {
        return binarySearch(nums, target, 0, nums.length - 1);
    }

    private int binarySearch(int[] nums, int target, int lo, int hi) {
        if (lo > hi) return -1; // base case: not found

        int mid = lo + (hi - lo) / 2;

        if (nums[mid] == target) return mid;
        else if (nums[mid] < target) return binarySearch(nums, target, mid + 1, hi);
        else return binarySearch(nums, target, lo, mid - 1);
    }
}
JavaScript
var search = function(nums, target) {
    function binarySearch(lo, hi) {
        if (lo > hi) return -1; // base case: not found

        const mid = lo + Math.floor((hi - lo) / 2);

        if (nums[mid] === target) return mid;
        else if (nums[mid] < target) return binarySearch(mid + 1, hi);
        else return binarySearch(lo, mid - 1);
    }

    return binarySearch(0, nums.length - 1);
};

Common Mistakes and Why They Happen

⚠️ Mistake 1: Using lo < hi Instead of lo <= hi

Causes the algorithm to miss a 1-element search space. When lo == hi, there's still one element to check. lo < hi skips it and incorrectly returns -1.

Fix: Always use while lo <= hi.

⚠️ Mistake 2: lo = mid or hi = mid Instead of mid ± 1

When nums[mid] != target, mid is already ruled out. Setting lo = mid keeps mid in the search space. When lo and hi are adjacent (e.g., lo=4, hi=5, mid=4), this sets lo = 4 again — an infinite loop that never terminates.

Fix: Always use lo = mid + 1 and hi = mid - 1.

⚠️ Mistake 3: hi = len(nums) Instead of len(nums) - 1

Initializing hi = len(nums) means hi starts out-of-bounds. If mid ever equals len(nums) - 1 when hi is len(nums), it still works by coincidence — but it breaks the loop invariant and can cause index-out-of-bounds in variant problems where you access nums[hi] directly.

Fix: Always initialize hi = len(nums) - 1.

⚠️ Mistake 4: (lo + hi) // 2 in Java/C++

When both lo and hi are near Integer.MAX_VALUE, their sum overflows a 32-bit integer and becomes negative, producing a completely wrong mid.

Fix: Always use lo + (hi - lo) / 2.

Complexity Analysis

ApproachTimeSpaceNotes
IterativeO(log n)O(1)Halves search space each iteration. Only 3 integer variables regardless of array size.
RecursiveO(log n)O(log n)Each recursive call adds a stack frame. log₂(10^4) ≈ 14 frames max for given constraints.

For the given constraints (nums.length <= 10^4), both approaches are perfectly fine. In production with extremely large datasets, iterative is strictly preferred due to O(1) space.

The 6 Binary Search Variants Every Interview Tests

LC #704 tests binary search in its simplest form. Real interview problems almost always use a variant. Here are the six most common, in order of increasing difficulty:

ProblemThe TwistKey Change to TemplateDifficulty
LC #35 — Search Insert PositionTarget may not exist; find where it would be insertedReturn lo (not -1) when loop ends — lo is always the insertion pointEasy
LC #34 — Find First and Last PositionArray has duplicates; find leftmost AND rightmost occurrenceTwo separate binary searches: one biases left (hi = mid), one biases right (lo = mid)Medium
LC #33 — Search in Rotated Sorted ArrayArray is sorted but rotated at some pivotDetermine which half is sorted, then check if target falls in it; search that halfMedium
LC #153 — Find Minimum in Rotated Sorted ArrayNo explicit target; find the pivot/minimumCompare nums[mid] to nums[hi] to determine which side has the minimumMedium
LC #162 — Find Peak ElementArray has no target; find any local maximumCompare nums[mid] to nums[mid+1]; move toward the higher neighborMedium
LC #875 — Koko Eating BananasSearch space is the answer value, not array indicesBinary search on the answer range [1, max(piles)]; check feasibility with a helperMedium

💡 The Insight That Unlocks All Variants

Binary search doesn't require a sorted array — it requires a monotonic condition. Anywhere you can say "if X is true at index i, it's true for all indices beyond i" (or before i), binary search applies. Koko Eating Bananas (LC #875) has no sorted array at all — the search space is the eating rate itself, and the condition "can finish in h hours at rate k?" is monotonically true for all k above some threshold.

The Universal Binary Search Template

This template adapts to every binary search variant by changing only the condition inside the loop:

Universal Template (Python)
def binary_search(nums, target):
    lo, hi = 0, len(nums) - 1   # ← adjust search space for variant problems

    while lo <= hi:
        mid = lo + (hi - lo) // 2

        if CONDITION(mid):       # ← this is the only thing that changes per problem
            return mid
        elif MOVE_RIGHT(mid):    # ← target is to the right
            lo = mid + 1
        else:                    # ← target is to the left
            hi = mid - 1

    return -1                    # ← sometimes return lo for insertion-point variants

# LC #704: CONDITION = nums[mid]==target, MOVE_RIGHT = nums[mid]

How to Remember This

The 4 Invariants to Memorize

  1. Loop condition: lo <= hi — handles 1-element arrays correctly
  2. Mid formula: lo + (hi - lo) // 2 — overflow-safe always
  3. Move right: lo = mid + 1mid is already checked, skip it
  4. Move left: hi = mid - 1mid is already checked, skip it

✅ Spaced Repetition Schedule

  1. Today: Implement iterative binary search from scratch. Trace through both examples above on paper. Verify the 1-element edge case manually.
  2. Day 2: Solve LC #35 (Search Insert Position). The only change: return lo at the end instead of -1.
  3. Day 4: Solve LC #34 (Find First and Last Position). Requires two separate binary searches — one biased left, one biased right.
  4. Day 7: Solve LC #33 (Search in Rotated Sorted Array). The template stays the same; you just add logic to determine which half is sorted.
  5. Day 10: Solve LC #875 (Koko Eating Bananas). This teaches you to binary search on the answer space — the most powerful generalization of the pattern.
  6. Day 14: Solve LC #4 (Median of Two Sorted Arrays) — the Hard variant that combines binary search with partition logic.

Tag on DSAPrep.dev under Array and Binary Search.

Track on DSAPrep.dev →