Algorithm Search⌘K

Circular linked list in Python

List whose last node points back to the first, suited to round-robin iteration.

File
circular_linked_list.py, 105 lines
Level
I0 not checked yet
Toolchain
Python 3.14, pytest 9
Test framework
pytest
Run the tests
python3 -m pytest
circular_linked_list.pyRawGitHub
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/circular-linked-list/. The authors are listed in prov/N4ZP5Y.jsonl.

from typing import Generic, Iterator, TypeVar

T = TypeVar("T")


class Node(Generic[T]):
    """One cell of the ring: an item and a pointer to the next node."""

    __slots__ = ("key", "next")

    def __init__(self, key: T) -> None:
        self.key = key
        self.next: "Node[T]" = self


class CircularLinkedList(Generic[T]):
    """A singly linked ring with one pointer to the last node.

    push_front, push_back, pop_front and rotate cost O(1) worst case.
    Space: n nodes. pop_front and rotate need a non-empty list.
    """

    def __init__(self) -> None:
        self.last: Node[T] | None = None
        self.n = 0

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

    def __iter__(self) -> Iterator[T]:
        if self.last is None:
            return
        x = self.last.next
        for _ in range(self.n):
            yield x.key
            x = x.next

    def push_front(self, x: T) -> None:
        """Add x at the front. Cost O(1)."""
        v = Node(x)  # @line L1
        if self.last is None:  # @line L2
            v.next = v
            self.last = v
        else:
            v.next, self.last.next = self.last.next, v  # @line L3
        self.n += 1

    def push_back(self, x: T) -> None:
        """Add x at the back. Cost O(1)."""
        self.push_front(x)  # @line L4
        assert self.last is not None
        self.last = self.last.next  # @line L5

    def pop_front(self) -> T:
        """Remove and return the front item. Cost O(1)."""
        if self.last is None:
            raise IndexError("pop from empty list")
        v = self.last.next  # @line L6
        if v is self.last:  # @line L7
            self.last = None
        else:
            self.last.next = v.next
        self.n -= 1
        return v.key  # @line L8

    def rotate(self) -> None:
        """Move the front item to the back. Cost O(1)."""
        if self.last is None:
            raise IndexError("rotate of empty list")
        self.last = self.last.next  # @line L9

    def front(self) -> T:
        if self.last is None:
            raise IndexError("front of empty list")
        return self.last.next.key

    def check_invariant(self) -> None:
        """Check that the nodes form one ring of n distinct nodes."""
        if self.n == 0:
            assert self.last is None
            return
        assert self.last is not None
        seen = set()
        x = self.last
        for _ in range(self.n):
            x = x.next
            assert id(x) not in seen, "ring shorter than n"
            seen.add(id(x))
        assert x is self.last, "ring longer than n or broken"


def josephus(n: int, k: int) -> list[int]:
    """Return the order in which people 1..n leave when every kth leaves. Cost O(nk)."""
    ring: CircularLinkedList[int] = CircularLinkedList()
    for i in range(1, n + 1):
        ring.push_back(i)
    order = []
    while len(ring) > 0:
        for _ in range(k - 1):
            ring.rotate()
        order.append(ring.pop_front())
    return order
test_circular_linked_list.py 79 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/circular-linked-list/. The authors are listed in prov/N4ZP5Y.jsonl.

import random

import pytest

from circular_linked_list import CircularLinkedList, josephus


def test_page_example() -> None:
    L: CircularLinkedList[int] = CircularLinkedList()
    for x in (1, 2, 3):
        L.push_back(x)
    L.push_front(0)
    assert list(L) == [0, 1, 2, 3] and L.last.key == 3
    L.rotate()
    assert list(L) == [1, 2, 3, 0] and L.last.key == 0
    assert L.pop_front() == 1
    L.rotate()
    assert list(L) == [3, 0, 2] and L.last.key == 2
    assert L.pop_front() == 3
    assert list(L) == [0, 2]
    L.check_invariant()


def test_josephus_example() -> None:
    assert josephus(7, 3) == [3, 6, 2, 7, 5, 1, 4]
    assert josephus(1, 5) == [1]
    assert josephus(5, 1) == [1, 2, 3, 4, 5]


def test_empty_and_single() -> None:
    L: CircularLinkedList[str] = CircularLinkedList()
    L.check_invariant()
    with pytest.raises(IndexError):
        L.pop_front()
    with pytest.raises(IndexError):
        L.rotate()
    L.push_back("a")
    assert L.last is not None and L.last.next is L.last
    L.rotate()
    assert list(L) == ["a"]
    assert L.pop_front() == "a"
    assert L.last is None
    L.check_invariant()


def test_random_against_list() -> None:
    rng = random.Random(4242)
    for _ in range(600):
        L: CircularLinkedList[int] = CircularLinkedList()
        model: list[int] = []
        for _ in range(rng.randrange(1, 40)):
            op = rng.randrange(4)
            x = rng.randrange(100)
            if op == 0:
                L.push_front(x); model.insert(0, x)
            elif op == 1:
                L.push_back(x); model.append(x)
            elif op == 2 and model:
                assert L.pop_front() == model.pop(0)
            elif op == 3 and model:
                L.rotate(); model.append(model.pop(0))
            L.check_invariant()
            assert list(L) == model
            if model:
                assert L.front() == model[0]


def test_josephus_random_against_list() -> None:
    rng = random.Random(7)
    for _ in range(500):
        n, k = rng.randrange(1, 15), rng.randrange(1, 6)
        people, i, order = list(range(1, n + 1)), 0, []
        while people:
            i = (i + k - 1) % len(people)
            order.append(people.pop(i))
        assert josephus(n, k) == order

What the next level needs

  • the tests did not run in this build

Read the full page about Circular linked list