Bit vector in Python
A fixed-size array of bits giving constant-time membership, insertion and deletion over a small universe.
- File
bit_set.py, 94 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/bit-set/. The authors are listed in prov/C0Q5CB.jsonl.
class BitSet:
"""A set of integers from {0, ..., n - 1} stored as an array of w-bit words.
Element i is bit (i mod w) of word i // w. Bits at positions n and above
in the last word are always 0. insert, delete and contains cost O(1);
count, complement and union cost O(n / w) word operations; next_member
costs O(n / w) in the worst case. Space: ceil(n / w) words.
"""
def __init__(self, n: int = 0, w: int = 64) -> None:
if n < 0 or w < 1:
raise ValueError("need n >= 0 and w >= 1")
self.n = n
self.w = w
self._mask = (1 << w) - 1
self.words = [0] * ((n + w - 1) // w) # @line L1
def _check_index(self, i: int) -> None:
if not 0 <= i < self.n:
raise IndexError("element out of the universe")
def insert(self, i: int) -> None:
"""Add i to the set. Precondition: 0 <= i < n. Cost O(1)."""
self._check_index(i)
self.words[i // self.w] |= 1 << (i % self.w) # @line L2
def delete(self, i: int) -> None:
"""Remove i from the set if present. Precondition: 0 <= i < n. Cost O(1)."""
self._check_index(i)
self.words[i // self.w] &= ~(1 << (i % self.w)) & self._mask # @line L3
def contains(self, i: int) -> bool:
"""Return True if i is in the set. Precondition: 0 <= i < n. Cost O(1)."""
self._check_index(i)
return (self.words[i // self.w] >> (i % self.w)) & 1 == 1 # @line L4
def count(self) -> int:
"""Return the number of members. Cost O(n / w) popcounts."""
c = 0
for word in self.words:
c += word.bit_count()
return c
def next_member(self, i: int) -> int | None:
"""Return the least member >= i, or None. Precondition: i >= 0. Cost O(n / w)."""
j = i // self.w
if j >= len(self.words):
return None
x = self.words[j] & ~((1 << (i % self.w)) - 1) & self._mask
while x == 0:
j += 1
if j == len(self.words):
return None
x = self.words[j]
return j * self.w + ((x & -x).bit_length() - 1)
def complement(self) -> None:
"""Replace the set by its complement in {0, ..., n - 1}. Cost O(n / w)."""
for j in range(len(self.words)):
self.words[j] = ~self.words[j] & self._mask
r = self.n % self.w
if r != 0:
self.words[-1] &= (1 << r) - 1
def union(self, other: "BitSet") -> None:
"""Add every member of other, a set over the same n and w. Cost O(n / w)."""
if other.n != self.n or other.w != self.w:
raise ValueError("universes differ")
for j in range(len(self.words)):
self.words[j] |= other.words[j]
def members(self) -> list[int]:
"""Return the members in increasing order. Cost O(n / w + size)."""
out = []
for j, word in enumerate(self.words):
while word:
b = word & -word
out.append(j * self.w + b.bit_length() - 1)
word ^= b
return out
def check_invariant(self) -> None:
"""Assert the representation invariant: the right number of words,
each a w-bit word, and no bit at a position >= n."""
assert len(self.words) == (self.n + self.w - 1) // self.w
for word in self.words:
assert 0 <= word <= self._mask
r = self.n % self.w
if r != 0 and self.words:
assert self.words[-1] >> r == 0
test_bit_set.py 91 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/bit-set/. The authors are listed in prov/C0Q5CB.jsonl.
import random
import pytest
from bit_set import BitSet
def test_page_example() -> None:
s = BitSet(10, w=4)
assert s.words == [0, 0, 0]
s.insert(1)
s.insert(6)
s.insert(9)
assert s.words == [0b0010, 0b0100, 0b0010]
assert s.contains(6)
s.delete(1)
assert not s.contains(1)
s.insert(4)
assert s.words == [0b0000, 0b0101, 0b0010]
assert s.count() == 3
assert s.next_member(5) == 6
assert s.next_member(7) == 9
assert s.next_member(10) is None
s.complement()
s.check_invariant()
assert s.words == [0b1111, 0b1010, 0b0001]
assert s.members() == [0, 1, 2, 3, 5, 7, 8]
def test_empty_and_edges() -> None:
s = BitSet()
assert s.count() == 0 and s.members() == [] and s.next_member(0) is None
s.complement()
s.check_invariant()
t = BitSet(64)
t.complement()
t.check_invariant()
assert t.count() == 64 and t.words == [2**64 - 1]
t.delete(63)
assert t.next_member(63) is None and t.next_member(62) == 62
with pytest.raises(IndexError):
t.insert(64)
with pytest.raises(IndexError):
t.contains(-1)
def test_delete_absent_and_insert_twice() -> None:
s = BitSet(5, w=8)
s.delete(3)
s.insert(3)
s.insert(3)
assert s.members() == [3] and s.count() == 1
def test_random_against_set() -> None:
rng = random.Random(1999)
for _ in range(60):
n = rng.randrange(0, 200)
w = rng.choice([1, 3, 4, 8, 64])
s, t = BitSet(n, w), BitSet(n, w)
model, model_t = set(), set()
for _ in range(300):
op = rng.randrange(7)
i = rng.randrange(n) if n else 0
if op == 0 and n:
s.insert(i)
model.add(i)
elif op == 1 and n:
s.delete(i)
model.discard(i)
elif op == 2 and n:
assert s.contains(i) == (i in model)
elif op == 3:
assert s.count() == len(model)
elif op == 4:
k = rng.randrange(n + 2)
bigger = [x for x in model if x >= k]
assert s.next_member(k) == (min(bigger) if bigger else None)
elif op == 5:
s.complement()
model = set(range(n)) - model
elif n:
t.insert(i)
model_t.add(i)
s.union(t)
model |= model_t
s.check_invariant()
assert s.members() == sorted(model)
What the next level needs
- the tests did not run in this build