Skip to content
Commitwise
Menu
Coding Interview

Binary search

Find things in sorted data in O(log n), write bounds that never loop forever, and use binary search on the answer, with Search Insert Position, First Bad Version and Koko Eating Bananas in Swift, Kotlin, Java and TypeScript.

beginner

The core idea with one simple example.Step by step, with more examples and exercises.A cheat sheet: key APIs, gotchas and trade-offs.Internals, trade-offs and production pitfalls.

“I’m thinking of a number between 1 and 1,000. Each guess, I’ll tell you higher or lower.” If you guess 1, 2, 3…, it can take a thousand tries. If you always guess the middle of what’s left, you need at most 10. For a million numbers, 20. For a billion, 30.

That’s binary searchBinary searchFinding a value in sorted data by checking the middle and discarding the half that can't contain it.Open in glossary →: the idea is famous for being simple, and the code is famous for being easy to get wrong by one. This tutorial covers both.

New to some of these? Open what you need, or skip ahead.

Basics:What are arrays and indexes?

An array is a row of values stored one after another: [5, 8, 2, 9]. Each position has an index, starting at 0: here 5 is at index 0 and 9 at index 3. The last index is always count - 1 (Swift) or size - 1 / length - 1 (Kotlin, Java, TypeScript).

Reading nums[2] is instant, whatever the array’s size. Reading an index that doesn’t exist (like nums[4] here) crashes with an “index out of range” error (in TypeScript it quietly gives undefined, a bug of its own), which is why loops usually go from 0 up to, but not including, the length.

Basics:What does O(n) mean?

Big-O describes how the amount of work grows with the size of the input n:

  • O(1): the same work whatever the size (reading array[5]).
  • O(log n): grows very slowly (about 20 steps for a million items).
  • O(n): grows in step with the input (one pass over the array).
  • O(n²): comparing every item with every other item, which gets slow fast.

You’ll see it next to every solution, for time and for extra space (memory). The Big-O tutorial explains it properly.

Part Not started

Halving a sorted array

Think of looking up a word in a paper dictionary. You open it near the middle, see that “mango” comes after the page you’re on, and from that moment you ignore the whole first half. Then you split the second half, and so on. Each look throws away half of what’s left.

It only works because the dictionary is sorted. With a sorted array:

  1. look at the middle element,
  2. if it’s the target, you’re done,
  3. if it’s smaller than the target, the target can only be to the right: continue there,
  4. if it’s bigger, continue to the left,
  5. if nothing is left, the target isn’t there.

Halving each step means O(log n) time: about 20 steps for a million elements.

Binary search finds the boundary of a monotonic predicate over an ordered domain in O(log n) evaluations: the first index where nums[i] >= target, the first version that’s bad, the smallest speed that’s fast enough. Searching sorted arrays is just the most common predicate.

The correctness details are all in the invariant: which interval (closed [low, high] or half-open [low, high)) still contains the answer, and making sure every iteration shrinks it, which rules out infinite loops. Senior-level questions add binary search on the answer (over a value range instead of an array), overflow in the midpoint, library APIs with surprising return values, and rotated or partially sorted inputs. All in the Detailed version.

func binarySearch(_ nums: [Int], _ target: Int) -> Int? {
    var low = 0
    var high = nums.count - 1                 // the answer is in [low, high]
    while low <= high {
        let mid = low + (high - low) / 2
        if nums[mid] == target { return mid }
        if nums[mid] < target {
            low = mid + 1                     // target is to the right of mid
        } else {
            high = mid - 1                    // target is to the left of mid
        }
    }
    return nil                                // nothing left: not found
}
fun binarySearch(nums: IntArray, target: Int): Int {
    var low = 0
    var high = nums.size - 1                  // the answer is in [low, high]
    while (low <= high) {
        val mid = low + (high - low) / 2
        when {
            nums[mid] == target -> return mid
            nums[mid] < target -> low = mid + 1    // target is to the right of mid
            else -> high = mid - 1                 // target is to the left of mid
        }
    }
    return -1                                 // nothing left: not found
}
static int binarySearch(int[] nums, int target) {
    int low = 0;
    int high = nums.length - 1;               // the answer is in [low, high]
    while (low <= high) {
        int mid = low + (high - low) / 2;
        if (nums[mid] == target) return mid;
        if (nums[mid] < target) {
            low = mid + 1;                    // target is to the right of mid
        } else {
            high = mid - 1;                   // target is to the left of mid
        }
    }
    return -1;                                // nothing left: not found
}
function binarySearch(nums: number[], target: number): number {
  let low = 0;
  let high = nums.length - 1;                  // the answer is in [low, high]
  while (low <= high) {
    const mid = low + Math.floor((high - low) / 2);   // / alone gives fractions
    if (nums[mid] === target) return mid;
    if (nums[mid] < target) {
      low = mid + 1;                           // target is to the right of mid
    } else {
      high = mid - 1;                          // target is to the left of mid
    }
  }
  return -1;                                   // nothing left: not found
}

