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
# 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