All free guides

Pass the coding test

Binary Search Variants: 7 Templates, Java + Python

Riya has solved 200 problems. Asked to write binary search on paper, she does it in forty seconds. Then: "your array has four equal elements - which index does that return?" She does not know. "Now return the first one." She rewrites it three times, and the third one loops forever .

16 min readFree, no email neededUpdated 11 September 2026

Step 3 · DSA

What binary search actually needs

😅

Riya has solved 200 problems. Asked to write binary search on paper, she does it in forty seconds. Then: "your array has four equal elements - which index does that return?" She does not know. "Now return the first one." She rewrites it three times, and the third one loops forever.

Binary search does not need a sorted array. It needs a yes-or-no question whose answer flips from no to yes exactly once as you move right. A sorted array is just the most common way to get one.

index 0 1 2 3 4 5 6 7 value 2 5 8 12 16 23 38 56 >= 16? N N N N Y Y Y Y ^ the flip - this line is what you are hunting

Every variant in this guide is the same algorithm asking a different question at that line. "Is nums[mid] the target?" "Is nums[mid] at least the target?" "Is this speed fast enough?" Change the question, keep the machinery.

If you can write the yes-or-no test, and it never flips back from Y to N, you can binary search it. Nothing else is required.

Template

The exact-match template

The invariant, in one sentence: at the top of every loop, if the target is in the array at all, it is inside nums[lo .. hi]. Every branch must keep that promise true. That is the whole algorithm; the rest is arithmetic.

# Python def binary_search(nums, target): lo, hi = 0, len(nums) - 1 # hi is a real index while lo <= hi: # lo == hi is still a live range mid = lo + (hi - lo) // 2 if nums[mid] == target: return mid elif nums[mid] < target: lo = mid + 1 # mid is ruled out else: hi = mid - 1 # mid is ruled out return -1 # lo > hi, range is empty // Java static int binarySearch(int[] nums, int target) { int lo = 0, hi = nums.length - 1; while (lo <= hi) { int mid = lo + (hi - lo) / 2; if (nums[mid] == target) return mid; else if (nums[mid] < target) lo = mid + 1; else hi = mid - 1; } return -1; }
nums = [2, 5, 8, 12, 16, 23, 38, 56, 72, 91] O(log n) time target 23, present target 7, absent lo hi mid nums[mid] action lo hi mid nums[mid] action 0 9 4 16 small, lo = 5 0 9 4 16 big, hi = 3 5 9 7 56 big, hi = 6 0 3 1 5 small, lo = 2 5 6 5 23 equal, return 5 2 3 2 8 big, hi = 1 lo 2 > hi 1, so return -1
The three questions

The off-by-one page

Almost every broken binary search is one of these three decisions made without thinking. Decide them on purpose and the bugs stop.

1. lo <= hi or lo < hi?lo <= hi when you return the answer on sight and hi is a real index still to be tested; it ends with lo > hi, meaning not there. lo < hi when hunting a boundary; it ends with lo == hi, and that surviving index is the answer, so never throw it away.
2. Why mid = lo + (hi - lo) // 2?Same number as (lo + hi) // 2 in Python. In Java lo + hi can pass 2147483647, wrap negative, and nums[negative] throws. It also rounds down: when lo < hi, mid can equal lo but never hi. That fact decides question 3.
3. hi = mid - 1 or hi = mid?Could mid itself still be the answer? Ruled out, drop it: hi = mid - 1. Still a candidate (first element at least target, a peak, a workable speed): hi = mid. But hi = mid is safe only inside a lo < hi loop; inside lo <= hi the range stops shrinking when lo == hi and it spins forever. And lo = mid is never safe.
Variant
Loop
Shrink from the top
Answer is
Exact match
lo <= hi
hi = mid - 1
returned inside
First / last occurrence
lo <= hi
hi = mid - 1
saved in ans
Lower / upper bound
lo < hi
hi = mid
lo at the end
Rotated array search
lo <= hi
hi = mid - 1
returned inside
Peak element
lo < hi
hi = mid
lo at the end
Search on the answer
lo < hi
hi = mid
lo at the end
Variant 1