Three details make this version correct:

  • low <= high: the search range includes both ends, so a range of one element (low == high) still gets checked.
  • mid + 1 and mid - 1: mid has already been checked, so it’s excluded. Every step makes the range smaller, so the loop can’t run forever.
  • low + (high - low) / 2 instead of (low + high) / 2: the same value, but it can’t overflow when the numbers are huge (more on that in the senior section). In TypeScript, / doesn’t round, so the division goes inside Math.floor.
Problemeasy

Search Insert Position

Given a sorted array of distinct integers and a target, return the index of the target if it’s there. If not, return the index where it would be inserted to keep the array sorted. It must run in O(log n).

nums = [1, 3, 5, 6], target = 5  → 2
nums = [1, 3, 5, 6], target = 2  → 1
nums = [1, 3, 5, 6], target = 7  → 4
›

Run the classic binary search. When the target isn’t found, the loop ends with low > high. Where is low pointing at that moment?

›

Everything left of low is smaller than the target, and everything from high + 1 on is bigger. When the loop ends, low == high + 1: exactly the insertion point.

SolutionTry it yourself firstTime O(log n)Space O(1)

Return low instead of “not found”. The loop keeps an invariant: every element before low is smaller than the target, and every element after high is bigger. When the range is empty, low is the first position whose element is bigger, which is where the target belongs.

func searchInsert(_ nums: [Int], _ target: Int) -> Int {
    var low = 0
    var high = nums.count - 1
    while low <= high {
        let mid = low + (high - low) / 2
        if nums[mid] == target { return mid }
        if nums[mid] < target { low = mid + 1 } else { high = mid - 1 }
    }
    return low
}
fun searchInsert(nums: IntArray, target: Int): Int {
    var low = 0
    var high = nums.size - 1
    while (low <= high) {
        val mid = low + (high - low) / 2
        when {
            nums[mid] == target -> return mid
            nums[mid] < target -> low = mid + 1
            else -> high = mid - 1
        }
    }
    return low
}
int searchInsert(int[] nums, int target) {
    int low = 0, high = nums.length - 1;
    while (low <= high) {
        int mid = low + (high - low) / 2;
        if (nums[mid] == target) return mid;
        if (nums[mid] < target) low = mid + 1; else high = mid - 1;
    }
    return low;
}
function searchInsert(nums: number[], target: number): number {
  let low = 0;
  let high = nums.length - 1;
  while (low <= high) {
    const mid = low + Math.floor((high - low) / 2);
    if (nums[mid] === target) return mid;
    if (nums[mid] < target) low = mid + 1; else high = mid - 1;
  }
  return low;
}

Watching it narrow

Here’s searchInsert([1, 3, 5, 6], 7), one step per row:

lowhighmidnums[mid]Compared with 7Next
0313smallerlow = 2
2325smallerlow = 3
3336smallerlow = 4
43——low > high: stopreturn 4

Three steps for four elements; about 20 for a million.

Predict the output

What gets printed?

print(searchInsert([1, 3, 5, 6], 2), searchInsert([1, 3, 5, 6], 7))
println("${searchInsert(intArrayOf(1, 3, 5, 6), 2)} ${searchInsert(intArrayOf(1, 3, 5, 6), 7)}")
System.out.println(searchInsert(new int[] {1, 3, 5, 6}, 2) + " " + searchInsert(new int[] {1, 3, 5, 6}, 7));
console.log(searchInsert([1, 3, 5, 6], 2), searchInsert([1, 3, 5, 6], 7));

