Algorithm Search⌘K

Pairing heap in Python

Simple self-adjusting heap whose practical performance rivals Fibonacci heaps.

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

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


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


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


class Node(Generic[K]):
    """A node of the tree: key, first child, next sibling, and prev.

    prev is the parent when the node is a first child, else the left sibling.
    """

    __slots__ = ("key", "child", "sibling", "prev")

    def __init__(self, key: K) -> None:
        self.key = key
        self.child: Node[K] | None = None
        self.sibling: Node[K] | None = None
        self.prev: Node[K] | None = None


def link(a: "Node[K] | None", b: "Node[K] | None") -> "Node[K] | None":
    """Link two heap-ordered trees whose roots have no siblings; return the new root."""
    if a is None:
        return b
    if b is None:
        return a
    if b.key < a.key:  # @line L2
        a, b = b, a
    b.sibling = a.child  # @line L3
    if a.child is not None:
        a.child.prev = b
    b.prev = a
    a.child = b
    a.sibling = None
    a.prev = None
    return a  # @line L4


class PairingHeap(Generic[K]):
    """A min-oriented pairing heap with handles.

    find_min is O(1). insert and meld do one link, O(1) actual time.
    delete_min does two-pass pairing. All operations take O(log n) amortized
    time; decrease_key takes O(1) actual time.
    """

    def __init__(self) -> None:
        self.root: Node[K] | None = None
        self.n = 0

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

    def is_empty(self) -> bool:
        return self.root is None

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

    def insert(self, key: K) -> Node[K]:
        """Insert key and return its handle."""
        x: Node[K] = Node(key)
        self.root = link(self.root, x)  # @line L5
        self.n += 1
        return x

    def meld(self, other: "PairingHeap[K]") -> None:
        """Move all items of other into self; other becomes empty."""
        if other is self:
            return
        self.root = link(self.root, other.root)  # @line L1
        self.n += other.n
        other.root, other.n = None, 0

    def delete_min(self) -> K:
        """Remove and return a smallest key. O(log n) amortized."""
        r = self.root
        if r is None:
            raise IndexError("delete_min on an empty heap")
        self.root = merge_pairs(r.child)  # @line L6
        if self.root is not None:
            self.root.prev = None
        self.n -= 1
        r.child = None
        return r.key  # @line L7

    def decrease_key(self, x: Node[K], key: K) -> None:
        """Lower the key of the handle x to key. O(1) actual time."""
        if x.key < key:
            raise ValueError("the new key is larger than the old key")
        x.key = key
        if x is self.root:
            return
        cut(x)
        self.root = link(self.root, x)

    def delete(self, x: Node[K]) -> K:
        """Remove the item of the handle x and return its key."""
        if x is self.root:
            return self.delete_min()
        cut(x)
        rest = merge_pairs(x.child)
        x.child = None
        if rest is not None:
            rest.prev = None
        self.root = link(self.root, rest)
        self.n -= 1
        return x.key

    def check_invariant(self) -> None:
        """Check heap order, the prev pointers and the size."""
        count = 0
        if self.root is not None:
            assert self.root.prev is None and self.root.sibling is None
            stack = [self.root]
            while stack:
                v = stack.pop()
                count += 1
                prev = v
                c = v.child
                while c is not None:
                    assert c.prev is prev
                    assert not c.key < v.key
                    stack.append(c)
                    prev = c
                    c = c.sibling
        assert count == self.n

    def keys(self) -> Iterator[K]:
        if self.root is None:
            return
        stack = [self.root]
        while stack:
            v = stack.pop()
            yield v.key
            c = v.child
            while c is not None:
                stack.append(c)
                c = c.sibling


def cut(x: "Node[K]") -> None:
    """Detach the subtree of x (x is not a root) from its parent's child list."""
    p = x.prev
    assert p is not None
    if p.child is x:
        p.child = x.sibling
    else:
        p.sibling = x.sibling
    if x.sibling is not None:
        x.sibling.prev = p
    x.sibling = None
    x.prev = None


