Meta binary search (also called one-sided binary search) finds a value in a sorted array without ever computing a midpoint or dividing anything. Instead of shrinking a [lo, hi] window, it assembles the answer index one bit at a time, from the highest bit down to the lowest. It lands on the exact same position a classic binary search would — it just builds the index in binary rather than bisecting a range.
Core idea. Start with pos = 0. For each bit i from high to low, tentatively set that bit:
new = pos + (1 << i). Keep the bit only if new is in bounds and a[new] <= target; otherwise
clear it. After the last bit, pos is the index of the largest value that is still <= target — check
a[pos] == target to decide if it is present. For a = [1, 3, 4, 7, 9, 12, 15, 20] and target 12,
the assembled index is 101 in binary, which is 5, and a[5] == 12.
Intuition
A classic binary search keeps two moving walls, lo and hi, and computes mid = (lo + hi) / 2 every step. Meta binary search throws the walls away and asks a different question: what are the binary digits of the answer?
Any index in an array of length n can be written in bit_length(n) bits. So the answer is some bit pattern like 101. Meta binary search decides those bits greedily from the most significant down. At each bit it asks: "if I switch this bit on, does the cell I land on still satisfy a[pos] <= target?" If yes, switching it on only moves us closer to (or onto) the answer, so we commit it. If no, that bit overshoots, so we leave it off. Because the array is sorted, this greedy choice is always safe — and after every bit has been decided, pos is the last index whose value does not exceed the target.
This is why it is called one-sided: pos only ever moves forward (it never backtracks left), and it is division-free — the only arithmetic is pos + (1 << i), a shift and an add. That makes it attractive on hardware where division is expensive, and it generalizes neatly to searching implicit or huge ranges where you would rather not materialize lo and hi.
Walk through it
Step through the animation on the right. The row is the sorted array a; the pointer pos marks the index being assembled, and the readout up top shows the binary index taking shape and the bit currently under test.
We start with pos = 0 and lg = 4 because n = 8 is 1000 in binary. The loop sweeps bits 4, 3, 2, 1, 0. Bit 4 would jump to index 16 and bit 3 to index 8 — both past the end of an 8-element array, so they are cleared immediately. Bit 2 proposes new = 4; since a[4] = 9 <= 12, the bit stays and pos snaps to 4. Bit 1 proposes new = 6, but a[6] = 15 > 12, so it is dropped and pos stays 4. Bit 0 proposes new = 5; since a[5] = 12 <= 12, the bit stays and pos becomes 5. The assembled index is 101. Finally we check a[5] = 12 == 12 — a match — so the target lives at index 5.
The code, line by line
def meta_search(a, x):
n = len(a)
lg = n.bit_length()
pos = 0
for i in range(lg, -1, -1):
new = pos + (1 << i)
if new < n and a[new] <= x:
pos = new
return pos if a[pos] == x else -1lg = n.bit_length()is how many bits any valid index can need; forn = 8that is4, so the loop tries bits4down to0.pos = 0is the index we build up. It starts at zero and only ever grows — that is the one-sided part.- Line 5 sweeps the bits from most significant to least, the reverse of how binary search narrows a range.
- Line 6,
new = pos + (1 << i), tentatively turns bition.1 << iis the bit value; there is no division anywhere. - Line 7 is the guard.
new < nkeeps the index in bounds, anda[new] <= xis the sortedness test: if the candidate value does not exceed the target, the answer is atnewor further right. - Line 8 commits the bit by advancing
pos = new. If the guard failed, we skip this and the bit stays0. - Line 9 makes the final decision. After the loop,
posis the last index witha[pos] <= x; it equalsxexactly whenxis present.
Complexity
| Case | Time | Notes |
|---|---|---|
| Time | O(log n) (fast) | one iteration per bit; bit_length(n) is about log2(n) |
| Space | O(1) (fast) | only pos and the loop counter; nothing is allocated |
O(1) (fast)The loop runs once per bit of the index, and an index needs about log2(n) bits, so the running time is O(log n) — identical to ordinary binary search. The work per step is a shift, an add, and one comparison, with no division and no extra memory.
When to use / pitfalls
Meta binary search shines when you want a branch-light, division-free search — for example on embedded
targets where integer division is costly, or when you are bisecting an enormous or implicit range and
would rather not track lo and hi explicitly. The interview signal is a sorted predicate that flips
from true to false exactly once: any place a classic binary search fits, the bit-by-bit version fits too.
Two things to watch. First, the bounds check must come before the array read — evaluate new < n
before a[new] (Python short-circuits and, so the order in line 7 matters) or you risk an
out-of-range access. Second, the loop finds the last index with a[pos] <= x, not a guarantee that
x exists; you must still test a[pos] == x at the end, exactly as line 9 does.
Practice
For a = [1, 3, 4, 7, 9, 12, 15, 20] and target 12, what is the assembled index in binary, and which decimal index does it equal?
1. How does meta binary search build the answer index?
2. Why is the technique called division-free?
3. When is a tentative bit kept rather than cleared?
4. What is the time complexity of meta binary search?