What the library returns

Predict the output

Searching for 4 in the sorted list [1, 3, 5], which isn't there. What gets printed?

print([1, 3, 5].firstIndex(where: { $0 >= 4 }))
println(listOf(1, 3, 5).binarySearch(4))
System.out.println(Arrays.binarySearch(new int[] {1, 3, 5}, 4));
console.log([1, 3, 5].findIndex((x) => x >= 4));

Kotlin, Java

binarySearch (Kotlin) and Arrays.binarySearch / Collections.binarySearch (Java) return -(insertion point) - 1 when the key is missing, and any index among duplicates: use a lower bound when you need the first.

Swift

The standard library has no binary search on Array; firstIndex(where:) is linear. Write a partitioningIndex, or use swift-algorithms.

TypeScript

There’s no built-in binary search; findIndex is linear. And / doesn’t round: compute mid with Math.floor (or >>> 1 for array indices).

Check this part

  1. What must be true about the data for binary search to work?

  2. About how many steps does binary search need for 1,000,000 elements?

  3. What does the standard library give you for an O(log n) search in a sorted Array?What does listOf(1, 3, 5).binarySearch(4) return?What does Arrays.binarySearch(new int[] {1, 3, 5}, 4) return?What does the standard library give you for an O(log n) search in a sorted array?

On sorted data, look at the middle, then throw away the half that can't contain the target: O(log n), about 20 steps for a million elements. When the target is missing, low ends at its insertion point.

Part Not started

The loop that never ends

A second style of binary search keeps a half-open range [low, high) and loops while low < high. It’s great for “find the first position where…” questions, and it has one classic trap:

Spot the bugFirst position of a value, sometimes forever

Tap the line with the bug.

func lowerBound(_ nums: [Int], _ target: Int) -> Int {
    var low = 0
    var high = nums.count
    while low < high {
        let mid = low + (high - low) / 2
        if nums[mid] < target { low = mid } else { high = mid }
    }
    return low
}
fun lowerBound(nums: IntArray, target: Int): Int {
    var low = 0
    var high = nums.size
    while (low < high) {
        val mid = low + (high - low) / 2
        if (nums[mid] < target) low = mid else high = mid
    }
    return low
}
static int lowerBound(int[] nums, int target) {
    int low = 0;
    int high = nums.length;
    while (low < high) {
        int mid = low + (high - low) / 2;
        if (nums[mid] < target) low = mid; else high = mid;
    }
    return low;
}
function lowerBound(nums: number[], target: number): number {
  let low = 0;
  let high = nums.length;
  while (low < high) {
    const mid = (low + high) >>> 1;
    if (nums[mid] < target) low = mid; else high = mid;
  }
  return low;
}

One template for every boundary

Most bugs come from mixing interval conventions. Pick one and derive everything from its invariant. The half-open “first true” template:

low = smallest candidate, high = largest candidate (or one past it)
while low < high:
    mid = low + (high - low) / 2      // rounds down, so mid < high
    if predicate(mid): high = mid     // answer is in [low, mid]
    else:              low = mid + 1  // answer is in [mid + 1, high]
return low                            // the first candidate where predicate is true
  • lower bound (first >= target): predicate nums[mid] >= target, high = n.
  • upper bound (first > target): predicate nums[mid] > target. Count of a value = upper − lower.
  • last true (for example, the last element <= target): lowerBound(first > target) − 1, rather than a second template with mid rounded up.
  • Rotated sorted array: at each step one half is sorted; check whether the target lies in that half’s range.
  • Real-valued answers (a square root, a minimum time): loop a fixed number of iterations (100 halvings is more than Double precision can use) instead of comparing floats for equality.
GoalTemplateNotes
Exact match[low, high], while low <= high, low = mid + 1 / high = mid - 1return low for the insertion point
First true / lower bound[low, high), while low < high, high = mid / low = mid + 1mid rounds down, so every branch shrinks
Last truefirst true of the opposite predicate, minus 1avoids rounding mid up
On the answersearch the value range with a monotonic feasible(x)O(log range × check)
Rotated arrayone half is always sorted; test whether the target is in itO(log n) without duplicates

