Algorithm Search⌘K

Singly linked list in Python

Nodes each holding a value and a pointer to the next node.

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

from typing import Generic, Iterator, TypeVar

T = TypeVar("T")


class Node(Generic[T]):
    """One node of the list: a key and a link to the next node (None is NIL)."""

    __slots__ = ("key", "next")

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


class SinglyLinkedList(Generic[T]):
    """A singly linked list with head and tail pointers and a size count.

    push_front, push_back, pop_front and insert_after cost O(1) in the worst
    case; search and delete cost O(n) in the worst case. Space: n nodes.
    """

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

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

    def __iter__(self) -> Iterator[T]:
        x = self.head
        while x is not None:
            yield x.key
            x = x.next

    def is_empty(self) -> bool:
        return self.n == 0

    def push_front(self, k: T) -> Node[T]:
        """Add k before the first node and return its node. Cost O(1)."""
        x = Node(k)  # @line L1
        x.next = self.head  # @line L2
        self.head = x  # @line L3
        if self.tail is None:  # @line L4
            self.tail = x
        self.n += 1  # @line L5
        return x

    def push_back(self, k: T) -> Node[T]:
        """Add k after the last node and return its node. Cost O(1)."""
        x = Node(k, None)  # @line L6
        if self.tail is None:  # @line L7
            self.head = x
        else:
            self.tail.next = x  # @line L8
        self.tail = x  # @line L9
        self.n += 1  # @line L10
        return x

    def pop_front(self) -> T:
        """Remove the first node and return its key. Precondition: not empty. Cost O(1)."""
        if self.head is None:
            raise IndexError("pop_front from an empty list")
        x = self.head  # @line L11
        self.head = x.next  # @line L12
        if self.head is None:  # @line L13
            self.tail = None
        self.n -= 1  # @line L14
        x.next = None
        return x.key  # @line L15

    def front(self) -> T:
        """Return the first key. Precondition: not empty. Cost O(1)."""
        if self.head is None:
            raise IndexError("front of an empty list")
        return self.head.key

    def search(self, k: T) -> Node[T] | None:
        """Return the first node with key k, or None. Cost O(n) in the worst case."""
        x = self.head  # @line S1
        while x is not None and x.key != k:  # @line S2
            x = x.next  # @line S3
        return x  # @line S4

    def insert_after(self, y: Node[T], k: T) -> Node[T]:
        """Add k right after the node y, which must be in this list. Cost O(1)."""
        x = Node(k)  # @line A1
        x.next = y.next  # @line A2
        y.next = x  # @line A3
        if self.tail is y:  # @line A4
            self.tail = x
        self.n += 1  # @line A5
        return x

    def delete(self, k: T) -> bool:
        """Remove the first node with key k. Return True if one was found. Cost O(n)."""
        prev: Node[T] | None = None  # @line D1
        x = self.head
        while x is not None and x.key != k:  # @line D2
            prev, x = x, x.next  # @line D3
        if x is None:  # @line D4
            return False
        if prev is None:  # @line D5
            self.head = x.next
        else:
            prev.next = x.next
        if self.tail is x:  # @line D6
            self.tail = prev
        self.n -= 1  # @line D7
        x.next = None
        return True  # @line D8

    def check_invariant(self) -> None:
        """Raise AssertionError unless the representation invariant holds."""
        seen: set[int] = set()
        last: Node[T] | None = None
        x = self.head
        while x is not None:
            assert id(x) not in seen, "the list has a cycle"
            seen.add(id(x))
            last = x
            x = x.next
        assert len(seen) == self.n, "n does not match the number of nodes"
        assert self.tail is last, "tail is not the last node"
test_singly_linked_list.py 95 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/singly-linked-list/. The authors are listed in prov/QXYA7A.jsonl.

import random

import pytest

from singly_linked_list import SinglyLinkedList


def test_example() -> None:
    lst: SinglyLinkedList[int] = SinglyLinkedList()
    lst.push_front(3)
    lst.push_back(7)
    lst.push_front(1)
    lst.push_back(9)
    assert list(lst) == [1, 3, 7, 9]
    node = lst.search(7)
    assert node is not None and node.key == 7
    lst.insert_after(node, 4)
    assert list(lst) == [1, 3, 7, 4, 9]
    assert lst.delete(3)
    assert list(lst) == [1, 7, 4, 9]
    assert lst.pop_front() == 1
    assert lst.delete(9)
    assert list(lst) == [7, 4]
    assert lst.tail is not None and lst.tail.key == 4
    assert not lst.delete(5)
    lst.check_invariant()


def test_edges() -> None:
    lst: SinglyLinkedList[int] = SinglyLinkedList()
    assert lst.is_empty() and len(lst) == 0
    assert lst.search(1) is None
    assert not lst.delete(1)
    with pytest.raises(IndexError):
        lst.pop_front()
    lst.push_back(5)
    assert lst.head is lst.tail
    assert lst.pop_front() == 5
    assert lst.head is None and lst.tail is None
    lst.check_invariant()
    # delete the only node: tail must become None
    lst.push_front(2)
    assert lst.delete(2)
    assert lst.tail is None
    lst.check_invariant()
    # duplicates: delete removes only the first one
    for k in (4, 4, 4):
        lst.push_back(k)
    assert lst.delete(4)
    assert list(lst) == [4, 4]
    # insert after the tail moves the tail
    t = lst.tail
    assert t is not None
    lst.insert_after(t, 6)
    assert lst.tail is not None and lst.tail.key == 6
    lst.push_back(8)
    assert list(lst) == [4, 4, 6, 8]
    lst.check_invariant()


def test_random_against_list() -> None:
    rng = random.Random(4127)
    for _ in range(600):
        lst: SinglyLinkedList[int] = SinglyLinkedList()
        model: list[int] = []
        for _ in range(rng.randint(0, 40)):
            op = rng.randrange(6)
            k = rng.randint(0, 6)
            if op == 0:
                lst.push_front(k)
                model.insert(0, k)
            elif op == 1:
                lst.push_back(k)
                model.append(k)
            elif op == 2:
                if model:
                    assert lst.pop_front() == model.pop(0)
            elif op == 3:
                node = lst.search(k)
                assert (node is not None) == (k in model)
                if node is not None:
                    lst.insert_after(node, k + 10)
                    model.insert(model.index(k) + 1, k + 10)
            elif op == 4:
                found = k in model
                assert lst.delete(k) == found
                if found:
                    model.remove(k)
            else:
                assert len(lst) == len(model)
            lst.check_invariant()
            assert list(lst) == model

What the next level needs

  • the tests did not run in this build

Read the full page about Singly linked list