Musterlösung Praktikum 6

This commit is contained in:
Oliver Hofmann 2026-06-01 11:51:33 +02:00
parent ed329d5d85
commit 779fbab7c8
5 changed files with 380 additions and 0 deletions

View File

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

View File

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

View File

@ -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}")

View File

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

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/(10,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)."
)