Algorithm Search⌘K

Thin heap in Python

Fibonacci-bound heap with a simpler structure and less space than Fibonacci heaps.

File
thin_heap.py, 254 lines
Level
I0 not checked yet
Toolchain
Python 3.14, pytest 9
Test framework
pytest
Run the tests
python3 -m pytest
thin_heap.pyRawGitHub
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/thin-heap/. The authors are listed in prov/DBACA1.jsonl.

from typing import Any, Generic, Optional, Protocol, TypeVar


class Ordered(Protocol):
    def __lt__(self, other: Any, /) -> bool: ...


K = TypeVar("K", bound=Ordered)


class Node(Generic[K]):
    """A node of a thin tree.

    child is the first child (the one of highest rank). right is the next
    sibling, or the next root in the root list. left is the previous sibling,
    or the parent if the node is a first child, or None for a root.
    """

    __slots__ = ("key", "rank", "child", "left", "right", "is_root")

    def __init__(self, key: K) -> None:
        self.key = key
        self.rank = 0
        self.child: Optional[Node[K]] = None
        self.left: Optional[Node[K]] = None
        self.right: Optional[Node[K]] = None
        self.is_root = True


def _rank(x: "Optional[Node[Any]]") -> int:
    return -1 if x is None else x.rank


def is_thin(x: "Node[Any]") -> bool:
    """A node is thin if its rank is 2 more than the rank of its first child."""
    return x.rank == _rank(x.child) + 2


