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