Algorithm Search⌘K

Skew heap in Python

Self-adjusting leftist-style heap with amortized O(log n) merge and no balance bookkeeping.

File
skew_heap.py, 129 lines
Level
I0 not checked yet
Toolchain
Python 3.14, pytest 9
Test framework
pytest
Run the tests
python3 -m pytest
skew_heap.pyRawGitHub
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/skew-heap/. The authors are listed in prov/PY5NQC.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: a key and two subtrees. There is no balance field."""

    __slots__ = ("key", "left", "right")

    def __init__(self, key: K) -> None:
        self.key = key
        self.left: Node[K] | None = None
        self.right: Node[K] | None = None


def merge(a: "Node[K] | None", b: "Node[K] | None") -> "Node[K] | None":
    """Top-down skew merge of two heap-ordered trees; return the new root.

    Equal to the recursive MERGE of the page, done with a loop, since the length
    of a merge path is bounded only by n.
    """
    if a is None:  # @line L1
        return b
    if b is None:
        return a
    if b.key < a.key:  # @line L2
        a, b = b, a
    root = a
    while True:
        nxt = a.right  # @line L3
        a.right = a.left  # @line L4
        if nxt is None:  # MERGE(NIL, b) = b
            a.left = b  # @line L5
            break
        if b.key < nxt.key:
            nxt, b = b, nxt
        a.left = nxt  # the smaller root of the rest; the merge goes on below it
        a = nxt
    return root  # @line L6


class SkewHeap(Generic[K]):
    """A min-heap stored as a skew heap (Sleator and Tarjan, 1986)."""

    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 True if the heap holds no item."""
        return self.root is None

    def insert(self, key: K) -> None:
        """Add key to the heap."""
        self.root = merge(self.root, Node(key))  # @line L7
        self.n += 1

    def find_min(self) -> K:
        """Return a smallest key; the heap must not be empty."""
        if self.root is None:
            raise IndexError("find_min on an empty heap")
        return self.root.key

    def delete_min(self) -> K:
        """Remove and return a smallest key; the heap must not be empty."""
        r = self.root
        if r is None:
            raise IndexError("delete_min on an empty heap")
        self.root = merge(r.left, r.right)  # @line L8
        self.n -= 1
        return r.key  # @line L9

    def meld(self, other: "SkewHeap[K]") -> None:
        """Move all items of other into this heap; other becomes empty."""
        self.root = merge(self.root, other.root)
        self.n += other.n
        other.root, other.n = None, 0

    def keys(self) -> Iterator[K]:
        """Yield all keys in preorder."""
        stack = [self.root] if self.root is not None else []
        while stack:
            v = stack.pop()
            yield v.key
            for c in (v.right, v.left):
                if c is not None:
                    stack.append(c)

    def heavy_count(self) -> int:
        """The potential of the analysis: nodes whose right subtree is larger than the left."""
        size: dict[int, int] = {}
        order: list[Node[K]] = []
        stack = [self.root] if self.root is not None else []
        while stack:
            v = stack.pop()
            order.append(v)
            stack.extend(c for c in (v.left, v.right) if c is not None)
        heavy = 0
        for v in reversed(order):
            sl = size[id(v.left)] if v.left is not None else 0
            sr = size[id(v.right)] if v.right is not None else 0
            size[id(v)] = sl + sr + 1
            heavy += sr > sl
        return heavy

    def check_invariant(self) -> None:
        """Check heap order and the count."""
        count = 0
        stack = [self.root] if self.root is not None else []
        while stack:
            v = stack.pop()
            count += 1
            for c in (v.left, v.right):
                if c is not None:
                    assert not (c.key < v.key), "heap order"
                    stack.append(c)
        assert count == self.n, "count"
test_skew_heap.py 95 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/skew-heap/. The authors are listed in prov/PY5NQC.jsonl.

import heapq
import random

import pytest

from skew_heap import Node, SkewHeap, merge


def sh(v: "Node[int] | None") -> str:
    """The tree as key(left, right), as written on the page."""
    if v is None:
        return "-"
    if v.left is None and v.right is None:
        return str(v.key)
    return f"{v.key}({sh(v.left)}, {sh(v.right)})"


def test_example_inserts() -> None:
    h: SkewHeap[int] = SkewHeap()
    seen = []
    for k in [5, 3, 8, 1, 7, 2]:
        h.insert(k)
        h.check_invariant()
        seen.append(sh(h.root))
    assert seen == ["5", "3(5, -)", "3(8, 5)", "1(3(8, 5), -)", "1(7, 3(8, 5))", "1(2(3(8, 5), -), 7)"]
    assert h.delete_min() == 1
    assert sh(h.root) == "2(7, 3(8, 5))"


def test_example_meld() -> None:
    x: SkewHeap[int] = SkewHeap()
    for k in [1, 3, 8, 5, 7, 2]:
        x.insert(k)
    y: SkewHeap[int] = SkewHeap()
    for k in [4, 6, 9, 10]:
        y.insert(k)
    assert sh(x.root) == "1(2(3(5, -), -), 7(8, -))"
    assert sh(y.root) == "4(6(10, -), 9)"
    x.meld(y)
    x.check_invariant()
    assert sh(x.root) == "1(4(7(9, 8), 6(10, -)), 2(3(5, -), -))"
    assert len(x) == 10 and y.is_empty()


def test_empty() -> None:
    h: SkewHeap[int] = SkewHeap()
    assert merge(None, None) is None
    with pytest.raises(IndexError):
        h.find_min()
    with pytest.raises(IndexError):
        h.delete_min()
    assert h.heavy_count() == 0


def test_long_paths_no_recursion() -> None:
    h: SkewHeap[int] = SkewHeap()
    for k in range(20000, 0, -1):
        h.insert(k)
    other: SkewHeap[int] = SkewHeap()
    for k in range(20001, 40001):
        other.insert(k)
    h.meld(other)
    out = [h.delete_min() for _ in range(2000)]
    assert out == list(range(1, 2001))


def test_random_against_heapq() -> None:
    rng = random.Random(1986)
    for _ in range(600):
        heaps = [SkewHeap[int]() for _ in range(3)]
        refs: list[list[int]] = [[] for _ in range(3)]
        for _ in range(rng.randrange(1, 60)):
            i = rng.randrange(3)
            op = rng.random()
            if op < 0.55:
                k = rng.randrange(-20, 20)
                heaps[i].insert(k)
                heapq.heappush(refs[i], k)
            elif op < 0.85:
                if refs[i]:
                    assert heaps[i].find_min() == refs[i][0]
                    assert heaps[i].delete_min() == heapq.heappop(refs[i])
            else:
                j = rng.randrange(3)
                if i != j:
                    heaps[i].meld(heaps[j])
                    refs[i] = refs[i] + refs[j]
                    heapq.heapify(refs[i])
                    refs[j] = []
            heaps[i].check_invariant()
            assert sorted(heaps[i].keys()) == sorted(refs[i])
            assert 0 <= heaps[i].heavy_count() <= len(heaps[i])

What the next level needs

  • the tests did not run in this build

Read the full page about Skew heap