class ThinHeap(Generic[K]):
    """Kaplan and Tarjan's thin heap: a min-heap with O(1) amortized decrease-key."""

    def __init__(self) -> None:
        self.head: Optional[Node[K]] = None  # singly linked root list
        self.tail: Optional[Node[K]] = None
        self.min: Optional[Node[K]] = None
        self.n = 0
        self.links = 0

    def __len__(self) -> int:
        return self.n

    def _add_root(self, x: Node[K]) -> None:
        x.is_root, x.left, x.right = True, None, None
        if x.rank != _rank(x.child) + 1:
            x.rank = _rank(x.child) + 1  # a root is never thin
        if self.tail is None:
            self.head = self.tail = x
        else:
            self.tail.right = x
            self.tail = x
        if self.min is None or x.key < self.min.key:
            self.min = x

    def _link(self, x: Node[K], y: Node[K]) -> Node[K]:
        """Link two roots of equal rank; the larger key becomes the first child."""
        self.links += 1
        if y.key < x.key:  # @line L1
            x, y = y, x
        y.is_root, y.left, y.right = False, x, x.child  # @line L2
        if x.child is not None:
            x.child.left = y
        x.child = y
        x.rank += 1
        return x

    def insert(self, key: K) -> Node[K]:
        """Add key and return its node, for decrease_key."""
        x = Node(key)
        self._add_root(x)  # @line L3
        self.n += 1
        return x

    def meld(self, other: "ThinHeap[K]") -> None:
        """Move all items of other into this heap, in O(1) time."""
        if other.head is None:
            return
        if self.tail is None:
            self.head, self.tail, self.min = other.head, other.tail, other.min
        else:
            self.tail.right = other.head
            self.tail = other.tail
            assert other.min is not None and self.min is not None
            if other.min.key < self.min.key:
                self.min = other.min
        self.n += other.n
        other.head = other.tail = other.min = None
        other.n = 0

    def find_min(self) -> K:
        if self.min is None:
            raise IndexError("find_min on an empty heap")
        return self.min.key

    def delete_min(self) -> K:
        """Remove and return a smallest key."""
        z = self.min
        if z is None:
            raise IndexError("delete_min on an empty heap")
        roots: list[Node[K]] = []
        x = self.head
        while x is not None:
            nxt = x.right
            if x is not z:
                roots.append(x)
            x = nxt
        c = z.child  # @line L4
        while c is not None:
            nxt = c.right
            roots.append(c)
            c = nxt
        buckets: dict[int, Node[K]] = {}
        for x in roots:
            x.is_root, x.left, x.right = True, None, None
            x.rank = _rank(x.child) + 1  # a thin child becomes normal
            while x.rank in buckets:  # @line L5
                x = self._link(x, buckets.pop(x.rank))
            buckets[x.rank] = x  # @line L6
        self.head = self.tail = self.min = None
        for r in sorted(buckets):  # @line L7
            self._add_root(buckets[r])
        self.n -= 1
        return z.key

    def decrease_key(self, x: Node[K], key: K) -> None:
        """Lower the key of node x to key, which must not be larger."""
        if x.key < key:
            raise ValueError("new key is larger")
        x.key = key  # @line L8
        if x.is_root:
            if self.min is not None and key < self.min.key:
                self.min = x
            return
        y, parent_case = self._cut(x)  # @line L9
        while True:  # @line L10
            if parent_case:  # @line L11
                if y.is_root:  # @line L12
                    y.rank = _rank(y.child) + 1
                    return
                if y.rank == _rank(y.child) + 2:  # @line L13
                    return  # y was normal and is now thin
                y, parent_case = self._cut(y)  # @line L14
            else:
                if not is_thin(y):  # @line L15
                    w = y.child
                    assert w is not None
                    y.child = w.right
                    if w.right is not None:
                        w.right.left = y
                    w.left, w.right = y, y.right
                    if y.right is not None:
                        y.right.left = w
                    y.right = w
                    return
                y.rank -= 1  # @line L16
                x, y = y, y.left
                assert y is not None
                parent_case = y.child is x

    def _cut(self, x: Node[K]) -> tuple[Node[K], bool]:
        """Cut the subtree of x and make x a root; return the old left neighbor and whether it is the parent."""
        y = x.left
        assert y is not None
        parent_case = y.child is x
        if parent_case:
            y.child = x.right
        else:
            y.right = x.right
        if x.right is not None:
            x.right.left = y
        self._add_root(x)
        return y, parent_case

    def check_invariant(self) -> None:
        """Check heap order, the thin-tree rank rules, the pointers and the rank bound."""
        fib = [1, 1]
        while len(fib) < 100:
            fib.append(fib[-1] + fib[-2])
        count = 0
        best: Optional[Node[K]] = None
        r = self.head
        last = None
        while r is not None:
            assert r.is_root and r.left is None
            assert r.rank == _rank(r.child) + 1, "a root must be normal"
            if best is None or r.key < best.key:
                best = r
            stack = [r]
            while stack:
                v = stack.pop()
                count += 1
                size_of = 0
                assert v.rank in (_rank(v.child) + 1, _rank(v.child) + 2)
                prev, c = v, v.child
                expect = _rank(v.child)
                while c is not None:
                    assert not c.is_root and c.left is prev
                    assert c.rank == expect, "child ranks must be consecutive"
                    assert not (c.key < v.key), "heap order"
                    stack.append(c)
                    expect -= 1
                    prev, c = c, c.right
                    size_of += 1
                assert expect == -1, "the last child must have rank 0"
            last = r
            r = r.right
        assert last is self.tail
        assert count == self.n
        if best is not None:
            assert self.min is not None and not (best.key < self.min.key)
        else:
            assert self.min is None
        for root in self._roots():
            self._check_size(root, fib)

    def _roots(self) -> list[Node[K]]:
        out = []
        r = self.head
        while r is not None:
            out.append(r)
            r = r.right
        return out

    def _check_size(self, root: Node[K], fib: list[int]) -> None:
        """A node of rank k has at least F(k+1) nodes in its subtree."""
        order: list[Node[K]] = []
        stack = [root]
        while stack:
            v = stack.pop()
            order.append(v)
            c = v.child
            while c is not None:
                stack.append(c)
                c = c.right
        size: dict[int, int] = {}
        for v in reversed(order):
            s, c = 1, v.child
            while c is not None:
                s += size[id(c)]
                c = c.right
            size[id(v)] = s
            assert s >= fib[v.rank], "rank bound"
