Algorithm Search⌘K

Linked list in Python

A sequence of nodes where each node stores a value and a pointer to the next, allowing constant time insertion and deletion at known positions.

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

from typing import Generic, Iterator, TypeVar

T = TypeVar("T")


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

    __slots__ = ("key", "next")

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


class LinkedList(Generic[T]):
    """A singly linked list with a head pointer and a count.

    push_front, insert_after, delete_front and delete_after cost O(1).
    search costs O(n) and node_at(i) costs O(i + 1). Space: n nodes.
    """

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

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

    def push_front(self, x: T) -> Node[T]:
        """Insert x before the first item and return its node. Cost O(1)."""
        v = Node(x)  # @line L1
        v.next = self.head  # @line L2
        self.head = v  # @line L3
        self.n += 1
        return v

    def insert_after(self, p: Node[T], x: T) -> Node[T]:
        """Insert x just after node p and return its node. Precondition: p is in the list. Cost O(1)."""
        v = Node(x)  # @line L4
        v.next = p.next  # @line L5
        p.next = v  # @line L6
        self.n += 1
        return v

    def delete_front(self) -> T:
        """Remove and return the first item. Precondition: the list is not empty. Cost O(1)."""
        v = self.head  # @line L7
        if v is None:
            raise IndexError("delete from an empty list")
        self.head = v.next  # @line L8
        self.n -= 1
        return v.key  # @line L9

    def delete_after(self, p: Node[T]) -> T:
        """Remove and return the item after node p. Precondition: p.next is a node. Cost O(1)."""
        v = p.next  # @line L10
        if v is None:
            raise IndexError("no node after p")
        p.next = v.next  # @line L11
        self.n -= 1
        return v.key  # @line L12

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

    def node_at(self, i: int) -> Node[T]:
        """Return the node at position i. Precondition: 0 <= i < n. Cost O(i + 1)."""
        if not 0 <= i < self.n:
            raise IndexError("position out of range")
        p = self.head  # @line S5
        for _ in range(i):  # @line S6
            p = p.next  # type: ignore[union-attr]
        return p  # type: ignore[return-value]  # @line S7

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

    def check_invariant(self) -> None:
        seen: set[int] = set()
        p = self.head
        while p is not None:
            assert id(p) not in seen, "the chain has a cycle"
            seen.add(id(p))
            p = p.next
        assert len(seen) == self.n
test_linked_list.py 73 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/linked-list/. The authors are listed in prov/8R26VK.jsonl.

import random

import pytest

from linked_list import LinkedList


def test_page_example() -> None:
    a: LinkedList[int] = LinkedList()
    a.push_front(3)
    seven = a.push_front(7)
    assert list(a) == [7, 3]
    five = a.insert_after(seven, 5)
    assert list(a) == [7, 5, 3]
    assert a.search(5) is five
    assert a.search(4) is None
    assert a.delete_after(five) == 3
    assert list(a) == [7, 5]
    assert a.delete_front() == 7
    assert list(a) == [5]
    a.check_invariant()


def test_edges() -> None:
    a: LinkedList[int] = LinkedList()
    with pytest.raises(IndexError):
        a.delete_front()
    with pytest.raises(IndexError):
        a.node_at(0)
    v = a.push_front(1)
    with pytest.raises(IndexError):
        a.delete_after(v)  # the last node has no successor
    a.insert_after(v, 1)
    assert a.search(1) is v  # the first of two equal keys
    assert a.node_at(1).next is None
    assert a.delete_front() == 1
    assert a.delete_front() == 1
    assert a.head is None and len(a) == 0


def test_random_against_list() -> None:
    rng = random.Random(8261)
    for _ in range(500):
        a: LinkedList[int] = LinkedList()
        model: list[int] = []
        for _ in range(40):
            op = rng.randrange(4)
            x = rng.randint(0, 5)
            if op == 0:
                i = rng.randint(0, len(model))
                if i == 0:
                    a.push_front(x)
                else:
                    a.insert_after(a.node_at(i - 1), x)
                model.insert(i, x)
            elif op == 1 and model:
                i = rng.randrange(len(model))
                got = a.delete_front() if i == 0 else a.delete_after(a.node_at(i - 1))
                assert got == model.pop(i)
            elif op == 2 and model:
                i = rng.randrange(len(model))
                assert a.node_at(i).key == model[i]
            else:
                p = a.search(x)
                if x in model:
                    assert p is a.node_at(model.index(x))
                else:
                    assert p is None
            a.check_invariant()
            assert list(a) == model

What the next level needs

  • the tests did not run in this build

Read the full page about Linked list