Hollow heap in Python
Simple heap with O(1) worst-case insert and decrease-key and O(log n) amortized delete.
- File
hollow_heap.py, 195 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/hollow-heap/. The authors are listed in prov/AQJK5W.jsonl.
from typing import Any, Generic, Optional, Protocol, TypeVar
class Ordered(Protocol):
def __lt__(self, other: Any, /) -> bool: ...
K = TypeVar("K", bound=Ordered)
V = TypeVar("V")
class Item(Generic[K, V]):
"""A handle to a value in a hollow heap; node is the full node holding it."""
__slots__ = ("value", "node")
def __init__(self, value: V) -> None:
self.value = value
self.node: Optional["Node[K, V]"] = None
class Node(Generic[K, V]):
"""A node of the DAG. A node is full if item is set, and hollow else.
child is the first child; next is the next sibling. A node with two
parents has ep, its extra (second) parent, and it is the last child of ep.
"""
__slots__ = ("item", "key", "rank", "child", "next", "ep")
def __init__(self, item: Item[K, V], key: K) -> None:
self.item: Optional[Item[K, V]] = item
self.key = key
self.rank = 0
self.child: Optional[Node[K, V]] = None
self.next: Optional[Node[K, V]] = None
self.ep: Optional[Node[K, V]] = None
item.node = self
def _add_child(w: Node[K, V], v: Node[K, V]) -> None:
w.next = v.child
v.child = w
def _link(v: Node[K, V], w: Node[K, V]) -> Node[K, V]:
if not v.key < w.key: # @line L1
_add_child(v, w) # @line L2
return w
_add_child(w, v) # @line L3
return v
def _children(x: Node[K, V]) -> list[Node[K, V]]:
out = []
w = x.child
while w is not None:
out.append(w)
if w.ep is x:
break
w = w.next
return out
class HollowHeap(Generic[K, V]):
"""The one-root hollow heap of Hansen, Kaplan, Tarjan and Zwick.
insert, meld, find_min and decrease_key take O(1) time in the worst case;
delete and delete_min take O(log N) amortized time, where N is the
number of nodes ever made. Keys need only "<".
"""
def __init__(self) -> None:
self.root: Optional[Node[K, V]] = None
self.size = 0
def __len__(self) -> int:
return self.size
def insert(self, value: V, key: K) -> Item[K, V]:
e: Item[K, V] = Item(value)
u = Node(e, key) # @line L4
self.root = u if self.root is None else _link(u, self.root) # @line L5
self.size += 1
return e
def meld(self, other: "HollowHeap[K, V]") -> None:
if other.root is not None:
self.root = other.root if self.root is None else _link(self.root, other.root) # @line L6
self.size += other.size
other.root, other.size = None, 0
def find_min(self) -> tuple[V, K]:
if self.root is None or self.root.item is None:
raise IndexError("find_min from an empty heap")
return self.root.item.value, self.root.key
def decrease_key(self, e: Item[K, V], k: K) -> None:
u = e.node
if u is None:
raise KeyError("item not in the heap")
if u.key < k:
raise ValueError("new key is larger than the current key")
assert self.root is not None
if u is self.root: # @line L7
u.key = k
return
v = Node(e, k) # @line L8
u.item = None # @line L9
v.rank = max(0, u.rank - 2) # @line L10
v.child = u # @line L11
u.ep = v
self.root = _link(v, self.root) # @line L12
def delete_min(self) -> tuple[V, K]:
if self.root is None:
raise IndexError("delete_min from an empty heap")
assert self.root.item is not None
e = self.root.item
key = self.root.key
self.delete(e)
return e.value, key
def delete(self, e: Item[K, V]) -> None:
u = e.node
if u is None:
raise KeyError("item not in the heap")
u.item = None # @line L13
e.node = None
self.size -= 1
assert self.root is not None
if self.root.item is not None: # @line L14
return
ranks: dict[int, Node[K, V]] = {}
h: Optional[Node[K, V]] = self.root # @line L15
h.next = None
while h is not None: # @line L16
w = h.child
x = h
h = h.next
while w is not None: # @line L17
u = w
w = w.next
if u.item is None: # @line L18
if u.ep is None: # @line L19
u.next = h
h = u
else: # @line L20
if u.ep is x:
w = None
else:
u.next = None
u.ep = None
else:
while u.rank in ranks: # @line L21
u = _link(u, ranks.pop(u.rank)) # @line L22
u.rank += 1
ranks[u.rank] = u # @line L23
x.child = None # @line L24
x.next = None
root: Optional[Node[K, V]] = None
for r in sorted(ranks): # @line L25
t = ranks[r]
t.next = None
root = t if root is None else _link(t, root) # @line L26
self.root = root
def check_invariant(self) -> None:
full = 0
if self.root is None:
assert self.size == 0
return
assert self.root.item is not None
seen: set[int] = set()
stack = [self.root]
parents: dict[int, int] = {}
while stack:
x = stack.pop()
if id(x) in seen:
continue
seen.add(id(x))
if x.item is not None:
full += 1
assert x.item.node is x
assert x.rank >= 0
for c in _children(x):
assert not c.key < x.key
parents[id(c)] = parents.get(id(c), 0) + 1
stack.append(c)
assert full == self.size
for c, p in parents.items():
assert p <= 2
test_hollow_heap.py 143 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/hollow-heap/. The authors are listed in prov/AQJK5W.jsonl.
import heapq
import random
import pytest
from hollow_heap import HollowHeap, Node, _children
def show(x: Node) -> str:
s = f"{x.item.value if x.item else 'H'}{x.key}/r{x.rank}"
ch = _children(x)
return s + ("[" + " ".join(show(c) for c in ch) + "]" if ch else "")
def test_example() -> None:
h: HollowHeap[int, str] = HollowHeap()
it = {v: h.insert(v, k) for v, k in [("a", 5), ("b", 3), ("c", 8), ("d", 6), ("e", 7)]}
assert show(h.root) == "b3/r0[e7/r0 d6/r0 c8/r0 a5/r0]"
h.decrease_key(it["c"], 1)
assert show(h.root) == "c1/r0[b3/r0[e7/r0 d6/r0 H8/r0 a5/r0] H8/r0]"
h.decrease_key(it["e"], 2)
assert show(h.root) == "c1/r0[e2/r0[H7/r0] b3/r0[H7/r0 d6/r0 H8/r0 a5/r0] H8/r0]"
h.check_invariant()
assert h.delete_min() == ("c", 1)
assert show(h.root) == "e2/r1[b3/r0[H7/r0 d6/r0 H8/r0 a5/r0] H7/r0]"
assert h.delete_min() == ("e", 2)
assert show(h.root) == "b3/r0[H7/r0 d6/r0 H8/r0 a5/r0]"
assert h.delete_min() == ("b", 3)
assert show(h.root) == "a5/r1[d6/r0]"
h.check_invariant()
assert len(h) == 2
def test_empty_and_errors() -> None:
h: HollowHeap[int, int] = HollowHeap()
h.check_invariant()
with pytest.raises(IndexError):
h.find_min()
with pytest.raises(IndexError):
h.delete_min()
e = h.insert(1, 10)
with pytest.raises(ValueError):
h.decrease_key(e, 11)
h.decrease_key(e, 4)
assert h.find_min() == (1, 4)
h.delete(e)
with pytest.raises(KeyError):
h.delete(e)
with pytest.raises(KeyError):
h.decrease_key(e, 0)
assert len(h) == 0
def test_meld() -> None:
a: HollowHeap[int, int] = HollowHeap()
b: HollowHeap[int, int] = HollowHeap()
for k in [9, 4, 7]:
a.insert(k, k)
for k in [8, 2, 6]:
b.insert(k, k)
a.meld(b)
assert len(a) == 6 and len(b) == 0
a.check_invariant()
assert [a.delete_min()[1] for _ in range(6)] == [2, 4, 6, 7, 8, 9]
def test_sort() -> None:
rng = random.Random(7)
keys = [rng.randrange(1000) for _ in range(300)]
h: HollowHeap[int, int] = HollowHeap()
for i, k in enumerate(keys):
h.insert(i, k)
out = [h.delete_min()[1] for _ in keys]
assert out == sorted(keys)
def test_random_against_model() -> None:
rng = random.Random(2015)
for _ in range(600):
h: HollowHeap[int, int] = HollowHeap()
model: dict[int, int] = {}
items = {}
nxt = 0
for _ in range(rng.randrange(1, 50)):
op = rng.randrange(5)
if op <= 1 or not model:
k = rng.randrange(40)
items[nxt] = h.insert(nxt, k)
model[nxt] = k
nxt += 1
elif op == 2:
v, k = h.delete_min()
assert k == min(model.values())
assert model.pop(v) == k
elif op == 3:
v = rng.choice(sorted(model))
k = rng.randrange(model[v] + 1)
h.decrease_key(items[v], k)
model[v] = k
else:
v = rng.choice(sorted(model))
h.delete(items[v])
del model[v]
h.check_invariant()
assert len(h) == len(model)
if model:
assert h.find_min()[1] == min(model.values())
def test_dijkstra_pattern() -> None:
rng = random.Random(3)
n = 60
adj = [[(rng.randrange(n), rng.randrange(1, 20)) for _ in range(4)] for _ in range(n)]
ref = [float("inf")] * n
ref[0] = 0
pq = [(0, 0)]
while pq:
d, u = heapq.heappop(pq)
if d > ref[u]:
continue
for v, w in adj[u]:
if d + w < ref[v]:
ref[v] = d + w
heapq.heappush(pq, (d + w, v))
dist = [float("inf")] * n
dist[0] = 0
h: HollowHeap[float, int] = HollowHeap()
handle = {0: h.insert(0, 0)}
done = set()
while len(h):
u, d = h.delete_min()
done.add(u)
for v, w in adj[u]:
if v not in done and d + w < dist[v]:
dist[v] = d + w
if v in handle and handle[v].node is not None:
h.decrease_key(handle[v], d + w)
else:
handle[v] = h.insert(v, d + w)
assert dist == ref
What the next level needs
- the tests did not run in this build