Algorithm Search⌘K

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
bit_set.pyRawGitHub
# 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

Read the full page about Bit vector