test_thin_heap.py 110 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/thin-heap/. The authors are listed in prov/DBACA1.jsonl.

import random

import pytest

from thin_heap import Node, ThinHeap, is_thin


def show(x: Node[int]) -> str:
    """Write a tree as key:rank(children), with t after the rank of a thin node."""
    s = f"{x.key}:{x.rank}{'t' if is_thin(x) else ''}"
    parts = []
    c = x.child
    while c is not None:
        parts.append(show(c))
        c = c.right
    return s + ("(" + ", ".join(parts) + ")" if parts else "")


def dump(h: ThinHeap[int]) -> str:
    return " ".join(show(r) for r in h._roots())


def test_page_example() -> None:
    h: ThinHeap[int] = ThinHeap()
    nodes = {k: h.insert(k) for k in range(10, 100, 10)}
    assert h.delete_min() == 10
    assert h.links == 7
    assert dump(h) == "20:3(60:2(80:1(90:0), 70:0), 40:1(50:0), 30:0)"
    h.decrease_key(nodes[50], 5)
    assert dump(h) == "20:3(60:2(80:1(90:0), 70:0), 40:1t, 30:0) 5:0"
    h.decrease_key(nodes[30], 3)
    assert dump(h) == "20:3(60:2t(70:0), 80:1(90:0), 40:0) 5:0 3:0"
    h.decrease_key(nodes[70], 7)
    assert dump(h) == "20:2(80:1(90:0), 40:0) 5:0 3:0 7:0 60:0"
    h.check_invariant()
    h.links = 0
    assert h.delete_min() == 3
    assert h.links == 1
    assert dump(h) == "60:0 5:1(7:0) 20:2(80:1(90:0), 40:0)"
    assert h.find_min() == 5
    h.check_invariant()


def test_edge_cases() -> None:
    h: ThinHeap[int] = ThinHeap()
    with pytest.raises(IndexError):
        h.find_min()
    with pytest.raises(IndexError):
        h.delete_min()
    x = h.insert(4)
    h.decrease_key(x, 1)
    assert h.find_min() == 1
    with pytest.raises(ValueError):
        h.decrease_key(x, 2)
    assert h.delete_min() == 1
    assert len(h) == 0
    h.check_invariant()


def test_meld() -> None:
    a: ThinHeap[int] = ThinHeap()
    b: ThinHeap[int] = ThinHeap()
    for k in [5, 9, 1]:
        a.insert(k)
    for k in [7, 0, 3]:
        b.insert(k)
    a.meld(b)
    assert len(a) == 6 and len(b) == 0
    a.check_invariant()
    assert [a.delete_min() for _ in range(6)] == [0, 1, 3, 5, 7, 9]


def alive(h: ThinHeap[int]) -> set[int]:
    seen = set()
    stack = list(h._roots())
    while stack:
        v = stack.pop()
        seen.add(id(v))
        c = v.child
        while c is not None:
            stack.append(c)
            c = c.right
    return seen


def test_random_against_model() -> None:
    rng = random.Random(2008)
    for _ in range(600):
        h: ThinHeap[int] = ThinHeap()
        ref: dict[int, Node[int]] = {}
        for _ in range(rng.randrange(1, 80)):
            op = rng.random()
            if op < 0.45 or not ref:
                x = h.insert(rng.randrange(1000))
                ref[id(x)] = x
            elif op < 0.75:
                x = rng.choice(list(ref.values()))
                h.decrease_key(x, x.key - rng.randrange(0, 60))
            else:
                smallest = min(x.key for x in ref.values())
                assert h.delete_min() == smallest
                live = alive(h)
                gone = [k for k in ref if k not in live]
                assert len(gone) == 1
                del ref[gone[0]]
            h.check_invariant()
            assert len(h) == len(ref)

What the next level needs

  • the tests did not run in this build

Read the full page about Thin heap