def merge_pairs(first: "Node[K] | None") -> "Node[K] | None":
    """Two-pass pairing of the list of trees that starts at first."""
    pairs: list[Node[K]] = []
    x = first
    while x is not None:  # @line L8: first pass, left to right
        a = x
        b = a.sibling
        if b is None:
            a.prev = None
            pairs.append(a)
            break
        x = b.sibling
        a.sibling = b.sibling = None
        a.prev = b.prev = None
        t = link(a, b)  # @line L9
        assert t is not None
        pairs.append(t)
    root: Node[K] | None = None
    for t in reversed(pairs):  # @line L10: second pass, right to left
        root = link(t, root)  # @line L11
    return root  # @line L12
test_pairing_heap.py 121 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/pairing-heap/. The authors are listed in prov/G7BE09.jsonl.

import random

import pytest

from pairing_heap import Node, PairingHeap


def shape(v: "Node[int] | None") -> object:
    """The tree as (key, [children]) for comparing with the page."""
    if v is None:
        return None
    kids = []
    c = v.child
    while c is not None:
        kids.append(shape(c))
        c = c.sibling
    return (v.key, kids)


def test_example() -> None:
    h: PairingHeap[int] = PairingHeap()
    for k in [7, 3, 9, 5, 8, 2, 6, 4]:
        h.insert(k)
        h.check_invariant()
    assert shape(h.root) == (2, [(4, []), (6, []), (3, [(8, []), (5, []), (9, []), (7, [])])])
    assert h.delete_min() == 2
    assert shape(h.root) == (3, [(4, [(6, [])]), (8, []), (5, []), (9, []), (7, [])])
    assert h.delete_min() == 3
    assert shape(h.root) == (4, [(5, [(7, []), (9, [])]), (8, []), (6, [])])
    h.check_invariant()


def test_edges() -> None:
    h: PairingHeap[int] = PairingHeap()
    with pytest.raises(IndexError):
        h.delete_min()
    with pytest.raises(IndexError):
        h.find_min()
    x = h.insert(1)
    assert h.delete(x) == 1 and h.is_empty()
    for _ in range(5):
        h.insert(4)
    assert [h.delete_min() for _ in range(5)] == [4] * 5
    y = h.insert(10)
    with pytest.raises(ValueError):
        h.decrease_key(y, 11)


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


def test_sorted_input_is_long() -> None:
    h: PairingHeap[int] = PairingHeap()
    for k in range(20000):
        h.insert(k)  # one root with 19999 children: no recursion depth problem
    assert [h.delete_min() for _ in range(20000)] == list(range(20000))


def test_random_against_model() -> None:
    rng = random.Random(1986)
    for _ in range(500):
        h: PairingHeap[int] = PairingHeap()
        model: dict[int, int] = {}
        handles: dict[int, Node[int]] = {}
        uid = 0
        for _ in range(rng.randint(1, 60)):
            op = rng.random()
            if op < 0.45 or not model:
                k = rng.randint(-50, 50)
                handles[uid] = h.insert(k)
                model[uid] = k
                uid += 1
            elif op < 0.65:
                got = h.delete_min()
                want = min(model.values())
                assert got == want
                # remove one handle with that key that is no longer in the heap
                live = {id(v) for v in iter_nodes(h)}
                gone = [u for u in model if id(handles[u]) not in live]
                assert len(gone) == 1
                del model[gone[0]]
            elif op < 0.85:
                u = rng.choice(list(model))
                k = model[u] - rng.randint(0, 30)
                h.decrease_key(handles[u], k)
                model[u] = k
            else:
                u = rng.choice(list(model))
                assert h.delete(handles[u]) == model[u]
                del model[u]
            h.check_invariant()
            assert len(h) == len(model)
            assert sorted(h.keys()) == sorted(model.values())
            if model:
                assert h.find_min() == min(model.values())


def iter_nodes(h: PairingHeap[int]) -> list[Node[int]]:
    out = []
    stack = [h.root] if h.root is not None else []
    while stack:
        v = stack.pop()
        out.append(v)
        c = v.child
        while c is not None:
            stack.append(c)
            c = c.sibling
    return out

What the next level needs

  • the tests did not run in this build

Read the full page about Pairing heap