Algorithm Search⌘K

Skew binary random-access list in Python

A persistent random-access list using skew binary numbers for O(1) cons and O(log n) index.

File
skew_binary_random_access_list.py, 164 lines
Level
I0 not checked yet
Toolchain
Python 3.14, pytest 9
Test framework
pytest
Run the tests
python3 -m pytest
skew_binary_random_access_list.pyRawGitHub
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/skew-binary-random-access-list/. The authors are listed in prov/RA7KJS.jsonl.

"""Okasaki's skew binary random-access list.

A version is a linked list of cells (weight, tree, rest). Each tree is a
complete binary tree of weight 2^k - 1 that stores its items in preorder.
The weights grow along the list, except that the first two may be equal;
they are the digits of n in skew binary. Nodes are tuples (x, left, right),
with None children for a leaf. Nothing is mutated, so every version stays
valid, and an update copies only one path and one prefix of the list.
"""

from typing import Any, Iterable, Iterator, List, Optional, Tuple

Tree = Tuple[Any, Any, Any]
Cells = Optional[Tuple[int, Tree, Any]]


class SkewList:
    """An immutable random-access list."""

    __slots__ = ("_cells", "_n")

    def __init__(self, _cells: Cells = None, _n: int = 0) -> None:
        """The empty list (use the methods to build others). Cost: O(1)."""
        self._cells = _cells
        self._n = _n

    @staticmethod
    def from_iterable(items: Iterable[Any]) -> "SkewList":
        """A list with the given items in order. Cost: Θ(n)."""
        xs = list(items)
        s = SkewList()
        for x in reversed(xs):
            s = s.cons(x)
        return s

    def __len__(self) -> int:
        """Number of items. Cost: O(1)."""
        return self._n

    def is_empty(self) -> bool:
        """Whether the list is empty. Cost: O(1)."""
        return self._cells is None

    def cons(self, x: Any) -> "SkewList":
        """A new list with x in front. Cost: Θ(1) worst case."""
        c = self._cells
        if c is not None and c[2] is not None and c[0] == c[2][0]:  # @line L1
            w1, t1, (w2, t2, rest) = c
            return SkewList((1 + w1 + w2, (x, t1, t2), rest), self._n + 1)  # @line L2
        return SkewList((1, (x, None, None), c), self._n + 1)  # @line L3

    def head(self) -> Any:
        """The first item. Cost: Θ(1)."""
        if self._cells is None:
            raise IndexError("head of an empty list")
        return self._cells[1][0]  # @line L4

    def tail(self) -> "SkewList":
        """The list without its first item. Cost: Θ(1) worst case."""
        if self._cells is None:
            raise IndexError("tail of an empty list")
        w, t, rest = self._cells  # @line L5
        if w == 1:  # @line L6
            return SkewList(rest, self._n - 1)
        h = w // 2  # @line L7
        return SkewList((h, t[1], (h, t[2], rest)), self._n - 1)

    def _find(self, i: int) -> Tuple[List[Tuple[int, Tree]], int, Tree, int, Cells]:
        if not 0 <= i < self._n:
            raise IndexError("index out of range")
        before: List[Tuple[int, Tree]] = []
        c = self._cells
        assert c is not None
        while i >= c[0]:  # @line L8
            before.append((c[0], c[1]))
            i -= c[0]
            c = c[2]
            assert c is not None
        w, t, rest = c  # @line L9
        return before, w, t, i, rest

    def __getitem__(self, i: int) -> Any:
        """Item i. Cost: O(min(i, log n))."""
        _, w, t, i, _ = self._find(i)
        while i != 0:  # @line L10
            w //= 2  # @line L11
            if i <= w:  # @line L12
                t, i = t[1], i - 1
            else:
                t, i = t[2], i - 1 - w
        return t[0]  # @line L13

    def update(self, i: int, y: Any) -> "SkewList":
        """A new list with item i set to y. Cost: O(min(i, log n))."""
        before, w, t, i, rest = self._find(i)  # @line L14
        path: List[Tuple[Tree, bool]] = []
        h = w
        while i != 0:
            h //= 2
            if i <= h:
                path.append((t, True))
                t, i = t[1], i - 1
            else:
                path.append((t, False))
                t, i = t[2], i - 1 - h
        new: Tree = (y, t[1], t[2])  # @line L15
        for p, went_left in reversed(path):  # @line L16
            new = (p[0], new, p[2]) if went_left else (p[0], p[1], new)
        cells: Cells = (w, new, rest)
        for bw, bt in reversed(before):  # @line L17
            cells = (bw, bt, cells)
        return SkewList(cells, self._n)

    def __iter__(self) -> Iterator[Any]:
        """Items in order, iteratively. Cost: Θ(n)."""
        c = self._cells
        while c is not None:
            stack = [c[1]]
            while stack:
                t = stack.pop()
                yield t[0]
                if t[2] is not None:
                    stack.append(t[2])
                if t[1] is not None:
                    stack.append(t[1])
            c = c[2]

    def to_list(self) -> List[Any]:
        """All items in order. Cost: Θ(n)."""
        return list(self)

    def weights(self) -> List[int]:
        """The tree weights, front first: the skew binary digits of n."""
        out, c = [], self._cells
        while c is not None:
            out.append(c[0])
            c = c[2]
        return out

    def check_invariant(self) -> None:
        """Check that weights are 2^k - 1, grow strictly after the first
        pair, sum to n, and that each tree is complete of its weight.
        Cost: Θ(n)."""
        ws = self.weights()
        assert sum(ws) == self._n
        for k, w in enumerate(ws):
            assert w >= 1 and (w + 1) & w == 0
            if k >= 1:
                assert ws[k - 1] < w or (k == 1 and ws[0] == w)
        c = self._cells
        while c is not None:
            stack = [(c[1], c[0])]
            while stack:
                t, w = stack.pop()
                if w == 1:
                    assert t[1] is None and t[2] is None
                else:
                    assert t[1] is not None and t[2] is not None
                    stack.append((t[1], w // 2))
                    stack.append((t[2], w // 2))
            c = c[2]
test_skew_binary_random_access_list.py 87 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/skew-binary-random-access-list/. The authors are listed in prov/RA7KJS.jsonl.

import random

import pytest

from skew_binary_random_access_list import SkewList


def test_example() -> None:
    expected = {1: [1], 2: [1, 1], 3: [3], 4: [1, 3], 5: [1, 1, 3], 6: [3, 3],
                7: [7], 8: [1, 7], 9: [1, 1, 7], 10: [3, 7]}
    s = SkewList()
    for n in range(1, 11):
        s = s.cons(10 - n)
        assert s.weights() == expected[n]
    assert s.to_list() == list(range(10))
    t2 = s._cells[2][1]
    assert t2[0] == 3 and t2[1][0] == 4 and t2[2][0] == 7 and t2[2][1][0] == 8
    assert s[8] == 8
    u = s.update(8, 80)
    assert u.to_list() == [0, 1, 2, 3, 4, 5, 6, 7, 80, 9]
    assert s[8] == 8
    assert u._cells[1] is s._cells[1]  # the first tree is shared
    assert u._cells[2][1][1] is t2[1]  # the subtree 4, 5, 6 is shared
    tl = s.tail()
    assert tl.weights() == [1, 1, 7] and tl.to_list() == list(range(1, 10))
    for v in (s, u, tl):
        v.check_invariant()


def test_edges() -> None:
    e = SkewList()
    assert e.is_empty() and len(e) == 0 and e.to_list() == []
    with pytest.raises(IndexError):
        e.head()
    with pytest.raises(IndexError):
        e.tail()
    with pytest.raises(IndexError):
        e[0]
    one = e.cons("a")
    assert one.head() == "a" and one.tail().is_empty()
    with pytest.raises(IndexError):
        one[1]
    with pytest.raises(IndexError):
        one.update(-1, "b")
    assert one.update(0, "b").to_list() == ["b"] and one.to_list() == ["a"]


def test_deep() -> None:
    n = 300000
    s = SkewList.from_iterable(range(n))
    assert len(s) == n and s[0] == 0 and s[n - 1] == n - 1 and s[123456] == 123456
    assert len(s.weights()) <= 2 * n.bit_length()
    u = s.update(n - 1, -1)
    assert u[n - 1] == -1 and s[n - 1] == n - 1
    t = s
    for _ in range(1000):
        t = t.tail()
    assert t.head() == 1000 and len(t) == n - 1000
    s.check_invariant()


def test_random_against_model() -> None:
    rng = random.Random(1995)
    for _ in range(500):
        versions = [(SkewList(), [])]
        for _ in range(rng.randint(1, 40)):
            s, m = rng.choice(versions)
            op = rng.randrange(4)
            if op == 0 or not m:
                x = rng.randint(0, 99)
                versions.append((s.cons(x), [x] + m))
            elif op == 1:
                versions.append((s.tail(), m[1:]))
            elif op == 2:
                i, y = rng.randrange(len(m)), rng.randint(0, 99)
                m2 = list(m)
                m2[i] = y
                versions.append((s.update(i, y), m2))
            else:
                i = rng.randrange(len(m))
                assert s[i] == m[i] and s.head() == m[0]
        for s, m in versions:
            assert s.to_list() == m and len(s) == len(m)
            s.check_invariant()

What the next level needs

  • the tests did not run in this build

Read the full page about Skew binary random-access list