From ed329d5d85b52ba5e4329499c252be55cedded84 Mon Sep 17 00:00:00 2001 From: Oliver Hofmann Date: Mon, 18 May 2026 14:48:56 +0200 Subject: [PATCH] =?UTF-8?q?Musterl=C3=B6sung=20Praktikum=205?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + praktika/05_b_baum/aufgabe1_graphviz.py | 62 ++++++++++++++ praktika/05_b_baum/aufgabe2_eigenschaften.py | 51 +++++++++++ praktika/05_b_baum/aufgabe3_disk_io.py | 89 ++++++++++++++++++++ 4 files changed, 203 insertions(+) create mode 100644 praktika/05_b_baum/aufgabe1_graphviz.py create mode 100644 praktika/05_b_baum/aufgabe2_eigenschaften.py create mode 100644 praktika/05_b_baum/aufgabe3_disk_io.py diff --git a/.gitignore b/.gitignore index c07c1ca..fc3b790 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ docs/ # OS .DS_Store +/.agents/ diff --git a/praktika/05_b_baum/aufgabe1_graphviz.py b/praktika/05_b_baum/aufgabe1_graphviz.py new file mode 100644 index 0000000..ae26136 --- /dev/null +++ b/praktika/05_b_baum/aufgabe1_graphviz.py @@ -0,0 +1,62 @@ +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 graphviz +from datetime import datetime +from b_tree import BTree +from utils.algo_context import AlgoContext +from utils.algo_array import Array + + +class BTreeVis(BTree): + """BTree-Erweiterung mit Graphviz-Ausgabe als Unterklasse.""" + + def graph_traversal(self): + def _node_rep(node): + label = " | ".join(str(node.value[i]) for i in range(node.n)) + dot.node(str(id(node)), label=label, shape="record", fontname="Arial") + + def _rec(node): + _node_rep(node) + if not node.leaf: + for i in range(node.n + 1): + child = node.children[i] + if child is not None: + dot.edge(str(id(node)), str(id(child))) + _rec(child) + + dot = graphviz.Digraph( + name=f"BTree_m{self.m}", + engine="dot", + node_attr={"fontname": "Arial"}, + format="pdf", + ) + _rec(self.root) + ts = datetime.now().strftime("%Y%m%d_%H%M%S") + dot.render(f"BTree_m{self.m}_{ts}.gv") + + +ctx = AlgoContext() +values = Array.from_file('data/seq1.txt', ctx) +tree = BTreeVis(3, ctx) +for cell in values: + tree.insert(cell) + +# graph_traversal() erzeugt eine DOT-Datei und rendert sie als PDF. +tree.graph_traversal() + +# ── Antworten zu den Beobachtungsfragen ────────────────────────────────────── + +# Wie viele Schlüssel hat die Wurzel? +# Ablesbar direkt aus tree.root.n. +print("Schlüssel in der Wurzel:", tree.root.n) + +# Liegen alle Blätter auf derselben Tiefe? +# Ja – das ist eine B-Baum-Invariante: alle Blätter haben immer +# dieselbe Tiefe, da Splits ausschließlich aufwärts propagieren. +# In der PDF sind alle Blattknoten auf der untersten Ebene. +print("Höhe des Baums:", tree.height()) \ No newline at end of file diff --git a/praktika/05_b_baum/aufgabe2_eigenschaften.py b/praktika/05_b_baum/aufgabe2_eigenschaften.py new file mode 100644 index 0000000..ab13118 --- /dev/null +++ b/praktika/05_b_baum/aufgabe2_eigenschaften.py @@ -0,0 +1,51 @@ +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) + +from b_tree import BTree +from utils.algo_context import AlgoContext +from utils.algo_array import Array + +ctx = AlgoContext() +values = Array.from_file('data/seq3.txt', ctx) + +tree3 = BTree(3, ctx) +tree5 = BTree(5, ctx) +for cell in values: + tree3.insert(cell) + tree5.insert(cell) + +# ── Welche Höhe ergibt sich für Ordnung 3 bzw. 5? ─────────────────────────── +print("Höhe Ordnung 3:", tree3.height()) +print("Höhe Ordnung 5:", tree5.height()) + +# Ordnung 3: Knoten fassen 2–5 Schlüssel → weniger Schlüssel pro Knoten, +# mehr Ebenen nötig. seq3.txt hat 10 000 Werte → Höhe ca. 5–7. +# Ordnung 5: Knoten fassen 4–9 Schlüssel → geringere Höhe, ca. 4–5. + +# ── Traversierung und Sortierungsprüfung ───────────────────────────────────── +results3 = [] +tree3.traversal(lambda v: results3.append(int(str(v)))) +print("Sortiert (Ordnung 3):", results3 == sorted(results3)) + +results5 = [] +tree5.traversal(lambda v: results5.append(int(str(v)))) +print("Sortiert (Ordnung 5):", results5 == sorted(results5)) + +# In-Order-Traversal eines B-Baums liefert immer eine sortierte Folge: +# Die B-Baum-Eigenschaft (linker Teilbaum < Schlüssel < rechter Teilbaum) +# gilt rekursiv für jeden Knoten. + +# ── Knoten mit Wert 0 suchen ───────────────────────────────────────────────── +node3 = tree3.search(0) +node5 = tree5.search(0) +print("Knoten mit 0 (Ordnung 3):", str(node3) if node3 else "nicht gefunden") +print("Knoten mit 0 (Ordnung 5):", str(node5) if node5 else "nicht gefunden") + +# search() gibt den Knoten zurück, der den gesuchten Schlüssel enthält. +# __str__ gibt alle Schlüssel des Knotens aus, z.B.: ( -2 0 3 ) +# Größere Ordnung → mehr Schlüssel pro Knoten → der Knoten mit 0 +# enthält bei m=5 tendenziell mehr Nachbarn als bei m=3. \ No newline at end of file diff --git a/praktika/05_b_baum/aufgabe3_disk_io.py b/praktika/05_b_baum/aufgabe3_disk_io.py new file mode 100644 index 0000000..638484d --- /dev/null +++ b/praktika/05_b_baum/aufgabe3_disk_io.py @@ -0,0 +1,89 @@ +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. \ No newline at end of file