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