Library: Kotlin binarySearch and Java Arrays.binarySearch/Collections.binarySearch return -(insertion point) - 1 when missing and any index among duplicates; Swift and JavaScript have none in the standard library. Gotchas: low = mid loops forever, (low + high) / 2 overflows int (and doesn’t round in TypeScript), sums in predicates need Long, input must be sorted (by the same comparator).

func lowerBound(_ nums: [Int], _ target: Int) -> Int {
    var low = 0, high = nums.count
    while low < high {
        let mid = low + (high - low) / 2
        if nums[mid] < target { low = mid + 1 } else { high = mid }
    }
    return low
}
fun lowerBound(nums: IntArray, target: Int): Int {
    var low = 0
    var high = nums.size
    while (low < high) {
        val mid = (low + high) ushr 1
        if (nums[mid] < target) low = mid + 1 else high = mid
    }
    return low
}
static int lowerBound(int[] nums, int target) {
    int low = 0, high = nums.length;
    while (low < high) {
        int mid = (low + high) >>> 1;
        if (nums[mid] < target) low = mid + 1; else high = mid;
    }
    return low;
}
function lowerBound(nums: number[], target: number): number {
  let low = 0;
  let high = nums.length;
  while (low < high) {
    const mid = (low + high) >>> 1;          // fine for array indices (< 2^32)
    if (nums[mid] < target) low = mid + 1; else high = mid;
  }
  return low;
}

Check this part

  1. In a binary search loop, why must every branch change low or high?

  2. In the [low, high] version, why do you set low = mid + 1 instead of low = mid?

  3. How do you find the last element <= target with the "first true" template?

  4. How many times does a value occur in a sorted array, in O(log n)?

The half-open style keeps [low, high) and loops while low < high, ideal for finding the first position where something holds. Every branch must shrink the range: with mid rounded down, low = mid + 1 and high = mid always do.

Part Not started

Binary search without an array

Binary search doesn’t need an array, only a question with a yes/no answer that switches once: no, no, no, …, yes, yes, yes. You’re looking for the first “yes”. Step by step:

  1. Find the range: the smallest and largest possible answer (versions 1 to n, speeds 1 to the biggest pile).
  2. Write the yes/no question for a candidate: “is version mid bad?”, “is speed mid fast enough?”
  3. Check that it’s monotonic: once it says yes, it says yes for every bigger candidate.
  4. Search for the first yes: if mid says yes, the answer is mid or to its left (high = mid); if no, it’s to the right (low = mid + 1). Stop when low == high.
Exerciseeasy

First Bad Version

You release versions 1 to n of an app. One version introduced a bug, and every version after it is also bad. Given an API isBadVersion(version), find the first bad version, calling the API as few times as possible.

n = 5, first bad = 4
isBadVersion(3) → false
isBadVersion(5) → true
isBadVersion(4) → true
→ 4
›

The answers look like false, false, false, true, true. That’s a sorted array of booleans, and you want the first true.

›

Use low = 1, high = n and while low < high. If mid is bad, the first bad version is mid or earlier, so keep mid: high = mid. Otherwise it’s after mid: low = mid + 1.

SolutionTry it yourself firstTime O(log n) callsSpace O(1)
func firstBadVersion(_ n: Int) -> Int {
    var low = 1
    var high = n
    while low < high {
        let mid = low + (high - low) / 2
        if isBadVersion(mid) {
            high = mid            // mid might be the first bad one
        } else {
            low = mid + 1         // the first bad one is after mid
        }
    }
    return low                    // low == high: the only candidate left
}
fun firstBadVersion(n: Int): Int {
    var low = 1
    var high = n
    while (low < high) {
        val mid = low + (high - low) / 2
        if (isBadVersion(mid)) {
            high = mid            // mid might be the first bad one
        } else {
            low = mid + 1         // the first bad one is after mid
        }
    }
    return low                    // low == high: the only candidate left
}
int firstBadVersion(int n) {
    int low = 1, high = n;
    while (low < high) {
        int mid = low + (high - low) / 2;
        if (isBadVersion(mid)) {
            high = mid;           // mid might be the first bad one
        } else {
            low = mid + 1;        // the first bad one is after mid
        }
    }
    return low;                   // low == high: the only candidate left
}
function firstBadVersion(n: number): number {
  let low = 1;
  let high = n;
  while (low < high) {
    const mid = low + Math.floor((high - low) / 2);
    if (isBadVersion(mid)) {
      high = mid;            // mid might be the first bad one
    } else {
      low = mid + 1;         // the first bad one is after mid
    }
  }
  return low;                // low == high: the only candidate left
}

