Algorithm Search⌘K

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
hollow_heap.pyRawGitHub
# 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

Read the full page about Hollow heap