codex/hash-bucket-collisions.py

hash-bucket-collisions.py
"""Draw verified modulo-seven buckets and the records retained by chaining.

Generate with: make _media/hash-bucket-collisions.png
Writes an opaque white PNG with this basename to the current directory.
Tested with Python 3.14.4 and Matplotlib 3.10.7.
"""

from pathlib import Path

import matplotlib.pyplot as plt
from matplotlib.patches import FancyArrowPatch, FancyBboxPatch


def main():
    keys = [0, 2, 3, 10, 11, 17, 24]
    buckets = [[] for _ in range(7)]
    for key in keys:
        buckets[key % 7].append(key)
    assert [key % 7 for key in keys] == [0, 2, 3, 3, 4, 3, 3]
    assert buckets[3] == [3, 10, 17, 24]
    assert sorted(key for bucket in buckets for key in bucket) == sorted(keys)
    assert 10 in buckets[10 % 7] and 31 not in buckets[31 % 7]
    assert 31 % 7 == 3

    blue, orange, gray = "#246b9c", "#b95122", "#62717d"
    plt.rcParams.update({"font.size": 12, "figure.facecolor": "white",
                         "axes.facecolor": "white", "savefig.facecolor": "white"})
    fig, ax = plt.subplots(figsize=(9.0, 5.2), dpi=120)
    fig.subplots_adjust(left=0.03, right=0.98, top=0.83, bottom=0.15)
    ax.set(xlim=(0, 10), ylim=(-0.65, 7.25))
    ax.axis("off")
    fig.suptitle(r"Same bucket does not mean same key: $h(k)=k\;\mathrm{mod}\;7$",
                 fontsize=16, y=0.96)
    ax.text(0.9, 7.0, "Stored keys", ha="center", weight="bold")
    ax.text(4.2, 7.0, "Bucket", ha="center", weight="bold")
    ax.text(7.3, 7.0, "Records kept by chaining", ha="center", weight="bold")

    def box(x, y, label, color, width=0.65):
        patch = FancyBboxPatch((x - width / 2, y - 0.27), width, 0.54,
                               boxstyle="round,pad=0.04", linewidth=1.5,
                               edgecolor=color, facecolor="white", zorder=3)
        ax.add_patch(patch)
        ax.text(x, y, str(label), ha="center", va="center", color=color,
                weight="bold", zorder=4)

    for row, key in enumerate(keys):
        y = 6 - row
        bucket = key % 7
        color = orange if bucket == 3 else blue
        box(0.9, y, key, color)
        ax.add_patch(FancyArrowPatch((1.35, y), (3.8, 6 - bucket),
                                    arrowstyle="->", mutation_scale=12,
                                    linewidth=1.4, color=color, alpha=0.78))

    for index, chain in enumerate(buckets):
        y = 6 - index
        color = orange if index == 3 else blue
        box(4.2, y, index, color)
        if not chain:
            ax.text(5.45, y, "empty", ha="center", va="center", color=gray,
                    fontsize=11)
            continue
        previous = 4.2
        for column, key in enumerate(chain):
            x = 5.45 + 1.2 * column
            box(x, y, key, color)
            ax.add_patch(FancyArrowPatch((previous + 0.4, y), (x - 0.4, y),
                                        arrowstyle="->", mutation_scale=11,
                                        linewidth=1.3, color=color))
            previous = x

    fig.text(0.5, 0.085, "Find 10: bucket 3, compare 3 then 10; keep both records.",
             ha="center", fontsize=12)
    fig.text(0.5, 0.035, "Find 31: bucket 3, compare all four keys, report absent.",
             ha="center", fontsize=12)
    fig.savefig(Path.cwd() / "hash-bucket-collisions.png", transparent=False)
    plt.close(fig)
    print(f"Bucket counts: {[len(bucket) for bucket in buckets]}; "
          "10 found after 2 comparisons; 31 absent after 4 comparisons")


if __name__ == "__main__":
    main()