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
# 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