Musterlösung Praktikum 4
This commit is contained in:
parent
064d392d98
commit
5aca875fa6
38
praktika/04_bin_baum/aufgabe1_graphviz.py
Normal file
38
praktika/04_bin_baum/aufgabe1_graphviz.py
Normal file
@ -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
|
||||
60
praktika/04_bin_baum/aufgabe2_traversierungen.py
Normal file
60
praktika/04_bin_baum/aufgabe2_traversierungen.py
Normal file
@ -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.
|
||||
57
praktika/04_bin_baum/aufgabe3_entartung.py
Normal file
57
praktika/04_bin_baum/aufgabe3_entartung.py
Normal file
@ -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.
|
||||
59
praktika/04_bin_baum/aufgabe4_avl_delete.py
Normal file
59
praktika/04_bin_baum/aufgabe4_avl_delete.py
Normal file
@ -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.
|
||||
85
praktika/04_bin_baum/aufgabe5_analyse.py
Normal file
85
praktika/04_bin_baum/aufgabe5_analyse.py
Normal file
@ -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.
|
||||
Loading…
x
Reference in New Issue
Block a user