Algorithm Search⌘K

Real-time queue in Python

A persistent queue with worst-case O(1) operations obtained by incrementally reversing the front list.

File
real_time_queue.py, 164 lines
Level
I0 not checked yet
Toolchain
Python 3.14, pytest 9
Test framework
pytest
Run the tests
python3 -m pytest
real_time_queue.pyRawGitHub
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/real-time-queue/. The authors are listed in prov/X9W66J.jsonl.

"""Okasaki's real-time queue: a persistent FIFO queue with O(1) worst case.

The queue is a lazy front stream f, a rear list r and a schedule s, which
is a suffix of f that has not been evaluated yet. Invariant: |s| = |f| - |r|.
Each push and pop forces one cell of s, so the rotation f ++ reverse(r) is
evaluated one step at a time, and no operation does more than O(1) work.
Memoized suspensions make this hold even when an old version is reused.
"""

from typing import Callable, Generic, Iterator, Optional, Tuple, TypeVar

T = TypeVar("T")

# Number of suspended steps evaluated so far, for tests of the O(1) bound.
STEPS = 0


class _Stream(Generic[T]):
    """A memoized lazy list: forcing gives None (empty) or (head, tail)."""

    __slots__ = ("_thunk", "_cell", "_done")

    def __init__(self, thunk: "Optional[Callable[[], Optional[Tuple[T, _Stream[T]]]]]" = None) -> None:
        self._thunk = thunk
        self._cell: "Optional[Tuple[T, _Stream[T]]]" = None
        self._done = thunk is None

    def force(self) -> "Optional[Tuple[T, _Stream[T]]]":
        if not self._done:
            global STEPS
            STEPS += 1
            assert self._thunk is not None
            self._cell = self._thunk()
            self._thunk = None
            self._done = True
        return self._cell

    def is_forced(self) -> bool:
        return self._done


_EMPTY: "_Stream[object]" = _Stream()


class _Cons(Generic[T]):
    """An immutable cell of the rear list."""

    __slots__ = ("head", "next")

    def __init__(self, head: T, next: "Optional[_Cons[T]]") -> None:
        self.head = head
        self.next = next


def _evaluated(head: T, tail: "_Stream[T]") -> "_Stream[T]":
    s: _Stream[T] = _Stream()
    s._cell = (head, tail)
    return s


def _rotate(f: "_Stream[T]", r: "_Cons[T]", a: "_Stream[T]") -> "_Stream[T]":
    """The stream f ++ reverse(r) ++ a, built lazily; needs |r| = |f| + 1."""

    def step() -> "Optional[Tuple[T, _Stream[T]]]":
        cell = f.force()  # @line L1
        if cell is None:
            return (r.head, a)
        assert r.next is not None
        return (cell[0], _rotate(cell[1], r.next, _evaluated(r.head, a)))

    return _Stream(step)


class RealTimeQueue(Generic[T]):
    """An immutable FIFO queue. Every operation is O(1) in the worst case."""

    __slots__ = ("_f", "_r", "_s", "_lenf", "_lenr", "_lens")

    def __init__(self) -> None:
        """The empty queue. Cost: O(1)."""
        self._f: _Stream[T] = _EMPTY  # type: ignore[assignment]
        self._r: Optional[_Cons[T]] = None
        self._s: _Stream[T] = _EMPTY  # type: ignore[assignment]
        self._lenf = 0
        self._lenr = 0
        self._lens = 0

    @staticmethod
    def _exec(
        f: "_Stream[T]", lenf: int, r: "Optional[_Cons[T]]", lenr: int, s: "_Stream[T]", lens: int
    ) -> "RealTimeQueue[T]":
        q: RealTimeQueue[T] = RealTimeQueue.__new__(RealTimeQueue)
        cell = s.force()
        if cell is not None:  # @line L2
            q._f, q._r, q._s = f, r, cell[1]
            q._lenf, q._lenr, q._lens = lenf, lenr, lens - 1
            return q
        assert r is not None
        f2 = _rotate(f, r, _EMPTY)  # type: ignore[arg-type]  # @line L3
        q._f, q._r, q._s = f2, None, f2
        q._lenf, q._lenr, q._lens = lenf + lenr, 0, lenf + lenr
        return q

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

    def is_empty(self) -> bool:
        """Whether the queue is empty. Cost: O(1)."""
        return self._lenf == 0

    def push(self, x: T) -> "RealTimeQueue[T]":
        """The queue with x added at the back. Cost: O(1) worst case."""
        return RealTimeQueue._exec(  # @line L4
            self._f, self._lenf, _Cons(x, self._r), self._lenr + 1, self._s, self._lens
        )

    def front(self) -> T:
        """The item at the front. Cost: O(1) worst case."""
        cell = self._f.force()
        if cell is None:  # @line L5
            raise IndexError("front of empty queue")
        return cell[0]

    def pop(self) -> "Tuple[T, RealTimeQueue[T]]":
        """The front item and the queue without it. Cost: O(1) worst case."""
        cell = self._f.force()
        if cell is None:  # @line L6
            raise IndexError("pop from empty queue")
        rest = RealTimeQueue._exec(  # @line L7
            cell[1], self._lenf - 1, self._r, self._lenr, self._s, self._lens
        )
        return cell[0], rest

    def __iter__(self) -> Iterator[T]:
        """Items from front to back, by repeated pop. Cost: Θ(n)."""
        q = self
        while not q.is_empty():
            x, q = q.pop()
            yield x

    def check_invariant(self) -> None:
        """Check |s| = |f| - |r|, that s is the unevaluated suffix of f, and |r|.

        The first |f| - |s| cells of f must be evaluated, and the stream after
        them must be s itself. Walks only evaluated cells. Cost: Θ(n).
        """
        assert self._lens == self._lenf - self._lenr >= 0
        n = 0
        c = self._r
        while c is not None:
            n += 1
            c = c.next
        assert n == self._lenr
        st = self._f
        for _ in range(self._lenf - self._lens):
            assert st.is_forced()
            cell = st.force()
            assert cell is not None
            st = cell[1]
        assert st is self._s
