Algorithm Search⌘K

Doubly linked list in Python

Nodes with pointers to both neighbours, so a node can be unlinked in O(1).

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

from typing import Generic, Iterator, TypeVar

T = TypeVar("T")


class Node(Generic[T]):
    """One cell: an item and pointers to the next and previous nodes."""

    __slots__ = ("key", "next", "prev")

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


class DoublyLinkedList(Generic[T]):
    """A doubly linked list closed into a ring by one sentinel node.

    insert_after, insert_before, delete and the four end operations cost
    O(1) worst case. search costs O(n). Space: n + 1 nodes.
    Precondition: a node passed as a handle belongs to this list.
    """

    def __init__(self) -> None:
        self.nil: Node[T] = Node(None)  # type: ignore[arg-type]
        self.n = 0

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

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

    def insert_after(self, p: Node[T], x: T) -> Node[T]:
        """Insert x after node p (p may be the sentinel). Cost O(1)."""
        v = Node(x)  # @line L1
        v.next, v.prev = p.next, p  # @line L2
        p.next.prev = v  # @line L3
        p.next = v  # @line L4
        self.n += 1
        return v

    def delete(self, v: Node[T]) -> T:
        """Unlink node v, which is not the sentinel, and return its key. Cost O(1)."""
        if v is self.nil:
            raise IndexError("cannot delete the sentinel")
        v.prev.next = v.next  # @line L5
        v.next.prev = v.prev  # @line L6
        self.n -= 1
        return v.key  # @line L7

    def search(self, k: T) -> Node[T] | None:
        """Return the first node with key k, or None. Cost O(n)."""
        x = self.nil.next
        while x is not self.nil and x.key != k:
            x = x.next
        return None if x is self.nil else x

    def insert_before(self, p: Node[T], x: T) -> Node[T]:
        return self.insert_after(p.prev, x)

    def push_front(self, x: T) -> Node[T]:
        return self.insert_after(self.nil, x)

    def push_back(self, x: T) -> Node[T]:
        return self.insert_after(self.nil.prev, x)

    def pop_front(self) -> T:
        if self.n == 0:
            raise IndexError("pop from empty list")
        return self.delete(self.nil.next)

    def pop_back(self) -> T:
        if self.n == 0:
            raise IndexError("pop from empty list")
        return self.delete(self.nil.prev)

    def check_invariant(self) -> None:
        """Check that next and prev form the same ring of n + 1 distinct nodes."""
        seen = {id(self.nil)}
        x = self.nil
        for _ in range(self.n):
            y = x.next
            assert y.prev is x, "prev does not match next"
            assert id(y) not in seen, "node repeated"
            seen.add(id(y))
            x = y
        assert x.next is self.nil and self.nil.prev is x, "ring not closed"
test_doubly_linked_list.py 95 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/doubly-linked-list/. The authors are listed in prov/B9V487.jsonl.

import random

import pytest

from doubly_linked_list import DoublyLinkedList


def test_page_example() -> None:
    L: DoublyLinkedList[int] = DoublyLinkedList()
    n1 = L.push_back(1)
    n2 = L.push_back(2)
    L.push_back(3)
    n5 = L.insert_after(n1, 5)
    assert list(L) == [1, 5, 2, 3]
    assert L.delete(n2) == 2
    assert list(L) == [1, 5, 3]
    L.push_front(9)
    assert L.pop_back() == 3
    assert list(L) == [9, 1, 5]
    assert L.search(5) is n5
    assert L.search(4) is None
    assert L.nil.next.key == 9 and L.nil.prev is n5
    L.check_invariant()


def test_empty() -> None:
    L: DoublyLinkedList[int] = DoublyLinkedList()
    L.check_invariant()
    assert len(L) == 0 and list(L) == []
    assert L.search(1) is None
    with pytest.raises(IndexError):
        L.pop_front()
    with pytest.raises(IndexError):
        L.pop_back()
    with pytest.raises(IndexError):
        L.delete(L.nil)


def test_single_item() -> None:
    L: DoublyLinkedList[str] = DoublyLinkedList()
    v = L.push_front("a")
    L.check_invariant()
    assert L.delete(v) == "a"
    L.check_invariant()
    assert list(L) == []
    L.push_back("b")
    assert L.pop_front() == "b"
    L.check_invariant()


def test_insert_before_and_duplicates() -> None:
    L: DoublyLinkedList[int] = DoublyLinkedList()
    a = L.push_back(7)
    L.insert_before(a, 7)
    first = L.search(7)
    assert first is not a and first is L.nil.next
    L.check_invariant()


def test_random_against_list() -> None:
    rng = random.Random(20261104)
    for _ in range(600):
        L: DoublyLinkedList[int] = DoublyLinkedList()
        model: list[int] = []
        nodes: list = []
        for _ in range(rng.randrange(1, 40)):
            op = rng.randrange(7)
            x = rng.randrange(10)
            if op == 0:
                nodes.insert(0, L.push_front(x)); model.insert(0, x)
            elif op == 1:
                nodes.append(L.push_back(x)); model.append(x)
            elif op == 2 and model:
                i = rng.randrange(len(model))
                nodes.insert(i + 1, L.insert_after(nodes[i], x)); model.insert(i + 1, x)
            elif op == 3 and model:
                i = rng.randrange(len(model))
                nodes.insert(i, L.insert_before(nodes[i], x)); model.insert(i, x)
            elif op == 4 and model:
                i = rng.randrange(len(model))
                assert L.delete(nodes.pop(i)) == model.pop(i)
            elif op == 5 and model:
                assert L.pop_front() == model.pop(0); nodes.pop(0)
            elif op == 6 and model:
                assert L.pop_back() == model.pop(); nodes.pop()
            L.check_invariant()
            assert list(L) == model and len(L) == len(model)
            got = L.search(x)
            if x in model:
                assert got is nodes[model.index(x)]
            else:
                assert got is None

What the next level needs

  • the tests did not run in this build

Read the full page about Doubly linked list