codex/counting-sort-reverse-stable-placement.py

counting-sort-reverse-stable-placement.py
"""Draw every placement of an original labelled-record counting-sort example.

Tested with Python 3.14, matplotlib 3.10.7 and numpy 2.3.5.
Run from _media; writes counting-sort-reverse-stable-placement.png to CWD.
"""

from pathlib import Path

import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle


def main():
    records = [(2, "A"), (0, "B"), (2, "C"), (1, "D"),
               (0, "E"), (3, "F"), (2, "G")]
    counts = [sum(key == k for key, _ in records) for k in range(4)]
    endpoints = []
    total = 0
    for count in counts:
        total += count
        endpoints.append(total)
    output = [None] * len(records)
    colors = ["#2475ad", "#c67c15", "#23855b", "#8259aa"]
    plt.rcParams.update({"font.size": 12, "font.family": "DejaVu Sans"})
    fig = plt.figure(figsize=(11, 7.9), dpi=100, facecolor="white")
    fig.suptitle("Stable placement: scan right to left, fill each range right to left", fontsize=17, y=.97)
    fig.text(.5, .917, "Endpoint update first: E[k] ← E[k] − 1; then write the complete record at output[E[k]].",
             ha="center", fontsize=12)
    ax = fig.add_axes((.025, .025, .95, .855))
    ax.set(xlim=(0, 13.4), ylim=(-.3, 10.8))
    ax.axis("off")

    def box(x, y, record, newest=False):
        face = "white" if record is None else colors[record[0]]
        ax.add_patch(Rectangle((x, y), .86, .52, facecolor=face,
                               alpha=1 if newest or record is None else .27,
                               edgecolor="#c3c9cf", linewidth=1))
        if record is not None:
            ax.text(x + .43, y + .26, f"({record[0]},{record[1]})", ha="center", va="center",
                    fontsize=12, color="white" if newest else "#20313d",
                    fontweight="bold" if newest else "normal")

    ax.text(.15, 10.3, "Input index i", fontsize=11)
    ax.text(.15, 9.7, "Input record", fontsize=11)
    for i, record in enumerate(records):
        x = 5.8 + i
        ax.text(x + .43, 10.35, str(i), ha="center", fontsize=11)
        box(x, 9.45, record, newest=True)
    ax.annotate("", xy=(5.8, 9.1), xytext=(12.6, 9.1), arrowprops={"arrowstyle": "->", "lw": 2})
    ax.text(9.2, 8.77, "Scan order: G, F, E, D, C, B, A", ha="center", fontsize=11)

    headers = [(.15, "Step"), (1.0, "Read i"), (2.1, "Record"),
               (3.8, "E[k]: before → after"), (9.2, "Output slots j = 0, 1, 2, 3, 4, 5, 6")]
    for x, title in headers:
        ax.text(x, 8.12, title, ha="center" if x == 9.2 else "left", fontsize=11, fontweight="bold")
    ax.plot([.15, 12.65], [7.9, 7.9], color="#bcc4cb", lw=1)
    for step, i in enumerate(range(len(records) - 1, -1, -1), start=1):
        record = records[i]
        key = record[0]
        old = endpoints[key]
        endpoints[key] -= 1
        j = endpoints[key]
        output[j] = record
        y = 7.1 - (step - 1) * .83
        for x, label in [(.32, str(step)), (1.2, str(i)), (2.15, f"({key},{record[1]})"),
                         (3.8, f"E[{key}]: {old} → {j}")]:
            ax.text(x, y + .26, label, va="center", fontsize=12)
        for slot, item in enumerate(output):
            box(5.8 + slot, y, item, newest=(slot == j))
    ax.text(.15, .97, "Stable output", fontweight="bold")
    for j, record in enumerate(output):
        box(5.8 + j, .69, record, newest=True)
        ax.text(6.23 + j, .42, str(j), ha="center", fontsize=10)
    ax.text(.15, -.05, "Equal-key arrival order survives:  B before E;  A before C before G.  Letters travel with their keys.",
            fontsize=12)
    fig.savefig(Path.cwd() / (Path(__file__).stem + ".png"), facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()