Written by Timon Gehr.
Contents
The goal of this text is to give an account of binary search that can be applied in more general settings than searching for an element in a sorted array.
Let be a monotone function mapping the interval to . This means that maps “small” numbers to and “large” numbers to . The issue is that we don’t know which numbers are still small, and which numbers should already be considered large, but does. We want to learn all the information that is encoded by .
The goal of binary search is therefore to determine the cutoff point between small numbers and large numbers without evaluating too many times.
Binary search finds a sequence of nested intervals such that the left borders of the intervals are small, and the right borders are large, and such that .
(More explicitly, this means that , where for each , either or and .)
Importantly, note that is the largest number that maps to , while is the smallest number that maps to .
Let be an arbitrary number with . Note that exactly one of the two intervals and has all the required properties of :
If , then the left border of the interval maps to , while the right border maps to . If , then the left border of the interval maps to , while the right border maps to .
Therefore, the following generic strategy will successfully determine :
int l = l_0, r = r_0; while (l + 1 != r) { // use some m_i with l_i < m_i < r_i: int m = ...; // TODO: choose m_i if (f(m) == 1) { // f(m) == 1, [l_{i+1}, r_{i+1}] = [l_i, m_i]. r = m; } else { // f(m) == 0, [l_{i+1}, r_{i+1}] = [m_i, r_i]. l = m; } } int l_k = l, r_k = r;
If we want to get a small number of steps in the worst case, should be guaranteed to cut the size of the interval in half. (It is harder to find the boundary in a longer interval. In the worst case, we will need to search in the longer part. The length of the longer part is minimized if cuts the interval in half.)
There are two choices of that satisfy all the requirements as long as :
- .
- .
Picking the first one, we obtain a working binary search algorithm:
int l = l_0, r = r_0; while (l + 1 != r) { // choose m such that l_i < m_i < r_i and // max(r_i - m_i, m_i - l_i) is minimal: int m = l + (r - l) / 2; if (f(m) == 1) { r = m; } else { l = m; } }
Note that binary search never evaluates and , because their values are already known, by assumption. This can sometimes be convenient, because we can just assume values for and , but don’t actually need to be able to compute them. (This is used for lower_bound below.)
Binary search runs in time , where is an upper bound on the running time of .
Examples
Task: find the first index into a sorted array whose value is at least .
Solution: use binary search with and . The result is . ( if no such index exists.)
int lower_bound(vector<int> &a, int x) { int n = (int)a.size(); int l_0 = -1, r_0 = n; // we assume a[-1] = -infinity and a[n] = +infinity int l = l_0, r = r_0; while (l + 1 != r) { int m = l + (r - l) / 2; if (x <= a[m]) { // f(m) = [x <= a_m] r = m; } else { l = m; } } int l_k = l, r_k = r; // we know that a[l_k]<x<=a[r_k]=a[l_k+1]. return r_k; // therefore, r_k is the first index whose value is >=x. }
Note that we have used and as sentinel values.
lower_bound runs in time , because can be evaluated in constant time .
Task: find the last index into a sorted array whose value is still smaller than .
Solution: use binary search with and . The result is . ( if no such index exists.)
The code is the same as the one for lower_bound, but this time we return l_k instead of r_k:
int lower_bound_minus_one(vector<int> &a, int x) { int n = (int)a.size(); int l_0 = -1, r_0 = n; int l = l_0, r = r_0; while (l + 1 != r) { int m = l + (r - l) / 2; if (x <= a[m]) { // f(m) = [x <= a_m] r = m; } else { l = m; } } int l_k = l, r_k = r; // we know that a[l_k]<x<=a[r_k]=a[l_k+1]. return l_k; // therefore, l_k is the last index whose value is `<x`. }
Task: Given an array of distinct positive integers , find the smallest positive integer not in the array using time and auxiliary space.
Solution: use binary search with and . The result is . We search for the smallest value , such that at least one number between and is missing. The function is monotone. (Why?)
int find_first_missing_positive_number(vector<int> &a) { int n = a.size(); // there are no numbers between 1 and 0: int l_0 = 0; // at least one number between 1 and n+1 must be missing: int r_0 = n+1; int l = l_0, r = r_0; while (l + 1 != r) { int m = l + (r - l) / 2; int num_smaller = 0; for (int y : a) { if (y <= m) { num_smaller += 1; } } // if there are less than m values between 1 and m, // then at least one value must be missing if (num_smaller < m) { r = m; } else { l = m; } } int l_k = l, r_k = r; return r_k; }
Note that there is a faster solution if we are allowed more than constant auxiliary space. Our assumption that there is no number that occurs multiple times is very important, as otherwise the definition of is incorrect. (Why?)
Task: Given a matrix, find the contiguous submatrix with the maximal median. Solution: Left as an exercise. (This is IOI 2010 task 3 on day 1.)