First occurrence

Duplicates are allowed, and you want the smallest index holding the target, or -1. Plain binary search returns whichever copy it happens to land on, which is why the follow-up question exists.

The change: a hit is no longer the end. Save it, then carry on searching the left half in case an earlier copy exists.

# Python def first_occurrence(nums, target): lo, hi = 0, len(nums) - 1 ans = -1 while lo <= hi: mid = lo + (hi - lo) // 2 if nums[mid] == target: ans = mid # a hit, maybe not the first hi = mid - 1 # so keep looking LEFT elif nums[mid] < target: lo = mid + 1 else: hi = mid - 1 return ans // Java static int firstOccurrence(int[] nums, int target) { int lo = 0, hi = nums.length - 1, ans = -1; while (lo <= hi) { int mid = lo + (hi - lo) / 2; if (nums[mid] == target) { ans = mid; hi = mid - 1; } else if (nums[mid] < target) lo = mid + 1; else hi = mid - 1; } return ans; }
nums = [1, 2, 2, 2, 2, 3, 5] target 2 answer 1 lo hi mid nums[mid] action 0 6 3 2 hit: ans = 3, keep going left, hi = 2 0 2 1 2 hit: ans = 1, keep going left, hi = 0 0 0 0 1 too small, lo = 1 lo 1 > hi 0, stop. Answer 1, the first 2.
Variant 2

Last occurrence, and counting

Same problem mirrored: the largest index holding the target. Exactly one line changes from the previous page, and it is the line that says which way to keep looking.

# Python def last_occurrence(nums, target): lo, hi = 0, len(nums) - 1 ans = -1 while lo <= hi: mid = lo + (hi - lo) // 2 if nums[mid] == target: ans = mid lo = mid + 1 # the only change: look RIGHT elif nums[mid] < target: lo = mid + 1 else: hi = mid - 1 return ans // Java static int lastOccurrence(int[] nums, int target) { int lo = 0, hi = nums.length - 1, ans = -1; while (lo <= hi) { int mid = lo + (hi - lo) / 2; if (nums[mid] == target) { ans = mid; lo = mid + 1; } else if (nums[mid] < target) lo = mid + 1; else hi = mid - 1; } return ans; }
nums = [1, 2, 2, 2, 2, 3, 5] target 2 answer 4 lo hi mid nums[mid] action 0 6 3 2 hit: ans = 3, keep going right, lo = 4 4 6 5 3 too big, hi = 4 4 4 4 2 hit: ans = 4, keep going right, lo = 5 lo 5 > hi 4, stop. Answer 4, the last 2.

Free follow-up: "how many times does the target appear?" is last - first + 1, or 0 when first is -1. Here 4 - 1 + 1 = 4, in O(log n) instead of a scan.

Variant 3

Lower bound and upper bound

Lower bound is the first index whose value is at least the target: where the target would be inserted. Upper bound is the first index whose value is strictly greater. Neither cares whether the target exists.

The change: hi starts at n, not n - 1, because the answer can be n itself (everything is smaller than the target). There is no equality branch, and the loop is lo < hi, so mid must be kept when it is still a candidate.

# Python - first index with nums[i] >= target def lower_bound(nums, target): lo, hi = 0, len(nums) # hi = n, one past the end while lo < hi: mid = lo + (hi - lo) // 2 if nums[mid] < target: lo = mid + 1 # mid is ruled out else: hi = mid # mid might BE the answer return lo # upper bound - first index with nums[i] > target - one character: if nums[mid] <= target: lo = mid + 1 // Java static int lowerBound(int[] nums, int target) { int lo = 0, hi = nums.length; while (lo < hi) { int mid = lo + (hi - lo) / 2; if (nums[mid] < target) lo = mid + 1; else hi = mid; } return lo; }
nums = [10, 20, 20, 30, 40] lower_bound(25) answer 3 lo hi mid nums[mid] action 0 5 2 20 20 < 25, lo = mid + 1 = 3 3 5 4 40 40 >= 25, hi = mid = 4 (mid is kept) 3 4 3 30 30 >= 25, hi = mid = 3 lo == hi == 3, answer 3 - where 25 would be inserted lower_bound(20) = 1, upper_bound(20) = 3, so 20 appears 3 - 1 = 2 times lower_bound(45) = 5 = n, which is exactly why hi starts at n
Variant 4

