From 5aca875fa6fd019598ff593ffd8462df0e9363b4 Mon Sep 17 00:00:00 2001 From: Oliver Hofmann Date: Mon, 11 May 2026 15:17:34 +0200 Subject: [PATCH] =?UTF-8?q?Musterl=C3=B6sung=20Praktikum=204?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- praktika/04_bin_baum/aufgabe1_graphviz.py | 38 +++++++++ .../04_bin_baum/aufgabe2_traversierungen.py | 60 +++++++++++++ praktika/04_bin_baum/aufgabe3_entartung.py | 57 +++++++++++++ praktika/04_bin_baum/aufgabe4_avl_delete.py | 59 +++++++++++++ praktika/04_bin_baum/aufgabe5_analyse.py | 85 +++++++++++++++++++ 5 files changed, 299 insertions(+) create mode 100644 praktika/04_bin_baum/aufgabe1_graphviz.py create mode 100644 praktika/04_bin_baum/aufgabe2_traversierungen.py create mode 100644 praktika/04_bin_baum/aufgabe3_entartung.py create mode 100644 praktika/04_bin_baum/aufgabe4_avl_delete.py create mode 100644 praktika/04_bin_baum/aufgabe5_analyse.py diff --git a/praktika/04_bin_baum/aufgabe1_graphviz.py b/praktika/04_bin_baum/aufgabe1_graphviz.py new file mode 100644 index 0000000..7193d9a --- /dev/null +++ b/praktika/04_bin_baum/aufgabe1_graphviz.py @@ -0,0 +1,38 @@ +import sys +import os +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..')) + +from utils.algo_context import AlgoContext +from utils.algo_array import Array +from vorlesung.L05_binaere_baeume.bin_tree import BinaryTree + +ctx = AlgoContext() +values = Array.from_file('data/seq0.txt', ctx) +tree = BinaryTree(ctx) +for cell in values: + tree.insert(cell.value) + +# graph_traversal() erzeugt eine DOT-Datei und rendert sie als PDF. +# Die Datei landet im Verzeichnis, aus dem das Skript gestartet wird. +tree.graph_traversal() + +# ── Antworten zu den Beobachtungsfragen ────────────────────────────────────── + +# Welcher Knoten ist die Wurzel? +# Das erste eingefügte Element wird immer die Wurzel, da der Baum anfangs +# leer ist. Hier ist das -59 (erste Zeile von seq0.txt). +print("Wurzel:", tree.root.value) # -59 + +# Wie viele Blattknoten hat der Baum? +# Ein Blattknoten hat weder ein linkes noch ein rechtes Kind. +blattknoten = [] +def merke_blatt(node): + if node.left is None and node.right is None: + blattknoten.append(node.value) +tree.in_order_traversal(merke_blatt) +print("Blattknoten:", blattknoten) # [-77, -50, 15, 48, 51, 58] → 6 Blätter + +# Wie groß ist die Höhe des Baums? +# height() zählt rekursiv die längste Kante von der Wurzel zu einem Blatt. +# Mit 14 Elementen und zufälliger Reihenfolge ergibt sich hier Höhe 6. +print("Höhe:", tree.root.height()) # 6 diff --git a/praktika/04_bin_baum/aufgabe2_traversierungen.py b/praktika/04_bin_baum/aufgabe2_traversierungen.py new file mode 100644 index 0000000..69a3e53 --- /dev/null +++ b/praktika/04_bin_baum/aufgabe2_traversierungen.py @@ -0,0 +1,60 @@ +import sys +import os +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..')) + +from utils.algo_context import AlgoContext +from utils.algo_array import Array +from vorlesung.L05_binaere_baeume.bin_tree import BinaryTree + +ctx = AlgoContext() +values = Array.from_file('data/seq0.txt', ctx) +tree = BinaryTree(ctx) +for cell in values: + tree.insert(cell.value) + +# ── in_order_traversal ─────────────────────────────────────────────────────── +print("In-Order:") +tree.in_order_traversal(lambda node: print(node.value, end=" ")) +print() +# Ausgabe: -87 -77 -59 -50 14 15 34 46 47 48 50 51 52 58 +# +# Warum sortiert? +# Die BST-Eigenschaft garantiert: linker Teilbaum < Wurzel < rechter Teilbaum. +# In-Order besucht genau in dieser Reihenfolge (links → Wurzel → rechts), +# was rekursiv angewendet eine aufsteigende Sortierung ergibt. +# In-Order-Traversal eines BST entspricht damit einer Sortierung der Schlüssel. + +# ── level_order_traversal ──────────────────────────────────────────────────── +print("\nLevel-Order:") +tree.level_order_traversal(lambda node, level: print(f"Ebene {level}: {node.value}")) +# Ausgabe: Wurzel zuerst, dann alle Knoten der Ebene 1, dann Ebene 2, … +# +# Was zeigt level_order, was in_order nicht zeigt? +# level_order_traversal entspricht einer Breitensuche (BFS). Sie zeigt, +# auf welcher Tiefenebene sich jeder Knoten befindet, und damit die +# tatsächliche Struktur des Baums – nicht nur die sortierte Schlüsselfolge. +# Zwei BSTs mit denselben Schlüsseln liefern identische in_order-Ausgaben, +# aber unterschiedliche level_order-Ausgaben, wenn ihre Struktur verschieden ist. + +# ── graph_traversal ────────────────────────────────────────────────────────── +# Relevante Stelle in bin_tree.py: +# +# def graph_traversal(self): +# def define_node(node, level, line): +# node.graphviz_rep(level, line, dot) ← Knoten positionieren +# ... +# self.tree_structure_traversal(define_node) ← Traversierung +# _rec(self.root) ← Kanten eintragen +# +# graph_traversal nutzt intern tree_structure_traversal – eine In-Order- +# Traversal, die zusätzlich zu jedem Knoten seine Tiefe (level) und seine +# In-Order-Position (line) mitliefert. +# +# level → y-Koordinate im Graphviz-Layout: je tiefer im Baum, desto weiter +# unten in der Abbildung (negiert: pos=f"{col},{-row}!") +# line → x-Koordinate: die In-Order-Position ergibt automatisch eine +# kollisionsfreie horizontale Anordnung ohne überlappende Knoten. +# +# tree_structure_traversal existiert also nicht als eigenständige Ausgabe- +# Traversal, sondern als Hilfsmittel für die Positionsberechnung in graph_traversal. +# Die Traversierungsreihenfolge ist identisch mit in_order_traversal. diff --git a/praktika/04_bin_baum/aufgabe3_entartung.py b/praktika/04_bin_baum/aufgabe3_entartung.py new file mode 100644 index 0000000..1f8b581 --- /dev/null +++ b/praktika/04_bin_baum/aufgabe3_entartung.py @@ -0,0 +1,57 @@ +import sys +import os +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..')) + +from utils.algo_context import AlgoContext +from vorlesung.L05_binaere_baeume.bin_tree import BinaryTree + +# ── Entarteter BST: sortierte Eingabe ──────────────────────────────────────── + +ctx = AlgoContext() +tree_sorted = BinaryTree(ctx) +for v in range(1, 9): + tree_sorted.insert(v) + +tree_sorted.graph_traversal() # → entarteter Baum sichtbar: eine gerade Kette + +print("Höhe (sortierte Eingabe):", tree_sorted.root.height()) # 8 + +ctx.comparisons = 0 +tree_sorted.search(8) +print("Vergleiche search(8) im entarteten Baum:", ctx.comparisons) # 16 +# Jeder Knoten (1 bis 8) erfordert zwei Vergleiche: value < node? (nein) +# und value > node? (ja, bis der gesuchte Knoten gefunden ist). +# Das entspricht einer linearen Suche: O(n). + +# ── Referenz: ausgewogene Einfügereihenfolge ───────────────────────────────── + +ctx2 = AlgoContext() +tree_balanced = BinaryTree(ctx2) +for v in [4, 2, 6, 1, 3, 5, 7, 8]: # Mitte zuerst → ausgewogener Baum + tree_balanced.insert(v) + +print("\nHöhe (ausgewogene Eingabe):", tree_balanced.root.height()) # 4 + +ctx2.comparisons = 0 +tree_balanced.search(8) +print("Vergleiche search(8) im ausgewogenen Baum:", ctx2.comparisons) # 8 + +# ── Antworten zu den Analysefragen ─────────────────────────────────────────── + +# Wie viele Vergleiche benötigt search(8) im entarteten Baum? +# 16 – der Baum entartet zur verketteten Liste, jeder Knoten wird besucht. + +# Wie viele im ausgewogenen Baum mit 8 Knoten? +# 8 – die Höhe beträgt 4, jede Ebene kostet 2 Vergleiche (< und >). + +# Welche Eingabereihenfolge erzeugt immer einen ausgewogenen BST? +# Median-First: stets den Median der aktuellen Teilfolge zuerst einfügen. +# Das setzt voraus, dass alle Elemente bereits bekannt sind – bei dynamischen +# Einfügungen (ein Element nach dem anderen, ohne Vorwissen) ist das nicht +# möglich. Deshalb reicht es nicht, nur die Einfügereihenfolge zu steuern. + +# Was muss eine Datenstruktur garantieren? +# Sie muss die Baumhöhe nach jeder Einfüge- oder Löschoperation automatisch +# auf O(log n) begrenzen – unabhängig von der Eingabereihenfolge. +# AVL-Bäume erreichen das, indem sie nach jeder Operation den Balance-Faktor +# prüfen und bei Bedarf Rotationen durchführen. diff --git a/praktika/04_bin_baum/aufgabe4_avl_delete.py b/praktika/04_bin_baum/aufgabe4_avl_delete.py new file mode 100644 index 0000000..03fc0bc --- /dev/null +++ b/praktika/04_bin_baum/aufgabe4_avl_delete.py @@ -0,0 +1,59 @@ +import sys +import os +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..')) + +from utils.algo_context import AlgoContext +from vorlesung.L05_binaere_baeume.avl_tree import AVLTree + +ctx = AlgoContext() +tree = AVLTree(ctx) +for v in [10, 5, 15, 3, 7, 12, 20, 1, 4, 6, 8]: + tree.insert(v) + +print("Ausgangszustand:") +tree.in_order_traversal(lambda n: print(n.value, end=" ")) +print() +tree.graph_traversal() + +# ── Fall 1: Blattknoten löschen (1 ist Blatt) ──────────────────────────────── +tree.delete(1) +print("\nNach Löschen von 1 (Blatt):") +tree.in_order_traversal(lambda n: print(n.value, end=" ")) +print() +tree.graph_traversal() + +# ── Fall 2: Knoten mit einem Kind löschen (3 hat jetzt nur noch Kind 4) ────── +tree.delete(3) +print("\nNach Löschen von 3 (ein Kind):") +tree.in_order_traversal(lambda n: print(n.value, end=" ")) +print() +tree.graph_traversal() + +# ── Fall 3: Knoten mit zwei Kindern löschen (10 hat linkes Kind 5, rechtes 15) +tree.delete(10) +print("\nNach Löschen von 10 (zwei Kinder):") +tree.in_order_traversal(lambda n: print(n.value, end=" ")) +print() +tree.graph_traversal() +# Inorder-Nachfolger von 10 ist 12 (kleinstes Element im rechten Teilbaum). +# 12 übernimmt den Platz von 10, anschließend wird rebalanciert. + +# ── Antworten zu den Verständnisfragen ─────────────────────────────────────── + +# Warum muss node.parent = parent nach dem Löschen neu gesetzt werden? +# +# BinaryTree kennt keine parent-Zeiger — delete() gibt lediglich +# (nachfolgender_knoten, elternknoten) als plain-Python-Tupel zurück. +# AVLTree pflegt parent-Zeiger, weil balance() sie benötigt, um den Pfad +# von einem Knoten aufwärts zur Wurzel zu traversieren. +# Ohne die Aktualisierung würde balance() an einem veralteten parent-Zeiger +# hängen und die Rebalancierung unvollständig oder fehlerhaft ausführen. + +# Warum beginnt die Rebalancierung mit balance(parent) statt balance(node)? +# +# Das Löschen verändert die Höhe des Teilbaums, in dem der Knoten stand. +# Diese Höhenänderung kann den Balance-Faktor des Elternknotens (und aller +# Vorfahren bis zur Wurzel) aus dem Gleichgewicht bringen. +# Der gelöschte (oder ersetzte) Knoten selbst ist entweder weg oder ein Blatt +# — er ist kein sinnvoller Startpunkt für die Aufwärts-Traversal. +# balance(parent) startet genau dort, wo die Höhenänderung zuerst spürbar ist. diff --git a/praktika/04_bin_baum/aufgabe5_analyse.py b/praktika/04_bin_baum/aufgabe5_analyse.py new file mode 100644 index 0000000..57dd429 --- /dev/null +++ b/praktika/04_bin_baum/aufgabe5_analyse.py @@ -0,0 +1,85 @@ +import sys +import os +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..')) + +import random +from utils.algo_context import AlgoContext +from vorlesung.L05_binaere_baeume.bin_tree import BinaryTree +from vorlesung.L05_binaere_baeume.avl_tree import AVLTree + +# ── Theoretische Herleitung ─────────────────────────────────────────────────── + +# AVL-Baum / ausgeglichener BST +# Bei jedem Schritt der Suche wird genau ein Teilbaum mit (ungefähr) halber +# Größe weiterverfolgt: +# +# T(n) = T(n/2) + O(1) +# +# Master-Theorem: a=1, b=2, f(n)=O(1) +# log_b(a) = log_2(1) = 0 → f(n) = O(n^0) = O(n^log_b(a)) +# → Fall 2: T(n) = O(log n) + +# BST – Worst Case (sortierte Eingabe, entarteter Baum) +# Der Baum ist eine verkettete Liste; jeder Schritt reduziert das Problem um 1: +# +# T(n) = T(n-1) + O(1) +# +# Kein Master-Theorem (b > 1 gefordert). Direkte Auflösung durch Entfalten: +# T(n) = T(n-2) + 2·c = … = T(0) + n·c → O(n) + +# BST – zufällige Eingabe (Expected Case) +# Bei zufälliger Einfügereihenfolge teilt die Wurzel den Schlüsselraum im +# Erwartungswert in zwei ungefähr gleich große Hälften – analog zum +# durchschnittlichen Fall bei Quicksort. Daraus folgt eine erwartete Höhe +# von O(log n), empirisch ca. 2,5 · log₂(n). Ein formaler Beweis erfordert +# eine Wahrscheinlichkeitsanalyse über alle n! Permutationen. + +# ── Empirische Messung ─────────────────────────────────────────────────────── + +SIZES = [10, 100, 500] + +print(f"{'n':>5} {'BST zufällig':>14} {'BST sortiert':>14} {'AVL sortiert':>14}") +print("-" * 55) + +for n in SIZES: + # BST, zufällige Eingabe + ctx = AlgoContext() + tree = BinaryTree(ctx) + values = random.sample(range(n * 10), n) + for v in values: + tree.insert(v) + ctx.comparisons = 0 + tree.search(values[-1]) + cmp_random = ctx.comparisons + + # BST, sortierte Eingabe + ctx = AlgoContext() + tree = BinaryTree(ctx) + for v in range(1, n + 1): + tree.insert(v) + ctx.comparisons = 0 + tree.search(n) + cmp_sorted = ctx.comparisons + + # AVL-Baum, sortierte Eingabe + ctx = AlgoContext() + tree = AVLTree(ctx) + for v in range(1, n + 1): + tree.insert(v) + ctx.comparisons = 0 + tree.search(n) + cmp_avl = ctx.comparisons + + print(f"{n:>5} {cmp_random:>14} {cmp_sorted:>14} {cmp_avl:>14}") + +# ── Abschlussfrage ──────────────────────────────────────────────────────────── +# +# Stimmen die Messwerte mit den theoretischen Komplexitätsklassen überein? +# +# BST sortiert wächst linear mit n (2·n Vergleiche für das letzte Element), +# was die hergeleitete O(n)-Komplexität bestätigt. +# +# BST zufällig und AVL sortiert wachsen logarithmisch: für n=500 liegen die +# Vergleiche bei ca. 20 bzw. 18 – weit unter dem linearen Worst Case. +# Das bestätigt O(log n) für beide, wobei der AVL-Baum die Schranke +# garantiert und der zufällige BST sie nur im Erwartungswert einhält.