Binary search¶
Binary search is a method that allows for quicker search of something by splitting the search interval into two. Its most common application is searching values in sorted arrays, however the splitting idea is crucial in many other typical tasks.
Search in sorted arrays¶
The most typical problem that leads to the binary search is as follows. You're given a sorted array
Binary search of the value $7$ in an array.
The image by [AlwaysAngry](https://commons.wikimedia.org/wiki/User:AlwaysAngry) is distributed under CC BY-SA 4.0 license.
Now assume that we know two indices
When it is impossible to pick
Since in the worst case we will always reduce to larger segment of
In other words, from the worst-case scenario perspective it is optimal to always pick
Taking
Logarithmic number of steps is drastically better than that of linear search. For example, for
Lower bound and upper bound¶
It is often convenient to find the position of the first element that is greater or equal than
Together, lower and upper bounds produce a possibly empty half-interval of the array elements that are equal to
Implementation¶
The explanation above provides a rough description of the algorithm. For the implementation details, we'd need to be more precise.
We will maintain a pair
When
Finally, to be specific about the value of
Then the implementation could look like this:
... // a sorted array is stored as a[0], a[1], ..., a[n-1]
int l = -1, r = n;
while (r - l > 1) {
int m = (l + r) / 2;
if (k < a[m]) {
r = m; // a[l] <= k < a[m] <= a[r]
} else {
l = m; // a[l] <= a[m] <= k < a[r]
}
}
During the execution of the algorithm, we never evaluate neither
Note. Calculating m
as m = (r + l) / 2
can lead to overflow if l
and r
are two positive integers, and this error lived about 9 years in JDK as described in the blogpost. Some alternative approaches include e.g. writing m = l + (r - l) / 2
which always works for positive integer l
and r
, but might still overflow if l
is a negative number. If you use C++20, it offers an alternative solution in the form of m = std::midpoint(l, r)
which always works correctly.
Search on arbitrary predicate¶
Let
The binary search, the way it is described above, finds the partition of the array by the predicate
Proof of correctness supposing a transition point exists, that is
... // f(i) is a boolean function such that f(0) <= ... <= f(n-1)
int l = -1, r = n;
while (r - l > 1) {
int m = (l + r) / 2;
if (f(m)) {
r = m; // 0 = f(l) < f(m) = 1
} else {
l = m; // 0 = f(m) < f(r) = 1
}
}
Binary search on the answer¶
Such situation often occurs when we're asked to compute some value, but we're only capable of checking whether this value is at least
among all possible pairs of
Equivalently, it rewrites as
so now we need to check whether there is a subarray of a new array
Continuous search¶
Let
Without loss of generality assume that
The value
For example, let
Search with powers of 2¶
Another noteworthy way to do binary search is, instead of maintaining an active segment, to maintain the current pointer
This paradigm is widely used in tasks around trees, such as finding lowest common ancestor of two vertices or finding an ancestor of a specific vertex that has a certain height. It could also be adapted to e.g. find the
Practice Problems¶
- LeetCode - Find First and Last Position of Element in Sorted Array
- LeetCode - Search Insert Position
- LeetCode - First Bad Version
- LeetCode - Valid Perfect Square
- LeetCode - Find Peak Element
- LeetCode - Search in Rotated Sorted Array
- LeetCode - Find Right Interval
- Codeforces - Interesting Drink
- Codeforces - Magic Powder - 1
- Codeforces - Another Problem on Strings
- Codeforces - Frodo and pillows
- Codeforces - GukiZ hates Boxes
- Codeforces - Enduring Exodus
- Codeforces - Chip 'n Dale Rescue Rangers