Musterlösung Praktikum 5

This commit is contained in:
Oliver Hofmann 2026-05-18 14:48:56 +02:00
parent 5aca875fa6
commit ed329d5d85
4 changed files with 203 additions and 0 deletions

1
.gitignore vendored
View File

@ -23,3 +23,4 @@ docs/
# OS
.DS_Store
/.agents/

View File

@ -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())

View File

@ -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 25 Schlüssel → weniger Schlüssel pro Knoten,
# mehr Ebenen nötig. seq3.txt hat 10 000 Werte → Höhe ca. 57.
# Ordnung 5: Knoten fassen 49 Schlüssel → geringere Höhe, ca. 45.
# ── 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.

View File

@ -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.