codex/binary-heap-array-and-tree.py

binary-heap-array-and-tree.py
"""Draw a zero-indexed min-heap; tested with Python 3.14 / matplotlib 3.10.7.

Run from _media to write binary-heap-array-and-tree.png there.
"""

from pathlib import Path

import matplotlib

matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.patches import Circle, Rectangle


VALUES = (2, 5, 3, 9, 7, 8, 4)
COLORS = ("#006b8f", "#955400", "#267547", "#7b4397",
          "#955400", "#267547", "#267547")


def check_heap(values):
    """Check dense level-order shape, inverse indices, and every heap edge."""
    present = set(range(len(values)))
    for i in present:
        if i:
            parent = (i - 1) // 2
            assert parent in present
            assert i in (2 * parent + 1, 2 * parent + 2)
            assert values[parent] <= values[i]
        left, right = 2 * i + 1, 2 * i + 2
        assert right not in present or left in present
    return len(values) - 1 if values else 0


def main():
    assert check_heap(VALUES) == 6
    assert VALUES != tuple(sorted(VALUES))
    assert VALUES[-1] == 4 and max(VALUES) == 9
    plt.rcParams.update({"font.family": "DejaVu Sans", "font.size": 12})
    fig, ax = plt.subplots(figsize=(10, 6.8), facecolor="white")
    ax.set(xlim=(-0.3, 9.7), ylim=(-0.45, 6.35), aspect="equal")
    ax.axis("off")
    ax.text(4.7, 6.05, "Binary min-heap: an array is also a tree",
            ha="center", fontsize=18, weight="bold", color="#173244")
    ax.text(0.1, 5.48, "index i", fontsize=11, color="#475569")
    ax.text(0.1, 4.84, "key A[i]", fontsize=11, color="#475569")
    for i, key in enumerate(VALUES):
        x = 1.65 + i
        ax.add_patch(Rectangle((x - 0.45, 4.48), 0.9, 0.7,
                               facecolor="#f3f7fa", edgecolor=COLORS[i],
                               linewidth=1.8))
        ax.text(x, 5.48, str(i), ha="center", color=COLORS[i], weight="bold")
        ax.text(x, 4.84, str(key), ha="center", va="center", fontsize=17)
    positions = {0: (4.65, 3.75), 1: (2.4, 2.55), 2: (6.9, 2.55),
                 3: (1.3, 1.3), 4: (3.5, 1.3), 5: (5.8, 1.3),
                 6: (8.0, 1.3)}
    for child in range(1, len(VALUES)):
        parent = (child - 1) // 2
        x0, y0 = positions[parent]
        x1, y1 = positions[child]
        ax.plot([x0, x1], [y0, y1], color="#9baebc", linewidth=1.8, zorder=1)
    for i, (x, y) in positions.items():
        ax.add_patch(Circle((x, y), 0.31, facecolor="white",
                            edgecolor=COLORS[i], linewidth=2, zorder=3))
        ax.text(x, y, str(VALUES[i]), ha="center", va="center",
                fontsize=16, zorder=4)
        ax.text(x, y - 0.53, f"i = {i}", ha="center", fontsize=11,
                color=COLORS[i], weight="bold")
    ax.text(8.6, 3.7, "root\ni = 0", ha="center", va="center", color="#006b8f")
    ax.text(0.1, 0.25, "Children: 2i + 1, 2i + 2       Parent: floor((i − 1) / 2), i > 0",
            fontsize=12, color="#173244")
    ax.text(0.1, -0.18, "Parent key ≤ child key.  Not sorted: 5 > 3.  Last key 4 is not the maximum 9.",
            fontsize=11, color="#173244")
    fig.subplots_adjust(left=0.01, right=0.99, bottom=0.02, top=0.99)
    fig.savefig(Path.cwd() / "binary-heap-array-and-tree.png", dpi=110,
                facecolor="white", transparent=False)
    plt.close(fig)
    print("7 dense indices; 6 heap-order edges; index 4 has parent 1; "
          "root minimum 2; last key 4 != maximum 9")


if __name__ == "__main__":
    main()