Algorithm Search⌘K

Binary heap in Python

Complete binary tree stored in an array that supports insert and extract-min in O(log n).

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

from collections.abc import Iterable
from typing import Any, Generic, Protocol, TypeVar


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


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


class BinaryHeap(Generic[T]):
    """A binary min-heap of keys in a 0-based Python list.

    The parent of index i is (i - 1) // 2 and its children are 2i + 1 and
    2i + 2. insert, extract_min and decrease_key take O(log n) time (insert
    is amortized, because the list grows), find_min takes O(1) time and
    build takes O(n) time. Keys need only "<".
    """

    def __init__(self) -> None:
        self.a: list[T] = []

    def __len__(self) -> int:
        return len(self.a)

    def _sift_up(self, i: int) -> None:
        a = self.a
        while i > 0 and a[i] < a[(i - 1) // 2]:  # @line L1
            p = (i - 1) // 2
            a[i], a[p] = a[p], a[i]  # @line L2
            i = p  # @line L3

    def _sift_down(self, n: int, i: int) -> None:
        a = self.a
        while True:  # @line L4
            m = i  # @line L5
            left, right = 2 * i + 1, 2 * i + 2
            if left < n and a[left] < a[m]:  # @line L6
                m = left
            if right < n and a[right] < a[m]:  # @line L7
                m = right
            if m == i:  # @line L8
                return
            a[i], a[m] = a[m], a[i]  # @line L9
            i = m  # @line L10

    def insert(self, k: T) -> None:
        """Add the key k. O(log n) amortized time."""
        self.a.append(k)  # @line L11 @line L12
        self._sift_up(len(self.a) - 1)  # @line L13

    def find_min(self) -> T:
        """Return a minimum key. Raise IndexError on an empty heap."""
        if not self.a:  # @line L14
            raise IndexError("heap underflow")
        return self.a[0]  # @line L15

    def extract_min(self) -> T:
        """Remove and return a minimum key. Raise IndexError on an empty heap."""
        a = self.a
        if not a:  # @line L16
            raise IndexError("heap underflow")
        smallest = a[0]  # @line L17
        last = a.pop()  # @line L19
        if a:
            a[0] = last  # @line L18
            self._sift_down(len(a), 0)  # @line L20
        return smallest  # @line L21

    def decrease_key(self, i: int, k: T) -> None:
        """Lower the key at the 0-based index i to k. Raise ValueError if k is larger."""
        if not 0 <= i < len(self.a):
            raise IndexError("index out of range")
        if self.a[i] < k:  # @line L22
            raise ValueError("new key is larger than the current key")
        self.a[i] = k  # @line L23
        self._sift_up(i)  # @line L24

    def build(self, items: Iterable[T]) -> None:
        """Replace the contents by the given keys and make a heap in O(n) time."""
        self.a = list(items)  # @line L25
        n = len(self.a)
        for i in range(n // 2 - 1, -1, -1):  # @line L26
            self._sift_down(n, i)

    def check_invariant(self) -> None:
        """Raise AssertionError if a key is smaller than its parent."""
        a = self.a
        for i in range(1, len(a)):
            assert not a[i] < a[(i - 1) // 2], f"heap order fails at index {i}"
test_binary_heap.py 89 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/binary-heap/. The authors are listed in prov/1QKQ6A.jsonl.

import random

import pytest

from binary_heap import BinaryHeap


def test_example() -> None:
    h: BinaryHeap[int] = BinaryHeap()
    h.build([5, 3, 8, 1, 9, 2])
    assert h.a == [1, 3, 2, 5, 9, 8]
    h.insert(4)
    assert h.a == [1, 3, 2, 5, 9, 8, 4]
    h.insert(0)
    assert h.a == [0, 1, 2, 3, 9, 8, 4, 5]
    assert h.extract_min() == 0
    assert h.a == [1, 3, 2, 5, 9, 8, 4]
    h.decrease_key(4, 0)
    assert h.a == [0, 1, 2, 5, 3, 8, 4]
    assert h.extract_min() == 0
    assert h.a == [1, 3, 2, 5, 4, 8]
    h.check_invariant()


def test_edges() -> None:
    h: BinaryHeap[int] = BinaryHeap()
    with pytest.raises(IndexError):
        h.find_min()
    with pytest.raises(IndexError):
        h.extract_min()
    h.build([])
    assert len(h) == 0
    h.insert(7)
    assert h.find_min() == 7
    assert h.extract_min() == 7
    assert len(h) == 0
    h.build([2, 2, 2, 2])
    assert [h.extract_min() for _ in range(4)] == [2, 2, 2, 2]
    h.build([3, 1])
    with pytest.raises(ValueError):
        h.decrease_key(1, 5)
    h.build([1, 2, 3])
    h.decrease_key(2, 3)
    h.check_invariant()


def test_sorted_and_reversed() -> None:
    for keys in (list(range(50)), list(range(50, 0, -1))):
        h: BinaryHeap[int] = BinaryHeap()
        for k in keys:
            h.insert(k)
            h.check_invariant()
        assert [h.extract_min() for _ in keys] == sorted(keys)


def test_random_against_model() -> None:
    rng = random.Random(6121)
    for _ in range(500):
        h: BinaryHeap[int] = BinaryHeap()
        model: list[int] = [rng.randint(-20, 20) for _ in range(rng.randint(0, 12))]
        h.build(model)
        h.check_invariant()
        for _ in range(rng.randint(0, 40)):
            op = rng.random()
            if op < 0.4:
                k = rng.randint(-20, 20)
                h.insert(k)
                model.append(k)
            elif op < 0.75:
                if model:
                    want = min(model)
                    assert h.find_min() == want
                    assert h.extract_min() == want
                    model.remove(want)
                else:
                    with pytest.raises(IndexError):
                        h.extract_min()
            elif model:
                i = rng.randrange(len(h))
                old = h.a[i]
                k = old - rng.randint(0, 10)
                h.decrease_key(i, k)
                model.remove(old)
                model.append(k)
            h.check_invariant()
            assert sorted(h.a) == sorted(model)

What the next level needs

  • the tests did not run in this build

Read the full page about Binary heap