Algorithm Search⌘K

Direct-address table in Python

Array indexed directly by key from a small universe, with O(1) operations and no collisions.

File
direct_address_table.py, 154 lines
Level
I0 not checked yet
Toolchain
Python 3.14, pytest 9
Test framework
pytest
Run the tests
python3 -m pytest
direct_address_table.pyRawGitHub
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/direct-address-table/. The authors are listed in prov/QBRTYG.jsonl.

import random
from collections.abc import Iterator
from typing import Generic, TypeVar

V = TypeVar("V")


class _Nil:
    """The empty-slot marker. No user value is this object."""

    def __repr__(self) -> str:
        return "NIL"


NIL = _Nil()


class DirectAddressTable(Generic[V]):
    """A dictionary on the keys 0, ..., u - 1, with the value of key k in slot k.

    Search, insert and delete take O(1) time in the worst case. Creation takes
    O(u) time and the table uses O(u) space, whatever the number of keys.
    """

    def __init__(self, u: int) -> None:
        if u < 0:
            raise ValueError("the universe size must be at least 0")
        self.u = u
        self.n = 0
        self.slots: list[object] = [NIL] * u

    def _check(self, k: int) -> None:
        if not 0 <= k < self.u:
            raise KeyError(k)

    def search(self, k: int) -> V | None:
        """Return the value of k, or None if k is not a key."""
        self._check(k)
        v = self.slots[k]  # @line L1
        return None if v is NIL else v  # type: ignore[return-value]

    def contains(self, k: int) -> bool:
        """Return True if k is a key, also when its value is None."""
        self._check(k)
        return self.slots[k] is not NIL

    def insert(self, k: int, v: V) -> None:
        """Set the value of k to v."""
        self._check(k)
        if self.slots[k] is NIL:  # @line L2
            self.n += 1
        self.slots[k] = v  # @line L3

    def delete(self, k: int) -> bool:
        """Remove k. Return True if k was a key."""
        self._check(k)
        was = self.slots[k] is not NIL
        if was:  # @line L4
            self.n -= 1
        self.slots[k] = NIL  # @line L5
        return was

    def __len__(self) -> int:
        return self.n

    def keys(self) -> Iterator[int]:
        """Yield the keys in increasing order, in O(u) time."""
        return (k for k in range(self.u) if self.slots[k] is not NIL)

    def check_invariant(self) -> None:
        """Assert (D2) of the page; (D1) holds by the representation."""
        assert len(self.slots) == self.u
        assert self.n == sum(1 for s in self.slots if s is not NIL)


class SparseDirectAddressTable(Generic[V]):
    """The direct-address table of Briggs and Torczon, which needs no cleared array.

    The arrays val, idx and key start with arbitrary contents; here they are
    filled with random garbage to model memory that was not initialized. Each
    operation, and clear, takes O(1) time; keys() takes O(n) time.
    """

    def __init__(self, u: int, garbage_seed: int | None = None) -> None:
        if u < 0:
            raise ValueError("the universe size must be at least 0")
        g = random.Random(garbage_seed)
        self.u = u
        self.val: list[object] = [g.randint(-u - 5, u + 5) for _ in range(u)]
        self.idx: list[int] = [g.randint(-u - 5, u + 5) for _ in range(u)]
        self.key: list[int] = [g.randint(-u - 5, u + 5) for _ in range(u)]
        self.t = 0

    def _check(self, k: int) -> None:
        if not 0 <= k < self.u:
            raise KeyError(k)

    def _valid(self, k: int) -> bool:
        i = self.idx[k]  # @line L6
        return 0 <= i < self.t and self.key[i] == k  # @line L7

    def search(self, k: int) -> V | None:
        """Return the value of k, or None if k is not a key."""
        self._check(k)
        return self.val[k] if self._valid(k) else None  # type: ignore[return-value]  # @line L8

    def contains(self, k: int) -> bool:
        """Return True if k is a key."""
        self._check(k)
        return self._valid(k)

    def insert(self, k: int, v: V) -> None:
        """Set the value of k to v."""
        self._check(k)
        if not self._valid(k):  # @line L9
            self.idx[k] = self.t  # @line L10
            self.key[self.t] = k
            self.t += 1
        self.val[k] = v  # @line L11

    def delete(self, k: int) -> bool:
        """Remove k. Return True if k was a key."""
        self._check(k)
        if not self._valid(k):  # @line L12
            return False
        i = self.idx[k]  # @line L13
        j = self.key[self.t - 1]
        self.key[i] = j  # @line L14
        self.idx[j] = i
        self.t -= 1
        return True

    def clear(self) -> None:
        """Remove all keys in O(1) time."""
        self.t = 0

    def __len__(self) -> int:
        return self.t

    def keys(self) -> list[int]:
        """Return the stored keys, in insertion order up to the swaps of delete."""
        return self.key[: self.t]

    def check_invariant(self) -> None:
        """Assert (S1) and (S2) of the correctness proof."""
        assert 0 <= self.t <= self.u
        stored = self.key[: self.t]
        assert len(set(stored)) == self.t  # (S1)
        for i, k in enumerate(stored):
            assert 0 <= k < self.u
            assert self.idx[k] == i  # (S2)
