3 ms·
I am a huge proponent of "bitwise" binary search. Consider r to be the result index we wish to find in range [0, n), that is, the first index where a predicate
by orlp 5y ago
I am a huge proponent of "bitwise" binary search. Consider r to be the result
index we wish to find in range [0, n), that is, the first index where a
predicate returns false, or n if there is no such predicate. We can construct this index bit-by-bit, starting from
the most significant bit:
# Extracts the highest set bit for n > 0.
# This can be 1-5 instructions on most machines using count leading zeros.
def highest_bit_set(n):
return 1 << (n.bit_length() - 1)
def binary_search(n, predicate):
# Construct largest possible r such that predicate(r) is true, or 0.
if n == 0: return 0
r = 0
bit = highest_bit_set(n)
while bit:
if (r | bit) < n and predicate(r | bit): r |= bit
bit >>= 1
# Return smallest possible r such that predicate(r) is false, or n.
if r > 0 or predicate(0): r += 1
return r
I find this to be significantly easier to reason about, although I do have a lot
of experience with bitwise manipulation. And it is trivial to extend to search
a range [lo, hi):
def binary_search_range(lo, hi, predicate):
return binary_search(hi - lo, lambda i: predicate(lo + i))
It's similarly trivial to reason they do exactly log2(n) or log2(hi - lo) iterations and are thus worst-case optimal.
- hddqsb 5y agoNote that with this version, the expected number of calls to `predicate` can be 1 more than strictly necessary (depending on n). This happens when n is one more than a power of 2, e.g. if n is 65 then `bit` is 64 and the first call will be `predicate(64)`, which is basically useless in reducing the search space.
- orlp 5y agoThis is true, if your comparison operator is very expensive you should (very, very carefully) implement the traditional binary search.
- truly 5y agoThis is called bisection search (search for the largest/smallest value satisfying a given predicate). It also reminds me a little bit of exponential search (upper bound unknown), although obviously the upper bound is known in your implementation. Bisection: https://en.wikipedia.org/wiki/Bisection_method https://en.wikipedia.org/wiki/Bisection_method Exponential search: https://en.wikipedia.org/wiki/Exponential_search https://en.wikipedia.org/wiki/Exponential_search A practical application of bisection search is in git bisect (https://git-scm.com/docs/git-bisect https://git-scm.com/docs/git-bisect) to find the first revision that introduced a bug.