import sys import os _root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) _l06 = os.path.join(_root, 'vorlesung', 'L06_b_baeume') if _l06 not in sys.path: sys.path.insert(0, _l06) if _root not in sys.path: sys.path.insert(0, _root) import matplotlib.pyplot as plt from b_tree import BTree from b_tree_node import BTreeNode from utils.algo_context import AlgoContext from utils.algo_array import Array def count_loads(node: BTreeNode) -> int: if node is None: return 0 total = node.loaded_count for child in node.children: total += count_loads(child) return total def count_saves(node: BTreeNode) -> int: if node is None: return 0 total = node.saved_count for child in node.children: total += count_saves(child) return total N = 500 orders = [2, 3, 5, 10, 20] stats = {} ctx = AlgoContext() base = Array.random(N, -1000, 1000, ctx) for m in orders: ctx.reset() tree = BTree(m, ctx) for cell in base: tree.insert(cell) stats[m] = { "comparisons": ctx.comparisons, "loads": count_loads(tree.root), "saves": count_saves(tree.root), "height": tree.height(), } print(f"{'m':>4} {'Vergleiche':>12} {'Loads':>8} {'Saves':>8} {'Höhe':>6}") for m, s in stats.items(): print(f"{m:>4} {s['comparisons']:>12} {s['loads']:>8} {s['saves']:>8} {s['height']:>6}") fig, axes = plt.subplots(2, 2, figsize=(10, 8)) labels = ["comparisons", "loads", "saves", "height"] titles = ["Vergleiche", "Disk-Loads", "Disk-Saves", "Baumhöhe"] for ax, label, title in zip(axes.flat, labels, titles): ax.plot(orders, [stats[m][label] for m in orders], marker="o") ax.set_xlabel("Ordnung m") ax.set_ylabel(title) ax.set_title(title) plt.tight_layout() plt.show() # ── Antworten zu den Analysefragen ─────────────────────────────────────────── # Warum sinkt die Zahl der Disk-Zugriffe mit wachsendem m? # Größeres m → mehr Schlüssel pro Knoten → weniger Knoten insgesamt → # geringere Baumhöhe. Jeder Einfügepfad durchläuft weniger Knoten, # also werden weniger load()/save()-Aufrufe ausgelöst. # Warum steigen die internen Vergleiche mit wachsendem m? # Die Suche innerhalb eines Knotens ist immer linear: bis zu 2m-1 Schlüssel # werden verglichen. Die Gesamtkosten pro Einfügung betragen daher # O(m) · O(log_m n) = O(m / log(m) · log n). # Da m/log(m) monoton wächst, steigen die Vergleiche mit jedem größeren m — # nicht ab einem Kipppunkt, sondern kontinuierlich. Das ist der grundlegende # Trade-off: mehr m spart Disk-I/O, kostet aber mehr CPU-Vergleiche. # Was bedeutet das für die Wahl von m bei Blockgröße 4 KB? # Ein Knoten enthält 2m-1 Integer-Schlüssel (je 8 Byte) und 2m Kindzeiger. # Die Kindzeiger sind Blocknummern auf dem Speichermedium (je 8 Byte) — # also keine Speicheradressen, sondern Verweise auf andere Disk-Blöcke. # m wird so gewählt, dass ein Knoten genau in einen 4-KB-Block passt: # (2m-1) · 8 + 2m · 8 = 4096 # 8 · (4m - 1) = 4096 → m ≈ 128 # Disk-I/O-Kosten dominieren gegenüber CPU-Vergleichen → großes m ist optimal.