Weak heap in Python
Heap variant with a relaxed order that makes heapsort use fewer comparisons.
- File
weak_heap.py, 108 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/weak-heap/. The authors are listed in prov/PYFMNC.jsonl.
from typing import Any, Generic, Iterable, Protocol, TypeVar
class Ordered(Protocol):
def __lt__(self, other: Any, /) -> bool: ...
K = TypeVar("K", bound=Ordered)
class WeakHeap(Generic[K]):
"""A min weak heap in an array a with one reverse bit r[i] per node.
The children of node i are 2i + r[i] (left) and 2i + 1 - r[i] (right).
The root a[0] has only a right child, a[1]. Weak-heap order: every node
j > 0 has a key at least the key of its distinguished ancestor.
"""
def __init__(self, items: Iterable[K] = ()) -> None:
"""Build a weak heap with n - 1 comparisons."""
self.a: list[K] = list(items)
self.r: list[int] = [0] * len(self.a)
self.comparisons = 0
for j in range(len(self.a) - 1, 0, -1): # @line L5
self._join(self.d_ancestor(j), j) # @line L6
def __len__(self) -> int:
return len(self.a)
def is_empty(self) -> bool:
"""Return True if the heap holds no item."""
return not self.a
def d_ancestor(self, j: int) -> int:
"""The distinguished ancestor of j > 0: the parent of the first right child on the way up."""
while (j & 1) == self.r[j >> 1]: # @line L1
j >>= 1
return j >> 1 # @line L2
def _join(self, i: int, j: int) -> bool:
"""Join the weak heap rooted at j to i = d_ancestor(j); return True if no swap."""
self.comparisons += 1
if self.a[j] < self.a[i]: # @line L3
self.a[i], self.a[j] = self.a[j], self.a[i]
self.r[j] ^= 1 # @line L4
return False
return True
def find_min(self) -> K:
"""Return a smallest key; the heap must not be empty."""
if not self.a:
raise IndexError("find_min on an empty heap")
return self.a[0]
def _sift_down(self, n: int) -> None:
"""Restore the order of a[0 .. n-1] after a[0] changed."""
if n < 2:
return
x = 1 # @line L7
while 2 * x + self.r[x] < n: # @line L8
x = 2 * x + self.r[x]
while x > 0: # @line L9
self._join(0, x) # @line L10
x >>= 1
def delete_min(self) -> K:
"""Remove and return a smallest key with at most ceil(lg n) comparisons."""
if not self.a:
raise IndexError("delete_min on an empty heap")
top = self.a[0]
last = self.a.pop() # @line L11
self.r.pop()
if self.a:
self.a[0] = last
self._sift_down(len(self.a)) # @line L12
return top
def insert(self, x: K) -> None:
"""Add x and sift it up along distinguished ancestors."""
j = len(self.a)
self.a.append(x) # @line L13
self.r.append(0)
if j > 0 and j & 1 == 0:
self.r[j >> 1] = 0 # the parent was a leaf; make j its left child
while j != 0: # @line L14
i = self.d_ancestor(j)
if self._join(i, j): # @line L15
break
j = i
def check_invariant(self) -> None:
"""Check weak-heap order for every node."""
for j in range(1, len(self.a)):
assert not (self.a[j] < self.a[self.d_ancestor(j)]), f"order at {j}"
def weak_heapsort(items: Iterable[K]) -> tuple[list[K], int]:
"""Sort with Dutton's weak-heapsort; return the sorted list and the comparisons."""
h = WeakHeap(items)
a, n = h.a, len(h.a)
for m in range(n - 1, 0, -1):
a[0], a[m] = a[m], a[0]
h._sift_down(m)
a.reverse()
return a, h.comparisons
test_weak_heap.py 82 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/weak-heap/. The authors are listed in prov/PYFMNC.jsonl.
import heapq
import math
import random
import pytest
from weak_heap import WeakHeap, weak_heapsort
def test_example_build() -> None:
h = WeakHeap([8, 3, 6, 1, 5, 9, 2, 7])
assert h.a == [1, 5, 6, 2, 8, 9, 3, 7]
assert h.r == [0, 1, 0, 1, 1, 0, 1, 0]
assert h.comparisons == 7
h.check_invariant()
def test_example_delete_and_insert() -> None:
h = WeakHeap([8, 3, 6, 1, 5, 9, 2, 7])
h.comparisons = 0
assert h.delete_min() == 1
assert h.a == [2, 5, 6, 7, 8, 9, 3]
assert h.r == [0, 1, 0, 0, 1, 0, 1]
assert h.comparisons == 2
h.comparisons = 0
h.insert(4)
assert h.a == [2, 5, 6, 4, 8, 9, 3, 7]
assert h.r == [0, 1, 0, 0, 1, 0, 1, 1]
assert h.comparisons == 2
h.check_invariant()
def test_edge_cases() -> None:
h: WeakHeap[int] = WeakHeap()
with pytest.raises(IndexError):
h.find_min()
with pytest.raises(IndexError):
h.delete_min()
h.insert(3)
h.insert(1)
assert h.find_min() == 1
assert [h.delete_min(), h.delete_min()] == [1, 3]
assert weak_heapsort([]) == ([], 0)
assert weak_heapsort([5]) == ([5], 0)
assert weak_heapsort([2, 2, 1, 2])[0] == [1, 2, 2, 2]
def test_sort_comparison_bound() -> None:
rng = random.Random(1993)
for n in range(2, 260):
for _ in range(3):
data = [rng.randrange(1000) for _ in range(n)]
out, c = weak_heapsort(data)
assert out == sorted(data)
k = math.ceil(math.log2(n))
assert c <= n * k - 2**k + n - 1
def test_random_against_heapq() -> None:
rng = random.Random(2012)
for _ in range(600):
start = [rng.randrange(-30, 30) for _ in range(rng.randrange(0, 20))]
h = WeakHeap(start)
ref = list(start)
heapq.heapify(ref)
h.check_invariant()
for _ in range(rng.randrange(1, 50)):
if rng.random() < 0.55 or not ref:
x = rng.randrange(-30, 30)
h.insert(x)
heapq.heappush(ref, x)
else:
assert h.find_min() == ref[0]
n = len(h)
h.comparisons = 0
assert h.delete_min() == heapq.heappop(ref)
assert h.comparisons <= max(0, math.ceil(math.log2(n)))
h.check_invariant()
assert sorted(h.a) == sorted(ref)
What the next level needs
- the tests did not run in this build