Search in a rotated sorted array

A sorted array was cut once and the pieces swapped: [4, 5, 6, 7, 0, 1, 2]. Comparing the target with nums[mid] alone tells you nothing now.

The insight: cut anywhere and at least one half is fully sorted. Find which, check whether the target lies inside its range, and throw the other half away. The <= matters: with two elements left, lo equals mid.

# Python def search_rotated(nums, target): lo, hi = 0, len(nums) - 1 while lo <= hi: mid = lo + (hi - lo) // 2 if nums[mid] == target: return mid if nums[lo] <= nums[mid]: # left half sorted if nums[lo] <= target < nums[mid]: hi = mid - 1 else: lo = mid + 1 else: # right half sorted if nums[mid] < target <= nums[hi]: lo = mid + 1 else: hi = mid - 1 return -1 // Java static int searchRotated(int[] nums, int target) { int lo = 0, hi = nums.length - 1; while (lo <= hi) { int mid = lo + (hi - lo) / 2; if (nums[mid] == target) return mid; if (nums[lo] <= nums[mid]) { if (nums[lo] <= target && target < nums[mid]) hi = mid - 1; else lo = mid + 1; } else { if (nums[mid] < target && target <= nums[hi]) lo = mid + 1; else hi = mid - 1; } } return -1; }
nums = [4, 5, 6, 7, 0, 1, 2] target 0 answer 4 lo hi mid nums[mid] sorted half target inside it? 0 6 3 7 left 4 .. 7 no -> lo = mid + 1 = 4 4 6 5 1 left 0 .. 1 yes -> hi = mid - 1 = 4 4 4 4 0 equal, return 4
Variant 5

Find a peak element

A peak is bigger than both its neighbours; anything off either end counts as minus infinity, and no two neighbours are equal. Return the index of any peak, in O(log n). There is no sorted array here at all.

The insight: compare nums[mid] with nums[mid + 1]. Rising? Then the values to the right must either keep rising into the edge or turn down somewhere, and either way a peak exists to the right, so lo = mid + 1. Falling? Then mid itself may be the peak, so hi = mid. Never discard a candidate.

# Python def find_peak(nums): lo, hi = 0, len(nums) - 1 while lo < hi: # boundary hunt, so lo < hi mid = lo + (hi - lo) // 2 if nums[mid] < nums[mid + 1]: # mid + 1 always exists here lo = mid + 1 # rising: peak is to the right else: hi = mid # falling: mid may be the peak return lo // Java static int findPeak(int[] nums) { int lo = 0, hi = nums.length - 1; while (lo < hi) { int mid = lo + (hi - lo) / 2; if (nums[mid] < nums[mid + 1]) lo = mid + 1; else hi = mid; } return lo; }
nums = [1, 2, 1, 3, 5, 6, 4] answer index 5, value 6 lo hi mid nums[mid] nums[mid+1] action 0 6 3 3 5 rising, lo = mid + 1 = 4 4 6 5 6 4 falling, hi = mid = 5 4 5 4 5 6 rising, lo = mid + 1 = 5 lo == hi == 5, peak at index 5, value 6

Why mid + 1 is always a valid index: the loop only runs while lo < hi, and mid rounds down, so mid < hi and mid + 1 is at most hi.

Variant 6

Binary search on the answer

This is the one that separates people. There is no sorted array in the input. You invent the array: it is the range of every answer you could possibly give, and you binary search that.

How to spot the shape. Three questions. What is the smallest and largest the answer could be? Given a candidate answer, can I check it in one simple pass? And is that check monotonic: if a value works, does every bigger value also work? Three yes answers means binary search the range.

