Binary search is not about sorted arrays. It is about any yes/no question whose answer flips exactly once along a line. Find the flip, and you have halved the world.
Everyone can write the textbook version. Interviews test the modified versions: rotated arrays, first and last occurrence, peaks, and the big one, binary search on the answer, where there is no array to search at all. All of them are the same loop with a different question in the middle.
F F F F T T T, with a single flip, binary search applies. Sortedness is the most common way to get monotonicity, but it is not the only one.1..10⁹ with a cheap feasibility check.n is exhausted in about log₂n steps. Every difficulty in this pattern comes from one place: proving the discard is safe.true”. Then there is one loop to remember instead of five.Binary search is notorious for off-by-one errors. The cure is to pick one convention and never deviate. Two conventions are worth knowing.
[left, right]. Best for finding an exact match.
left = 0, right = n - 1.left <= right.left = mid + 1. Discard right: right = mid - 1.left > right, nothing found.[left, right). Best for finding a boundary.
left = 0, right = n.left < right.left = mid + 1. Discard right: right = mid.left == right, the boundary.mid = left + (right - left) / 2 the midpoint rounds down, so mid can equal left but never equals right when left < right. That means right = mid is safe and left = mid is not: it can leave the range unchanged forever. If you ever need left = mid, round the midpoint up instead, with mid = left + (right - left + 1) / 2.Write mid = left + (right - left) / 2 rather than (left + right) / 2. In C# an int holds at most about 2.1 billion. If left + right goes past that, it wraps to a negative number without any error, because C# arithmetic is unchecked by default. The negative mid then throws IndexOutOfRangeException. This exact bug sat in the JDK’s own binary search for nine years. Interviewers notice the safe form.
/ in C# truncates toward zero, not toward minus infinity like Python’s //. For non-negative ranges the two agree. If your search range can be negative, as in a search over answer values from -10⁹ up, (right - left) / 2 is still safe because right - left is never negative. Keep that form and the rounding stays “down”.public static class TemplateExact
{
/// <summary>Template A: index of target in a sorted array, or -1.</summary>
/// <param name="nums">Ascending integers.</param>
/// <param name="target">The value to find.</param>
/// <returns>An index holding target, or -1.</returns>
/// <example><c>Find([1, 3, 5, 7, 9, 11, 13], 9)</c> returns 4.</example>
public static int Find(int[] nums, int target)
{
// Closed range [left, right]. 0 is the first index. Length - 1 is the last.
int left = 0, right = nums.Length - 1;
// Invariant: if target is in nums, its index is inside [left, right].
// <= because a range with left == right still holds one candidate.
while (left <= right)
{
// / 2: integer midpoint, rounds down for non-negative ints.
// left + (right - left) / 2 cannot overflow. (left + right) / 2 can.
int mid = left + (right - left) / 2;
if (nums[mid] == target)
{
return mid; // exact hit, done
}
if (nums[mid] < target)
{
left = mid + 1; // + 1: mid is too small, so drop it too
}
else
{
right = mid - 1; // - 1: mid is too large, so drop it too
}
}
return -1; // -1: the "not found" signal, never a real index
}
}
mid throws away the half that cannot hold the target, plus mid itself.Reading the figure. Blue cells are still in the range. Amber is mid. Red cells are discarded. Green is the answer. The purple letters mark left, mid, and right. Notice that mid is dropped with + 1 or - 1, since it was already checked.
public static class FirstTrue
{
/// <summary>
/// Template B: smallest x in [lo, hi] with predicate(x) true, or hi + 1 if none.
/// Requires the predicate to be monotone: once true, true forever.
/// </summary>
/// <param name="lo">The smallest candidate x.</param>
/// <param name="hi">The largest candidate x. Must be below int.MaxValue.</param>
/// <param name="predicate">A yes/no test: false, false, ..., then true forever.</param>
/// <returns>The first x where predicate is true, or hi + 1.</returns>
/// <example>
/// With nums = [1, 3, 3, 5, 8]: <c>Find(0, 4, i => nums[i] >= 4)</c> returns 3.
/// </example>
public static int Find(int lo, int hi, Func<int, bool> predicate)
{
// lo, hi: the smallest and largest candidate x.
// Lower bound example: lo = 0, hi = nums.Length - 1.
// predicate(x): a yes/no test that is false, false, ..., then true forever.
// Lower bound example: predicate(i) is nums[i] >= target.
// Peak example: predicate(i) is arr[i] > arr[i + 1].
// + 1: right is one past the last candidate, so "none true" returns hi + 1.
int left = lo, right = hi + 1;
// Invariant: every x below left is false. Every x at or above right is true
// (or past the end). The answer is always inside [left, right].
// < not <=: stop when left == right, the range is down to one spot.
while (left < right)
{
int mid = left + (right - left) / 2; // / 2 rounds down, so mid < right
if (predicate(mid))
{
right = mid; // mid might be the answer, so keep it
}
else
{
left = mid + 1; // + 1: mid is definitely not, so discard it
}
}
return left; // left == right: the first true, or hi + 1
}
}
This one template covers lower bound, upper bound, insertion position, first occurrence, last occurrence, and every “binary search on the answer” problem. If you memorise one, memorise this one.
left and right together until they meet on the first true.Reading the figure. Each cell shows predicate(i), which is nums[i] >= 4. The extra cell at index 5 is the “none true” slot, hi + 1. Blue is still in range, amber is mid, red is discarded, green is the answer. A T at mid keeps mid with right = mid. An F drops it with left = mid + 1.
Array.BinarySearch and List<T>.BinarySearch return the index when they find the value. When they do not, they return a negative number, the bitwise complement of the insertion point. So ~result gives the insertion point. Two traps follow. First, with duplicates the hit can be any copy, not the first, so it is not a lower bound. Second, using a negative result as an index throws. There is no bisect_left in the base library. In an interview, write Template B. Problem 3 shows a comparer trick that makes Array.BinarySearch act as a true lower bound.This is the version that separates candidates, and it deserves its own heading because there is no array in it at all.
x such that something is achievable. Checking “is x achievable” is easy and linear. And achievability is monotone: if x works then anything larger works too. Then binary search over the range of possible answers, not over any input array, calling the feasibility check at each step.public static class MinimumFeasible
{
/// <summary>Smallest value in [lo, hi] that works, assuming feasibility is monotone.</summary>
/// <param name="lo">Smallest possible answer.</param>
/// <param name="hi">Largest possible answer. Assumed to work.</param>
/// <param name="isFeasible">True if answer x works. Once true, larger x stays true.</param>
/// <returns>The smallest feasible value.</returns>
/// <example>
/// Koko with piles [3, 6, 7, 11] and h = 8:
/// <c>Find(1, 11, k => hoursAt(k) <= 8)</c> returns 4.
/// </example>
public static long Find(long lo, long hi, Func<long, bool> isFeasible)
{
// long, not int: answer ranges such as 1..10^12 do not fit in an int.
// lo, hi: the range of possible answers, not array indexes.
// Koko example: lo = 1 (slowest speed), hi = piles.Max() (one pile per hour).
// isFeasible(x): true if answer x works. Once true, larger x stays true.
// Koko example: can she eat every pile within h hours at speed x?
// No + 1 on hi: hi is assumed to work, so the answer always exists.
long left = lo, right = hi;
// Invariant: the smallest feasible value is inside [left, right].
while (left < right)
{
long mid = left + (right - left) / 2; // / 2 rounds down, so mid < right
if (isFeasible(mid))
{
right = mid; // mid works, but something smaller might too
}
else
{
left = mid + 1; // + 1: mid fails, and so does everything below
}
}
return left; // left == right: the smallest value that works
}
}
piles.Max(). Check: can she finish within h hours at this speed?w.Max() to w.Sum(). Check: does a greedy pack fit in d days?k pieces under that cap?m runs of k bloomed flowers by that day?nums[mid].Reading the figure. The top row lists every possible speed and the hours it would take for piles = [3, 6, 7, 11]. A speed is feasible (T) when the hours are at most 8. Amber is the speed tested, red is ruled out, green is the answer. The search calls the check only four times, not eleven.
Total cost is O(log(range) × cost of one check). When you see “minimise the maximum” or “maximise the minimum”, say “binary search on the answer” immediately. It is the single most reliable pattern-recognition win in the whole set.
left = mid. With a rounding-down midpoint, that can leave the range unchanged. Use mid + 1, or round the midpoint up.right = n with while left <= right reads past the end. Pick one row of the table and stay in it.mid from a boundary search. Boundary searches return left, after the loop.nums.Length or may point at a different value. Check before you index.Array.BinarySearch result as an index. A miss returns a negative number. Apply ~ first, then validate.(left + right) / 2 out of habit. In C# it overflows once the sum passes int.MaxValue. Write the safe form and say why.Given a sorted array of distinct integers and a target, return its index, or -1 if it is absent. Must be O(log n).
left > right means the range is empty and the target is not there.public static class BinarySearch
{
/// <summary>Index of target in a sorted array of distinct integers, or -1.</summary>
/// <param name="nums">Ascending, distinct integers.</param>
/// <param name="target">The value to find.</param>
/// <returns>The index of target, or -1 if it is not present.</returns>
/// <example><c>Search([-1, 0, 3, 5, 9, 12], 9)</c> returns 4.</example>
public static int Search(int[] nums, int target)
{
// Closed range. 0 is the first index. nums.Length - 1 is the last index.
int left = 0, right = nums.Length - 1;
// Invariant: if target is in nums, its index is inside [left, right].
// <= because left == right still leaves one candidate to check.
while (left <= right)
{
// Overflow-safe form. Required in C#, where int wraps past 2^31 - 1.
// / 2: integer midpoint, rounds down.
int mid = left + (right - left) / 2;
if (nums[mid] == target)
{
return mid;
}
if (nums[mid] < target)
{
left = mid + 1; // + 1: mid and everything left of it is too small
}
else
{
right = mid - 1; // - 1: mid and everything right of it is too large
}
}
return -1; // range is empty: -1 means "not present"
}
}
nums = [-1, 0, 3, 5, 9, 12], target = 2:
nums[2] = 3. Too big, so right = 1.nums[0] = -1. Too small, so left = 1.nums[1] = 0. Too small, so left = 2.left > right, so return -1.Reading the figure. Blue cells are still in the range. Amber is mid. Red cells are discarded. Green is the answer. There is no green cell here. After the third step left passes right, the range is empty, and the loop exits with -1.
The range starts at size n and at least halves each iteration, so after k iterations it is at most n / 2ᵏ. The loop stops when the size drops below 1, which needs k > log₂n. For a billion elements that is 30 iterations.
right = -1, the loop never runs, returns -1.[left, right]. Every branch discards only values that cannot be the target, so the invariant survives.”A sorted array of distinct values has been rotated at some unknown pivot, for example [4,5,6,7,0,1,2]. Find the index of a target, or -1. Must be O(log n).
nums[left] with nums[mid] to find which half is the clean one, then check whether the target falls inside that half’s known range. If it does, search there. If it does not, it must be in the messy half.The reasoning is always about the sorted half, because that is the only half whose contents you can reason about from its two endpoints.
public static class RotatedSearch
{
/// <summary>Index of target in a rotated sorted array of distinct values, or -1.</summary>
/// <param name="nums">A sorted array rotated at an unknown pivot. Values are distinct.</param>
/// <param name="target">The value to find.</param>
/// <returns>Its index, or -1.</returns>
/// <example><c>Search([4, 5, 6, 7, 0, 1, 2], 0)</c> returns 4.</example>
public static int Search(int[] nums, int target)
{
// Closed range. 0 is the first index. nums.Length - 1 is the last index.
int left = 0, right = nums.Length - 1;
// Invariant: if target is in nums, its index is inside [left, right].
while (left <= right)
{
int mid = left + (right - left) / 2; // / 2: integer midpoint, rounds down
if (nums[mid] == target)
{
return mid;
}
// <= not <: when two elements remain, mid == left and they are equal.
if (nums[left] <= nums[mid])
{
// Left half [left, mid] is sorted, so its range is known exactly.
// mid is excluded with < because we already know it is not target.
if (nums[left] <= target && target < nums[mid])
{
right = mid - 1; // - 1: target is in the left half, drop mid
}
else
{
left = mid + 1; // + 1: target is not there, drop mid too
}
}
else
{
// Then the right half [mid, right] must be the sorted one.
if (nums[mid] < target && target <= nums[right])
{
left = mid + 1; // + 1: target is in the right half, drop mid
}
else
{
right = mid - 1; // - 1: target is not there, drop mid too
}
}
}
return -1; // range is empty: -1 means "not present"
}
}
nums[left] <= nums[mid] and not <mid equals left, so the two values are the same and a strict < would wrongly classify the left half as unsorted. With distinct values, <= is correct and safe. If duplicates are allowed, this test breaks down entirely: for [1, 1, 1, 0, 1] you cannot tell which half is sorted, and the standard fix is to shrink left by one when nums[left] == nums[mid] == nums[right], which makes the worst case O(n). Raise this. It is the standard follow-up.nums = [4,5,6,7,0,1,2], target = 0:
nums[3] = 7. Left half is sorted, 4 to 7. 0 is not in [4, 7), so go right.nums[5] = 1. Left half is sorted, 0 to 1. 0 is in [0, 1), so go left.nums[4] = 0. Match, return 4.Reading the figure. Blue cells are in range, amber is mid, red is discarded, green is the answer. The purple bracket marks the half that is sorted. In step 1, 0 is outside 4 to 7, so the whole left half goes. In step 2, 0 is inside 0 to 1, so the search keeps the left part.
Find the rotation point with a separate binary search (the smallest element), then run a normal binary search on the correct segment. Two clean passes, still O(log n), and easier to get right under pressure. The one-pass version above is what most interviewers expect, but naming the two-pass option shows you have thought about the trade-off between cleverness and clarity.
Given a sorted array that may contain duplicates, return the first and last index of a target as [first, last], or [-1, -1] if it is absent. Must be O(log n).
target is one before the first index of target + 1. So a single LowerBound helper, called twice, answers both halves. For non-integer values you would write a matching UpperBound instead.public static class SearchRange
{
/// <summary>
/// First index where nums[index] >= target, or nums.Length if there is none.
/// The predicate "nums[i] >= target" is false then true, with one flip.
/// </summary>
/// <param name="nums">Ascending integers.</param>
/// <param name="target">The bound. A long, so callers can pass int.MaxValue + 1L.</param>
/// <returns>The insertion point for target.</returns>
/// <example><c>LowerBound([5, 7, 7, 8, 8, 10], 8)</c> returns 3.</example>
public static int LowerBound(int[] nums, long target)
{
// Half-open [left, right). 0 is the first index. right = nums.Length is one
// past the end, so "every value is smaller" can return nums.Length.
int left = 0, right = nums.Length;
// Invariant: nums[..left] are all < target. nums[right..] are all >= target.
while (left < right)
{
int mid = left + (right - left) / 2; // / 2 rounds down, so mid < right
if (nums[mid] < target)
{
left = mid + 1; // + 1: mid fails, discard it
}
else
{
right = mid; // mid might be the answer, keep it
}
}
return left; // left == right: the first index >= target
}
/// <summary>First and last index of target in a sorted array, or [-1, -1].</summary>
/// <param name="nums">Ascending integers, duplicates allowed.</param>
/// <param name="target">The value to locate.</param>
/// <returns>[firstIndex, lastIndex], or [-1, -1] if target is absent.</returns>
/// <example><c>Find([5, 7, 7, 8, 8, 10], 8)</c> returns [3, 4].</example>
public static int[] Find(int[] nums, int target)
{
int first = LowerBound(nums, target);
// LowerBound always returns a valid insertion point, which may be past
// the end or may point at a larger value. Both mean "not present".
// The length check comes first so nums[first] never reads past the end.
if (first == nums.Length || nums[first] != target)
{
return [-1, -1]; // -1, -1: the "absent" answer the problem asks for
}
// The last target sits just before the first value greater than target.
// + 1L: the next integer up, done in long so int.MaxValue + 1 cannot wrap.
// - 1: step back from that first larger value onto the last target.
int last = LowerBound(nums, target + 1L) - 1;
return [first, last];
}
}
nums = [5, 7, 7, 8, 8, 10], target = 8. LowerBound(8) returns 3, the first index with a value at least 8. LowerBound(9) returns 5, the first index with a value at least 9, so the last 8 is at index 4. Answer [3, 4].
Now target = 6. LowerBound(6) returns 1, but nums[1] is 7, not 6, so the guard fires and the answer is [-1, -1]. That validation step is essential. A boundary search never tells you the value is present, only where it would go.
Reading the figure. Each row is one call to LowerBound. A cell shows T when nums[i] reaches the target, and green marks the first T. The run of 8s (amber) starts at the first boundary and ends one cell before the second.
public static class SearchRangeBcl
{
// Never returns 0, so BinarySearch never stops on an equal value. It keeps going
// left on ties and ends at the complement (~) of the first index >= value.
// -1 = "element is smaller, go right". 1 = "element is bigger or equal, go left".
private static readonly Comparer<int> Lower = Comparer<int>.Create((a, b) => a < b ? -1 : 1);
// Same trick, but ties go right: ends at ~ of the first index > value.
private static readonly Comparer<int> Upper = Comparer<int>.Create((a, b) => a <= b ? -1 : 1);
/// <summary>
/// What you would ship: Array.BinarySearch with a comparer that never says "equal",
/// which turns it into a lower bound and an upper bound.
/// </summary>
/// <param name="nums">Ascending integers, duplicates allowed.</param>
/// <param name="target">The value to locate.</param>
/// <returns>[firstIndex, lastIndex], or [-1, -1] if target is absent.</returns>
/// <example><c>Find([5, 7, 7, 8, 8, 10], 8)</c> returns [3, 4].</example>
public static int[] Find(int[] nums, int target)
{
// ~ undoes the complement. The search never "finds", so the result is always < 0.
int first = ~Array.BinarySearch(nums, target, Lower);
// Past the end, or a different value there: target is absent.
if (first == nums.Length || nums[first] != target)
{
return [-1, -1]; // -1, -1: the "absent" answer
}
// Upper: first index with nums[i] > target.
// - 1: step back onto the last copy of target.
int last = ~Array.BinarySearch(nums, target, Upper) - 1;
return [first, last];
}
}
The comparer never returns 0, so Array.BinarySearch never stops on a match. On ties, Lower says “go left” and Upper says “go right”. The search runs off the end of the range and returns the complement of the boundary. Lower gives the lower bound, the first index with a value at least target. Upper gives the upper bound, the first index with a value greater than target. Knowing which is which is worth stating. It is clever, so in an interview write LowerBound by hand first and offer this second.
first == nums.Length, caught by the length check. That check must come first, or nums[first] throws IndexOutOfRangeException.[0, n - 1].LowerBound returns 0, which equals nums.Length, so [-1, -1].target + 1: real in C#. With target = int.MaxValue an int sum wraps to int.MinValue. The code passes target + 1L, a long, so it cannot wrap. Writing a separate UpperBound also works.An array is a mountain: it strictly increases to a single peak, then strictly decreases. Return the index of the peak, in O(log n).
arr[i] > arr[i + 1]?” is monotone: false all the way up the mountain, then true all the way down, flipping exactly once at the peak. That is the F F F T T T shape from Figure 11.1, so Template B applies directly. This problem is the best demonstration that binary search is about monotone predicates, not about sortedness.public static class MountainPeak
{
/// <summary>Index of the single peak of a mountain array.</summary>
/// <param name="arr">Strictly increasing then strictly decreasing, length >= 3.</param>
/// <returns>The index i where arr[i] is the maximum.</returns>
/// <example><c>PeakIndex([0, 2, 5, 8, 4, 1])</c> returns 3.</example>
public static int PeakIndex(int[] arr)
{
// 0 is the first index. arr.Length - 1 is the last. The peak is in between.
int left = 0, right = arr.Length - 1;
// Invariant: the peak is somewhere in [left, right].
// < not <=: stop when one index is left, that index is the peak.
while (left < right)
{
int mid = left + (right - left) / 2; // / 2 rounds down, so mid < right
// mid + 1 is safe: mid < right, so mid + 1 is at most right.
// + 1 compares mid with its right neighbour to see the slope.
if (arr[mid] < arr[mid + 1])
{
left = mid + 1; // + 1: still climbing, so mid is not the peak
}
else
{
right = mid; // descending or at the peak, so mid may be it
}
}
return left; // left == right, and that is the peak
}
}
right = mid and not mid - 1When arr[mid] > arr[mid + 1], the array is already descending at mid, so mid itself might be the peak. Discarding it with mid - 1 would lose the answer. In the other branch, arr[mid] < arr[mid + 1] proves mid is not the peak, so mid + 1 is safe. Discard only what you have proved cannot be the answer. That sentence is the whole discipline of this pattern.
Note also that mid + 1 is always a valid index inside the loop: the guard is left < right, so mid < right ≤ n - 1. No bounds check is needed, and saying so shows you checked.
arr = [0, 2, 5, 8, 4, 1]:
left = 3.right = 4.right = 3.arr[mid] with its right neighbour tells you which slope you are on. That is enough to halve the range.Reading the figure. The bars show the mountain. The F/T row is arr[i] > arr[i + 1], false while climbing and true going down. Blue is in range, amber is mid, red is discarded, green is the peak. On a climb the code drops mid. On a descent it keeps mid, since it might be the peak.
nums[-1] and nums[n] treated as negative infinity. Remarkably, the identical code works. If arr[mid] < arr[mid + 1] then the rising slope to the right must eventually turn over or hit the boundary, so a peak exists in [mid + 1, right]. The same argument runs the other way. The invariant is not “the peak” but “a peak”, and it survives. Being able to explain why is a strong finish.[0, 1, 0]: returns 1.[0, 5, 4, 3] or [1, 2, 3, 0].left < right, not <=. With <= and right = mid it never terminates.true. One template covers lower bound, upper bound, first, last, and insertion point.[left, right] or half-open [left, right), and never mix them.mid and mid ± 1 every time.left = mid an infinite loop. Use mid + 1, or round up.left + (right - left) / 2, use long for big answer ranges, and apply ~ to a missed BinarySearch result.Pattern 12, Dynamic Programming, is the last and the largest. Where binary search discards half the possibilities, DP keeps all of them but makes sure no subproblem is ever solved twice.