This is exactly what git bisect does to find the commit that broke a build: with 1,000 commits, about 10 test runs. Notice the shape: while low < high, high = mid for “yes”, low = mid + 1 for “no”, return low. It finds the first “yes” for any monotonic question, which is the template you’ll reuse most.

Binary search on the answer

When a problem asks for the minimum (or maximum) value that satisfies a condition, and checking a candidate is easier than computing the answer directly, binary search over the answer’s range. The total cost is O(log(range) × cost of one check). Signals: “minimum capacity to ship within D days”, “minimize the largest sum when splitting into k parts”, “smallest divisor such that the sum is under a threshold”, “Koko eating bananas” (the exercise below). The monotonicity argument is what you say out loud: if speed s is enough, any speed above s is too.

The midpoint that overflows

Spot the bugFirst Bad Version with two billion releases

Tap the line with the bug.

func firstBadVersion(_ n: Int) -> Int {
    var low = 1
    var high = n
    while low < high {
        let mid = (low + high) / 2
        if isBadVersion(mid) { high = mid } else { low = mid + 1 }
    }
    return low
}
fun firstBadVersion(n: Int): Int {
    var low = 1
    var high = n
    while (low < high) {
        val mid = (low + high) / 2
        if (isBadVersion(mid)) high = mid else low = mid + 1
    }
    return low
}
int firstBadVersion(int n) {
    int low = 1;
    int high = n;
    while (low < high) {
        int mid = (low + high) / 2;
        if (isBadVersion(mid)) high = mid; else low = mid + 1;
    }
    return low;
}
function firstBadVersion(n: number): number {
  let low = 1;
  let high = n;
  while (low < high) {
    const mid = (low + high) >> 1;
    if (isBadVersion(mid)) high = mid; else low = mid + 1;
  }
  return low;
}

Exercisemedium

Koko Eating Bananas

There are piles of bananas, and Koko has h hours. Each hour she picks one pile and eats k bananas from it; if the pile has fewer than k, she eats them all and does nothing else that hour. Return the minimum integer speed k that lets her finish every pile within h hours.

piles = [3, 6, 7, 11], h = 8           → 4
piles = [30, 11, 23, 4, 20], h = 5     → 30
piles = [30, 11, 23, 4, 20], h = 6     → 23

Constraints: up to 10⁴ piles, each up to 10⁹ bananas, and piles.length <= h <= 10⁹.

›

You can’t compute k directly, but for a given k you can count the hours easily: each pile takes ⌈pile / k⌉ hours. Is “k is fast enough” monotonic?

›

Search k in [1, max(piles)] (at the biggest pile’s speed, every pile takes one hour). Find the first k where the total hours are <= h. Watch out for overflow in the hour total and in the ceiling division.

SolutionTry it yourself firstTime O(n log m), m = largest pileSpace O(1)
func minEatingSpeed(_ piles: [Int], _ h: Int) -> Int {
    var low = 1
    var high = piles.max()!
    while low < high {
        let speed = low + (high - low) / 2
        let hours = piles.reduce(0) { $0 + ($1 - 1) / speed + 1 }   // Int is 64-bit: no overflow
        if hours <= h { high = speed } else { low = speed + 1 }
    }
    return low
}
fun minEatingSpeed(piles: IntArray, h: Int): Int {
    var low = 1
    var high = piles.max()
    while (low < high) {
        val speed = low + (high - low) / 2
        val hours = piles.sumOf { ((it - 1) / speed + 1).toLong() }   // ceil without overflow, Long total
        if (hours <= h) high = speed else low = speed + 1
    }
    return low
}
int minEatingSpeed(int[] piles, int h) {
    int low = 1, high = Arrays.stream(piles).max().getAsInt();
    while (low < high) {
        int speed = low + (high - low) / 2;
        long hours = 0;
        for (int pile : piles) hours += (pile - 1) / speed + 1;          // ceil without overflow
        if (hours <= h) high = speed; else low = speed + 1;
    }
    return low;
}
function minEatingSpeed(piles: number[], h: number): number {
  let low = 1;
  let high = Math.max(...piles);
  while (low < high) {
    const speed = low + Math.floor((high - low) / 2);
    let hours = 0;
    for (const pile of piles) hours += Math.ceil(pile / speed);   // exact: every value is far below 2^53
    if (hours <= h) high = speed; else low = speed + 1;
  }
  return low;
}