test_real_time_queue.py 102 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/real-time-queue/. The authors are listed in prov/X9W66J.jsonl.

import random

import pytest

import real_time_queue as rtq
from real_time_queue import RealTimeQueue


def _state(q):  # type: ignore[no-untyped-def]
    return (q._lenf, q._lenr, q._lens)


def test_example() -> None:
    q0: RealTimeQueue[int] = RealTimeQueue()
    q1 = q0.push(1)
    assert _state(q1) == (1, 0, 1)
    q2 = q1.push(2)
    assert _state(q2) == (1, 1, 0)
    q3 = q2.push(3)
    assert _state(q3) == (3, 0, 3)
    q4 = q3.push(4)
    assert _state(q4) == (3, 1, 2)
    q5 = q4.push(5)
    assert _state(q5) == (3, 2, 1)
    x, q6 = q5.pop()
    assert x == 1 and _state(q6) == (2, 2, 0)
    q7 = q6.push(6)
    assert _state(q7) == (5, 0, 5)
    assert list(q7) == [2, 3, 4, 5, 6]
    assert list(q5) == [1, 2, 3, 4, 5]
    assert list(q2) == [1, 2]
    for q in (q0, q1, q2, q3, q4, q5, q6, q7):
        q.check_invariant()


def test_edges() -> None:
    e: RealTimeQueue[str] = RealTimeQueue()
    assert e.is_empty() and len(e) == 0 and list(e) == []
    with pytest.raises(IndexError):
        e.pop()
    with pytest.raises(IndexError):
        e.front()
    one = e.push("a")
    assert one.front() == "a" and len(one) == 1
    x, back = one.pop()
    assert x == "a" and back.is_empty() and not one.is_empty()
    back.check_invariant()


def test_deep_and_worst_case_steps() -> None:
    q: RealTimeQueue[int] = RealTimeQueue()
    worst = 0
    for i in range(100000):
        before = rtq.STEPS
        q = q.push(i)
        worst = max(worst, rtq.STEPS - before)
    assert len(q) == 100000
    snapshot = q
    out = []
    for _ in range(50000):
        before = rtq.STEPS
        x, q = q.pop()
        worst = max(worst, rtq.STEPS - before)
        out.append(x)
    assert out == list(range(50000))
    # Reuse an old version many times: each pop is still O(1).
    for _ in range(1000):
        before = rtq.STEPS
        x, _ = snapshot.pop()
        worst = max(worst, rtq.STEPS - before)
        assert x == 0
    assert worst <= 3
    q.check_invariant()
    snapshot.check_invariant()


def test_random_version_tree() -> None:
    rng = random.Random(1999)
    for _ in range(500):
        versions = [RealTimeQueue()]
        model: "list[list[int]]" = [[]]
        for _ in range(rng.randint(1, 60)):
            v = rng.randrange(len(versions))
            if rng.random() < 0.6 or not model[v]:
                x = rng.randint(0, 99)
                versions.append(versions[v].push(x))
                model.append(model[v] + [x])
            else:
                x, q = versions[v].pop()
                assert x == model[v][0]
                versions.append(q)
                model.append(model[v][1:])
            versions[-1].check_invariant()
        for q, m in zip(versions, model):
            assert len(q) == len(m)
            assert list(q) == m
            if m:
                assert q.front() == m[0]
            q.check_invariant()

What the next level needs

  • the tests did not run in this build

Read the full page about Real-time queue