Wavelet tree in Python
A balanced binary tree over the alphabet with a bit vector per node, giving access, rank and select on sequences in compressed space.
- File
wavelet_tree.py, 149 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/wavelet-tree/. The authors are listed in prov/K5P9KD.jsonl.
from __future__ import annotations
from collections.abc import Sequence
W = 64
class RankBits:
"""A static bit vector with rank in O(1) (a count before each word) and
select by binary search over those counts."""
def __init__(self, bits: Sequence[int]) -> None:
self.n = len(bits)
self.words = [0] * (self.n // W + 1)
for i, x in enumerate(bits):
if x:
self.words[i // W] |= 1 << (i % W)
self.before = [0]
for x in self.words:
self.before.append(self.before[-1] + x.bit_count())
def __len__(self) -> int:
return self.n
def access(self, i: int) -> int:
return (self.words[i // W] >> (i % W)) & 1
def rank1(self, i: int) -> int:
k = i // W
return self.before[k] + (self.words[k] & ((1 << (i % W)) - 1)).bit_count()
def rank0(self, i: int) -> int:
return i - self.rank1(i)
def select(self, j: int, bit: int) -> int:
"""Return the position of the j-th bit equal to `bit`, for j >= 1."""
def count(k: int) -> int: # wanted bits in words 0..k-1
return self.before[k] if bit else k * W - self.before[k]
lo, hi = 0, len(self.words) - 1
while lo < hi:
mid = (lo + hi + 1) // 2
if count(mid) < j:
lo = mid
else:
hi = mid - 1
x = self.words[lo] if bit else ~self.words[lo]
for _ in range(j - count(lo) - 1):
x &= x - 1
p = lo * W + (x & -x).bit_length() - 1
if p >= self.n:
raise IndexError(j)
return p
class _Node:
__slots__ = ("bits", "left", "right")
def __init__(self) -> None:
self.bits = RankBits([])
self.left: _Node | None = None
self.right: _Node | None = None
class WaveletTree:
"""A wavelet tree over the alphabet [0, sigma) with a bit vector at each inner node."""
def __init__(self, seq: Sequence[int], sigma: int) -> None:
if sigma < 1 or any(not 0 <= c < sigma for c in seq):
raise ValueError("symbols must be in [0, sigma)")
self.n, self.sigma = len(seq), sigma
self.root = self._build(list(seq), 0, sigma)
def _build(self, s: list[int], a: int, b: int) -> _Node:
v = _Node()
if b - a > 1:
m = (a + b) // 2
v.bits = RankBits([1 if c >= m else 0 for c in s])
v.left = self._build([c for c in s if c < m], a, m)
v.right = self._build([c for c in s if c >= m], m, b)
return v
def __len__(self) -> int:
return self.n
def access(self, i: int) -> int:
"""Return S[i], for 0 <= i < n."""
if not 0 <= i < self.n:
raise IndexError(i)
v, a, b = self.root, 0, self.sigma # @line L1
while b - a > 1: # @line L2
m = (a + b) // 2 # @line L3
if v.bits.access(i) == 0: # @line L4
i, v, b = v.bits.rank0(i), v.left, m
else: # @line L5
i, v, a = v.bits.rank1(i), v.right, m
assert v is not None
return a # @line L6
def rank(self, c: int, i: int) -> int:
"""Return the number of c in S[0..i-1], for 0 <= i <= n."""
if not 0 <= i <= self.n:
raise IndexError(i)
if not 0 <= c < self.sigma:
return 0
v, a, b = self.root, 0, self.sigma # @line L7
while b - a > 1: # @line L8
m = (a + b) // 2 # @line L9
if c < m: # @line L10
i, v, b = v.bits.rank0(i), v.left, m
else: # @line L11
i, v, a = v.bits.rank1(i), v.right, m
assert v is not None
return i # @line L12
def select(self, c: int, j: int) -> int:
"""Return the position of the j-th c, for 1 <= j <= rank(c, n)."""
if not 0 <= c < self.sigma or not 1 <= j <= self.rank(c, self.n):
raise IndexError((c, j))
return self._select(self.root, 0, self.sigma, c, j)
def _select(self, v: _Node, a: int, b: int, c: int, j: int) -> int:
if b - a == 1: # @line L13
return j - 1
m = (a + b) // 2 # @line L14
if c < m: # @line L15
assert v.left is not None
return v.bits.select(self._select(v.left, a, m, c, j) + 1, 0)
assert v.right is not None # @line L16
return v.bits.select(self._select(v.right, m, b, c, j) + 1, 1)
def quantile(self, l: int, r: int, k: int) -> int:
"""Return the k-th smallest symbol of S[l..r-1], for 1 <= k <= r - l."""
if not 0 <= l < r <= self.n or not 1 <= k <= r - l:
raise IndexError((l, r, k))
v, a, b = self.root, 0, self.sigma # @line L17
while b - a > 1: # @line L18
m = (a + b) // 2 # @line L19
z = v.bits.rank0(r) - v.bits.rank0(l)
if k <= z: # @line L20
l, r, v, b = v.bits.rank0(l), v.bits.rank0(r), v.left, m
else: # @line L21
k -= z
l, r, v, a = v.bits.rank1(l), v.bits.rank1(r), v.right, m
assert v is not None
return a # @line L22
test_wavelet_tree.py 74 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/wavelet-tree/. The authors are listed in prov/K5P9KD.jsonl.
import random
import pytest
from wavelet_tree import WaveletTree
CODE = {"a": 0, "b": 1, "c": 2, "d": 3, "r": 4}
S = [CODE[ch] for ch in "abracadabra"]
def bits(v) -> str:
return "".join(str(v.bits.access(i)) for i in range(len(v.bits)))
def check_all(seq: list[int], sigma: int, rng: random.Random) -> None:
t = WaveletTree(seq, sigma)
n = len(seq)
assert [t.access(i) for i in range(n)] == seq
for c in range(sigma):
r = 0
for i in range(n + 1):
assert t.rank(c, i) == r
if i < n and seq[i] == c:
r += 1
pos = [i for i, x in enumerate(seq) if x == c]
for j, p in enumerate(pos, 1):
assert t.select(c, j) == p
for _ in range(30 if n else 0):
l = rng.randrange(n)
r = rng.randrange(l + 1, n + 1)
k = rng.randrange(1, r - l + 1)
assert t.quantile(l, r, k) == sorted(seq[l:r])[k - 1]
def test_page_example() -> None:
t = WaveletTree(S, 5)
assert bits(t.root) == "00101010010"
assert bits(t.root.left) == "0100010"
assert bits(t.root.right) == "1011"
assert bits(t.root.right.right) == "101"
assert t.access(4) == CODE["c"]
assert t.rank(CODE["a"], 7) == 3
assert t.select(CODE["a"], 4) == 7
assert t.rank(CODE["b"], 9) == 2
assert t.select(CODE["r"], 2) == 9
assert t.quantile(2, 9, 4) == CODE["b"]
check_all(S, 5, random.Random(1))
def test_edges() -> None:
rng = random.Random(2)
check_all([], 1, rng)
check_all([0], 1, rng)
check_all([0, 0, 0], 1, rng)
check_all([3, 3, 3], 4, rng)
check_all(list(range(100)), 100, rng)
check_all([1, 0] * 70, 2, rng)
t = WaveletTree([0, 2], 4)
assert t.rank(1, 2) == 0 and t.rank(3, 2) == 0
with pytest.raises(IndexError):
t.select(1, 1)
with pytest.raises(ValueError):
WaveletTree([4], 4)
def test_random_against_lists() -> None:
rng = random.Random(2003)
for _ in range(500):
sigma = rng.randrange(1, 20)
n = rng.randrange(0, 80)
check_all([rng.randrange(sigma) for _ in range(n)], sigma, rng)
What the next level needs
- the tests did not run in this build