Koko: piles [3, 6, 7, 11], h = 8 hours. Speeds she could pick: speed 1 2 3 4 5 6 7 8 9 10 11 hours 27 15 10 8 8 6 5 5 5 5 4 <= 8 ? N N N Y Y Y Y Y Y Y Y ^ the first Y. That is the answer: 4.
# the shape, every time lo, hi = smallest_possible_answer, largest_possible_answer while lo < hi: mid = lo + (hi - lo) // 2 if feasible(mid): hi = mid # mid works, but something smaller might too else: lo = mid + 1 # mid is too small, rule it out return lo # the smallest value that works

Same skeleton for minimum capacity to ship packages in D days: the range is from the heaviest single package to the sum of all of them, and feasible(cap) counts how many days that capacity needs.

Variant 6, worked

Koko eating bananas

Koko has piles of bananas and h hours. She picks one speed s and keeps it. Each hour she eats from a single pile; if it has less than s left, she finishes it and waits. Find the smallest s that clears every pile within h hours.

Range: 1 to the largest pile. Check: hours at speed s is the sum of ceil(pile / s). Monotonic: faster never takes more hours. So search the range.

# Python def min_eating_speed(piles, h): def hours(speed): total = 0 for p in piles: total += (p + speed - 1) // speed # ceiling division return total lo, hi = 1, max(piles) while lo < hi: mid = lo + (hi - lo) // 2 if hours(mid) <= h: hi = mid else: lo = mid + 1 return lo // Java static int hours(int[] piles, int speed) { int total = 0; for (int p : piles) total += (p + speed - 1) / speed; return total; } static int minEatingSpeed(int[] piles, int h) { int lo = 1, hi = 0; for (int p : piles) hi = Math.max(hi, p); while (lo < hi) { int mid = lo + (hi - lo) / 2; if (hours(piles, mid) <= h) hi = mid; else lo = mid + 1; } return lo; }
piles = [3, 6, 7, 11] h = 8 answer 4 O(n log(max pile)) lo hi mid hours(mid) <= 8 ? action 1 11 6 6 yes hi = mid = 6 1 6 3 10 no lo = mid + 1 = 4 4 6 5 8 yes hi = mid = 5 4 5 4 8 yes hi = mid = 4 lo == hi == 4, answer 4
Debug list

The five mistakes

Each of these either spins forever or quietly returns the wrong index. Every example below was run.

1. lo = mid instead of lo = mid + 1Spins forever. On [1, 3, 5, 7] looking for 7 it reaches lo = 2, hi = 3, mid = 2, sees 5 < 7, sets lo = 2 again, and repeats that state until the judge kills it. Because mid rounds down, lo = mid can leave the range unchanged. Only lo = mid + 1 is ever safe.
2. hi = mid inside a while lo <= hi loopSpins forever. When lo == hi you get mid == lo == hi, and hi = mid changes nothing. Pair them properly: lo <= hi goes with hi = mid - 1, and lo < hi goes with hi = mid.
3. Returning lo from a lo < hi loop without checking itMisses the answer, silently. A boundary search always ends with lo pointing somewhere, even when the target is not present. On [2, 5, 8, 12, 16] searching for 7 it returns index 2, whose value is 8. If you need an exact match, test nums[lo] == target before returning.
4. hi = len(nums) - 1 for lower boundMisses the answer at the end. The correct reply for "where does 99 go in [10, 20, 30]" is 3, one past the last index. Starting hi at n - 1 caps the answer at 2. Bounds are the one variant where hi starts at n.
5. nums[lo] < nums[mid] in the rotated searchMisses the answer on small ranges. When two elements are left, mid equals lo, so the strict < says "left half not sorted" and the wrong branch runs. On [1, 0] searching for 0 it returns -1 instead of 1. Write nums[lo] <= nums[mid].

Want this on your phone?

Everything above, as a PDF built to read on a phone the morning of the interview. It costs nothing — we ask for an email so we can send it, and that is all.

Binary Search Variants Every Interview Asks