About 30 iterations (log₂ 10⁹) of an O(n) check: O(n log m). The two overflow traps are realistic: (pile + speed - 1) / speed, the usual ceiling trick, overflows int when both are near 10⁹, and 10⁴ piles × up to 10⁹ hours each overflows an int total. The same structure solves Capacity To Ship Packages Within D Days and Split Array Largest Sum: only the predicate changes.

Deep dives

Deep diveSearch in a rotated sorted arrayInterview favorite

[4, 5, 6, 7, 0, 1, 2] is a sorted array rotated at an unknown point. Binary search still works, because at every step one half is sorted:

  • If nums[low] <= nums[mid], the left half is sorted. If the target lies in [nums[low], nums[mid]), search left; otherwise search right.
  • Otherwise the right half is sorted; check whether the target lies in (nums[mid], nums[high]] the same way.

O(log n) with distinct values. With duplicates ([1, 1, 1, 0, 1]) you sometimes can’t tell which half is sorted and must shrink by one, so the worst case becomes O(n); say that in an interview.

  • Signals for searching the answer: “minimum capacity / speed / largest sum such that…”, and checking one candidate is easier than computing the answer. State the monotonicity out loud.
  • Compute mid from the distance (low + (high - low) / 2), and use 64-bit totals in predicates.

Kotlin, Java

(low + high) / 2 overflows Int/int above 2³¹ − 1 and goes negative; the JDK’s own binary search had this bug until 2006. >>> 1 (ushr 1) also works for non-negative values.

Swift

Int is 64-bit on iPhone and Mac, but overflow traps, so a search over a range near Int.max crashes with (low + high) / 2.

TypeScript

(low + high) >> 1 converts to a 32-bit signed integer first, so above 2³¹ − 1 it goes negative. Math.floor((low + high) / 2) is exact up to 2⁵³.

Check this part

  1. Which question can binary search answer even without an array?

  2. In First Bad Version, version mid is bad. Where is the first bad version?

  3. What makes a problem suitable for binary search on the answer?

  4. With 64-bit Int, when does let mid = (low + high) / 2 still fail?Why is (low + high) / 2 dangerous with Int indices?Why is (low + high) / 2 dangerous with int indices?Why is (low + high) >> 1 dangerous for values above two billion?

Binary search only needs a yes/no question that switches once: no, no, yes, yes. Search the first yes over versions, speeds or capacities, in O(log range × cost of one check), and keep midpoints and sums from overflowing.

Key takeaways

  • Binary search halves a sorted range every step: O(log n), about 20 steps for a million elements.
  • Every branch must shrink the range: low = mid + 1, high = mid - 1 (or high = mid in the low < high style).
  • When the target is missing, low ends at its insertion point.
  • Any yes/no question that switches once (no, no, yes, yes) can be binary searched, even without an array.
  • Pick one interval convention and reason from its invariant; derive lower/upper bounds and “last true” from “first true”.
  • Binary search on the answer: monotonic feasibility, O(log range × check), watch overflow in the check.
  • Compute midpoints as low + (high - low) / 2; JVM ints wrap silently and Swift traps.
  • Know the library contracts: negative insertion points, unspecified index among duplicates, and no stdlib binary search in Swift.

Finished this page?Nice work, this page is complete!

Mark it as done to track your progress. It's saved in this browser, no account needed.

Its cards are now in your Bites review ⚡
Esc

Type to search the whole site.