Compare commits

...
18 Commits
57 changed files with 2259 additions and 482 deletions

No files matched your search

+2
View File
@@ -19,6 +19,8 @@ build/
CLAUDE.md
.claude/
specs/
docs/
# OS
.DS_Store
/.agents/
+157
View File
@@ -0,0 +1,157 @@
# Algorithmen und Datenstrukturen – SoSe 26
Dieses Repository enthält den Code zur Vorlesung. Neben den Vorlesungsbeispielen
bietet es ein kleines Framework, mit dem ihr eure eigenen Algorithmen
**automatisch auf Operationen messen** könnt – ohne den Algorithmus selbst
anfassen zu müssen.
---
## Wozu das Framework?
In der Vorlesung analysieren wir Algorithmen theoretisch mit der O-Notation.
Das Framework erlaubt euch, diese Theorie **empirisch zu überprüfen**: Ihr
implementiert euren Algorithmus genauso wie in Python üblich – nur dass ihr
statt `list` und `int` die Wrapper-Typen `Array` und `Int` verwendet. Das
Framework zählt dann im Hintergrund automatisch Vergleiche, Lese- und
Schreibzugriffe sowie arithmetische Operationen.
---
## Schnellstart
### 1. Kontext anlegen
```python
from utils.algo_context import AlgoContext
from utils.algo_array import Array
from utils.algo_range import Range
ctx = AlgoContext()
```
`AlgoContext` ist der Zähler. Ihr legt ihn einmal an und gebt ihn an eure
Datenstrukturen weiter.
### 2. Array befüllen
```python
z = Array.random(10, -100, 100, ctx) # 10 Zufallszahlen zwischen -100 und 100
# oder:
z = Array([5, 3, 8, 1, 9, 2], ctx) # eigene Werte
```
### 3. Algorithmus schreiben
Schreibt euren Algorithmus wie gewohnt. `Range` ist ein Drop-in-Ersatz für
`range` und liefert ebenfalls `Int`-Objekte – allerdings ohne Verbindung zum
Zähler. Dadurch wird **Indexarithmetik** (`j - 1`, `i + 1`) nicht mitgezählt,
während Operationen auf Array-*Inhalten* (die ja den echten `ctx` tragen) weiterhin
erfasst werden. Das entspricht der üblichen Konvention: gezählt werden nur
Operationen auf den Daten, nicht die Schleifensteuerung.
```python
def insertion_sort(z: Array, ctx: AlgoContext):
for i in Range(1, len(z)):
elem = z[i] # liest z[i] (1 Lesezugriff)
j = int(i) - 1
while j >= 0 and z[j] > elem: # vergleicht (1 Vergleich pro Schritt)
z[j + 1] = z[j] # schreibt (1 Schreibzugriff)
j -= 1
z[j + 1] = elem
```
### 4. Ausführen und Ergebnis ablesen
```python
insertion_sort(z, ctx)
print(ctx.comparisons) # Anzahl Vergleiche
print(ctx.reads) # Lesezugriffe
print(ctx.writes) # Schreibzugriffe
print(ctx.additions) # Additionen
```
Oder kompakt:
```python
print(ctx.summary())
```
### 5. Komplexität über mehrere Eingabegrößen plotten
```python
def analyze(sort_func, sizes):
ctx = AlgoContext()
for size in sizes:
ctx.reset()
z = Array.random(size, -100, 100, ctx)
sort_func(z, ctx)
ctx.save_stats(size)
ctx.plot_stats(["comparisons", "writes"])
analyze(insertion_sort, range(10, 201, 10))
```
Das öffnet ein Matplotlib-Fenster mit dem Verlauf der gezählten Operationen
über die Eingabegröße – ihr seht direkt, ob euer Algorithmus z.B. quadratisch oder
linear wächst.
---
## Verfügbare Zähler
| Attribut | Was wird gezählt |
|-----------------|-------------------------------------------|
| `comparisons` | `<`, `>`, `<=`, `>=`, `==`, `!=` |
| `reads` | Lesezugriffe auf `Int`-Werte |
| `writes` | Schreibzugriffe (Zuweisung, `swap`) |
| `additions` | `+`, `+=` |
| `subtractions` | `-`, `-=` |
| `multiplications` | `*`, `*=` |
| `divisions` | `/`, `//`, `/=` |
| `bitops` | `&`, `\|`, `^`, `<<`, `>>` |
---
## Struktur des Repositories
```
utils/ Framework-Dateien (AlgoContext, Int, Array, Range)
vorlesung/ Vorlesungsbeispiele, geordnet nach Lektionsnummer
praktika/ Aufgabenstellungen der Praktika
playground/ Eure eigenen Abgaben und Experimente
data/ Beispieldatensätze
```
Die Vorlesungsbeispiele in `vorlesung/` sind vollständig mit dem Framework
instrumentiert und können als Vorlage für eigene Implementierungen dienen.
## Datenpfade
Die Demodaten in `data/` werden über `path()` aus `utils.algo_path` adressiert.
Die Funktion liefert immer den absoluten Pfad relativ zum Projektverzeichnis –
unabhängig davon, aus welchem Verzeichnis ihr das Skript startet oder welche IDE
ihr verwendet:
```python
from utils.algo_path import path
z = Array.from_file(path("data/seq0.txt"), ctx)
```
## PriorityQueue
Für Graphalgorithmen (z.B. Dijkstra) steht eine fertige `PriorityQueue` bereit.
Sie basiert intern auf einem Heap – das Prinzip habt ihr in der Vorlesung zu
Heap Sort kennengelernt. Als Abkürzung dürft ihr die fertige Klasse verwenden:
```python
from utils.algo_priority_queue import PriorityQueue
pq = PriorityQueue()
pq.add_or_update("A", 0) # Knoten mit Priorität (Distanz) eintragen
pq.add_or_update("B", 5)
pq.add_or_update("B", 2) # Priorität aktualisieren
node, dist = pq.pop() # liefert ("A", 0)
```
+146
View File
@@ -0,0 +1,146 @@
from utils.algo_context import AlgoContext
from utils.algo_array import Array
from utils.algo_int import Int
from utils.algo_range import Range
def max_sequence_1(z: Array, ctx: AlgoContext):
"""O(n³) – dreifach verschachtelte Schleife."""
n = len(z)
m = Int(float('-inf'), ctx)
s = Int(0, ctx)
l = Int(0, ctx)
r = Int(0, ctx)
for i in Range(n):
for j in Range(int(i), n):
s.set(0)
for k in Range(int(i), int(j)):
s += z[k]
if s > m:
m.set(s)
l.set(Int(int(i), ctx))
r.set(Int(int(j), ctx))
return m, l, r
def max_sequence_2(z: Array, ctx: AlgoContext):
"""O(n²) – doppelt verschachtelte Schleife."""
n = len(z)
m = Int(float('-inf'), ctx)
s = Int(0, ctx)
left = Int(0, ctx)
right = Int(0, ctx)
for i in Range(n):
s.set(0)
for j in Range(int(i), n):
s += z[j]
if s > m:
m.set(s)
left.set(Int(int(i), ctx))
right.set(Int(int(j), ctx))
return m, left, right
def _finde_zwischen(z: Array, ctx: AlgoContext, l: int, m: int, r: int):
"""Hilfsfunktion für max_sequence_3: findet die maximale Teilsumme,
die die Mitte m überschreitet (3. Fall von Teile-und-Herrsche)."""
links_max = Int(float('-inf'), ctx)
s = Int(0, ctx)
links = Int(l, ctx)
for i in Range(m, l - 1, -1):
s += z[i]
if s > links_max:
links_max.set(s)
links.set(Int(int(i), ctx))
rechts_max = Int(float('-inf'), ctx)
s.set(0)
rechts = Int(m + 1, ctx)
for i in Range(m + 1, r + 1):
s += z[i]
if s > rechts_max:
rechts_max.set(s)
rechts.set(Int(int(i), ctx))
return links_max + rechts_max, links, rechts
def max_sequence_3(z: Array, ctx: AlgoContext, l: int = None, r: int = None):
"""O(n ld n) – Teile-und-Herrsche."""
if l is None:
l = 0
if r is None:
r = len(z) - 1
if l == r:
return z[l], Int(l, ctx), Int(r, ctx)
m = (l + r) // 2
links_max, links_l, links_r = max_sequence_3(z, ctx, l, m)
rechts_max, rechts_l, rechts_r = max_sequence_3(z, ctx, m + 1, r)
zwi_max, zwi_l, zwi_r = _finde_zwischen(z, ctx, l, m, r)
if links_max >= rechts_max and links_max >= zwi_max:
return links_max, links_l, links_r
if rechts_max >= links_max and rechts_max >= zwi_max:
return rechts_max, rechts_l, rechts_r
return zwi_max, zwi_l, zwi_r
def max_sequence_4(z: Array, ctx: AlgoContext):
"""O(n) – Kadane's Algorithmus."""
n = len(z)
m = Int(float('-inf'), ctx)
cur_sum = Int(0, ctx)
cur_left = Int(0, ctx)
left = Int(0, ctx)
right = Int(0, ctx)
for i in Range(n):
cur_sum += z[i]
if cur_sum > m:
m.set(cur_sum)
left.set(cur_left)
right.set(Int(int(i), ctx))
if cur_sum < 0:
cur_sum.set(0)
cur_left.set(Int(int(i) + 1, ctx))
return m, left, right
def example(max_sequence_func):
ctx = AlgoContext()
data = [-59, 52, 46, 14, -50, 58, -87, -77, 34, 15]
print(data)
z = Array(data, ctx)
m, l, r = max_sequence_func(z, ctx)
print(m, l, r)
def seq(filename, max_sequence_func):
ctx = AlgoContext()
z = Array.from_file(filename, ctx)
m, l, r = max_sequence_func(z, ctx)
print(m, l, r)
def analyze_complexity(max_sequence_func, sizes):
ctx = AlgoContext()
for size in sizes:
ctx.reset()
z = Array.random(size, -100, 100, ctx)
max_sequence_func(z, ctx)
ctx.save_stats(size)
ctx.plot_stats(["additions"])
if __name__ == '__main__':
example(max_sequence_3)
for filename in ["data/seq0.txt", "data/seq1.txt"]:
print(filename)
seq(filename, max_sequence_3)
analyze_complexity(max_sequence_3, [10, 20, 30, 40, 50, 60, 70, 80, 90, 100])
+47
View File
@@ -0,0 +1,47 @@
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..'))
import matplotlib.pyplot as plt
from utils.algo_context import AlgoContext
from utils.algo_array import Array
from vorlesung.L03_fortgeschrittenes_sortieren.quick_sorting import quick_sort
from vorlesung.L03_fortgeschrittenes_sortieren.heap_sorting import heap_sort
from merge_sorting import merge_sort
SIZES = list(range(10, 210, 10))
algorithms = [
("Quicksort", quick_sort),
("Heapsort", heap_sort),
("Merge Sort", merge_sort),
]
comparisons = {name: [] for name, _ in algorithms}
writes = {name: [] for name, _ in algorithms}
for name, sort_func in algorithms:
ctx = AlgoContext()
for n in SIZES:
ctx.reset()
z = Array.random(n, -1000, 1000, ctx)
sort_func(z, ctx)
comparisons[name].append(ctx.comparisons)
writes[name].append(ctx.writes)
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8), sharex=True)
for name in comparisons:
ax1.plot(SIZES, comparisons[name], label=name, marker='o', markersize=3)
ax1.set_ylabel("Vergleiche")
ax1.set_title("Quicksort vs. Heapsort vs. Merge Sort")
ax1.legend()
for name in writes:
ax2.plot(SIZES, writes[name], label=name, marker='o', markersize=3)
ax2.set_ylabel("Schreibzugriffe")
ax2.set_xlabel("n")
ax2.legend()
plt.tight_layout()
plt.show()
+42
View File
@@ -0,0 +1,42 @@
import random
import pygame
from utils.algo_game import Game
from utils.algo_context import AlgoContext
from utils.algo_array import Array
from merge_sorting import merge_sort_stepwise
WHITE = (255, 255, 255)
BLUE = (0, 0, 255)
class MergeGame(Game):
def __init__(self):
super().__init__("Merge Game", fps=10, size=(400, 400))
random.seed()
l = list(range(1, 101))
random.shuffle(l)
self.ctx = AlgoContext()
self.z = Array(l, self.ctx)
self.finished = False
self.sort_generator = merge_sort_stepwise(self.z, self.ctx)
def update_game(self):
if not self.finished:
try:
next(self.sort_generator)
except StopIteration:
self.finished = True
return True
def draw_game(self):
self.screen.fill(WHITE)
for i, cell in enumerate(self.z):
x = 50 + i*3
y = 350 - cell.value * 3
pygame.draw.rect(self.screen, BLUE, (x, y, 3, 3))
super().draw_game()
if __name__ == "__main__":
s = MergeGame()
s.run()
+71
View File
@@ -0,0 +1,71 @@
from utils.algo_array import Array
from utils.algo_context import AlgoContext
from utils.algo_range import Range
from time import perf_counter as pfc
def merge_sort_stepwise(z: Array, ctx: AlgoContext, l=None, r=None, buffer=None):
if l is None:
l = 0
if r is None:
r = len(z) - 1
if buffer is None:
buffer = Array([0] * len(z), ctx)
if l < r:
m = (l + r) // 2
yield from merge_sort_stepwise(z, ctx, l, m, buffer)
yield from merge_sort_stepwise(z, ctx, m + 1, r, buffer)
yield from merge(z, l, m, r, buffer)
def merge(z: Array, l: int, m: int, r: int, buffer: Array):
i, j, k = l, m + 1, l
while i <= m and j <= r:
if z[i] <= z[j]:
buffer[k] = z[i]
i += 1
else:
buffer[k] = z[j]
j += 1
k += 1
while i <= m:
buffer[k] = z[i]
i += 1
k += 1
while j <= r:
buffer[k] = z[j]
j += 1
k += 1
for k in Range(l, r + 1):
z[k] = buffer[k]
yield z
def merge_sort(z: Array, ctx: AlgoContext):
for _ in merge_sort_stepwise(z, ctx):
pass
def sort_file(filename, sort_func):
ctx = AlgoContext()
z = Array.from_file(filename, ctx)
sort_func(z, ctx)
return z
def analyze_complexity(sort_func, sizes, presorted=False):
ctx = AlgoContext()
for size in sizes:
ctx.reset()
if presorted:
z = Array.sorted(size, ctx)
else:
z = Array.random(size, -100, 100, ctx)
sort_func(z, ctx)
ctx.save_stats(size)
ctx.plot_stats(["comparisons", "writes"])
if __name__ == '__main__':
analyze_complexity(merge_sort, [10, 20, 30, 40, 50, 60, 70, 80, 90, 100])
for filename in ["data/seq0.txt", "data/seq1.txt", "data/seq2.txt", "data/seq3.txt"]:
print(filename)
start = pfc()
print(sort_file(filename, merge_sort))
print(f"Dauer: {pfc() - start:.4f}s")
@@ -0,0 +1,127 @@
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_int import Int
class PriorityQueue:
"""Max-Heap-basierte Vorrang-Warteschlange mit Operationszählung.
Höhere Prioritätswerte werden zuerst entnommen. Prioritäten werden als
Int-Objekte gespeichert, sodass Vergleiche automatisch im AlgoContext
gezählt werden. Swaps zählen je 2 reads + 2 writes.
"""
def __init__(self, ctx: AlgoContext = None):
self._ctx = ctx if ctx is not None else AlgoContext()
self._heap = [] # Liste von (Int priority, item)
def insert(self, item, priority):
"""Fügt item mit der gegebenen Priorität ein."""
self._heap.append((Int(priority, self._ctx), item))
self._ctx.writes += 1
self._sift_up(len(self._heap) - 1)
def pop(self):
"""Entfernt das Element mit der höchsten Priorität und gibt es zurück."""
if self.is_empty():
raise IndexError("Priority queue is empty")
self._swap(0, len(self._heap) - 1)
self._ctx.reads += 1
_, item = self._heap.pop()
if self._heap:
self._sift_down(0)
return item
def peek(self):
"""Gibt das Element mit der höchsten Priorität zurück (ohne Entfernung)."""
if self.is_empty():
raise IndexError("Priority queue is empty")
self._ctx.reads += 1
return self._heap[0][1]
def is_empty(self):
"""Gibt True zurück, wenn die Warteschlange leer ist."""
return len(self._heap) == 0
def __len__(self):
return len(self._heap)
def __repr__(self):
pairs = sorted(self._heap, key=lambda t: t[0].value, reverse=True)
return "PriorityQueue([" + ", ".join(f"{p.value}:{v}" for p, v in pairs) + "])"
# ── interne Heap-Operationen ───────────────────────────────────────────────
#
# Terminologie-Hinweis: In der Vorlesung heißt die Abwärts-
# Operation MAX-HEAPIFY – sie stellt die Heap-Eigenschaft für einen
# Teilbaum wieder her. _sift_down ist funktional identisch damit, folgt
# aber der in der übrigen Literatur gebräuchlicheren Richtungsbezeichnung
# (sift = „sieben/sinken lassen").
#
# _sift_up hat kein direktes Gegenstück in der Vorlesung, weil Heapsort
# nie ein Element einfügt: Dort wird nur nach unten geheapifiziert.
# Für eine vollständige Priority Queue mit insert() wird jedoch auch die
# Aufwärts-Richtung benötigt.
def _swap(self, i, j):
self._ctx.reads += 2
self._ctx.writes += 2
self._heap[i], self._heap[j] = self._heap[j], self._heap[i]
def _sift_up(self, i):
while i > 0:
parent = (i - 1) // 2
if self._heap[i][0] > self._heap[parent][0]: # Int: 2 reads + 1 cmp
self._swap(i, parent)
i = parent
else:
break
def _sift_down(self, i):
n = len(self._heap)
while True:
largest, left, right = i, 2 * i + 1, 2 * i + 2
if left < n and self._heap[left][0] > self._heap[largest][0]:
largest = left
if right < n and self._heap[right][0] > self._heap[largest][0]:
largest = right
if largest == i:
break
self._swap(i, largest)
i = largest
# ── Demo & Auswertung ─────────────────────────────────────────────────────────
if __name__ == '__main__':
import matplotlib.pyplot as plt
import random
SIZES = list(range(10, 210, 10))
metrics = {m: [] for m in ("comparisons", "reads", "writes")}
for n in SIZES:
ctx = AlgoContext()
pq = PriorityQueue(ctx)
for p in random.sample(range(n * 10), n):
pq.insert(None, p)
while not pq.is_empty():
pq.pop()
for m in metrics:
metrics[m].append(getattr(ctx, m))
titles = {"comparisons": "Vergleiche", "reads": "Lesezugriffe", "writes": "Schreibzugriffe"}
fig, axes = plt.subplots(len(metrics), 1, figsize=(10, 9), sharex=True)
for ax, (metric, values) in zip(axes, metrics.items()):
ax.plot(SIZES, values, marker='o', markersize=3)
ax.set_ylabel(titles[metric])
axes[0].set_title("PriorityQueue – n inserts + n pops")
axes[-1].set_xlabel("n")
plt.tight_layout()
plt.show()
@@ -0,0 +1,88 @@
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.L03_fortgeschrittenes_sortieren.quick_sorting import quick_sort, partition
def median3(z: Array, l: int, r: int) -> int:
"""
Gibt den Index des wertmäßig mittleren Elements aus z[l], z[m], z[r] zurück.
Vergleiche werden durch den AlgoContext der Array-Elemente gezählt.
"""
m = (l + r) // 2
a, b, c = z[l], z[m], z[r]
if a <= b:
if b <= c:
return m # a <= b <= c → b ist Median
elif a <= c:
return r # a <= c < b → c ist Median
else:
return l # c < a <= b → a ist Median
else:
if a <= c:
return l # b < a <= c → a ist Median
elif b <= c:
return r # b <= c < a → c ist Median
else:
return m # c < b < a → b ist Median
def partition_median3(z: Array, l: int, r: int, ctx: AlgoContext) -> int:
pivot_idx = median3(z, l, r)
if pivot_idx != r:
z.swap(pivot_idx, r) # Pivot an den rechten Rand
return partition(z, l, r, ctx)
def quick_sort_m3(z: Array, ctx: AlgoContext, l: int = None, r: int = None):
if l is None:
l = 0
if r is None:
r = len(z) - 1
if l < r:
q = partition_median3(z, l, r, ctx)
quick_sort_m3(z, ctx, l, q - 1)
quick_sort_m3(z, ctx, q + 1, r)
if __name__ == '__main__':
import matplotlib.pyplot as plt
SIZES = list(range(10, 210, 10))
algorithms = [
("Quick (klassisch)", quick_sort),
("Quick (Median-3)", quick_sort_m3),
]
comparisons = {name: [] for name, _ in algorithms}
writes = {name: [] for name, _ in algorithms}
for name, sort_func in algorithms:
ctx = AlgoContext()
for n in SIZES:
ctx.reset()
z = Array.random(n, -1000, 1000, ctx)
sort_func(z, ctx)
comparisons[name].append(ctx.comparisons)
writes[name].append(ctx.writes)
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8), sharex=True)
for name in comparisons:
ax1.plot(SIZES, comparisons[name], label=name, marker='o', markersize=3)
ax1.set_ylabel("Vergleiche")
ax1.set_title("Quick Sort: klassisch vs. Median-of-Three")
ax1.legend()
for name in writes:
ax2.plot(SIZES, writes[name], label=name, marker='o', markersize=3)
ax2.set_ylabel("Schreibzugriffe")
ax2.set_xlabel("n")
ax2.legend()
plt.tight_layout()
plt.show()
+38
View 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
@@ -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.
@@ -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.
@@ -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
View 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.
+62
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())
@@ -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.
+89
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.
@@ -0,0 +1,58 @@
import sys, os
_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
_l07 = os.path.join(_root, 'vorlesung', 'L07_hashtable')
if _l07 not in sys.path: sys.path.insert(0, _l07)
if _root not in sys.path: sys.path.insert(0, _root)
from collections.abc import Callable
from utils.algo_context import AlgoContext
from utils.algo_int import Int
class HashTableChaining:
"""Hashtabelle mit Verkettung (Chaining) zur Kollisionsauflösung."""
def __init__(self, m: int, h_func: Callable, ctx: AlgoContext):
self.ctx = ctx
self.m = Int(m, ctx)
self.h = h_func
self.table = [[] for _ in range(m)]
self._n = 0
def _slot(self, x: Int) -> int:
return int(self.h(x, self.m))
def insert(self, x: Int) -> bool:
j = self._slot(x)
for elem in self.table[j]:
if elem == x: # Duplikat – Vergleich wird gezählt
return False
self.table[j].append(x)
self._n += 1
return True
def search(self, x: Int) -> bool:
j = self._slot(x)
for elem in self.table[j]:
if elem == x: # Vergleich wird gezählt
return True
return False
def delete(self, x: Int) -> bool:
j = self._slot(x)
for i, elem in enumerate(self.table[j]):
if elem == x: # Vergleich wird gezählt
self.table[j].pop(i)
self._n -= 1
return True
return False
def __str__(self):
rows = []
for i, chain in enumerate(self.table):
vals = ' -> '.join(str(e.value) for e in chain) if chain else '—'
rows.append(f'[{i:2d}] {vals}')
return '\n'.join(rows)
def alpha(self) -> float:
return self._n / int(self.m)
@@ -0,0 +1,49 @@
import sys, os
_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
_l07 = os.path.join(_root, 'vorlesung', 'L07_hashtable')
_p06 = os.path.dirname(__file__)
if _l07 not in sys.path: sys.path.insert(0, _l07)
if _root not in sys.path: sys.path.insert(0, _root)
if _p06 not in sys.path: sys.path.insert(0, _p06)
from utils.algo_context import AlgoContext
from utils.algo_array import Array
from utils.algo_int import Int
from analyze_hashtable import h
from aufgabe1_chaining import HashTableChaining
ctx = AlgoContext()
values = Array.from_file('data/seq0.txt', ctx)
ht = HashTableChaining(20, h, ctx)
for cell in values:
ht.insert(cell)
# ── a) Belegungsfaktor ───────────────────────────────────────────────────────
print("=== a) Nach Einfügen von seq0.txt (14 Werte, m=20) ===")
print(ht)
print(f"\nBelegungsfaktor α = {ht._n}/{int(ht.m)} = {ht.alpha():.2f}")
# ── b) Löschen von 52 ────────────────────────────────────────────────────────
print("\n=== b) Nach delete(52) ===")
ht.delete(Int(52, ctx))
print(ht)
print(f"Belegungsfaktor α = {ht._n}/{int(ht.m)} = {ht.alpha():.2f}")
# ── c) Erneut seq0.txt einfügen ──────────────────────────────────────────────
print("\n=== c) Erneut seq0.txt einfügen ===")
for cell in values:
ht.insert(cell)
print(f"Belegungsfaktor α = {ht._n}/{int(ht.m)} = {ht.alpha():.2f} (kann > 1 sein!)")
print("insert kann bei Verkettung nie False zurückgeben –")
print("die Kette wächst unbegrenzt, es gibt kein 'Tabelle voll'.")
# ── d) Erklärung kein DELETED_MARK ───────────────────────────────────────────
print("\n=== d) Warum kein DELETED_MARK? ===")
print(
"Bei offener Adressierung wird DELETED_MARK benötigt, damit die\n"
"Sondierungssequenz bei search nicht vorzeitig abbricht. Bei Verkettung\n"
"wird die vollständige Liste eines Slots linear durchsucht – ein gelöschtes\n"
"Element wird einfach aus der Liste entfernt. Es gibt keine Sequenz, die\n"
"'unterbrochen' werden könnte, daher ist kein Tombstone notwendig."
)
@@ -0,0 +1,97 @@
import sys, os, math
_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
_l07 = os.path.join(_root, 'vorlesung', 'L07_hashtable')
if _l07 not in sys.path: sys.path.insert(0, _l07)
if _root not in sys.path: sys.path.insert(0, _root)
from utils.algo_context import AlgoContext
from utils.algo_array import Array
from utils.algo_int import Int
from hashtable import HashTableOpenAddressing, DELETED_MARK, UNUSED_MARK
from analyze_hashtable import h, f
ctx = AlgoContext()
values = Array.from_file('data/seq0.txt', ctx)
ht = HashTableOpenAddressing(20, f, ctx)
for cell in values:
ht.insert(cell)
# ── a) delete(52) ────────────────────────────────────────────────────────────
print("=== a) Nach delete(52) ===")
ht.delete(Int(52, ctx))
print(ht)
slot_52 = int(h(Int(52, ctx), ht.m))
print(f"\nSlot h(52) = {slot_52}: Inhalt = {ht.table[Int(slot_52, ctx)].value!r} ← DELETED_MARK")
# ── b) Warum search bei DELETED_MARK weitersucht ─────────────────────────────
print("\n=== b) Warum search bei DELETED_MARK fortgesetzt wird ===")
print(
"search prüft in jeder Iteration: Ist der aktuelle Slot UNUSED? → Abbruch\n"
"(der gesuchte Wert wurde nie an eine spätere Position sondiert, also ist\n"
"er nicht vorhanden). Ist der Slot DELETED_MARK? → weitersuchen, denn der\n"
"Wert könnte durch einen früheren Eintrag über diesen Slot hinaus sondiert\n"
"worden sein. DELETED_MARK erhält die Sondierungssequenz aufrecht."
)
# ── c) Konkretes Gegenbeispiel: UNUSED_MARK statt DELETED_MARK ───────────────
print("\n=== c) Konkretes Gegenbeispiel ===")
# Wir bauen eine kleine Tabelle, in der das Problem klar sichtbar ist.
ctx2 = AlgoContext()
ht2 = HashTableOpenAddressing(20, f, ctx2)
for cell in values:
ht2.insert(cell)
# Finde einen Wert, der über denselben Slot wie 52 sondiert
# h(52) = slot_52; suche einen anderen Wert v mit h(v)=slot_52 oder f(v,0)=slot_52
slot_52_v = int(h(Int(52, ctx2), ht2.m))
print(f"h(52) = {slot_52_v}")
# Prüfe: gibt es einen Wert im Baum, der bei i=0 auf slot_52 trifft?
# Sondierungssequenz für jeden eingefügten Wert anzeigen
from utils.algo_int import Int as I
_A = (math.sqrt(5) - 1) / 2
def h_raw(v, m=20):
full = v * _A
return int(abs(full - int(full)) * m)
def f_raw(v, i, m=20):
return (h_raw(v, m) + i + 5*i*i) % m
print("\nSondierungssequenzen (erste 3 Schritte) für seq0.txt-Werte:")
raw_vals = [int(str(c)) for c in values]
for v in raw_vals:
seq = [f_raw(v, i) for i in range(3)]
print(f" h({v:4d}) = {h_raw(v):2d}, f(·,0)={seq[0]:2d}, f(·,1)={seq[1]:2d}, f(·,2)={seq[2]:2d}")
# 52 belegt slot_52; angenommen 58 kollidiert und liegt hinter 52
# → delete(52) mit UNUSED würde search(58) frühzeitig abbrechen
print(
"\nAngenommen, zwei Werte v1 und v2 sondieren über denselben Slot:\n"
" v1 landet in Slot s (erste Sondierung)\n"
" v2 kollidiert bei Slot s und landet bei s' (zweite Sondierung)\n"
"Wird v1 gelöscht und Slot s auf UNUSED gesetzt, bricht search(v2)\n"
"bei Slot s ab – obwohl v2 noch in s' liegt. Das Ergebnis ist 'nicht\n"
"gefunden', obwohl v2 vorhanden ist. DELETED_MARK verhindert das."
)
# ── d) Wo landet 24 nach delete(52)? ─────────────────────────────────────────
print("\n=== d) Wo landet 24 nach delete(52)? ===")
h24 = h_raw(24)
print(f"h(24) = int({{24·A}} mod 1 · 20) = {h24}")
for i in range(5):
slot = f_raw(24, i)
val = ht.table[Int(slot, ctx)].value
status = "frei (DELETED/UNUSED)" if val in (DELETED_MARK, UNUSED_MARK) else f"belegt ({val})"
print(f"f(24, {i}, 20) = {slot:2d} → {status}")
if val in (DELETED_MARK, UNUSED_MARK):
print(f" ⇒ 24 landet in Slot {slot}")
break
ht.insert(Int(24, ctx))
print(f"\nVerifikation __str__ (relevante Slots):")
for idx in [0, 1, 2, 3, 16, 17]:
v = ht.table[Int(idx, ctx)].value
marker = " ← 24 eingefügt" if str(v) == "24" else ""
print(f" Slot {idx:2d}: {v}{marker}")
@@ -0,0 +1,70 @@
import sys, os
_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
_l07 = os.path.join(_root, 'vorlesung', 'L07_hashtable')
_p06 = os.path.dirname(__file__)
if _l07 not in sys.path: sys.path.insert(0, _l07)
if _root not in sys.path: sys.path.insert(0, _root)
if _p06 not in sys.path: sys.path.insert(0, _p06)
from utils.algo_context import AlgoContext
from utils.algo_array import Array
from hashtable import HashTableOpenAddressing
from analyze_hashtable import h, f
from aufgabe1_chaining import HashTableChaining
ctx = AlgoContext()
values = Array.from_file('data/seq1.txt', ctx)
n = len(values)
# ── a) Größe 90 ───────────────────────────────────────────────────────────────
ht90 = HashTableOpenAddressing(90, f, ctx)
inserted90 = sum(1 for cell in values if ht90.insert(cell))
rejected90 = n - inserted90
print("=== a) HashTableOpenAddressing m=90, seq1.txt (100 Werte) ===")
print(f"Eingefügt: {inserted90}, Abgewiesen: {rejected90}, "
f"Freie Slots: {90 - inserted90}")
print(
"Ursache: Die quadratische Sondierungsfunktion f(x,i) = (h(x)+i+5i²) mod m\n"
"erzeugt für m=90 (keine Primzahl) zyklische Sondierungssequenzen, die nicht\n"
"alle 90 Slots abdecken. Sobald alle erreichbaren Slots belegt sind, schlägt\n"
"insert fehl – auch wenn noch andere Slots frei sind."
)
# ── b) Größe 89 ───────────────────────────────────────────────────────────────
ctx2 = AlgoContext()
values2 = Array.from_file('data/seq1.txt', ctx2)
ht89 = HashTableOpenAddressing(89, f, ctx2)
inserted89 = sum(1 for cell in values2 if ht89.insert(cell))
rejected89 = n - inserted89
print(f"\n=== b) HashTableOpenAddressing m=89 (Primzahl), seq1.txt ===")
print(f"Eingefügt: {inserted89}, Abgewiesen: {rejected89}")
print(
"Bei Primzahlgröße garantiert die quadratische Sondierung, dass mindestens\n"
"m/2 verschiedene Slots erreicht werden (bei geeigneten Konstanten c1, c2\n"
"sogar alle m). Für m=89 ist die Sondierungssequenz deutlich länger, bevor\n"
"sie sich wiederholt – dadurch können mehr Werte platziert werden."
)
# ── c) HashTableChaining m=20, seq1.txt ───────────────────────────────────────
ctx3 = AlgoContext()
values3 = Array.from_file('data/seq1.txt', ctx3)
htc = HashTableChaining(20, h, ctx3)
for cell in values3:
htc.insert(cell)
print(f"\n=== c) HashTableChaining m=20, seq1.txt ===")
unique = len(set(int(str(c)) for c in values3))
print(f"Eingefügt: {htc._n} (seq1.txt hat {n} Werte, davon {unique} eindeutig; "
f"Duplikate werden nicht doppelt gespeichert)")
print(f"Belegungsfaktor α = {htc._n}/{int(htc.m)} = {htc.alpha():.2f}")
print("Verkettung hat keine Kapazitätsgrenze – die Ketten wachsen unbegrenzt.")
# ── d) Theoretische Sondierungsanzahl ─────────────────────────────────────────
print("\n=== d) Mittlere Sondierungsanzahl 1/(1-α) ===")
for alpha in [0.5, 0.9]:
probes = 1 / (1 - alpha)
print(f" α = {alpha}: 1/(1−{alpha}) = {probes:.1f} Sondierungen")
print(
"Bis α ≈ 0,7 bleibt die offene Adressierung praktikabel (≈ 3,3 Sondierungen).\n"
"Für α > 0,8 steigen die Kosten rapide; α = 0,9 bedeutet bereits 10 Sondierungen\n"
"im Schnitt. Als Faustregel gilt: Tabelle vergrößern (Rehashing), wenn α > 0,75."
)
+106
View File
@@ -0,0 +1,106 @@
import sys, os, math
_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
_l07 = os.path.join(_root, 'vorlesung', 'L07_hashtable')
_p06 = os.path.dirname(__file__)
if _l07 not in sys.path: sys.path.insert(0, _l07)
if _root not in sys.path: sys.path.insert(0, _root)
if _p06 not in sys.path: sys.path.insert(0, _p06)
import matplotlib.pyplot as plt
from utils.algo_context import AlgoContext
from utils.algo_array import Array
from hashtable import HashTableOpenAddressing
from analyze_hashtable import h, f
from aufgabe1_chaining import HashTableChaining
def measure(m: int, target_alpha: float, seed: int = 42):
"""Befüllt beide Tabellentypen bis Ziel-α, misst Vergleiche beim Suchen."""
n = int(m * target_alpha)
import random
random.seed(seed)
ctx_oa = AlgoContext()
vals_oa = Array.random(n, -10000, 10000, ctx_oa)
ht_oa = HashTableOpenAddressing(m, f, ctx_oa)
inserted = []
for cell in vals_oa:
if ht_oa.insert(cell):
inserted.append(cell)
ctx_oa.reset()
for cell in inserted:
ht_oa.search(cell)
cmp_oa = ctx_oa.comparisons
ctx_ch = AlgoContext()
vals_ch = Array.random(n, -10000, 10000, ctx_ch)
ht_ch = HashTableChaining(m, h, ctx_ch)
for cell in vals_ch:
ht_ch.insert(cell)
ctx_ch.reset()
for i, chain in enumerate(ht_ch.table):
for elem in chain:
ht_ch.search(elem)
cmp_ch = ctx_ch.comparisons
return cmp_oa, cmp_ch
sizes = [50, 100, 200, 500, 1000]
for target_alpha, label in [(0.7, "α ≈ 0,7"), (0.9, "α ≈ 0,9")]:
oa_vals, ch_vals = [], []
for m in sizes:
coa, cch = measure(m, target_alpha)
oa_vals.append(coa)
ch_vals.append(cch)
print(f"\n{'m':>6} {'OA Vergl.':>12} {'Chaining Vergl.':>16} ({label})")
for m, co, cc in zip(sizes, oa_vals, ch_vals):
print(f"{m:>6} {co:>12} {cc:>16}")
# ── Plot ──────────────────────────────────────────────────────────────────────
fig, axes = plt.subplots(1, 2, figsize=(11, 4))
for ax, target_alpha, label in zip(axes, [0.7, 0.9], ["α ≈ 0,7", "α ≈ 0,9"]):
oa_vals, ch_vals = [], []
for m in sizes:
coa, cch = measure(m, target_alpha)
oa_vals.append(coa)
ch_vals.append(cch)
ax.plot(sizes, oa_vals, marker='o', label='Offene Adressierung')
ax.plot(sizes, ch_vals, marker='s', label='Verkettung')
ax.set_xlabel('Tabellengröße m')
ax.set_ylabel('Vergleiche (Suchen aller Werte)')
ax.set_title(label)
ax.legend()
plt.tight_layout()
plt.savefig('vergleich_strategien.pdf')
plt.show()
# ── Antworten ─────────────────────────────────────────────────────────────────
print("\n=== a) Welche Strategie benötigt mehr Vergleiche? ===")
print(
"Bei gleichem α erzeugt die offene Adressierung mehr Vergleiche:\n"
"Kollisionen führen zu langen Sondierungssequenzen, bei denen jeder\n"
"besuchte Slot einen Vergleich kostet. Die Verkettung sucht nur in der\n"
"jeweiligen Kette (im Schnitt α/2 Vergleiche pro Suche)."
)
print("\n=== b) α = 0,9 ===")
print(
"Die offene Adressierung verschlechtert sich drastisch: 1/(1−0,9) = 10\n"
"Sondierungen im Schnitt – das ist im Plot klar erkennbar. Bei der\n"
"Verkettung wächst die mittlere Kettenlänge auf α = 0,9, also ca. 1 Extra-\n"
"Vergleich. Der Nachteil der offenen Adressierung wird bei hohem α dominant."
)
print("\n=== c) Systemvorteile ===")
print(
"Verkettung: Keine Kapazitätsgrenze; einfacheres Löschen (kein\n"
" Tombstone); gut parallelisierbar (eine Kette pro Slot).\n"
"Offene Adressierung: Besseres Cache-Verhalten (alle Einträge im\n"
" zusammenhängenden Array, keine Pointer-Indirektion);\n"
" geringerer Speicherverbrauch (keine Listenknoten)."
)
+89
View File
@@ -0,0 +1,89 @@
import time
import sys
import os
from vorlesung.L08_graphen.graph import AdjacencyListGraph
GRID = [
"Sabqponm",
"abcryxxl",
"accszExk",
"acctuvwj",
"abdefghi",
]
def height(ch):
if ch == 'S': return ord('a')
if ch == 'E': return ord('z')
return ord(ch)
def build_graph(grid):
rows = len(grid)
cols = len(grid[0])
g = AdjacencyListGraph()
for r in range(rows):
for c in range(cols):
g.insert_vertex(f"{r}_{c}")
for r in range(rows):
for c in range(cols):
for dr, dc in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols:
if height(grid[nr][nc]) - height(grid[r][c]) <= 1:
g.connect(f"{r}_{c}", f"{nr}_{nc}")
return g, rows, cols
def find_start_end(grid):
start = end = None
for r, row in enumerate(grid):
for c, ch in enumerate(row):
if ch == 'S': start = f"{r}_{c}"
if ch == 'E': end = f"{r}_{c}"
return start, end
def solve(grid, label="Beispielgelände"):
g, rows, cols = build_graph(grid)
start, end = find_start_end(grid)
t0 = time.perf_counter()
dist_map, pred_map = g.bfs(start)
t1 = time.perf_counter()
pfad = g.path(end, pred_map)
def fmt(node):
r, c = node.split("_")
return f"({r},{c})"
print(f"=== {label} ===")
print(f"|V| = {rows * cols}, |E| <= {rows * cols * 4} (real |E| gezählt separat)")
print(f"Pfadlänge: {len(pfad) - 1} Schritte")
print(f"Pfad: {' -> '.join(fmt(n) for n in pfad)}")
print(f"BFS-Laufzeit: {(t1 - t0) * 1000:.4f} ms")
print()
return rows * cols, t1 - t0
# --- a) Modellierung ---------------------------------------------------------
# Adjazenzliste ist besser geeignet: Das Grid ist licht (sparse).
# Jeder Knoten hat maximal 4 Nachbarn => |E| <= 4|V|.
# Eine Adjazenzmatrix hätte |V|^2 Einträge; für ein 41x122-Grid wären das
# über 25 Millionen Einträge, obwohl nur ~4*|V| davon belegt wären.
# --- b) Pfadsuche -------------------------------------------------------------
v_small, t_small = solve(GRID, "Beispielgelände (8x5)")
# --- c) AoC-Karte (optional) --------------------------------------------------
if len(sys.argv) > 1 and os.path.exists(sys.argv[1]):
with open(sys.argv[1]) as f:
aoc_grid = [line.rstrip() for line in f if line.strip()]
v_large, t_large = solve(aoc_grid, f"AoC-Karte ({len(aoc_grid)}x{len(aoc_grid[0])})")
ratio_v = v_large / v_small
ratio_t = t_large / t_small if t_small > 0 else float('inf')
print(f"Verhältnis |V|: {ratio_v:.1f}x")
print(f"Verhältnis Laufzeit: {ratio_t:.1f}x")
print(f"Lineares Wachstum bestätigt: {'ja' if abs(ratio_t / ratio_v - 1) < 0.5 else 'nein'}")
# --- d) Optional: bester Startpunkt (AoC Teil 2) ------------------------------
# Hinweis: Statt BFS von jedem 'a'-Feld zu starten, dreht man den Graphen um
# und startet eine einzige BFS von E rückwärts. Der erste erreichte 'a'-Knoten
# liefert den kürzesten Pfad.
+55
View File
@@ -0,0 +1,55 @@
from vorlesung.L08_graphen.graph import AdjacencyListGraph
DEPENDENCIES = {
"main": ["parser", "codegen"],
"parser": ["lexer", "ast"],
"codegen": ["ast", "optimizer"],
"optimizer": ["utils"],
"ast": ["utils"],
"lexer": [],
"utils": [],
}
# --- a) Graph aufbauen -------------------------------------------------------
# Kantendefinition: A -> B bedeutet "A muss vor B gebaut werden" (B hängt von A ab).
# Kanten zeigen also von der Voraussetzung zum Abhängigen – nur so liefert
# die absteigende Sortierung nach leave_map die korrekte Baureihenfolge.
g = AdjacencyListGraph()
for module in DEPENDENCIES:
g.insert_vertex(module)
for module, deps in DEPENDENCIES.items():
for dep in deps:
g.connect(dep, module) # dep -> module: dep muss vor module gebaut werden
g.graph("BuildDependencies")
# --- b) DFS ausführen --------------------------------------------------------
enter_map, leave_map, pred_map = g.dfs()
print("Zeitmarken (DFS):")
for v in sorted(enter_map, key=lambda v: enter_map[v]):
print(f" {v.value:12s} begin={enter_map[v]:2d} end={leave_map[v]:2d}")
print()
# --- c) Topologische Sortierung ----------------------------------------------
sorted_vertices = sorted(leave_map, key=lambda v: leave_map[v], reverse=True)
reihenfolge = [v.value for v in sorted_vertices]
print("Topologische Reihenfolge (Build-Reihenfolge):")
print(" " + " -> ".join(reihenfolge))
print()
# Überprüfung: Für jede Abhängigkeit A->B muss A nach B in der Liste kommen.
pos = {name: i for i, name in enumerate(reihenfolge)}
ok = all(pos[dep] < pos[module]
for module, deps in DEPENDENCIES.items()
for dep in deps)
print(f"Alle Abhängigkeiten eingehalten: {ok}")
print()
# --- d) Komplexität ----------------------------------------------------------
v_count = len(list(g.all_vertices()))
e_count = sum(len(g.get_adjacent_vertices(v.value)) for v in g.all_vertices())
print(f"|V| = {v_count}, |E| = {e_count}")
# Jeder Knoten wird genau einmal grau (begin) und einmal schwarz (end) => O(|V|)
# Jede Kante wird beim ersten Besuch des Ausgangsknotens genau einmal betrachtet => O(|E|)
# Zusammen: O(|V| + |E|)
@@ -0,0 +1,84 @@
import time
from vorlesung.L08_graphen.graph import AdjacencyListGraph
# AoC 2024, Tag 16 – erstes Beispiel-Labyrinth (Antwort: 7036)
GRID = """\
###############
#.......#....E#
#.#.###.#.###.#
#.....#.#...#.#
#.###.#####.#.#
#.#.#.......#.#
#.#.#####.###.#
#...........#.#
###.#.#####.#.#
#...#.....#.#.#
#.#.#.###.#.#.#
#.....#...#.#.#
#.###.#.#.#.#.#
#S..#.....#...#
###############""".strip().split('\n')
# Richtungen: 0=N, 1=O, 2=S, 3=W (O = Ost = Startrichtung des Rentiers)
DR = [-1, 0, 1, 0]
DC = [0, 1, 0, -1]
def build_graph(grid):
g = AdjacencyListGraph()
rows, cols = len(grid), len(grid[0])
start = end = None
for r in range(rows):
for c in range(cols):
if grid[r][c] == '#':
continue
if grid[r][c] == 'S':
start = (r, c)
elif grid[r][c] == 'E':
end = (r, c)
for d in range(4):
g.insert_vertex(f"{r},{c},{d}")
for r in range(rows):
for c in range(cols):
if grid[r][c] == '#':
continue
for d in range(4):
src = f"{r},{c},{d}"
# Vorwärtsbewegen (Kosten 1)
nr, nc = r + DR[d], c + DC[d]
if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] != '#':
g.connect(src, f"{nr},{nc},{d}", 1)
# Drehen links und rechts (Kosten 1000 je Drehung)
g.connect(src, f"{r},{c},{(d - 1) % 4}", 1000)
g.connect(src, f"{r},{c},{(d + 1) % 4}", 1000)
return g, start, end
# --- a) + b) Graph aufbauen und Dijkstra ausführen ---
g, start, end = build_graph(GRID)
v_count = len(list(g.all_vertices()))
e_count = len(g.all_edges())
print(f"Graph: |V| = {v_count}, |E| = {e_count}")
start_state = f"{start[0]},{start[1]},1" # Startposition, Blickrichtung Ost (1)
dist_map, pred_map = g.dijkstra(start_state)
# Bestes Zielfeld: E in beliebiger Richtung (4 zulässige Zielzustände)
end_vertices = [g.get_vertex(f"{end[0]},{end[1]},{d}") for d in range(4)]
best_end = min(end_vertices, key=lambda v: dist_map[v])
print(f"Gesamtkosten: {dist_map[best_end]}") # Erwartung: 11048
pfad = g.path(best_end.value, pred_map)
print(f"Pfad: {len(pfad)} Zustände ({len(pfad) - 1} Schritte)")
print("Erste Schritte:", " -> ".join(pfad[:5]), "...")
print("Letzte Schritte: ...", " -> ".join(pfad[-3:]))
# --- c) Laufzeitmessung ---
t0 = time.perf_counter()
g.dijkstra(start_state)
t1 = time.perf_counter()
print(f"\nLaufzeit (Beispiel): {(t1 - t0) * 1000:.3f} ms")
@@ -0,0 +1,82 @@
from vorlesung.L08_graphen.graph import AdjacencyListGraph
# Korrigierte Kantenliste (Vorlesungsbeispiel).
# Hinweis: Das Arbeitsblatt enthält c->b:-2, c->e:5, d->c:-5, e->c:4 –
# diese Werte erzeugen einen negativen Zyklus (b->c->b: 1-2=-1) im
# Basisgraphen und müssen in der Aufgabenstellung korrigiert werden.
# Korrekte Werte (kein Vorzyklus, Ergebnis: b=7, c=2, d=4, e=6):
EDGES = [
('a', 'b', 9),
('a', 'd', 4),
('b', 'c', 1),
('b', 'd', 2),
('c', 'b', 5), # war -2 im Entwurf
('c', 'e', 4), # war 5 im Entwurf
('d', 'c', -2), # war -5 im Entwurf
('d', 'e', 2),
]
def bellman_ford(graph, start_name):
"""Bellman-Ford: kürzeste Wege mit Erkennung negativer Zyklen.
Gibt (distance_map, predecessor_map, has_negative_cycle) zurück.
|V|-1 Relaxationsdurchläufe über alle Kanten, dann ein Prüfdurchlauf.
"""
distance_map = {}
predecessor_map = {}
for v in graph.all_vertices():
distance_map[v] = float('inf')
predecessor_map[v] = None
start = graph.get_vertex(start_name)
distance_map[start] = 0
n = len(list(graph.all_vertices()))
for _ in range(n - 1):
for src_name, dst_name, weight in graph.all_edges():
src = graph.get_vertex(src_name)
dst = graph.get_vertex(dst_name)
if distance_map[src] + weight < distance_map[dst]:
distance_map[dst] = distance_map[src] + weight
predecessor_map[dst] = src
# Prüfdurchlauf: jede weitere Verbesserung zeigt einen negativen Zyklus
has_negative_cycle = False
for src_name, dst_name, weight in graph.all_edges():
src = graph.get_vertex(src_name)
dst = graph.get_vertex(dst_name)
if distance_map[src] + weight < distance_map[dst]:
has_negative_cycle = True
break
return distance_map, predecessor_map, has_negative_cycle
# --- a) + b) Basisgraph ---
g = AdjacencyListGraph()
for v in ['a', 'b', 'c', 'd', 'e']:
g.insert_vertex(v)
for src, dst, w in EDGES:
g.connect(src, dst, w)
dist, pred, neg_cycle = bellman_ford(g, 'a')
print(f"Negativer Zyklus: {neg_cycle}")
print("Kürzeste Distanzen:")
for name in ['a', 'b', 'c', 'd', 'e']:
v = g.get_vertex(name)
pfad = g.path(name, pred)
print(f" {name}: d={dist[v]}, Pfad: {' -> '.join(pfad)}")
# --- d) Komplexität ---
v_count = len(list(g.all_vertices()))
e_count = len(g.all_edges())
print(f"\n|V| = {v_count}, |E| = {e_count}")
# --- c) Negativer Zyklus ---
print("\n--- Mit negativem Zyklus (e -> b, Gewicht -8) ---")
g.connect('e', 'b', -8)
dist2, pred2, neg_cycle2 = bellman_ford(g, 'a')
print(f"Negativer Zyklus: {neg_cycle2}")
# Betroffener Zyklus: b -> c -> e -> b (1 + 4 + (-8) = -3 < 0)
+186
View File
@@ -0,0 +1,186 @@
import math
import sys
from itertools import combinations
from vorlesung.L08_graphen.graph import AdjacencyListGraph
# ---------------------------------------------------------------------------
# b) Union-Find
# ---------------------------------------------------------------------------
class DisjointSet:
"""Union-Find mit Pfadkompression und Union-by-Size."""
def __init__(self):
self._parent = {}
self._size = {}
def make_set(self, v):
self._parent[v] = v
self._size[v] = 1
def find_set(self, v):
if self._parent[v] != v:
self._parent[v] = self.find_set(self._parent[v]) # Pfadkompression
return self._parent[v]
def union(self, u, v):
"""Vereinigt die Schaltkreise von u und v.
Gibt True zurück, wenn sie vorher getrennt waren."""
ru, rv = self.find_set(u), self.find_set(v)
if ru == rv:
return False
if self._size[ru] < self._size[rv]:
ru, rv = rv, ru
self._parent[rv] = ru
self._size[ru] += self._size[rv]
return True
def size(self, v):
return self._size[self.find_set(v)]
def circuit_sizes(self):
"""Alle Schaltkreisgrößen, absteigend sortiert."""
return sorted(
[self._size[v] for v in self._parent if self._parent[v] == v],
reverse=True
)
# ---------------------------------------------------------------------------
# Hilfsfunktionen
# ---------------------------------------------------------------------------
def euclidean(a, b):
return math.sqrt(sum((ai - bi) ** 2 for ai, bi in zip(a, b)))
def parse_boxes(text):
boxes = []
for line in text.strip().splitlines():
line = line.strip()
if line:
boxes.append(tuple(int(x) for x in line.split(',')))
return boxes
# ---------------------------------------------------------------------------
# c–e) Kruskal
# ---------------------------------------------------------------------------
def solve(text, k=1000):
boxes = parse_boxes(text)
n = len(boxes)
print(f"Anzahl Boxen: {n}")
print(f"Potenzielle Verbindungen: {n * (n - 1) // 2}")
# a) Alle Paare nach Distanz sortiert
edges = sorted(
((euclidean(a, b), a, b) for a, b in combinations(boxes, 2)),
key=lambda e: e[0]
)
ds = DisjointSet()
for b in boxes:
ds.make_set(b)
# --- Teil 1: die k kürzesten Paare verbinden (inkl. No-Ops) ---
for _, u, v in edges[:k]:
ds.union(u, v)
idx = k
sizes = ds.circuit_sizes()
top3 = sizes[:3]
result1 = top3[0] * top3[1] * top3[2]
print(f"\n--- Teil 1 (k={k}) ---")
print(f"Schaltkreisgrößen: {sizes}")
print(f"Produkt der 3 größten: {top3[0]} × {top3[1]} × {top3[2]} = {result1}")
# --- Teil 2: weiter bis ein einziger Schaltkreis ---
last_u = last_v = None
while idx < len(edges):
_, u, v = edges[idx]
idx += 1
if ds.union(u, v):
last_u, last_v = u, v
if ds.size(last_u) == n:
break
result2 = last_u[0] * last_v[0]
print(f"\n--- Teil 2 ---")
print(f"Letzte Verbindung: {last_u} -- {last_v}")
print(f"Produkt der X-Koordinaten: {last_u[0]} × {last_v[0]} = {result2}")
# --- Verifikation mit graph.mst_kruskal() ---
# mst_kruskal() liefert alle MST-Kanten aufsteigend nach Gewicht sortiert.
# Die letzte Kante ist die schwerste im MST = die zuletzt hinzugefügte
# Kante bei Kruskal = identisch mit dem Ergebnis aus Teil 2.
# connect() ist gerichtet; beide Richtungen werden eingetragen, damit
# all_edges() alle Paare sieht. Der Same-Set-Check in mst_kruskal()
# filtert die Rückrichtung als No-Op heraus.
g = AdjacencyListGraph()
for b in boxes:
g.insert_vertex(f"{b[0]},{b[1]},{b[2]}")
for a, b in combinations(boxes, 2):
name_a = f"{a[0]},{a[1]},{a[2]}"
name_b = f"{b[0]},{b[1]},{b[2]}"
d = euclidean(a, b)
g.connect(name_a, name_b, d)
g.connect(name_b, name_a, d)
mst_edges, _ = g.mst_kruskal()
last = mst_edges[-1]
gu = tuple(int(x) for x in last[0].split(','))
gv = tuple(int(x) for x in last[1].split(','))
result2_graph = gu[0] * gv[0]
assert result2_graph == result2, (
f"Verifikation fehlgeschlagen: graph.mst_kruskal() → {result2_graph}, "
f"eigene Lösung → {result2}"
)
print(f"Verifikation mit graph.mst_kruskal() bestanden.")
return result1, result2
# ---------------------------------------------------------------------------
# Beispieldaten (AoC 2025, Tag 8 – 20 Junction Boxes)
# Erwartetes Ergebnis: Teil 1 (k=10) → 40, Teil 2 → 25272
# ---------------------------------------------------------------------------
BEISPIEL = """
162,817,812
57,618,57
906,360,560
592,479,940
352,342,300
466,668,158
542,29,236
431,825,988
739,650,466
52,470,668
216,146,977
819,987,18
117,168,530
805,96,715
346,949,466
970,615,88
941,993,340
862,61,35
984,92,344
425,690,689
"""
if __name__ == '__main__':
if len(sys.argv) == 1:
print("=== Selbsttest mit Beispieldaten ===")
r1, r2 = solve(BEISPIEL, k=10)
assert r1 == 40, f"Teil 1 erwartet 40, erhalten {r1}"
assert r2 == 25272, f"Teil 2 erwartet 25272, erhalten {r2}"
print("Selbsttest bestanden.")
else:
filename = sys.argv[1]
k = int(sys.argv[2]) if len(sys.argv) > 2 else 1000
with open(filename) as f:
text = f.read()
solve(text, k)
+3 -1
View File
@@ -1,4 +1,6 @@
from utils.algo_context import AlgoContext
from utils.algo_int import Int
from utils.algo_array import Array
from utils.algo_range import irange
from utils.algo_range import Range
from utils.algo_path import path
from utils.algo_priority_queue import PriorityQueue
+3 -3
View File
@@ -2,7 +2,7 @@ from __future__ import annotations
from random import randint
from utils.algo_context import AlgoContext, _NullContext, NULL_CTX
from utils.algo_int import Int
from utils.project_dir import get_path
from utils.algo_path import path
class Array:
@@ -130,8 +130,8 @@ class Array:
limit : int | None
Optionale Obergrenze für die Anzahl eingelesener Zeilen.
"""
path = get_path(filename)
with open(path) as f:
filepath = path(filename)
with open(filepath) as f:
lines = f.readlines()
if limit is not None:
lines = lines[:limit]
+1 -1
View File
@@ -117,7 +117,7 @@ class _NullContext:
"""
Kontext der alle Operationen stillschweigend ignoriert.
Wird intern von irange() verwendet, damit Schleifenindex-Arithmetik
Wird intern von Range() verwendet, damit Schleifenindex-Arithmetik
standardmäßig nicht mitgezählt wird.
__setattr__ ist absichtlich ein no-op: ``ctx.reads += 1`` bleibt wirkungslos.
File renamed without changes.
+8
View File
@@ -212,6 +212,14 @@ class Int:
self._value //= other._value
return self
def __imod__(self, other):
other = self._wrap(other)
self._ctx.reads += 2
self._ctx.divisions += 1
self._ctx.writes += 1
self._value %= other._value
return self
# ------------------------------------------------------------------
# Bitoperationen (2 reads + 1 bitop + 1 write für in-place)
# ------------------------------------------------------------------
+24
View File
@@ -0,0 +1,24 @@
from pathlib import Path
def path(filename) -> Path:
"""Gibt den absoluten Pfad zu einer Datei im Projektverzeichnis zurück.
Funktioniert unabhängig vom Arbeitsverzeichnis und der verwendeten IDE,
da der Pfad relativ zur Position dieses Moduls berechnet wird.
Beispiel
--------
from utils.algo_path import path
z = Array.from_file(path("data/seq0.txt"), ctx)
"""
project_dir = Path(__file__).resolve().parent.parent
return project_dir / filename
if __name__ == "__main__":
filename = path("data/seq0.txt")
print(filename)
print(filename.resolve())
print(filename.is_file())
print(filename.exists())
@@ -35,6 +35,6 @@ if __name__ == "__main__":
pq.add_or_update('task3', float('inf'))
print(pq.pop()) # Should print ('task1', 1)
pq.add_or_update('task2', 0) # Update priority of 'task1'
pq.add_or_update('task2', 0) # Update priority of 'task2'
print(pq.pop()) # Should print ('task2', 0)
print(pq.pop()) # Should print ('task3', 3)
+8 -8
View File
@@ -3,14 +3,14 @@ from utils.algo_context import AlgoContext, NULL_CTX
from utils.algo_int import Int
def irange(start_or_stop, stop=None, step: int = 1, ctx: AlgoContext = None):
def Range(start_or_stop, stop=None, step: int = 1, ctx: AlgoContext = None):
"""
Drop-in Ersatz für range(), der Int-Objekte zurückgibt.
Wird wie Pythons range() aufgerufen:
irange(stop)
irange(start, stop)
irange(start, stop, step)
Range(stop)
Range(start, stop)
Range(start, stop, step)
Indexarithmetik und Zählung
---------------------------
@@ -20,14 +20,14 @@ def irange(start_or_stop, stop=None, step: int = 1, ctx: AlgoContext = None):
sollen in die Komplexitätsanalyse einfließen.
Mit ctx-Argument werden auch Indexoperationen gezählt:
for j in irange(n, ctx=ctx): ...
for j in Range(n, ctx=ctx): ...
Beispiel
--------
ctx = AlgoContext()
z = Array.random(10, 0, 99, ctx)
for i in irange(len(z) - 1): # i ist Int, Arithmetik nicht gezählt
if z[i] > z[i + 1]: # Vergleich gezählt (z-Zellen haben ctx)
for i in Range(len(z) - 1): # i ist Int, Arithmetik nicht gezählt
if z[i] > z[i + 1]: # Vergleich gezählt (z-Zellen haben ctx)
z.swap(i, i + 1)
"""
_ctx = ctx if ctx is not None else NULL_CTX
@@ -38,7 +38,7 @@ def irange(start_or_stop, stop=None, step: int = 1, ctx: AlgoContext = None):
start, stop_ = int(start_or_stop), int(stop)
step = int(step)
assert step != 0, "irange: step darf nicht 0 sein"
assert step != 0, "Range: step darf nicht 0 sein"
num = start
if step > 0:
-13
View File
@@ -1,13 +0,0 @@
from pathlib import Path
def get_path(filename) -> Path:
this_dir = Path(__file__).resolve().parent
project_dir = this_dir.parent
return project_dir / filename
if __name__ == "__main__":
filename = get_path("data/seq0.txt")
print(filename)
print(filename.resolve())
print(filename.is_file())
print(filename.exists())
-175
View File
@@ -1,175 +0,0 @@
import unittest
from utils.algo_context import AlgoContext
from utils.algo_int import Int
from utils.algo_array import Array
class TestArray(unittest.TestCase):
def setUp(self):
self.ctx = AlgoContext()
# ------------------------------------------------------------------
# Erzeugung
# ------------------------------------------------------------------
def test_from_list(self):
z = Array([3, 1, 4, 1, 5], self.ctx)
self.assertEqual(len(z), 5)
self.assertEqual(z[0].value, 3)
self.assertEqual(z[4].value, 5)
def test_random(self):
z = Array.random(20, 0, 99, self.ctx)
self.assertEqual(len(z), 20)
for cell in z:
self.assertGreaterEqual(cell.value, 0)
self.assertLessEqual(cell.value, 99)
def test_sorted(self):
z = Array.sorted(5, self.ctx)
values = [z[i].value for i in range(5)]
self.assertEqual(values, [0, 1, 2, 3, 4])
def test_from_file(self):
z = Array.from_file("data/seq0.txt", self.ctx)
self.assertGreater(len(z), 0)
def test_random_uses_no_write_counts(self):
"""Fabrikmethoden sollen keine Schreibzugriffe verzeichnen."""
z = Array.random(10, 0, 9, self.ctx)
self.assertEqual(self.ctx.writes, 0)
# ------------------------------------------------------------------
# Zugriff
# ------------------------------------------------------------------
def test_getitem_returns_int_cell(self):
z = Array([10, 20], self.ctx)
cell = z[0]
self.assertIsInstance(cell, Int)
self.assertEqual(cell.value, 10)
def test_getitem_with_int_index(self):
z = Array([10, 20, 30], self.ctx)
i = Int(2, self.ctx)
self.assertEqual(z[i].value, 30)
def test_getitem_no_read_count(self):
"""Array-Zugriff allein soll keinen read-Zähler erhöhen."""
z = Array([5, 6, 7], self.ctx)
_ = z[0]
self.assertEqual(self.ctx.reads, 0)
def test_setitem_plain_int(self):
z = Array([1, 2, 3], self.ctx)
z[0] = 99
self.assertEqual(z[0].value, 99)
self.assertEqual(self.ctx.writes, 1)
self.assertEqual(self.ctx.reads, 0)
def test_setitem_int_object(self):
z = Array([1, 2, 3], self.ctx)
v = Int(42, self.ctx)
z[1] = v
self.assertEqual(z[1].value, 42)
self.assertEqual(self.ctx.writes, 1)
self.assertEqual(self.ctx.reads, 1)
def test_setitem_cell_to_cell(self):
"""z[i] = z[j] kopiert den Wert (keine Alias-Referenz)."""
z = Array([10, 20], self.ctx)
z[0] = z[1]
self.assertEqual(z[0].value, 20)
# Wert ändern – z[1] darf sich nicht mitändern
z[0] = 99
self.assertEqual(z[1].value, 20)
# ------------------------------------------------------------------
# Swap
# ------------------------------------------------------------------
def test_swap_exchanges_values(self):
z = Array([1, 2, 3, 4, 5], self.ctx)
z.swap(0, 4)
self.assertEqual(z[0].value, 5)
self.assertEqual(z[4].value, 1)
def test_swap_counts_reads_and_writes(self):
z = Array([1, 2], self.ctx)
z.swap(0, 1)
self.assertEqual(self.ctx.reads, 2)
self.assertEqual(self.ctx.writes, 2)
def test_swap_with_int_indices(self):
z = Array([10, 20, 30], self.ctx)
i = Int(0, self.ctx)
j = Int(2, self.ctx)
z.swap(i, j)
self.assertEqual(z[0].value, 30)
self.assertEqual(z[2].value, 10)
# ------------------------------------------------------------------
# Vergleich über Array-Zellen (zählt beim Int, nicht beim Array)
# ------------------------------------------------------------------
def test_cell_comparison_counts_in_ctx(self):
z = Array([5, 3], self.ctx)
result = z[0] > z[1]
self.assertTrue(result)
self.assertEqual(self.ctx.comparisons, 1)
self.assertEqual(self.ctx.reads, 2)
# ------------------------------------------------------------------
# Iteration
# ------------------------------------------------------------------
def test_iteration(self):
z = Array([1, 2, 3], self.ctx)
values = [c.value for c in z]
self.assertEqual(values, [1, 2, 3])
# ------------------------------------------------------------------
# Länge
# ------------------------------------------------------------------
def test_len(self):
z = Array([1, 2, 3, 4], self.ctx)
self.assertEqual(len(z), 4)
def test_length_returns_int(self):
z = Array([1, 2, 3], self.ctx)
n = z.length()
self.assertIsInstance(n, Int)
self.assertEqual(n.value, 3)
class TestArrayIntegration(unittest.TestCase):
"""Bubble Sort als Integrationstest für das gesamte Framework."""
def test_bubble_sort_produces_correct_result(self):
import sys, os
sys.path.insert(0, os.path.join(os.path.dirname(__file__),
'../vorlesung/L02_elementares_sortieren'))
from bubble_sorting import bubble_sort
ctx = AlgoContext()
z = Array([5, 3, 1, 4, 2], ctx)
bubble_sort(z, ctx)
values = [z[i].value for i in range(len(z))]
self.assertEqual(values, [1, 2, 3, 4, 5])
def test_bubble_sort_counts_comparisons(self):
import sys, os
sys.path.insert(0, os.path.join(os.path.dirname(__file__),
'../vorlesung/L02_elementares_sortieren'))
from bubble_sorting import bubble_sort
ctx = AlgoContext()
z = Array([5, 4, 3, 2, 1], ctx) # worst case
bubble_sort(z, ctx)
# n=5: maximal n*(n-1)/2 = 10 Vergleiche
self.assertGreater(ctx.comparisons, 0)
self.assertLessEqual(ctx.comparisons, 10)
if __name__ == "__main__":
unittest.main()
-193
View File
@@ -1,193 +0,0 @@
import unittest
from utils.algo_context import AlgoContext
from utils.algo_int import Int
class TestInt(unittest.TestCase):
def setUp(self):
self.ctx = AlgoContext()
def _int(self, v):
return Int(v, self.ctx)
# ------------------------------------------------------------------
# Vergleiche
# ------------------------------------------------------------------
def test_comparison_counts_reads_and_compare(self):
a, b = self._int(5), self._int(3)
result = a > b
self.assertTrue(result)
self.assertEqual(self.ctx.comparisons, 1)
self.assertEqual(self.ctx.reads, 2)
def test_all_comparison_operators(self):
a, b = self._int(4), self._int(4)
self.assertTrue(a == b)
self.assertFalse(a != b)
self.assertTrue(a <= b)
self.assertTrue(a >= b)
self.assertFalse(a < b)
self.assertFalse(a > b)
self.assertEqual(self.ctx.comparisons, 6)
self.assertEqual(self.ctx.reads, 12)
def test_comparison_with_plain_int(self):
a = self._int(5)
result = a > 3 # 3 wird auto-gewrappt, kein extra Zähler
self.assertTrue(result)
self.assertEqual(self.ctx.comparisons, 1)
self.assertEqual(self.ctx.reads, 2)
def test_eq_with_none_returns_false(self):
a = self._int(1)
self.assertFalse(a == None) # noqa: E711
self.assertEqual(self.ctx.comparisons, 0) # keine Zählung
# ------------------------------------------------------------------
# Arithmetik (binär)
# ------------------------------------------------------------------
def test_addition_returns_new_int(self):
a, b = self._int(3), self._int(4)
c = a + b
self.assertIsInstance(c, Int)
self.assertEqual(c.value, 7)
self.assertEqual(self.ctx.additions, 1)
self.assertEqual(self.ctx.reads, 2)
self.assertEqual(self.ctx.writes, 0)
def test_subtraction(self):
a, b = self._int(10), self._int(4)
c = a - b
self.assertEqual(c.value, 6)
self.assertEqual(self.ctx.subtractions, 1)
def test_multiplication(self):
a, b = self._int(3), self._int(4)
c = a * b
self.assertEqual(c.value, 12)
self.assertEqual(self.ctx.multiplications, 1)
def test_floordiv(self):
a, b = self._int(10), self._int(3)
c = a // b
self.assertEqual(c.value, 3)
self.assertEqual(self.ctx.divisions, 1)
def test_mod(self):
a, b = self._int(10), self._int(3)
c = a % b
self.assertEqual(c.value, 1)
self.assertEqual(self.ctx.divisions, 1)
def test_arithmetic_with_plain_int(self):
a = self._int(5)
c = a + 3
self.assertEqual(c.value, 8)
self.assertEqual(self.ctx.additions, 1)
def test_result_shares_context(self):
a = self._int(5)
b = self._int(3)
c = a + b # c hat denselben ctx
_ = c > self._int(0) # Vergleich auf c zählt im selben ctx
self.assertEqual(self.ctx.comparisons, 1)
# ------------------------------------------------------------------
# Augmented assignment
# ------------------------------------------------------------------
def test_iadd_counts_read_add_write(self):
a, b = self._int(5), self._int(3)
a += b
self.assertEqual(a.value, 8)
self.assertEqual(self.ctx.reads, 2)
self.assertEqual(self.ctx.additions, 1)
self.assertEqual(self.ctx.writes, 1)
def test_isub(self):
a, b = self._int(10), self._int(4)
a -= b
self.assertEqual(a.value, 6)
self.assertEqual(self.ctx.subtractions, 1)
self.assertEqual(self.ctx.writes, 1)
def test_imul(self):
a, b = self._int(3), self._int(4)
a *= b
self.assertEqual(a.value, 12)
self.assertEqual(self.ctx.multiplications, 1)
self.assertEqual(self.ctx.writes, 1)
def test_iadd_with_plain_int(self):
a = self._int(5)
a += 1
self.assertEqual(a.value, 6)
self.assertEqual(self.ctx.writes, 1)
# ------------------------------------------------------------------
# set()
# ------------------------------------------------------------------
def test_set_plain_counts_one_write(self):
a = self._int(0)
a.set(42)
self.assertEqual(a.value, 42)
self.assertEqual(self.ctx.writes, 1)
self.assertEqual(self.ctx.reads, 0)
def test_set_int_counts_write_and_read(self):
a = self._int(0)
b = self._int(7)
a.set(b)
self.assertEqual(a.value, 7)
self.assertEqual(self.ctx.writes, 1)
self.assertEqual(self.ctx.reads, 1)
# ------------------------------------------------------------------
# Typkonvertierung
# ------------------------------------------------------------------
def test_int_conversion(self):
a = self._int(5)
self.assertEqual(int(a), 5)
def test_index_usage(self):
a = self._int(2)
lst = [10, 20, 30]
self.assertEqual(lst[a], 30) # __index__
def test_hash(self):
a = self._int(5)
self.assertEqual(hash(a), hash(5))
# ------------------------------------------------------------------
# AlgoContext.reset()
# ------------------------------------------------------------------
def test_context_reset(self):
a, b = self._int(3), self._int(4)
_ = a > b
self.ctx.reset()
self.assertEqual(self.ctx.comparisons, 0)
self.assertEqual(self.ctx.reads, 0)
class TestIntNullCtx(unittest.TestCase):
"""Int ohne expliziten ctx (irange-Verwendung) – keine Zählung."""
def test_arithmetic_without_ctx_produces_no_counts(self):
from utils.algo_context import NULL_CTX
a = Int(5) # uses NULL_CTX by default
b = Int(3)
_ = a + b
_ = a > b
# NULL_CTX hat feste 0-Attribute – keine Ausnahme, keine Zählung
self.assertEqual(NULL_CTX.additions, 0)
self.assertEqual(NULL_CTX.comparisons, 0)
if __name__ == "__main__":
unittest.main()
Whitespace-only changes.
+102
View File
@@ -0,0 +1,102 @@
from utils.algo_context import AlgoContext
from utils.algo_array import Array
from utils.algo_int import Int
from utils.algo_range import Range
def max_sequence_1(z: Array, ctx: AlgoContext):
"""O(n³) – dreifach verschachtelte Schleife."""
n = len(z)
m = Int(float('-inf'), ctx)
s = Int(0, ctx)
l = Int(0, ctx)
r = Int(0, ctx)
for i in Range(n):
for j in Range(int(i), n):
s.set(0)
for k in Range(int(i), int(j)):
s += z[k]
if s > m:
m.set(s)
l.set(Int(int(i), ctx))
r.set(Int(int(j), ctx))
return m, l, r
def max_sequence_2(z: Array, ctx: AlgoContext):
"""O(n²) – doppelt verschachtelte Schleife."""
n = len(z)
m = Int(float('-inf'), ctx)
s = Int(0, ctx)
left = Int(0, ctx)
right = Int(0, ctx)
for i in Range(n):
s.set(0)
for j in Range(int(i), n):
s += z[j]
if s > m:
m.set(s)
left.set(Int(int(i), ctx))
right.set(Int(int(j), ctx))
return m, left, right
def max_sequence_4(z: Array, ctx: AlgoContext):
"""O(n) – Kadane's Algorithmus."""
n = len(z)
m = Int(float('-inf'), ctx)
cur_sum = Int(0, ctx)
cur_left = Int(0, ctx)
left = Int(0, ctx)
right = Int(0, ctx)
for i in Range(n):
cur_sum += z[i]
if cur_sum > m:
m.set(cur_sum)
left.set(cur_left)
right.set(Int(int(i), ctx))
if cur_sum < 0:
cur_sum.set(0)
cur_left.set(Int(int(i) + 1, ctx))
return m, left, right
def example(max_sequence_func):
ctx = AlgoContext()
data = [-59, 52, 46, 14, -50, 58, -87, -77, 34, 15]
print(data)
z = Array(data, ctx)
m, l, r = max_sequence_func(z, ctx)
print(m, l, r)
def seq(filename, max_sequence_func):
ctx = AlgoContext()
z = Array.from_file(filename, ctx)
m, l, r = max_sequence_func(z, ctx)
print(m, l, r)
def analyze_complexity(max_sequence_func, sizes):
ctx = AlgoContext()
for size in sizes:
ctx.reset()
z = Array.random(size, -100, 100, ctx)
max_sequence_func(z, ctx)
ctx.save_stats(size)
ctx.plot_stats(["additions"])
if __name__ == '__main__':
example(max_sequence_2)
for filename in ["data/seq0.txt", "data/seq1.txt"]:
print(filename)
seq(filename, max_sequence_2)
analyze_complexity(max_sequence_2, [10, 20, 30, 40, 50, 60, 70, 80, 90, 100])
@@ -1,6 +1,6 @@
import random
import pygame
from utils.game import Game
from utils.algo_game import Game
from utils.algo_context import AlgoContext
from utils.algo_array import Array
from bubble_sorting import bubble_sort_stepwise
@@ -1,6 +1,6 @@
from utils.algo_context import AlgoContext
from utils.algo_array import Array
from utils.algo_range import irange
from utils.algo_range import Range
def bubble_sort_stepwise(z: Array, ctx: AlgoContext):
@@ -11,8 +11,8 @@ def bubble_sort_stepwise(z: Array, ctx: AlgoContext):
Wird von bubble_game.py für die Visualisierung verwendet.
"""
n = len(z)
for i in irange(n - 1):
for j in irange(n - 1, i, -1):
for i in Range(n - 1):
for j in Range(n - 1, i, -1):
if z[j - 1] > z[j]:
z.swap(j - 1, j)
yield z
@@ -27,7 +27,7 @@ def bubble_sort2_stepwise(z: Array, ctx: AlgoContext):
n = len(z)
while True:
swapped = False
for i in irange(n - 1):
for i in Range(n - 1):
if z[i] > z[i + 1]:
z.swap(i, i + 1)
swapped = True
@@ -1,6 +1,6 @@
import random
import pygame
from utils.game import Game
from utils.algo_game import Game
from utils.algo_context import AlgoContext
from utils.algo_array import Array
from insert_sorting import insert_sort_stepwise
@@ -1,7 +1,7 @@
from utils.algo_context import AlgoContext
from utils.algo_array import Array
from utils.algo_int import Int
from utils.algo_range import irange
from utils.algo_range import Range
def insert_sort_stepwise(z: Array, ctx: AlgoContext):
@@ -13,7 +13,7 @@ def insert_sort_stepwise(z: Array, ctx: AlgoContext):
n = len(z)
elem = Int(0, ctx) # Zwischenregister für das einzufügende Element
for i in irange(n):
for i in Range(n):
elem.set(z[i]) # 1 read + 1 write
j = Int(int(i), ctx)
while j > 0 and z[j - 1] > elem:
@@ -1,6 +1,6 @@
import random
import pygame
from utils.game import Game
from utils.algo_game import Game
from utils.algo_context import AlgoContext
from utils.algo_array import Array
from select_sorting import select_sort_stepwise
@@ -1,7 +1,7 @@
from utils.algo_context import AlgoContext
from utils.algo_array import Array
from utils.algo_int import Int
from utils.algo_range import irange
from utils.algo_range import Range
def select_sort_stepwise(z: Array, ctx: AlgoContext):
@@ -13,9 +13,9 @@ def select_sort_stepwise(z: Array, ctx: AlgoContext):
n = len(z)
cur_min = Int(0, ctx) # Index des aktuellen Minimums
for i in irange(n):
for i in Range(n):
cur_min.set(Int(int(i), ctx))
for j in irange(int(i) + 1, n):
for j in Range(int(i) + 1, n):
if z[j] < z[cur_min]:
cur_min.set(Int(int(j), ctx))
z.swap(int(i), int(cur_min))
@@ -1,6 +1,6 @@
import random
import pygame
from utils.game import Game
from utils.algo_game import Game
from utils.algo_context import AlgoContext
from utils.algo_array import Array
from heap_sorting import heap_sort_stepwise
@@ -1,6 +1,6 @@
import random
import pygame
from utils.game import Game
from utils.algo_game import Game
from utils.algo_context import AlgoContext
from utils.algo_array import Array
from quick_sorting import quick_sort_stepwise
@@ -1,6 +1,6 @@
from utils.algo_context import AlgoContext
from utils.algo_array import Array
from utils.algo_range import irange
from utils.algo_range import Range
def count_sort(a: Array, b: Array, k: int, ctx: AlgoContext):
@@ -14,15 +14,15 @@ def count_sort(a: Array, b: Array, k: int, ctx: AlgoContext):
c = Array([0] * (k + 1), ctx) # Zählarray
# Häufigkeiten zählen
for j in irange(len(a)):
for j in Range(len(a)):
c[a[j]] = c[a[j]] + 1
# Kumulierte Summen bilden
for i in irange(1, k + 1):
for i in Range(1, k + 1):
c[i] = c[i] + c[i - 1]
# Stabil in b einsortieren (rückwärts für Stabilität)
for j in irange(len(a) - 1, -1, -1):
for j in Range(len(a) - 1, -1, -1):
b[c[a[j]] - 1] = a[j]
c[a[j]] = c[a[j]] - 1
@@ -4,19 +4,23 @@ from vorlesung.L05_binaere_baeume.avl_tree import AVLTree
def analyze_complexity(sizes):
import random
sizes = list(sizes)
elements = random.sample(range(max(sizes) * 10), max(sizes))
ctx = AlgoContext()
tree = AVLTree(ctx)
prev = 0
for size in sizes:
z = Array.random(size, -100, 100, ctx)
tree = AVLTree(ctx)
for i in range(size - 1):
tree.insert(z[i].value)
for i in range(prev, size - 1):
tree.insert(elements[i])
ctx.reset()
tree.insert(z[size - 1].value)
tree.insert(elements[size - 1])
ctx.save_stats(size)
prev = size
ctx.plot_stats(["comparisons"])
if __name__ == "__main__":
sizes = range(1, 1001, 2)
sizes = range(10, 501, 10)
analyze_complexity(sizes)
@@ -4,19 +4,25 @@ from vorlesung.L05_binaere_baeume.bin_tree import BinaryTree
def analyze_complexity(sizes):
import random
sizes = list(sizes)
# Alle Elemente vorab erzeugen – Baum wird inkrementell aufgebaut statt
# für jede Größe neu, dadurch O(n log n) statt O(n²) Gesamtaufwand.
elements = random.sample(range(max(sizes) * 10), max(sizes))
ctx = AlgoContext()
tree = BinaryTree(ctx)
prev = 0
for size in sizes:
z = Array.random(size, -100, 100, ctx)
tree = BinaryTree(ctx)
for i in range(size - 1):
tree.insert(z[i].value)
for i in range(prev, size - 1): # Warm-up: ohne Messung einfügen
tree.insert(elements[i])
ctx.reset()
tree.insert(z[size - 1].value)
tree.insert(elements[size - 1]) # gemessener Insert
ctx.save_stats(size)
prev = size
ctx.plot_stats(["comparisons"])
if __name__ == "__main__":
sizes = range(1, 1001, 2)
sizes = range(10, 501, 10)
analyze_complexity(sizes)
@@ -1,6 +1,6 @@
import random
import pygame
from utils.game import Game
from utils.algo_game import Game
from utils.algo_context import AlgoContext
from avl_tree import AVLTree
+3 -3
View File
@@ -1,6 +1,6 @@
from vorlesung.L05_binaere_baeume.bin_tree_node import BinaryTreeNode
from utils.algo_context import AlgoContext
from utils.project_dir import get_path
from utils.algo_path import path
from datetime import datetime
import graphviz
@@ -42,7 +42,7 @@ class BinaryTree:
def search(self, value):
current = self.root
value = self.new_node(value)
while current:
while current is not None:
if value < current:
current = current.left
elif value > current:
@@ -148,7 +148,7 @@ class BinaryTree:
self.tree_structure_traversal(define_node)
_rec(self.root)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
dot.render(get_path(f"{self.graph_filename()}_{timestamp}.gv"))
dot.render(path(f"{self.graph_filename()}_{timestamp}.gv"))
if __name__ == "__main__":
@@ -1,6 +1,6 @@
import random
import pygame
from utils.game import Game
from utils.algo_game import Game
from utils.algo_context import AlgoContext
from bin_tree import BinaryTree
@@ -16,8 +16,8 @@ class BinaryTreeNode(Int):
self.right = None
def height(self):
left_height = self.left.height() if self.left else 0
right_height = self.right.height() if self.right else 0
left_height = self.left.height() if self.left is not None else 0
right_height = self.right.height() if self.right is not None else 0
return 1 + max(left_height, right_height)
def __repr__(self):
+1 -1
View File
@@ -2,7 +2,7 @@ from collections.abc import Callable
from utils.algo_context import AlgoContext
from utils.algo_array import Array
from utils.algo_int import Int
from utils.algo_range import irange
from utils.algo_range import Range
UNUSED_MARK = "UNUSED"
-45
View File
@@ -1,45 +0,0 @@
from vorlesung.L08_graphen.graph import Graph, AdjacencyMatrixGraph
from utils.project_dir import get_path
graph = AdjacencyMatrixGraph()
start = ""
end = ""
def read_file(filename: str = "data/aoc2212.txt"):
"""Read a file and return the content as a string."""
def adjust_char(char):
"""Adjust character for comparison."""
if char == 'S':
return 'a'
elif char == 'E':
return 'z'
return char
global start, end
with open(get_path(filename), "r") as file:
quest = file.read().strip().splitlines()
for row, line in enumerate(quest):
for col, char in enumerate(line):
label = f"{row},{col}"
graph.insert_vertex(label)
if char == "S":
start = label
if char == "E":
end = label
for row, line in enumerate(quest):
for col, char in enumerate(line):
for neighbor in [(row - 1, col), (row, col - 1), (row + 1, col), (row, col + 1)]:
if 0 <= neighbor[0] < len(quest) and 0 <= neighbor[1] < len(line):
if ord(adjust_char(quest[neighbor[0]][neighbor[1]])) <= ord(adjust_char(char)) + 1:
label1 = f"{row},{col}"
label2 = f"{neighbor[0]},{neighbor[1]}"
graph.connect(label1, label2)
# Lösung des Adventskalenders 2022, Tag 12
read_file("data/aoc2212test.txt")
graph.graph()
distance_map, predecessor_map = graph.bfs(start)
print(distance_map[graph.get_vertex(end)])
print(graph.path(end, predecessor_map))
+4 -4
View File
@@ -5,8 +5,8 @@ import graphviz
import math
import heapq
from datetime import datetime
from utils.project_dir import get_path
from utils.priority_queue import PriorityQueue
from utils.algo_path import path
from utils.algo_priority_queue import PriorityQueue
from vorlesung.L09_mst.disjoint import DisjointValue
@@ -127,7 +127,7 @@ class Graph:
predecessor_map[vertex] = None
while white_vertices:
v = white_vertices.pop()
v = next(iter(white_vertices))
dfs_visit(v)
return enter_map, leave_map, predecessor_map
@@ -155,7 +155,7 @@ class Graph:
dot.edge(str(id(self.get_vertex(edge[0]))), str(id(self.get_vertex(edge[1]))), label=str(edge[2]))
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"{filename}_{timestamp}.gv"
filename = get_path(filename)
filename = path(filename)
dot.render(filename)
def dijkstra(self, start_name: str) -> tuple[dict[Vertex, float], dict[Vertex, Vertex | None]]: