Pairing heap in Python
Simple self-adjusting heap whose practical performance rivals Fibonacci heaps.
- File
pairing_heap.py, 186 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/pairing-heap/. The authors are listed in prov/G7BE09.jsonl.
from typing import Any, Generic, Iterator, Protocol, TypeVar
class Ordered(Protocol):
def __lt__(self, other: Any, /) -> bool: ...
K = TypeVar("K", bound=Ordered)
class Node(Generic[K]):
"""A node of the tree: key, first child, next sibling, and prev.
prev is the parent when the node is a first child, else the left sibling.
"""
__slots__ = ("key", "child", "sibling", "prev")
def __init__(self, key: K) -> None:
self.key = key
self.child: Node[K] | None = None
self.sibling: Node[K] | None = None
self.prev: Node[K] | None = None
def link(a: "Node[K] | None", b: "Node[K] | None") -> "Node[K] | None":
"""Link two heap-ordered trees whose roots have no siblings; return the new root."""
if a is None:
return b
if b is None:
return a
if b.key < a.key: # @line L2
a, b = b, a
b.sibling = a.child # @line L3
if a.child is not None:
a.child.prev = b
b.prev = a
a.child = b
a.sibling = None
a.prev = None
return a # @line L4
class PairingHeap(Generic[K]):
"""A min-oriented pairing heap with handles.
find_min is O(1). insert and meld do one link, O(1) actual time.
delete_min does two-pass pairing. All operations take O(log n) amortized
time; decrease_key takes O(1) actual time.
"""
def __init__(self) -> None:
self.root: Node[K] | None = None
self.n = 0
def __len__(self) -> int:
return self.n
def is_empty(self) -> bool:
return self.root is None
def find_min(self) -> K:
if self.root is None:
raise IndexError("find_min on an empty heap")
return self.root.key
def insert(self, key: K) -> Node[K]:
"""Insert key and return its handle."""
x: Node[K] = Node(key)
self.root = link(self.root, x) # @line L5
self.n += 1
return x
def meld(self, other: "PairingHeap[K]") -> None:
"""Move all items of other into self; other becomes empty."""
if other is self:
return
self.root = link(self.root, other.root) # @line L1
self.n += other.n
other.root, other.n = None, 0
def delete_min(self) -> K:
"""Remove and return a smallest key. O(log n) amortized."""
r = self.root
if r is None:
raise IndexError("delete_min on an empty heap")
self.root = merge_pairs(r.child) # @line L6
if self.root is not None:
self.root.prev = None
self.n -= 1
r.child = None
return r.key # @line L7
def decrease_key(self, x: Node[K], key: K) -> None:
"""Lower the key of the handle x to key. O(1) actual time."""
if x.key < key:
raise ValueError("the new key is larger than the old key")
x.key = key
if x is self.root:
return
cut(x)
self.root = link(self.root, x)
def delete(self, x: Node[K]) -> K:
"""Remove the item of the handle x and return its key."""
if x is self.root:
return self.delete_min()
cut(x)
rest = merge_pairs(x.child)
x.child = None
if rest is not None:
rest.prev = None
self.root = link(self.root, rest)
self.n -= 1
return x.key
def check_invariant(self) -> None:
"""Check heap order, the prev pointers and the size."""
count = 0
if self.root is not None:
assert self.root.prev is None and self.root.sibling is None
stack = [self.root]
while stack:
v = stack.pop()
count += 1
prev = v
c = v.child
while c is not None:
assert c.prev is prev
assert not c.key < v.key
stack.append(c)
prev = c
c = c.sibling
assert count == self.n
def keys(self) -> Iterator[K]:
if self.root is None:
return
stack = [self.root]
while stack:
v = stack.pop()
yield v.key
c = v.child
while c is not None:
stack.append(c)
c = c.sibling
def cut(x: "Node[K]") -> None:
"""Detach the subtree of x (x is not a root) from its parent's child list."""
p = x.prev
assert p is not None
if p.child is x:
p.child = x.sibling
else:
p.sibling = x.sibling
if x.sibling is not None:
x.sibling.prev = p
x.sibling = None
x.prev = None
def merge_pairs(first: "Node[K] | None") -> "Node[K] | None":
"""Two-pass pairing of the list of trees that starts at first."""
pairs: list[Node[K]] = []
x = first
while x is not None: # @line L8: first pass, left to right
a = x
b = a.sibling
if b is None:
a.prev = None
pairs.append(a)
break
x = b.sibling
a.sibling = b.sibling = None
a.prev = b.prev = None
t = link(a, b) # @line L9
assert t is not None
pairs.append(t)
root: Node[K] | None = None
for t in reversed(pairs): # @line L10: second pass, right to left
root = link(t, root) # @line L11
return root # @line L12
test_pairing_heap.py 121 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/pairing-heap/. The authors are listed in prov/G7BE09.jsonl.
import random
import pytest
from pairing_heap import Node, PairingHeap
def shape(v: "Node[int] | None") -> object:
"""The tree as (key, [children]) for comparing with the page."""
if v is None:
return None
kids = []
c = v.child
while c is not None:
kids.append(shape(c))
c = c.sibling
return (v.key, kids)
def test_example() -> None:
h: PairingHeap[int] = PairingHeap()
for k in [7, 3, 9, 5, 8, 2, 6, 4]:
h.insert(k)
h.check_invariant()
assert shape(h.root) == (2, [(4, []), (6, []), (3, [(8, []), (5, []), (9, []), (7, [])])])
assert h.delete_min() == 2
assert shape(h.root) == (3, [(4, [(6, [])]), (8, []), (5, []), (9, []), (7, [])])
assert h.delete_min() == 3
assert shape(h.root) == (4, [(5, [(7, []), (9, [])]), (8, []), (6, [])])
h.check_invariant()
def test_edges() -> None:
h: PairingHeap[int] = PairingHeap()
with pytest.raises(IndexError):
h.delete_min()
with pytest.raises(IndexError):
h.find_min()
x = h.insert(1)
assert h.delete(x) == 1 and h.is_empty()
for _ in range(5):
h.insert(4)
assert [h.delete_min() for _ in range(5)] == [4] * 5
y = h.insert(10)
with pytest.raises(ValueError):
h.decrease_key(y, 11)
def test_meld() -> None:
a: PairingHeap[int] = PairingHeap()
b: PairingHeap[int] = PairingHeap()
for k in [5, 1, 9]:
a.insert(k)
for k in [4, 0, 7]:
b.insert(k)
a.meld(b)
assert len(a) == 6 and b.is_empty()
a.check_invariant()
assert [a.delete_min() for _ in range(6)] == [0, 1, 4, 5, 7, 9]
def test_sorted_input_is_long() -> None:
h: PairingHeap[int] = PairingHeap()
for k in range(20000):
h.insert(k) # one root with 19999 children: no recursion depth problem
assert [h.delete_min() for _ in range(20000)] == list(range(20000))
def test_random_against_model() -> None:
rng = random.Random(1986)
for _ in range(500):
h: PairingHeap[int] = PairingHeap()
model: dict[int, int] = {}
handles: dict[int, Node[int]] = {}
uid = 0
for _ in range(rng.randint(1, 60)):
op = rng.random()
if op < 0.45 or not model:
k = rng.randint(-50, 50)
handles[uid] = h.insert(k)
model[uid] = k
uid += 1
elif op < 0.65:
got = h.delete_min()
want = min(model.values())
assert got == want
# remove one handle with that key that is no longer in the heap
live = {id(v) for v in iter_nodes(h)}
gone = [u for u in model if id(handles[u]) not in live]
assert len(gone) == 1
del model[gone[0]]
elif op < 0.85:
u = rng.choice(list(model))
k = model[u] - rng.randint(0, 30)
h.decrease_key(handles[u], k)
model[u] = k
else:
u = rng.choice(list(model))
assert h.delete(handles[u]) == model[u]
del model[u]
h.check_invariant()
assert len(h) == len(model)
assert sorted(h.keys()) == sorted(model.values())
if model:
assert h.find_min() == min(model.values())
def iter_nodes(h: PairingHeap[int]) -> list[Node[int]]:
out = []
stack = [h.root] if h.root is not None else []
while stack:
v = stack.pop()
out.append(v)
c = v.child
while c is not None:
stack.append(c)
c = c.sibling
return out
What the next level needs
- the tests did not run in this build