van Emde Boas layout in Python
Recursive memory layout of a static binary search tree that gives O(log_B N) block transfers per search.
- File
van_emde_boas_layout.py, 127 lines- Level
- I0 not checked yet
- Toolchain
- Python 3.14, pytest 9
- Test framework
- pytest
- Run the tests
python3 -m pytest
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/van-emde-boas-layout/. The authors are listed in prov/VCF4TV.jsonl.
from typing import Any, Generic, Optional, Protocol, TypeVar
class Ordered(Protocol):
def __lt__(self, other: Any, /) -> bool: ...
K = TypeVar("K", bound=Ordered)
def veb_pos(h: int, d: int, p: int) -> int:
"""Position, in the van Emde Boas layout of a complete binary tree with
h levels, of the node at depth d (the root has depth 0) whose path from
the root is given by the d bits of p, most significant first, with 0 for
left and 1 for right. The top tree has h // 2 levels."""
if h == 1: # @line L1
return 0
ht = h // 2 # @line L2
hb = h - ht
if d < ht: # @line L3
return veb_pos(ht, d, p) # the top tree holds the same path
j = p >> (d - ht) # @line L4
rest = p & ((1 << (d - ht)) - 1)
return (1 << ht) - 1 + j * ((1 << hb) - 1) + veb_pos(hb, d - ht, rest) # @line L5
class VEBLayout(Generic[K]):
"""A static search tree on sorted distinct keys, stored as a complete
binary search tree in the van Emde Boas layout.
The tree has h levels and 2**h - 1 slots; the slots after the N keys hold
None, which acts as plus infinity. A search follows one root-to-leaf
path and touches O(log_B N) blocks of B slots, for every B, without
knowing B.
"""
def __init__(self, keys: list[K]) -> None:
if any(not (a < b) for a, b in zip(keys, keys[1:])):
raise ValueError("keys must be sorted and distinct")
n = len(keys)
h = 1
while (1 << h) - 1 < n: # @line L6
h += 1
self.h = h
self.n = n
size = (1 << h) - 1
padded: list[Optional[K]] = list(keys) + [None] * (size - n)
self.slots: list[Optional[K]] = [None] * size
for d in range(h): # @line L7
span = 1 << (h - d)
for x in range(1 << d):
rank = x * span + span // 2 - 1 # in-order rank of the node
self.slots[veb_pos(h, d, x)] = padded[rank]
def __len__(self) -> int:
return self.n
def _less(self, x: Optional[K], k: K) -> bool:
return x is not None and x < k
def path(self, k: K) -> list[int]:
"""The slots read by a search for k, from the root down."""
out: list[int] = []
d, p = 0, 0
while d < self.h:
i = veb_pos(self.h, d, p)
out.append(i)
x = self.slots[i]
if x is not None and not (x < k) and not (k < x):
break
p = 2 * p + (1 if self._less(x, k) else 0)
d += 1
return out
def predecessor(self, k: K) -> Optional[K]:
"""The largest key <= k, or None."""
d, p = 0, 0 # @line L8
best: Optional[K] = None
while d < self.h: # @line L9
x = self.slots[veb_pos(self.h, d, p)] # @line L10
if x is not None and not (x < k) and not (k < x): # @line L11
return x
if self._less(x, k): # @line L12
best = x
p = 2 * p + 1
else:
p = 2 * p
d += 1 # @line L13
return best # @line L14
def __contains__(self, k: K) -> bool:
x = self.predecessor(k)
return x is not None and not (x < k)
def blocks_touched(self, k: K, block_size: int, offset: int = 0) -> int:
"""The number of distinct blocks of block_size slots met by the
search for k, when slot 0 is at position offset of its block."""
return len({(offset + i) // block_size for i in self.path(k)})
def inorder(self) -> list[K]:
"""The keys in in-order, read through the layout."""
out: list[K] = []
def walk(d: int, p: int) -> None:
if d == self.h:
return
walk(d + 1, 2 * p)
x = self.slots[veb_pos(self.h, d, p)]
if x is not None:
out.append(x)
walk(d + 1, 2 * p + 1)
walk(0, 0)
return out
def check_invariant(self) -> None:
"""Raise AssertionError if the layout is not a valid search tree."""
size = (1 << self.h) - 1
assert len(self.slots) == size and (self.n == 0 or (size - 1) // 2 < self.n <= size)
positions = sorted(veb_pos(self.h, d, x) for d in range(self.h) for x in range(1 << d))
assert positions == list(range(size))
keys = self.inorder()
assert len(keys) == self.n
assert all(a < b for a, b in zip(keys, keys[1:]))
test_van_emde_boas_layout.py 84 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/van-emde-boas-layout/. The authors are listed in prov/VCF4TV.jsonl.
import bisect
import math
import random
import pytest
from van_emde_boas_layout import VEBLayout, veb_pos
def test_example() -> None:
t = VEBLayout(list(range(1, 16)))
assert t.h == 4
# top tree of 2 levels, then 4 bottom trees of 2 levels, left to right
assert t.slots == [8, 4, 12, 2, 1, 3, 6, 5, 7, 10, 9, 11, 14, 13, 15]
assert t.path(11) == [0, 2, 9, 11]
assert t.predecessor(11) == 11 and 11 in t
t2 = VEBLayout([10 * k for k in range(1, 16)])
assert t2.predecessor(105) == 100 and 105 not in t2
assert t2.predecessor(5) is None and t2.predecessor(999) == 150
t.check_invariant()
def test_paper_example_blocks() -> None:
# N = 2^16 - 1 keys and B = 16, as on the cache-oblivious model page
n, B = 2**16 - 1, 16
t = VEBLayout(list(range(n)))
keys = range(0, n, 7)
assert max(t.blocks_touched(k, B) for k in keys) <= 7
assert max(t.blocks_touched(k, B, 5) for k in keys) <= 8
assert max(t.blocks_touched(k, B) for k in range(n)) == 7
def bfs_blocks(k: int) -> int:
d, x, seen = 0, 0, set()
while d < 16:
seen.add(((1 << d) - 1 + x) // B)
r = x * (1 << (16 - d)) + (1 << (15 - d)) - 1
if r == k:
break
x = 2 * x + (1 if r < k else 0)
d += 1
return len(seen)
assert max(bfs_blocks(k) for k in keys) == 13
def test_edges() -> None:
with pytest.raises(ValueError):
VEBLayout([2, 1])
e: VEBLayout[int] = VEBLayout([])
assert e.predecessor(3) is None and 3 not in e and len(e) == 0
e.check_invariant()
one = VEBLayout([5])
assert one.predecessor(5) == 5 and one.predecessor(4) is None
one.check_invariant()
def test_against_bisect() -> None:
rng = random.Random(17)
for _ in range(500):
n = rng.randrange(0, 200)
keys = sorted(rng.sample(range(1000), n))
t = VEBLayout(keys)
t.check_invariant()
assert t.inorder() == keys
for _ in range(20):
q = rng.randrange(-5, 1005)
i = bisect.bisect_right(keys, q)
want = keys[i - 1] if i else None
assert t.predecessor(q) == want
assert (q in t) == (q in keys)
def test_log_b_bound() -> None:
for h in (10, 14):
n = 2**h - 1
t = VEBLayout(list(range(n)))
rng = random.Random(h)
for B in (2, 4, 8, 32, 64, 256):
worst = max(t.blocks_touched(rng.randrange(n), B, rng.randrange(B)) for _ in range(300))
assert worst <= max(2, 8 * math.log2(n + 1) / math.log2(B + 1))
assert sorted(veb_pos(5, d, x) for d in range(5) for x in range(1 << d)) == list(range(31))
What the next level needs
- the tests did not run in this build