test_direct_address_table.py 92 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/direct-address-table/. The authors are listed in prov/QBRTYG.jsonl.

import random

import pytest

from direct_address_table import DirectAddressTable, SparseDirectAddressTable


def test_page_example() -> None:
    t: DirectAddressTable[str] = DirectAddressTable(10)
    t.insert(3, "a")
    t.insert(7, "b")
    t.insert(0, "c")
    assert len(t) == 3
    t.insert(3, "d")
    assert len(t) == 3 and t.search(3) == "d"
    assert t.delete(7)
    assert not t.delete(5)
    assert len(t) == 2
    assert t.search(7) is None and list(t.keys()) == [0, 3]
    t.check_invariant()


def test_sparse_page_example() -> None:
    t: SparseDirectAddressTable[int] = SparseDirectAddressTable(8, garbage_seed=1)
    for k in [5, 2, 7]:
        t.insert(k, 10 * k)
    assert t.keys() == [5, 2, 7]
    assert [t.idx[5], t.idx[2], t.idx[7]] == [0, 1, 2]
    assert t.delete(5)
    assert t.keys() == [7, 2] and t.idx[7] == 0 and t.idx[5] == 0
    assert not t.contains(5)
    t.idx[4] = 1  # garbage that points into the used part
    assert not t.contains(4)
    t.check_invariant()


def test_none_value_and_range() -> None:
    t: DirectAddressTable[None] = DirectAddressTable(3)
    t.insert(1, None)
    assert t.contains(1) and t.search(1) is None and len(t) == 1
    for bad in (-1, 3):
        with pytest.raises(KeyError):
            t.search(bad)
    s: SparseDirectAddressTable[int] = SparseDirectAddressTable(0)
    with pytest.raises(KeyError):
        s.insert(0, 1)
    assert len(DirectAddressTable(0)) == 0


def test_sparse_clear() -> None:
    t: SparseDirectAddressTable[int] = SparseDirectAddressTable(20, garbage_seed=3)
    for k in range(0, 20, 3):
        t.insert(k, k)
    t.clear()
    assert len(t) == 0 and all(not t.contains(k) for k in range(20))
    t.insert(4, 9)
    assert t.search(4) == 9 and t.keys() == [4]
    t.check_invariant()


def test_random_against_dict() -> None:
    rng = random.Random(2024)
    for trial in range(600):
        u = rng.randint(1, 30)
        a: DirectAddressTable[int] = DirectAddressTable(u)
        b: SparseDirectAddressTable[int] = SparseDirectAddressTable(u, garbage_seed=trial)
        model: dict[int, int] = {}
        for _ in range(rng.randint(0, 60)):
            k = rng.randrange(u)
            op = rng.random()
            if op < 0.45:
                v = rng.randint(0, 99)
                a.insert(k, v)
                b.insert(k, v)
                model[k] = v
            elif op < 0.75:
                assert a.delete(k) == (k in model) == b.delete(k)
                model.pop(k, None)
            elif op < 0.78:
                b.clear()
                for key in list(model):
                    a.delete(key)
                model.clear()
            else:
                assert a.search(k) == model.get(k) == b.search(k)
            a.check_invariant()
            b.check_invariant()
            assert len(a) == len(model) == len(b)
            assert list(a.keys()) == sorted(model) == sorted(b.keys())

What the next level needs

  • the tests did not run in this build

Read the full page about Direct-address table