Binary Search
Halve a sorted range on every step to find a value, a boundary, or the smallest answer that works, in O(log n) time.
How It Works
Binary search finds a target in a sorted array by checking the middle element and throwing away the half that can't hold the answer. Every step halves what's left, so a million elements take about 20 steps. That's O(log n) instead of the O(n) of scanning.
The version that won't let you down isn't "find x". It's find the first index where a condition becomes true. In sorted nums, the condition nums[i] >= target is false for a run at the start and true for everything after. The first true index is the lower bound. Exact search is lower bound plus one check. The first index where nums[i] > target is the upper bound, and the last copy of target, if there is one, sits just before it.
target = 5
index 0 1 2 3 4 5 6
nums 1 3 3 5 5 8 9
>= 5? F F F T T T T
^ first T: lower bound = 3
lo hi mid nums[mid]
0 7 3 5 not < 5, so hi = mid = 3
0 3 1 3 < 5, so lo = mid + 1 = 2
2 3 2 3 < 5, so lo = mid + 1 = 3
3 3 lo == hi: the answer is 3lo is F and everything from hi on is T. Each step narrows the gap until lo and hi meet at the first T.Lower Bound, Step by Step
Step 1 of 7
nums
- mid, being tested
- Best answer so far
- Ruled out
- Lower bound
Look for the first index where nums[i] >= 5 in the range [0, 7). hi starts past the end, because every value might be smaller.
All 7 steps as text
- Look for the first index where
nums[i] >= 5in the range[0, 7).histarts past the end, because every value might be smaller. mid = 3andnums[3] = 5isn't below5, so index3might be the answer. Keep it withhi = mid = 3.- Indices
4to6are out, since index3passes first. Index3waits athias the best answer so far. The search goes on in[0, 3). mid = 1andnums[1] = 3is below5. Sorted order means everything left of it is too, so setlo = mid + 1 = 2.- Indices
0to1are out, all below5. The search goes on in[2, 3). mid = 2andnums[2] = 3is below5. Sorted order means everything left of it is too, so setlo = mid + 1 = 3.lo == hi == 3, so the range is empty and the loop stops.lowerBoundreturns3, the first index wherenums[i] >= 5.
The same framing works with no array at all. Binary search on the answer applies when a problem asks for the smallest (or largest) value that works, and "works" is monotonic: if a speed of 4 is fast enough, every faster speed is too. Pick a range that surely holds the answer, test the middle value with a check function, and keep the half that can still contain the first value that passes. That takes O(log R) checks for a range of R candidate values.
- Sorted array. Exact match, first or last occurrence, insertion point.
- Partly sorted array. In a rotated sorted array, one side of
midis always sorted, and that's enough to decide which half to keep. - Answer space. Minimum speed, minimum capacity, largest minimum distance: search the candidate values with a yes/no check.
When to Reach for It
- The input is sorted, or was sorted and then rotated.
- The constraints rule out O(n), or the question asks for O(log n).
- You need the first or last position of a value, or where it would be inserted.
- The question asks for the minimum or maximum value that satisfies a condition, and checking one candidate is easy.
- A yes/no test is monotonic: once a guess passes, every bigger guess passes too.
Complexity
| Operation | Time | Space |
|---|---|---|
| Search a sorted array | O(log n) | O(1) |
| Sort once, then answer q searches | O((n + q) log n) | O(1) to O(n), depending on the sort |
| Binary search on the answer (R candidates, O(n) check) | O(n log R) | O(1) |
The Template
Lower Bound
lowerBound returns the first index whose value is at least target, or nums.length if every value is smaller. It searches the half-open range [lo, hi), and hi starts at nums.length because "past the end" is a valid answer.
- If
nums[mid] < target, thenmidand everything left of it are too small, so setlo = mid + 1. - Otherwise
midmight be the answer. Keep it in range withhi = mid. - When
lo == hi, the range is empty andlois the answer: the first index that passed, ornums.lengthif none did.
search shows how exact search falls out of it. Lower bound lands on the first value that is at least target. If that index exists and holds target, you've found it. If not, target isn't in the array, and the index tells you where it would be inserted.
Time O(log n)Space O(1)
def lower_bound(nums: list[int], target: int) -> int:
"""First index in sorted `nums` whose value is >= `target`, or len(nums)
if every value is smaller."""
lo, hi = 0, len(nums) # Half-open [lo, hi): "past the end" is a valid answer.
while lo < hi:
mid = lo + (hi - lo) // 2
if nums[mid] < target:
lo = mid + 1 # mid and everything left of it are too small.
else:
hi = mid # mid could be the answer, so keep it in range.
return lo # lo == hi: the one index left.
def search(nums: list[int], target: int) -> int:
"""Index of `target` in sorted `nums`, or -1. It's lower bound plus one check."""
i = lower_bound(nums, target)
return i if i < len(nums) and nums[i] == target else -1
if __name__ == "__main__":
nums = [1, 3, 3, 5, 8]
print(lower_bound(nums, 3)) # 1, the first of the two 3s
print(lower_bound(nums, 4)) # 3, where 4 would go
print(lower_bound(nums, 9)) # 5, past the end
print(search(nums, 5)) # 3
print(search(nums, 4)) # -1// First index in sorted `nums` whose value is >= `target`, or nums.length
// if every value is smaller.
export function lowerBound(nums: number[], target: number): number {
let lo = 0;
let hi = nums.length; // Half-open [lo, hi): "past the end" is a valid answer.
while (lo < hi) {
const mid = lo + Math.floor((hi - lo) / 2);
if (nums[mid] < target) lo = mid + 1; // mid and everything left of it are too small.
else hi = mid; // mid could be the answer, so keep it in range.
}
return lo; // lo == hi: the one index left.
}
// Index of `target` in sorted `nums`, or -1. It's lower bound plus one check.
export function search(nums: number[], target: number): number {
const i = lowerBound(nums, target);
return i < nums.length && nums[i] === target ? i : -1;
}
const nums = [1, 3, 3, 5, 8];
console.log(lowerBound(nums, 3)); // 1, the first of the two 3s
console.log(lowerBound(nums, 4)); // 3, where 4 would go
console.log(lowerBound(nums, 9)); // 5, past the end
console.log(search(nums, 5)); // 3
console.log(search(nums, 4)); // -1package main
import "fmt"
// lowerBound returns the first index in sorted nums whose value is >= target,
// or len(nums) if every value is smaller.
func lowerBound(nums []int, target int) int {
lo, hi := 0, len(nums) // Half-open [lo, hi): "past the end" is a valid answer.
for lo < hi {
mid := lo + (hi-lo)/2
if nums[mid] < target {
lo = mid + 1 // mid and everything left of it are too small.
} else {
hi = mid // mid could be the answer, so keep it in range.
}
}
return lo // lo == hi: the one index left.
}
// search returns the index of target in sorted nums, or -1. It's lower bound
// plus one check.
func search(nums []int, target int) int {
i := lowerBound(nums, target)
if i < len(nums) && nums[i] == target {
return i
}
return -1
}
func main() {
nums := []int{1, 3, 3, 5, 8}
fmt.Println(lowerBound(nums, 3)) // 1, the first of the two 3s
fmt.Println(lowerBound(nums, 4)) // 3, where 4 would go
fmt.Println(lowerBound(nums, 9)) // 5, past the end
fmt.Println(search(nums, 5)) // 3
fmt.Println(search(nums, 4)) // -1
}using System;
public class Solution
{
// First index in sorted nums whose value is >= target, or nums.Length
// if every value is smaller.
public static int LowerBound(int[] nums, int target)
{
int lo = 0, hi = nums.Length; // Half-open [lo, hi): "past the end" is a valid answer.
while (lo < hi)
{
int mid = lo + (hi - lo) / 2;
if (nums[mid] < target) lo = mid + 1; // mid and everything left of it are too small.
else hi = mid; // mid could be the answer, so keep it in range.
}
return lo; // lo == hi: the one index left.
}
// Index of target in sorted nums, or -1. It's lower bound plus one check.
public static int Search(int[] nums, int target)
{
int i = LowerBound(nums, target);
return i < nums.Length && nums[i] == target ? i : -1;
}
public static void Main()
{
int[] nums = { 1, 3, 3, 5, 8 };
Console.WriteLine(LowerBound(nums, 3)); // 1, the first of the two 3s
Console.WriteLine(LowerBound(nums, 4)); // 3, where 4 would go
Console.WriteLine(LowerBound(nums, 9)); // 5, past the end
Console.WriteLine(Search(nums, 5)); // 3
Console.WriteLine(Search(nums, 4)); // -1
}
}public class Solution {
// First index in sorted nums whose value is >= target, or nums.length
// if every value is smaller.
static int lowerBound(int[] nums, int target) {
int lo = 0, hi = nums.length; // Half-open [lo, hi): "past the end" is a valid answer.
while (lo < hi) {
int mid = lo + (hi - lo) / 2;
if (nums[mid] < target) lo = mid + 1; // mid and everything left of it are too small.
else hi = mid; // mid could be the answer, so keep it in range.
}
return lo; // lo == hi: the one index left.
}
// Index of target in sorted nums, or -1. It's lower bound plus one check.
static int search(int[] nums, int target) {
int i = lowerBound(nums, target);
return i < nums.length && nums[i] == target ? i : -1;
}
public static void main(String[] args) {
int[] nums = {1, 3, 3, 5, 8};
System.out.println(lowerBound(nums, 3)); // 1, the first of the two 3s
System.out.println(lowerBound(nums, 4)); // 3, where 4 would go
System.out.println(lowerBound(nums, 9)); // 5, past the end
System.out.println(search(nums, 5)); // 3
System.out.println(search(nums, 4)); // -1
}
}Worked Problems
Each solution stays folded until you open it, so you can try the problem first. Every one is a complete program in Python, TypeScript, Go, C# and Java, run and checked against its expected output.
Problem
A sorted array of distinct integers has been rotated: some prefix was moved to the end, so [0, 1, 2, 4, 5, 6, 7] might now read [4, 5, 6, 7, 0, 1, 2]. Given the rotated array and a target, return the target's index, or -1 if it's missing. Your solution must run in O(log n).
Example: nums = [4, 5, 6, 7, 0, 1, 2] and target = 0 returns 4.
Show SolutionHide Solution
Approach
The array as a whole isn't sorted, but split it at any mid and at least one half is. If nums[lo] <= nums[mid], the left half from lo to mid is in order. Otherwise the drop from the largest value back to the smallest sits in the left half, so the right half from mid to hi is in order.
A sorted half is easy to test: target can only be in it if it lies between that half's two ends. If it does, search there. If not, it can only be in the other half. Either way you discard half the range, so the search stays O(log n).
Time O(log n)Space O(1)
def search_rotated(nums: list[int], target: int) -> int:
"""Index of `target` in a rotated sorted array of distinct values, or -1."""
lo, hi = 0, len(nums) - 1 # Closed [lo, hi]: we return as soon as mid matches.
while lo <= hi:
mid = lo + (hi - lo) // 2
if nums[mid] == target:
return mid
if nums[lo] <= nums[mid]:
# The left half nums[lo..mid] is sorted. Keep it only if target fits in it.
if nums[lo] <= target < nums[mid]:
hi = mid - 1
else:
lo = mid + 1
else:
# The right half nums[mid..hi] is sorted. Keep it only if target fits in it.
if nums[mid] < target <= nums[hi]:
lo = mid + 1
else:
hi = mid - 1
return -1
if __name__ == "__main__":
print(search_rotated([4, 5, 6, 7, 0, 1, 2], 0)) # 4
print(search_rotated([4, 5, 6, 7, 0, 1, 2], 3)) # -1
print(search_rotated([1], 0)) # -1
print(search_rotated([5, 1, 3], 3)) # 2// Index of `target` in a rotated sorted array of distinct values, or -1.
export function searchRotated(nums: number[], target: number): number {
let lo = 0;
let hi = nums.length - 1; // Closed [lo, hi]: we return as soon as mid matches.
while (lo <= hi) {
const mid = lo + Math.floor((hi - lo) / 2);
if (nums[mid] === target) return mid;
if (nums[lo] <= nums[mid]) {
// The left half nums[lo..mid] is sorted. Keep it only if target fits in it.
if (nums[lo] <= target && target < nums[mid]) hi = mid - 1;
else lo = mid + 1;
} else {
// The right half nums[mid..hi] is sorted. Keep it only if target fits in it.
if (nums[mid] < target && target <= nums[hi]) lo = mid + 1;
else hi = mid - 1;
}
}
return -1;
}
console.log(searchRotated([4, 5, 6, 7, 0, 1, 2], 0)); // 4
console.log(searchRotated([4, 5, 6, 7, 0, 1, 2], 3)); // -1
console.log(searchRotated([1], 0)); // -1
console.log(searchRotated([5, 1, 3], 3)); // 2package main
import "fmt"
// searchRotated returns the index of target in a rotated sorted array of
// distinct values, or -1.
func searchRotated(nums []int, target int) int {
lo, hi := 0, len(nums)-1 // Closed [lo, hi]: we return as soon as mid matches.
for lo <= hi {
mid := lo + (hi-lo)/2
if nums[mid] == target {
return mid
}
if nums[lo] <= nums[mid] {
// The left half nums[lo..mid] is sorted. Keep it only if target fits in it.
if nums[lo] <= target && target < nums[mid] {
hi = mid - 1
} else {
lo = mid + 1
}
} else {
// The right half nums[mid..hi] is sorted. Keep it only if target fits in it.
if nums[mid] < target && target <= nums[hi] {
lo = mid + 1
} else {
hi = mid - 1
}
}
}
return -1
}
func main() {
fmt.Println(searchRotated([]int{4, 5, 6, 7, 0, 1, 2}, 0)) // 4
fmt.Println(searchRotated([]int{4, 5, 6, 7, 0, 1, 2}, 3)) // -1
fmt.Println(searchRotated([]int{1}, 0)) // -1
fmt.Println(searchRotated([]int{5, 1, 3}, 3)) // 2
}using System;
public class Solution
{
// Index of target in a rotated sorted array of distinct values, or -1.
public static int SearchRotated(int[] nums, int target)
{
int lo = 0, hi = nums.Length - 1; // Closed [lo, hi]: we return as soon as mid matches.
while (lo <= hi)
{
int mid = lo + (hi - lo) / 2;
if (nums[mid] == target) return mid;
if (nums[lo] <= nums[mid])
{
// The left half nums[lo..mid] is sorted. Keep it only if target fits in it.
if (nums[lo] <= target && target < nums[mid]) hi = mid - 1;
else lo = mid + 1;
}
else
{
// The right half nums[mid..hi] is sorted. Keep it only if target fits in it.
if (nums[mid] < target && target <= nums[hi]) lo = mid + 1;
else hi = mid - 1;
}
}
return -1;
}
public static void Main()
{
Console.WriteLine(SearchRotated(new[] { 4, 5, 6, 7, 0, 1, 2 }, 0)); // 4
Console.WriteLine(SearchRotated(new[] { 4, 5, 6, 7, 0, 1, 2 }, 3)); // -1
Console.WriteLine(SearchRotated(new[] { 1 }, 0)); // -1
Console.WriteLine(SearchRotated(new[] { 5, 1, 3 }, 3)); // 2
}
}public class Solution {
// Index of target in a rotated sorted array of distinct values, or -1.
static int searchRotated(int[] nums, int target) {
int lo = 0, hi = nums.length - 1; // Closed [lo, hi]: we return as soon as mid matches.
while (lo <= hi) {
int mid = lo + (hi - lo) / 2;
if (nums[mid] == target) return mid;
if (nums[lo] <= nums[mid]) {
// The left half nums[lo..mid] is sorted. Keep it only if target fits in it.
if (nums[lo] <= target && target < nums[mid]) hi = mid - 1;
else lo = mid + 1;
} else {
// The right half nums[mid..hi] is sorted. Keep it only if target fits in it.
if (nums[mid] < target && target <= nums[hi]) lo = mid + 1;
else hi = mid - 1;
}
}
return -1;
}
public static void main(String[] args) {
System.out.println(searchRotated(new int[] {4, 5, 6, 7, 0, 1, 2}, 0)); // 4
System.out.println(searchRotated(new int[] {4, 5, 6, 7, 0, 1, 2}, 3)); // -1
System.out.println(searchRotated(new int[] {1}, 0)); // -1
System.out.println(searchRotated(new int[] {5, 1, 3}, 3)); // 2
}
}Problem
Koko has piles of bananas and h hours. Each hour she picks one pile and eats up to k bananas from it. If the pile has fewer than k left, she finishes it and waits out the rest of the hour. Find the smallest whole number k that lets her eat every banana within h hours. You can assume h is at least the number of piles.
Example: piles = [3, 6, 7, 11] and h = 8 returns 4. At speed 4 the piles take 1, 2, 2 and 3 hours, 8 in total. At speed 3 they take 10.
Show SolutionHide Solution
Approach
There's no sorted array here. What's sorted is the answer space. Speeds run from 1 to max(piles), and the test "can she finish in h hours at speed k?" is monotonic, because eating faster never takes longer. So the answers look like F F F T T T, and you want the first T. That's lower bound, run over speeds instead of indexes.
piles = [3, 6, 7, 11], h = 8
speed 1 2 3 4 5 6 ... 11
hours 27 15 10 8 8 6 ... 4
<= 8? F F F T T T ... T
^ slowest speed that works: 4max(piles) is a safe upper end: at that speed every pile takes exactly one hour, and h is at least the number of piles. Going faster never helps. Each check walks every pile once, so a guess costs O(n), and there are O(log m) guesses, where m is the largest pile.
Time O(n log m), m = largest pileSpace O(1)
def min_eating_speed(piles: list[int], h: int) -> int:
"""Slowest whole-number speed that lets Koko eat every pile within `h` hours."""
lo, hi = 1, max(piles) # At max(piles) every pile takes one hour.
while lo < hi:
mid = lo + (hi - lo) // 2
if hours_needed(piles, mid) <= h:
hi = mid # Fast enough: the answer is mid or slower.
else:
lo = mid + 1 # Too slow, and so is every speed below mid.
return lo
def hours_needed(piles: list[int], speed: int) -> int:
"""Hours to finish every pile at `speed` bananas per hour."""
# Round up: a partly eaten pile still costs a whole hour.
return sum((pile + speed - 1) // speed for pile in piles)
if __name__ == "__main__":
print(min_eating_speed([3, 6, 7, 11], 8)) # 4
print(min_eating_speed([30, 11, 23, 4, 20], 5)) # 30
print(min_eating_speed([30, 11, 23, 4, 20], 6)) # 23// Slowest whole-number speed that lets Koko eat every pile within `h` hours.
export function minEatingSpeed(piles: number[], h: number): number {
let lo = 1;
let hi = Math.max(...piles); // At max(piles) every pile takes one hour.
while (lo < hi) {
const mid = lo + Math.floor((hi - lo) / 2);
if (hoursNeeded(piles, mid) <= h) hi = mid; // Fast enough: the answer is mid or slower.
else lo = mid + 1; // Too slow, and so is every speed below mid.
}
return lo;
}
// Hours to finish every pile at `speed` bananas per hour.
function hoursNeeded(piles: number[], speed: number): number {
let hours = 0;
// Round up: a partly eaten pile still costs a whole hour.
for (const pile of piles) hours += Math.ceil(pile / speed);
return hours;
}
console.log(minEatingSpeed([3, 6, 7, 11], 8)); // 4
console.log(minEatingSpeed([30, 11, 23, 4, 20], 5)); // 30
console.log(minEatingSpeed([30, 11, 23, 4, 20], 6)); // 23package main
import (
"fmt"
"slices"
)
// minEatingSpeed returns the slowest whole-number speed that lets Koko eat
// every pile within h hours.
func minEatingSpeed(piles []int, h int) int {
lo, hi := 1, slices.Max(piles) // At max(piles) every pile takes one hour.
for lo < hi {
mid := lo + (hi-lo)/2
if hoursNeeded(piles, mid) <= h {
hi = mid // Fast enough: the answer is mid or slower.
} else {
lo = mid + 1 // Too slow, and so is every speed below mid.
}
}
return lo
}
// hoursNeeded returns the hours to finish every pile at speed bananas per hour.
func hoursNeeded(piles []int, speed int) int {
hours := 0
for _, pile := range piles {
// Round up: a partly eaten pile still costs a whole hour.
hours += (pile + speed - 1) / speed
}
return hours
}
func main() {
fmt.Println(minEatingSpeed([]int{3, 6, 7, 11}, 8)) // 4
fmt.Println(minEatingSpeed([]int{30, 11, 23, 4, 20}, 5)) // 30
fmt.Println(minEatingSpeed([]int{30, 11, 23, 4, 20}, 6)) // 23
}using System;
using System.Linq;
public class Solution
{
// Slowest whole-number speed that lets Koko eat every pile within h hours.
public static int MinEatingSpeed(int[] piles, int h)
{
int lo = 1;
int hi = piles.Max(); // At max(piles) every pile takes one hour.
while (lo < hi)
{
int mid = lo + (hi - lo) / 2;
if (HoursNeeded(piles, mid) <= h) hi = mid; // Fast enough: the answer is mid or slower.
else lo = mid + 1; // Too slow, and so is every speed below mid.
}
return lo;
}
// Hours to finish every pile at speed bananas per hour. A long, because
// slow speeds over many big piles overflow an int.
static long HoursNeeded(int[] piles, int speed)
{
long hours = 0;
// Round up: a partly eaten pile still costs a whole hour.
foreach (int pile in piles) hours += (pile + speed - 1) / speed;
return hours;
}
public static void Main()
{
Console.WriteLine(MinEatingSpeed(new[] { 3, 6, 7, 11 }, 8)); // 4
Console.WriteLine(MinEatingSpeed(new[] { 30, 11, 23, 4, 20 }, 5)); // 30
Console.WriteLine(MinEatingSpeed(new[] { 30, 11, 23, 4, 20 }, 6)); // 23
}
}import java.util.Arrays;
public class Solution {
// Slowest whole-number speed that lets Koko eat every pile within h hours.
static int minEatingSpeed(int[] piles, int h) {
int lo = 1;
int hi = Arrays.stream(piles).max().getAsInt(); // At max(piles) every pile takes one hour.
while (lo < hi) {
int mid = lo + (hi - lo) / 2;
if (hoursNeeded(piles, mid) <= h) hi = mid; // Fast enough: the answer is mid or slower.
else lo = mid + 1; // Too slow, and so is every speed below mid.
}
return lo;
}
// Hours to finish every pile at speed bananas per hour. A long, because
// slow speeds over many big piles overflow an int.
static long hoursNeeded(int[] piles, int speed) {
long hours = 0;
// Round up: a partly eaten pile still costs a whole hour.
for (int pile : piles) hours += (pile + speed - 1) / speed;
return hours;
}
public static void main(String[] args) {
System.out.println(minEatingSpeed(new int[] {3, 6, 7, 11}, 8)); // 4
System.out.println(minEatingSpeed(new int[] {30, 11, 23, 4, 20}, 5)); // 30
System.out.println(minEatingSpeed(new int[] {30, 11, 23, 4, 20}, 6)); // 23
}
}Common Pitfalls
- Mixing the two loop styles. A half-open range uses
while lo < hiwithhi = mid. A closed range useswhile lo <= hiwithhi = mid - 1. Mixing them loops forever or skips the answer. - Writing
lo = midinstead oflo = mid + 1. Sincemidrounds down, oncehi = lo + 1the range stops shrinking and the loop never ends. - Reading
nums[i]right after a lower bound without checkingi < nums.length. When every value is smaller than the target, the answer is past the end. - Choosing an answer range that doesn't contain the answer, such as starting Koko's speeds at 0 (a division by zero) or ending them below
max(piles). - Overflowing inside the check. Koko's total hours at a slow speed can pass 2 billion, so add them up in a 64-bit integer.
Practice
More problems to try on your own.