Min-max heap in Python
Binary tree whose levels alternate min and max layers, giving both extremes in O(1) to find.
- File
min_max_heap.py, 135 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/min-max-heap/. The authors are listed in prov/HGEHQG.jsonl.
from typing import Any, Generic, Iterable, Protocol, TypeVar
class Ordered(Protocol):
def __lt__(self, other: Any, /) -> bool: ...
K = TypeVar("K", bound=Ordered)
def is_min_level(i: int) -> bool:
"""True if index i (0-based) is on a min level: levels 0, 2, 4, ..."""
return ((i + 1).bit_length() - 1) % 2 == 0
class MinMaxHeap(Generic[K]):
"""A double-ended priority queue in one array, after Atkinson et al. (1986).
The root is on level 0. Nodes on even levels are smaller than or equal to
all their descendants; nodes on odd levels are larger than or equal to them.
"""
def __init__(self, items: Iterable[K] = ()) -> None:
"""Build a heap from items in O(n) time, bottom up."""
self.a: list[K] = list(items)
for i in range(len(self.a) // 2 - 1, -1, -1):
self._push_down(i)
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 _better(self, x: K, y: K, min_level: bool) -> bool:
"""True if x should be above y on a level of the given kind."""
return x < y if min_level else y < x
def _push_down(self, i: int) -> None:
"""Move a[i] down until the min-max order holds in the subtree of i."""
a, n = self.a, len(self.a)
mn = is_min_level(i)
while 2 * i + 1 < n: # @line L1
m = 2 * i + 1 # @line L2
for j in (2 * i + 2, 4 * i + 3, 4 * i + 4, 4 * i + 5, 4 * i + 6):
if j < n and self._better(a[j], a[m], mn):
m = j
if m <= 2 * i + 2: # @line L3
if self._better(a[m], a[i], mn): # @line L4
a[m], a[i] = a[i], a[m]
return # @line L5
if not self._better(a[m], a[i], mn): # @line L6
return
a[m], a[i] = a[i], a[m] # @line L7
p = (m - 1) // 2
if self._better(a[p], a[m], mn): # @line L8
a[m], a[p] = a[p], a[m]
i = m # @line L9
def _push_up(self, i: int) -> None:
"""Move a[i] up until the min-max order holds on the path to the root."""
a = self.a
if i == 0: # @line L10
return
p = (i - 1) // 2 # @line L11
mn = is_min_level(i)
if self._better(a[i], a[p], not mn): # @line L12
a[i], a[p] = a[p], a[i] # @line L13
i, mn = p, not mn
while i >= 3 and self._better(a[i], a[(i - 3) // 4], mn): # @line L14
g = (i - 3) // 4
a[i], a[g] = a[g], a[i] # @line L15
i = g
def insert(self, x: K) -> None:
"""Add x to the heap in O(log n) time."""
self.a.append(x) # @line L20
self._push_up(len(self.a) - 1)
def find_min(self) -> K:
"""Return a smallest item; the heap must not be empty."""
if not self.a:
raise IndexError("find_min on an empty heap")
return self.a[0]
def _max_index(self) -> int:
a = self.a
if len(a) <= 2:
return len(a) - 1
return 1 if not a[1] < a[2] else 2
def find_max(self) -> K:
"""Return a largest item; the heap must not be empty."""
if not self.a:
raise IndexError("find_max on an empty heap")
return self.a[self._max_index()]
def delete_min(self) -> K:
"""Remove and return a smallest item in O(log n) time."""
return self._delete_at(0) # @line L16
def delete_max(self) -> K:
"""Remove and return a largest item in O(log n) time."""
if not self.a:
raise IndexError("delete_max on an empty heap")
return self._delete_at(self._max_index()) # @line L18
def _delete_at(self, m: int) -> K:
a = self.a
if not a:
raise IndexError("delete on an empty heap")
x = a[m] # @line L19
last = a.pop()
if m < len(a):
a[m] = last
self._push_down(m)
return x # @line L17
def check_invariant(self) -> None:
"""Check that each node is on the correct side of all its descendants."""
a, n = self.a, len(self.a)
for i in range(1, n):
j = (i - 1) // 2
while True: # every ancestor j of i
if is_min_level(j):
assert not (a[i] < a[j]), f"min order at {j}, {i}"
else:
assert not (a[j] < a[i]), f"max order at {j}, {i}"
if j == 0:
break
j = (j - 1) // 2
test_min_max_heap.py 82 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/min-max-heap/. The authors are listed in prov/HGEHQG.jsonl.
import random
import pytest
from min_max_heap import MinMaxHeap, is_min_level
START = [8, 71, 41, 31, 10, 11, 16, 46, 51, 31, 21, 13]
def test_levels() -> None:
assert [is_min_level(i) for i in range(16)] == [True] + [False] * 2 + [True] * 4 + [False] * 8 + [True]
def test_example() -> None:
h = MinMaxHeap(START)
h.check_invariant()
assert h.a == START
h.insert(5)
assert h.a == [5, 71, 41, 31, 10, 8, 16, 46, 51, 31, 21, 13, 11]
h.insert(80)
assert h.a == [5, 71, 80, 31, 10, 8, 16, 46, 51, 31, 21, 13, 11, 41]
assert h.find_min() == 5 and h.find_max() == 80
assert h.delete_min() == 5
assert h.a == [8, 71, 80, 31, 10, 11, 16, 46, 51, 31, 21, 13, 41]
assert h.delete_max() == 80
assert h.a == START
h.check_invariant()
def test_small_and_empty() -> None:
h: MinMaxHeap[int] = MinMaxHeap()
for f in (h.find_min, h.find_max, h.delete_min, h.delete_max):
with pytest.raises(IndexError):
f()
h.insert(4)
assert h.find_min() == 4 and h.find_max() == 4
assert h.delete_max() == 4 and h.is_empty()
h = MinMaxHeap([2, 1])
h.check_invariant()
assert (h.find_min(), h.find_max()) == (1, 2)
assert h.delete_max() == 2 and h.delete_min() == 1
def test_build_sorted_and_duplicates() -> None:
for data in (list(range(100)), list(range(100, 0, -1)), [3] * 40, [1, 2] * 30):
h = MinMaxHeap(data)
h.check_invariant()
out_lo: list[int] = []
out_hi: list[int] = []
while len(h):
out_lo.append(h.delete_min())
if len(h):
out_hi.append(h.delete_max())
h.check_invariant()
assert out_lo + out_hi[::-1] == sorted(data)
def test_random_against_sorted_list() -> None:
rng = random.Random(1986)
for _ in range(600):
start = [rng.randrange(-30, 30) for _ in range(rng.randrange(0, 20))]
h = MinMaxHeap(start)
ref = sorted(start)
h.check_invariant()
for _ in range(rng.randrange(1, 50)):
op = rng.random()
if op < 0.5:
x = rng.randrange(-30, 30)
h.insert(x)
ref.append(x)
ref.sort()
elif ref and op < 0.75:
assert h.find_min() == ref[0]
assert h.delete_min() == ref.pop(0)
elif ref:
assert h.find_max() == ref[-1]
assert h.delete_max() == ref.pop()
h.check_invariant()
assert sorted(h.a) == ref
What the next level needs
- the tests did not run in this build