Algorithm Search⌘K

Sequential search symbol table in Python

Symbol table stored as an unordered linked list of key-value pairs, searched by scanning from the head.

File
sequential_search_symbol_table.py, 101 lines
Level
I0 not checked yet
Toolchain
Python 3.14, pytest 9
Test framework
pytest
Run the tests
python3 -m pytest
sequential_search_symbol_table.pyRawGitHub
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/sequential-search-symbol-table/. The authors are listed in prov/5G5MVH.jsonl.

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

K = TypeVar("K")
V = TypeVar("V")


class _Node(Generic[K, V]):
    __slots__ = ("key", "value", "next")

    def __init__(self, key: K, value: V, nxt: "_Node[K, V] | None") -> None:
        self.key = key
        self.value = value
        self.next = nxt


class SequentialSearchST(Generic[K, V]):
    """A symbol table stored as an unordered singly linked list of key-value pairs.

    Keys need only an equality test. get, put and delete make at most n key
    compares on n keys (n for a miss and for a new key); a hit at position i
    makes i. Space Theta(n). The attribute compares counts the key compares.
    """

    def __init__(self) -> None:
        self.first: _Node[K, V] | None = None
        self.n = 0
        self.compares = 0

    def get(self, k: K) -> V | None:
        """Return the value of k, or None if k is absent."""
        x = self.first  # @line L1
        while x is not None:  # @line L2
            self.compares += 1
            if x.key == k:  # @line L3
                return x.value
            x = x.next  # @line L4
        return None  # @line L5

    def put(self, k: K, v: V) -> None:
        """Map k to v, replacing an old value; a new key goes to the front."""
        x = self.first  # @line L6
        while x is not None:  # @line L7
            self.compares += 1
            if x.key == k:  # @line L8
                x.value = v
                return
            x = x.next  # @line L9
        self.first = _Node(k, v, self.first)  # @line L10
        self.n += 1

    def delete(self, k: K) -> None:
        """Remove k and its value, if present."""
        prev: _Node[K, V] | None = None
        x = self.first
        while x is not None:
            self.compares += 1
            if x.key == k:
                break
            prev, x = x, x.next
        if x is None:
            return
        if prev is None:
            self.first = x.next
        else:
            prev.next = x.next
        self.n -= 1

    def contains(self, k: K) -> bool:
        """Return True if k is in the table."""
        x = self.first
        while x is not None:
            if x.key == k:
                return True
            x = x.next
        return False

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

    def keys(self) -> Iterator[K]:
        """The keys from the first node to the last."""
        x = self.first
        while x is not None:
            yield x.key
            x = x.next

    def check_invariant(self) -> None:
        """Assert (I1) and (I2): n nodes, no cycle, distinct keys."""
        seen: list[K] = []
        x, steps = self.first, 0
        while x is not None:
            assert steps < self.n, "cycle or wrong size"
            assert x.key not in seen, "duplicate key"
            seen.append(x.key)
            steps += 1
            x = x.next
        assert steps == self.n
test_sequential_search_symbol_table.py 75 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/sequential-search-symbol-table/. The authors are listed in prov/5G5MVH.jsonl.

import random

from sequential_search_symbol_table import SequentialSearchST


def test_example() -> None:
    st: SequentialSearchST[str, int] = SequentialSearchST()
    totals = []
    for v, k in enumerate("SEARCHEXAMPLE"):
        st.put(k, v)
        totals.append(st.compares)
        st.check_invariant()
    assert totals == [0, 1, 3, 6, 10, 15, 20, 26, 31, 38, 46, 55, 64]
    assert list(st.keys()) == list("LPMXHCRAES")
    assert [st.get(k) for k in "LPMXHCRAES"] == [11, 10, 9, 7, 5, 4, 3, 8, 12, 0]
    before = st.compares
    assert st.get("R") == 3 and st.compares - before == 7
    before = st.compares
    assert st.get("Z") is None and st.compares - before == 10


def test_edges() -> None:
    st: SequentialSearchST[int, str] = SequentialSearchST()
    assert st.get(1) is None and len(st) == 0 and not st.contains(1)
    st.delete(1)
    st.put(1, "a")
    st.put(1, "b")
    assert len(st) == 1 and st.get(1) == "b"
    st.put(2, "c")
    st.delete(1)
    assert list(st.keys()) == [2] and st.get(1) is None
    st.delete(2)
    assert len(st) == 0 and st.first is None
    st.check_invariant()


def test_build_compares() -> None:
    for n in range(0, 40):
        st: SequentialSearchST[int, int] = SequentialSearchST()
        for k in range(n):
            st.put(k, k)
        assert st.compares == n * (n - 1) // 2


def test_equal_values_not_identity() -> None:
    st: SequentialSearchST[str, int] = SequentialSearchST()
    a = "".join(["ke", "y"])
    st.put(a, 1)
    st.put("key", 2)
    assert len(st) == 1 and st.get("key") == 2


def test_random_against_dict() -> None:
    rng = random.Random(31)
    for _ in range(600):
        st: SequentialSearchST[int, int] = SequentialSearchST()
        model: dict[int, int] = {}
        for _ in range(rng.randint(0, 50)):
            k, r = rng.randrange(12), rng.random()
            if r < 0.4:
                assert st.get(k) == model.get(k)
                assert st.contains(k) == (k in model)
            elif r < 0.8:
                v = rng.randrange(1000)
                st.put(k, v)
                model[k] = v
            else:
                st.delete(k)
                model.pop(k, None)
            st.check_invariant()
            assert len(st) == len(model)
            assert sorted(st.keys()) == sorted(model)

What the next level needs

  • the tests did not run in this build

Read the full page about Sequential search symbol table