Klausurtrainer Informatik I
Systematische Vorbereitung auf alle 7 Aufgaben der Klausur „Informatik für Ingenieurwissenschaften I“: ausführliche Deep-Dive-Grundlagen, Visualisierungen, handschrift-taugliche Musterlösungen und viele Übungen mit kompletten Lösungen.
Die Klausur im Überblick
| Aufgabe | Thema | Seite | Punkte |
|---|---|---|---|
| 1 | Ausdrücke und Datentypen (Slicing, Indexierung) | 2 | 9 |
| 2 | Hoare-Kalkül: schwächste Vorbedingung + Schleifeninvariante | 3 | 12 |
| 3 | Komplexitäten: Ergebnis für k = 3 + Komplexitätsklasse (f1, f2, f3) | 4 | 18 |
| 4-A | XML: Adressbaum als XML-Code modellieren (Wahl) | 5 | 10 |
| 4-B | Reguläre Ausdrücke: Wortaufzählung + RegEx (Wahl) | 6 | 10 |
| 5 | Kontextfreie Grammatik: Wörter + check_word | 7 | 15 |
| 6 | Rekursion: Typ erkennen, Schleife, Hilfsfunktionen | 8 | 16 |
| 7 | OOP: Klasse StringNumber | 9 | 12 |
| Σ | maximal erreichbare Punktzahl (inkl. Aufgabe 4) | 92 |
Lernplan — Heute Abend & Morgen (Klausur: Montag)
- Heute Abend (3–4 h): Thema 1 (Ausdrücke) + Thema 3 (Komplexität) — mechanische, schnelle Punkte, zusammen 27 Punkte.
- Heute Abend, danach: Thema 2 (Hoare-Kalkül) — wp-Ketten und Invarianten-Beweise üben.
- Morgen Vormittag (2–3 h): Thema 4-B (RegEx) und 4-A (XML) anschauen, dann eine Variante wählen und dort Vollpunkte sichern (10 P). Danach Thema 5 (Grammatik).
- Morgen Nachmittag (2–3 h): Thema 6 (Rekursion) + Thema 7 (OOP).
- Morgen Abend (2 h): Alle Selbsttests noch einmal, Musterlösungen auf Papier nachschreiben. Aufgaben 1 und 3 unter Zeitdruck komplett durchrechnen — zusammen 27 Punkte.
- Danach: Früh schlafen, Handy weg — Montagmorgen die Punkte-Strategie oben im Kopf.
Klausur-Tipps (aus dem Original)
- Deckblatt: 4-A oder 4-B ankreuzen — sonst wird nicht gewertet.
- Schmierpapier zählt nur mit Vermerk bei der Aufgabe (Seite 10).
- Datentyp-Namen dürfen umgangssprachlich sein („Gleitpunktzahl“ statt
float). - Punkte-Strategie: Aufgabe 3 ist mit 18 Punkten die größte — zuerst sichere Teilaufgaben aller Blöcke rechnen.
- Handschriftliche Notation verwenden:
{ … }-Tripel,⇒-Pfeile, Mengen mitε— so wie in den Musterlösungen hier.
Ausdrücke und Datentypen
- Python-Ausdrücke auswerten können: Ergebniswert und Datentyp angeben.
- Operatoren
/,//,*,+auf Zahlen und Strings sicher beherrschen. - Indexierung (auch negativ) und Slicing an Listen, Tupeln und Strings.
- Verschachtelte Ausdrücke von innen nach außen auflösen.
- Fehlerfälle erkennen (
TypeError,IndexError).
1. Grundlegende Datentypen
Ein Datentyp legt fest, welche Werte ein Ausdruck annehmen kann und welche Operationen erlaubt sind. Wichtig für die Klausur:
| Typ | Bedeutung | Beispiele | Veränderlich? |
|---|---|---|---|
int | ganze Zahlen | 6, -3, 2**10 | — |
float | Gleitpunktzahlen | 6.0, 6., 0.5 | — |
str | Zeichenkette (Text) | 'a', "abc" | nein (immutable) |
list | Folge in eckigen Klammern | [1, 2, 3] | ja (mutable) |
tuple | Folge in runden Klammern | (1, 2, '3') | nein (immutable) |
bool | Wahrheitswert | True, False | — |
6 ist int, 6. ist float. Ein einzelner Punkt im Literal reicht — und der float „färbt“ das Ergebnis jeder Rechnung, an der er beteiligt ist.lst[0] = 9), Tupel nicht. In der Klausur genügt es, die beiden Typen zu unterscheiden und zu wissen: […] → list, (…) → tuple.2. Operatoren auf Zahlen
| Operator | Name | Beispiel | Ergebnis | Typ |
|---|---|---|---|---|
+ | Addition | 5 + 2 | 7 | int |
- | Subtraktion | 5 - 2 | 3 | int |
* | Multiplikation | 5 * 2 | 10 | int |
/ | echte Division | 6 / 3 | 2.0 | immer float |
// | Ganzzahldivision (abrunden) | 7 // 2 | 3 | int |
% | Modulo (Rest) | 7 % 2 | 1 | int |
** | Potenz | 2**3 | 8 | int |
** → * / // % → + -. Klammern heben jeden Vorrang auf. Zusätzlich: ** ist rechtsassoziativ (2**3**2 = 2**9 = 512), - und / sind linksassoziativ (10 - 4 - 3 = 3).3. Operatoren auf Strings
+— Verkettung:'ab' + 'cd' → 'abcd'.*— Vervielfachung, aber nur String × Zahl:'a' * 3 → 'aaa'. Auch3 * 'a' → 'aaa'.str * str(z. B.'a' * '3') führt zum TypeError.len('abc') → 3— Länge einer Zeichenkette.'a' in 'banana' → True— Teilstring-Test (Ergebnis: bool).'x' * 0 → ''— Multiplikation mit 0 ergibt den leeren String, kein Fehler.
str * int vervielfacht, str * str crasht (TypeError). Und: „String + Zahl“ ('a' + 1) ist ebenfalls ein TypeError — Python konkateniert nicht automatisch.4. Indexierung und Slicing — die komplette Theorie
Indizes zählen von 0 an. Negative Indizes zählen vom Ende: -1 ist das letzte Element, -2 das vorletzte usw.
Positiv: 0 1 2 3 4
┌────┬────┬────┬────┬────┐
│ H │ A │ L │ L │ O │
└────┴────┴────┴────┴────┘
Negativ: -5 -4 -3 -2 -1
Slicing-Syntax: seq[start:stop] und seq[start:stop:step].
- start — erster Index (inklusive). Weggelassen = Beginn der Sequenz.
- stop — Ende exklusiv: das Element an Index
stopist NICHT enthalten. Weggelassen = Ende der Sequenz. - step — Schrittweite;
-1dreht die Sequenz um:'abc'[::-1] → 'cba'. - Slicing liefert immer denselben Sequenztyp:
liste[a:b] → list,tupel[a:b] → tuple,'abc'[a:b] → str— auch wenn nur ein Element drin ist. - Außerhalb liegende Indizes werden abgeschnitten, nicht als Fehler gemeldet:
[1,2,3][1:99] → [2, 3]. Ein einzelner Index außerhalb (z. B.[1,2,3][5]) wirft dagegen einenIndexError.
| Ausdruck | Ergebnis | Kommentar |
|---|---|---|
'HALLO'[0] | 'H' | erster Index |
'HALLO'[-1] | 'O' | letztes Element |
'HALLO'[1:4] | 'ALL' | Indizes 1, 2, 3 — stop ist exklusiv |
'HALLO'[:3] | 'HAL' | start weggelassen = von vorn |
'HALLO'[3:] | 'LO' | stop weggelassen = bis zum Ende |
'HALLO'[::-1] | 'OLLAH' | Schritt -1: umgedreht |
[1, 2, 3][1:2] | [2] | Ergebnis ist eine Liste, nicht die Zahl 2! |
(1, 2, 3)[0:2] | (1, 2) | Ergebnis ist ein Tupel |
5. Verschachtelte Ausdrücke
Klammerausdrücke werden von innen nach außen ausgewertet. In Teilaufgabe e) steckt ein Index-Ausdruck in einem Index-Ausdruck:
(1, 2, '3')[[1, 2, 3][-2]] │ └── ① zuerst: [1,2,3][-2] → 2 └── ② dann: (1, 2, '3')[2] → '3'
Bei verschachtelten Listen wie [[1, 2], [3, 4]][1][0] gilt dasselbe: erst der äußere Zugriff [[1,2],[3,4]][1] → [3, 4], dann der innere [3, 4][0] → 3.
6. Häufige Fehlerarten erkennen
- TypeError: unpassende Typen kombiniert (
'a' * '3','a' + 1,'a' * 2.0). - IndexError: einzelner Index außerhalb der Sequenz (
[1, 2][5]). - Kein Fehler: Slicing mit außerhalb liegenden Grenzen (wird abgeschnitten).
Links oben die positiven Indizes, unten die negativen. Der gestrichelte Kasten zeigt, was das Slicing [1:2] ausschneidet: nur das Element an Index 1 — der Stop-Index 2 ist exklusiv.
Geben Sie für jeden der folgenden Ausdrücke das Ergebnis und dessen Datentyp an. (Beispiel: Das Ergebnis von 3.0*4.0 wäre 12.0, dessen Datentyp wäre float. Auf den genauen Namen des Datentyps kommt es hier nicht an — „Gleitpunktzahl“ wäre im Beispiel auch OK.)
a) 6 * 3 b) 6. * 3 c) 6 / 3 d) [[1, 2][0], [3, 4][1]] e) (1, 2, '3')[[1, 2, 3][-2]] f) [1, 2, 3][1:2]
a) 6 * 3 → 18 int b) 6. * 3 → 18.0 float c) 6 / 3 → 2.0 float d) [[1,2][0], [3,4][1]] → [1, 4] list e) (1,2,'3')[[1,2,3][-2]] → '3' str f) [1, 2, 3][1:2] → [2] list
6 * 3 → 18 · intZwei ganze Zahlen werden multipliziert — das Ergebnis ist die ganze Zahl 18. Es gibt keinen Grund für einen Punkt, also bleibt der Typ int.
6. * 3 → 18.0 · floatDer Punkt nach der 6 macht das Literal 6. zu einer Gleitpunktzahl. Operationen zwischen float und int liefern float — der float „gewinnt“.
6 / 3 → 2.0 · floatDie echte Division / liefert immer eine Gleitpunktzahl — auch wenn sie „glatt“ aufgeht. 2.0, nicht 2! Das ist die häufigste Falle dieser Aufgabe.
[[1, 2][0], [3, 4][1]] → [1, 4] · listVon innen nach außen: [1, 2][0] ist das Element an Index 0 der Liste [1, 2] → 1. [3, 4][1] ist das Element an Index 1 → 4. Die äußeren eckigen Klammern bauen daraus eine neue Liste: [1, 4].
(1, 2, '3')[[1, 2, 3][-2]] → '3' · strZuerst den Index ausrechnen: [1, 2, 3][-2] greift das vorletzte Element → 2. Damit wird aus dem Ausdruck (1, 2, '3')[2] — das Element an Index 2 des Tupels ist der String '3'. Ergebnis: '3', Typ str (nicht int — die Anführungszeichen sind entscheidend!).
[1, 2, 3][1:2] → [2] · listSlicing [start:stop]: Stop-Index 2 ist exklusiv, es bleibt nur Index 1 → Element 2. Weil Slicing immer eine Sequenz liefert, ist das Ergebnis die Liste [2] — nicht die Zahl 2.
6 / 3als2(int) notieren — richtig ist2.0(float).- Bei e) den Index-Ausdruck nicht zuerst auswerten.
- Bei f) als Ergebnis
2statt[2]schreiben — das Slicing „verpackt“ das Ergebnis in eine Liste. - Typ
strundintverwechseln, wenn eine Zahl als String vorliegt.
Übung 1.1 — Werte und Typen I (Basics)
Werten Sie aus und geben Sie Wert + Typ an:
1) 3 + 4 2) 3.0 + 4 3) 10 / 2 4) 10 // 2 5) 10 % 3 6) 2 ** 5✍ Lösung
1) 7 int (ganze Zahlen) 2) 7.0 float (float färbt) 3) 5.0 float (/ liefert IMMER float) 4) 5 int (Ganzzahldivision) 5) 1 int (Rest: 10 = 3·3 + 1) 6) 32 int (Potenz)
Übung 1.2 — Werte und Typen II (Strings und Listen)
Werten Sie aus und geben Sie Wert + Typ an:
1) 'ab' + 'cd' * 2
2) [10, 20, 30][-1]
3) ("x", "y")[0:1]
4) [[1, 2], [3, 4]][1][0]
5) 7 // 2
6) 7 % 2
7) 'ab' * 0
✍ Lösung
1) 'abcdcd' str (Vorrang: 'cd'*2 zuerst → 'cdcd', dann +)
2) 30 int (-1 = letztes Element)
3) ('x',) tuple (Slicing liefert 1-Element-TUPEL)
4) 3 int (erst [1] → [3,4], dann [0] → 3)
5) 3 int (Ganzzahldivision rundet ab)
6) 1 int (Rest von 7 ÷ 2)
7) '' str (Multiplikation mit 0 → leerer String, KEIN Fehler)
Übung 1.3 — Operator-Vorrang
Werten Sie aus (bei 4) und 5) entscheiden die Assoziativitätsregeln!):
1) 2 + 3 * 4 2) (2 + 3) * 4 3) 2 * 3 ** 2 4) 2 ** 3 ** 2 (rechtsassoziativ!) 5) 10 - 4 - 3 (linksassoziativ!) 6) 10 - 2 * 3 + 1✍ Lösung
1) 14 (3·4 zuerst: 2 + 12) 2) 20 (Klammer zuerst: 5·4) 3) 18 (3² zuerst: 2·9) 4) 512 (** ist rechtsassoziativ: 2^(3^2) = 2^9) 5) 3 (- ist linksassoziativ: (10-4)-3) 6) 5 (2·3 zuerst: 10 - 6 + 1)
Übung 1.4 — Slicing gezielt üben
Gegeben s = "Informatik". Welchen Wert liefern die Ausdrücke?
1) s[0] 5) s[-1] 2) s[0:4] 6) s[5:] 3) s[:5] 7) s[::-1] 4) s[3:6] 8) s[-6:-3]✍ Lösung
s = "Informatik" (Indizes 0–9) 1) 'I' 5) 'k' 2) 'Info' 6) 'atik' 3) 'Infor' 7) 'kitamrofnI' (Schritt -1: rückwärts) 4) 'orm' 8) 'orm' (Indizes 3–5)
Übung 1.5 — Verschachtelte Strukturen
Werten Sie aus (Wert + Typ):
1) [[1, 2, 3], [4, 5, 6]][1]
2) [[1, 2, 3], [4, 5, 6]][1][2]
3) (('a', 'b'), ('c', 'd'))[0][1]
4) [[1, 2][-1], [3, 4][-2]]
5) ['x', 'y'][1:2][0]
6) [1, [2, [3, [4]]]][1][1][1]
✍ Lösung
1) [4, 5, 6] list (äußere Liste, Element 1) 2) 6 int (erst [1], dann [2]) 3) 'b' str (erst Tupel 0, dann Tupel 1) 4) [2, 3] list ([-1]=2, [-2]=3 → neue Liste) 5) 'y' str ([1:2] → ['y'], davon [0] → 'y') 6) [4] list (1 → [2,[3,[4]]] → [3,[4]] → [4])
Übung 1.6 — Fehler erkennen
Welche Ausdrücke werfen einen Fehler — und welchen? (Kein Fehler = Wert angeben.)
1) 'a' + 1 2) [1, 2][5] 3) 'a' * 2.0 4) 2 + '2' 5) [1, 2, 3][1:2] 6) 'a' * '3' 7) [1, 2, 3][1:99]✍ Lösung
1) TypeError (str + int nicht erlaubt) 2) IndexError (Index 5 existiert nicht) 3) TypeError (str * float nicht erlaubt — nur int) 4) TypeError (int + str nicht erlaubt) 5) [2] list (kein Fehler!) 6) TypeError (str * str) 7) [2, 3] list (Slicing schneidet ab — KEIN IndexError)
Übung 1.7 — Klausurformat: 6 Ausdrücke wie in Aufgabe 1
Werten Sie aus — Ergebnis und Datentyp:
1) 3.0 * 2
2) 'ab' * 2
3) [1, 2, 3][-2:]
4) ('a', 'b', 'c')[1]
5) [[1][0], [2, 3][1]]
6) 'abc'[1:3]
✍ Lösung
1) 6.0 float (float färbt) 2) 'abab' str 3) [2, 3] list (von Index -2 bis Ende) 4) 'b' str 5) [1, 3] list (1 und 3 → neue Liste) 6) 'bc' str (Indizes 1–2)
Übung 1.8 — boolesche Ausdrücke, len und in
Werten Sie aus (Wert + Typ):
1) len('Informatik')
2) 'a' in 'banana'
3) 'x' in 'abc'
4) 3 < 5
5) 2 + 3 == 6
6) [1, 2, 3] == [1, 2, 3]
✍ Lösung
1) 10 int (10 Zeichen) 2) True bool (Substring vorhanden) 3) False bool 4) True bool 5) False bool (5 == 6 ist falsch) 6) True bool (Listen werden elementweise verglichen)
Hoare-Kalkül: wp und Schleifeninvarianten
- Die schwächste Vorbedingung eines Programms rückwärts berechnen (Zuweisungs- und Sequenzregel).
- Nachweisen, dass ein Prädikat Schleifeninvariante ist — als sauberer handschriftlicher Aufschrieb.
- Die Regeln auf Zuweisungen, Sequenzen und Schleifen sicher anwenden.
1. Hoare-Tripel
S die Vorbedingung P gilt, dann gilt nach der Ausführung die Nachbedingung Q (sofern S terminiert — „partielle Korrektheit“).PundQsind Prädikate (Bedingungen über Variablen), z. B.x > 0odery == 2·x.- Die schwächste Vorbedingung
wp(S, Q)ist die größte Menge von Startzuständen, von denen ausQnachSgarantiert gilt.
2. Die Regeln des Hoare-Kalküls (komplett)
| Anweisung | Regel (wp) | Bedeutung |
|---|---|---|
Zuweisung x = e | wp(x = e, Q) ≡ Q[x←e] | jedes x in Q durch e ersetzen |
Sequenz S1; S2 | wp(S1; S2, Q) ≡ wp(S1, wp(S2, Q)) | von unten nach oben rechnen |
Bedingung if B: S1 else: S2 | wp(if B …, Q) ≡ (B ∧ wp(S1, Q)) ∨ (¬B ∧ wp(S2, Q)) | beide Zweige berücksichtigen |
Schleife while B: S | Invariante I zeigen (Anfang, Erhaltung, Abschluss) | kein mechanisches wp |
wp(x = e, Q): Nimm Q, ersetze jede freie Variable x durch den Ausdruck e. Beispiel: wp(x = x + 2, {x > 8}) = {x + 2 > 8} ≡ {x > 6}.3. Die Sequenzregel als „wp-Kette“
Bei mehreren Anweisungen startet man bei der letzten Anweisung und arbeitet sich nach oben vor. Jede Zwischenbedingung ist gleichzeitig Nachbedingung der darüberliegenden Anweisung:
{ P }
S1: x = x - y
{ Q1 }
S2: y = y + x
{ Q2 }
S3: x = y - 1
{ Q3 ≡ x > 0 }
Gerechnet wird: Q2 = wp(S3, Q3), dann Q1 = wp(S2, Q2), dann P = wp(S1, Q1).
4. Schleifeninvariante — die komplette Beweismethode
Für while B: S wählt man ein Prädikat I und zeigt drei Eigenschaften:
- Anfangsgültigkeit:
{ I }gilt vor dem ersten Schleifendurchlauf. - Erhaltung:
{ I ∧ B } S { I }— nach jedem Rumpf-Durchlauf giltIwieder. - Abschluss:
I ∧ ¬B ⇒ Q— nach der Schleife folgt die Nachbedingung.
In Teilaufgabe b) wird nur Eigenschaft 2 verlangt. Der Aufschrieb ist immer gleich: Annahme I ∧ B, die neuen Werte der Variablen bestimmen (mit Strich: p', q'), dann die Gleichungen nachrechnen.
q < b ⇒ q + 1 ≤ b nutzt man, dass q, b ganze Zahlen sind. Bei reellen Zahlen wäre die Aussage falsch (q = 1.5, b = 2: 1.5 < 2, aber 2.5 > 2). Deshalb steht „Typ int“ ausdrücklich in der Aufgabe!Die wp-Berechnung ist eine rückwärts laufende Kette: Jede Anweisung übersetzt die Nachbedingung in ihre Vorbedingung, die zur Nachbedingung der darüberliegenden Anweisung wird.
Ableserichtung: ganz unten starten (Nachbedingung { x > 0 }), jede Zuweisung rückwärts „durchschieben“, oben ist die Vorbedingung erreicht.
Invariante als Schleifen-Flussdiagramm
a) Bestimmen Sie mittels des Hoare-Kalküls die schwächste Vorbedingung des Codestücks
x = x - y y = y + x x = y - 1
zur Nachbedingung x > 0. Die Variablen x und y haben dabei Werte vom Typ int.
b) Zeigen Sie, dass das Prädikat p == q*a and q <= b eine Schleifeninvariante ist für
while q < b:
p = p + a
q = q + 1
Die Variablen a, b, p und q haben dabei Werte vom Typ int.
Teil a) — Schwächste Vorbedingung (6 Punkte)
Strategie: Von unten nach oben. Nachbedingung Q ≡ { x > 0 }. Für die letzte Zuweisung x = y - 1 die Zuweisungsregel anwenden, dann für y = y + x, dann für x = x - y.
Ziel: { x > 0 }
S3: x = y - 1
wp: x := y - 1 einsetzen
⇒ { y - 1 > 0 } ≡ { y > 1 }
S2: y = y + x
wp: y := y + x einsetzen
⇒ { (y + x) > 1 }
S1: x = x - y
wp: x := x - y einsetzen
⇒ { y + (x - y) > 1 } ≡ { x > 1 }
Antwort: Schwächste Vorbedingung P ≡ { x > 1 }
x = y - 1 mit Nachbedingung { x > 0 }Zuweisungsregel: jedes x in der Nachbedingung wird durch y - 1 ersetzt: y - 1 > 0. Umformen: y > 1. (Nur y - 1 > 0 stehen zu lassen wäre auch ok, aber die einfachere Form ist sicherer.)
y = y + x mit Nachbedingung { y > 1 }Ersetze y durch y + x: y + x > 1. Wichtig: das x bleibt unangetastet — ersetzt wird nur die Variable, die links der Zuweisung steht.
x = x - y mit Nachbedingung { y + x > 1 }Ersetze x durch x - y: y + (x - y) > 1. Vereinfachen: y - y = 0, es bleibt x > 1. Damit ist die Kette fertig — die erste Anweisung ist erreicht.
x = 2, y = 5 (erfüllt x > 1). Rechne nach: x = 2-5 = -3; y = 5 + (-3) = 2; x = 2 - 1 = 1 … 1 > 0 ✓. Die Bedingung x > 1 ist also hinreichend — und jede noch schwächere Bedingung würde die Nachbedingung nicht mehr garantieren.Teil b) — Schleifeninvariante (6 Punkte)
Zu zeigen: Wenn I ≡ (p == q*a ∧ q ≤ b) gilt und die Schleifenbedingung q < b erfüllt ist, dann gilt I auch nach einem Rumpf-Durchlauf. Wir schreiben die neuen Werte mit Strich: p' = p + a und q' = q + 1.
I: p == q·a ∧ q ≤ b Annahme: I ∧ (q < b) gilt vor dem Rumpf. 1) p = p + a ⇒ p' = p + a Mit p == q·a folgt: p' = q·a + a = (q + 1)·a = q'·a ✓ 2) q = q + 1 ⇒ q' = q + 1 Aus q < b und q, b ∈ int folgt: q ≤ b - 1 ⇒ q' = q + 1 ≤ b ✓ Nach dem Rumpf gilt also wieder p' == q'·a ∧ q' ≤ b = I ∎ Damit ist I eine Schleifeninvariante.
Der Rumpf erhöht p um a. Da vorher p = q·a war, wird p' = q·a + a = (q+1)·a. Aber q' = q + 1 — also ist p' = q'·a. Der erste Teil der Invariante bleibt erhalten.
q wird um 1 erhöht. Wegen der Schleifenbedingung gilt q < b; bei ganzen Zahlen heißt das q ≤ b - 1, also q + 1 ≤ b. Damit bleibt auch q ≤ b erhalten. (Genau hier braucht man die Voraussetzung „Typ int“ — bei reellen Zahlen würde q + 1 ≤ b nicht folgen!)
Beide Teile gelten nach dem Rumpf weiter → I bleibt nach jedem Durchlauf erhalten. Das ist die Definition der Schleifeninvariante. (Bonus-Wissen: Vor dem ersten Durchlauf gilt mit p = 0, q = 0: 0 == 0·a ✓ und 0 ≤ b ✓ — die Invariante ist sogar von Anfang an gültig.)
- Bei der Zuweisungsregel in die falsche Richtung ersetzt (die Variable aus
Q, nicht aus der Zuweisung!). - Die Kette von unten nach oben rechnen, aber das Ergebnis von oben einsetzen.
- Bei b) nur sagen „die Schleife läuft bis q = b“ — es muss gezeigt werden, dass beide Teile von
I(p-Gleichung und q-Schranke) erhalten bleiben. - Klammerfehler bei der Substitution:
y + (x - y)statty + x - ysauber ausrechnen.
Übung 2.1 — wp einer einzelnen Zuweisung
Berechnen Sie jeweils die schwächste Vorbedingung:
1) wp(x = x * 2, { x > 8 })
2) wp(x = x - 3, { x ≥ 0 })
3) wp(y = y + x, { y > 10 })
✍ Lösung
1) wp(x = x·2, {x > 8}) = { x·2 > 8 } ≡ { x > 4 }
2) wp(x = x-3, {x ≥ 0}) = { x-3 ≥ 0 } ≡ { x ≥ 3 }
3) wp(y = y+x, {y > 10}) = { y + x > 10 } (x bleibt stehen)
Übung 2.2 — wp einer Sequenz (zwei Anweisungen)
Bestimmen Sie die schwächste Vorbedingung für die Nachbedingung y == 10:
x = x + y y = x✍ Lösung
Unten (y = x): wp(y = x, {y == 10}) = { x == 10 }
Oben (x = x + y): wp(x = x + y, {x == 10}) = { x + y == 10 }
Antwort: P ≡ { x + y == 10 }
Übung 2.3 — wp-Kette mit drei Anweisungen (Klausurformat)
Bestimmen Sie die schwächste Vorbedingung des Codestücks zur Nachbedingung { x > 0 } (alle Variablen vom Typ int):
x = x + 2 y = y + x x = x - y✍ Lösung
S3: x = x - y ⇒ wp = { x - y > 0 }
S2: y = y + x ⇒ wp = { (y + x) - x > 0 } ≡ { y > 0 }
S1: x = x + 2 ⇒ wp = { y > 0 } (y kommt in der Zuweisung nicht vor → bleibt)
Antwort: P ≡ { y > 0 }
Übung 2.4 — wp-Kette mit Vereinfachung (Klausurformat)
Bestimmen Sie die schwächste Vorbedingung zur Nachbedingung { y < 10 } (int-Variablen):
y = y * 2 x = x + y y = y - x✍ Lösung
S3: y = y - x ⇒ wp = { y - x < 10 }
S2: x = x + y ⇒ wp = { y - (x + y) < 10 } ≡ { -x < 10 } ≡ { x > -10 }
S1: y = y * 2 ⇒ wp = { x > -10 } (x unverändert)
Antwort: P ≡ { x > -10 }
Übung 2.5 — Invariante nachweisen (Klausurformat)
Zeigen Sie: n ≥ 0 ist eine Schleifeninvariante für while n > 0: n = n - 1.
Annahme: I ∧ B, also n ≥ 0 und n > 0. Nach n = n - 1 gilt n' = n - 1. Wegen n > 0 und n ∈ int folgt n ≥ 1, also n' = n - 1 ≥ 0. ✓ ⇒ I bleibt erhalten. ∎ Zusatz (Abschluss): Nach der Schleife gilt I ∧ ¬B, also n ≥ 0 ∧ n ≤ 0 ⇒ n == 0. Die Nachbedingung folgt.
Übung 2.6 — Invariante mit zwei Variablen
Zeigen Sie: x == y ist eine Schleifeninvariante für
while x < 10:
x = x + 1
y = y + 1
✍ Lösung
Annahme: x == y ∧ x < 10. Rumpf: x' = x + 1, y' = y + 1. Aus x == y folgt: x' = x + 1 = y + 1 = y'. ✓ ⇒ x == y bleibt erhalten. ∎ (Bonus: x == y gilt sogar für jede Startbelegung, wenn sie vorher galt.)
Übung 2.7 — Invariante + Nachbedingung (Multiplikations-Schleife)
Gegeben sei die Schleife
while q < b:
p = p + a
q = q + 1
mit Invariante I: p == q·a ∧ q ≤ b (Voraussetzung: p = 0, q = 0, a ≥ 0, b ≥ 0 vor der Schleife). Was gilt nach der Schleife für p?
Nach der Schleife: I ∧ ¬B ⇒ q ≥ b ∧ q ≤ b ⇒ q == b. Mit p == q·a folgt: p == b·a. Die Schleife berechnet also das Produkt a·b. (das ist die klassische Multiplikations-Schleife der Hoare-Vorlesung)
Komplexitäten: Ergebnis & O-Klasse
- Für eine Funktion mit Parameter
kdas Ergebnis des Aufrufsk = 3bestimmen (exakt ausrechnen!). - Die Anzahl der Operationen als Summe aufstellen und schließen.
- Eine möglichst kleine Komplexitätsklasse in Abhängigkeit von
kangeben — und in Abhängigkeit vonn = 2^k.
1. Was bedeutet Komplexität?
Die Laufzeitkomplexität beschreibt, wie die Anzahl der Operationen wächst, wenn die Eingabe größer wird. Üblich ist die O-Notation (obere Schranke): Konstante Faktoren und kleinere Terme werden vernachlässigt, es zählt nur die Wachstumsordnung.
O(1)— konstant: die Operationsanzahl hängt nicht von der Eingabe ab.O(log n)— logarithmisch: die Eingabe wird pro Schritt halbiert/verdoppelt.O(n)— linear.O(n log n)— quasi-linear (typisch für gutes Sortieren).O(n²)— quadratisch (verschachtelte Schleifen).O(2^n)— exponentiell (sehr schnell wachsend).
2k² + 5k + 100 ∈ O(k²). Und Konstanten verschwinden: 2·2^k ∈ O(2^k).2. Die Summenformeln, die du können musst
Gauß (arithmetisch): Σ_{i=1}^{n} i = n·(n+1)/2 ∈ O(n²)
Geometrisch: Σ_{i=0}^{k} 2^i = 2^{k+1} - 1 ∈ O(2^k)
Ungerade Zahlen: Σ_{i=1}^{n} (2i-1) = n² ∈ O(n²)
Die erste taucht bei f2 auf (m wird nur um 1 kleiner), die zweite bei f3 (m halbiert sich).
3. Logarithmus-Kurzinfo (wegen n = 2^k)
n = 2^k bedeutet k = log₂(n). Deshalb sind diese Umrechnungen in der Aufgabe überall im Spiel:
2^k = n— die Schleife überkist linear ink, aber logarithmisch inn.2^{2k} = (2^k)² = n².- Beispiele:
k=3 → n=8,k=5 → n=32,k=10 → n=1024.
4. Analyse-Methodik für Schleifen
- Fest verschachtelt:
for i in range(n): for j in range(m):mit festemm→n · mDurchläufe (f1). - m sinkt um 1:
while m > 0: for j in range(m); m = m - 1→ Summen + (n-1) + … + 1 = n(n+1)/2(f2). - m halbiert sich:
while m > 0: for j in range(m); m = m // 2→n + n/2 + … + 1 = 2n - 1(f3) — die geometrische Reihe.
k, aber die Schleifen laufen über n = 2^k. Deshalb muss man die Klasse doppelt angeben können: einmal in n (z. B. O(n²)) und einmal in k (dann O(2^{2k})). Beide Antworten sind richtig — mit Begründung gibt es volle Punkte.5. Ergebnis für k = 3
Das Ergebnis ist schlicht count nach allen Schleifen. Für k = 3 ist n = 2³ = 8. Am sichersten: die Summe aufstellen und n = 8 einsetzen (nicht abzählen!).
Die beiden f3-Kurven (durchgezogen) und die f1/f2-Kurven (gestrichelt): Schon ab k = 3 ist der Unterschied klar — f1 und f2 wachsen quadratisch, f3 nur linear.
Trace-Tabellen für k = 3 (n = 8)
f1: m = 8, beide Schleifen 8-mal → 8 · 8 = 64 f2: m = 8,7,…,1 → 8+7+6+5+4+3+2+1 = 36 f3: m = 8,4,2,1 → 8+4+2+1 = 15
Bestimmen Sie für folgende Funktionen das Ergebnis eines Aufrufs mit Parameter k = 3 und eine geeignete Komplexitätsklasse (möglichst klein und durch einen möglichst einfachen Ausdruck beschrieben) in Abhängigkeit von k ∈ ℕ.
a) def f1(k: int) -> int:
n = 2**k
m = n
count = 0
for i in range(n):
for j in range(m):
count = count + 1
return count
b) def f2(k: int) -> int:
n = 2**k
m = n
count = 0
while m > 0:
for j in range(m):
count = count + 1
m = m - 1
return count
c) def f3(k: int) -> int:
n = 2**k
m = n
count = 0
while m > 0:
for j in range(m):
count = count + 1
m = m // 2
return count
a) f1 — fest verschachtelt (6 Punkte)
f1: m = n bleibt konstant.
Innere Schleife: n Durchläufe (jeweils m = n Schritte)
Äußere Schleife: n Durchläufe
Operationen: T(k) = n · n = n² = (2^k)² = 2^{2k}
k = 3: n = 8, T = 8² = 64
Klasse: O(n²) bzw. O(2^{2k})
m wird in f1 nie verändert — beide Schleifen sind fest über n bzw. m = n. Produktregel: n · n Schritte. Mit n = 2^k ist das 2^k · 2^k = 2^{2k}.
b) f2 — m sinkt um 1 (6 Punkte)
f2: m durchläuft n, n-1, …, 1
Operationen: T(k) = n + (n-1) + … + 1
= n·(n+1)/2 (Gauß)
k = 3: n = 8, T = 8·9/2 = 36
Klasse: O(n²) bzw. O(2^{2k})
Der äußere while-Lauf startet bei m = n und zählt mit m = m - 1 herunter. Die innere Schleife macht jeweils m Schritte → Summe 1 bis n. Mit der Gauß-Formel n(n+1)/2. Der dominante Term ist n²/2 → O(n²).
c) f3 — m halbiert sich (6 Punkte)
f3: m durchläuft n, n/2, n/4, …, 1
Operationen: T(k) = n + n/2 + n/4 + … + 1
= Σ_{i=0}^{k} 2^i
= 2^{k+1} - 1 (geometrische Reihe)
= 2·2^k - 1 = 2n - 1
k = 3: n = 8, T = 15
Klasse: O(n) bzw. O(2^k)
Da sich m pro Durchlauf halbiert (Integer-Division), sind es nur k + 1 äußere Durchläufe: 8, 4, 2, 1. Das ist die geometrische Reihe Σ 2^i = 2^{k+1} - 1, umgeschrieben 2n - 1. Linear — nicht logarithmisch! Der Fehler „O(log n)“ entsteht nur, wenn man die Anzahl der äußeren Durchläufe (k+1) statt der Gesamtsumme betrachtet.
- Bei f2/f3 die Summe nicht aufstellen, sondern nur „n²“ raten — f3 ist eben nicht n², sondern nur 2n.
- f3 als
O(log n)notieren — das vergisst die innere Schleife, die bei jedem Durchlauf jamSchritte macht. - Ergebnis für k = 3 falsch, weil nicht mit
n = 8sondern mitk = 3gerechnet wird (z. B. 3·3 = 9). - Klasse nur in
nangeben — die Aufgabe verlangt sie in Abhängigkeit von k; beides hinschreiben ist am sichersten. - Konstanten in der Klasse lassen:
2^{k+1} - 1 ∈ O(2^k), nichtO(2^{k+1})als Endergebnis schreiben.
Übung 3.1 — Summen berechnen
1) Σ_{i=1}^{10} i
2) Σ_{i=0}^{6} 2^i
3) Σ_{i=1}^{k} i in O-Notation
4) Σ_{i=1}^{n} (2i-1) in O-Notation
5) Σ_{i=0}^{k} 3^i (geometrische Reihe mit Faktor 3)
✍ Lösung
1) 10·11/2 = 55
2) 2^7 - 1 = 127
3) k(k+1)/2 ∈ O(k²)
4) n² ∈ O(n²) (Summe der ungeraden Zahlen 1+3+5+…)
5) (3^{k+1} - 1) / 2 ∈ O(3^k)
Übung 3.2 — Muster erkennen: Code → Klasse
Geben Sie für jedes Codefragment die Komplexitätsklasse in n an (n = 2^k bzw. n = Eingabe):
1) for i in range(n):
for j in range(n): pass
2) for i in range(n):
for j in range(n):
for l in range(n): pass
3) while m > 0:
m = m // 2
4) while m < n:
m = m * 2
5) for i in range(n):
for j in range(i): pass
✍ Lösung
1) O(n²) (n·n, fest verschachtelt) 2) O(n³) (n·n·n) 3) O(log n) (m halbiert sich — log₂(n) Durchläufe) 4) O(log n) (m verdoppelt sich — gleiche Idee rückwärts) 5) O(n²) (0+1+2+…+(n-1) = n(n-1)/2)
Übung 3.3 — Ergebnis für k = 3 + Klasse (Klausurformat)
Bestimmen Sie Ergebnis (k = 3) und Komplexitätsklasse in k und in n:
1) def g1(k: int) -> int:
n = 2**k
count = 0
for i in range(n):
count = count + 1
return count
2) def g2(k: int) -> int:
n = 2**k
count = 0
for i in range(n):
for j in range(n):
count = count + 1
return count
3) def g3(k: int) -> int:
n = 2**k
count = 0
while n > 0:
count = count + 1
n = n // 2
return count
✍ Lösung
1) k=3: n=8 → count = 8
T(k) = 2^k ∈ O(2^k) = O(n)
2) k=3: n=8 → count = 64
T(k) = (2^k)² = 2^{2k} ∈ O(2^{2k}) = O(n²)
3) k=3: n=8 → count = 4 (8,4,2,1)
T(k) = k + 1 ∈ O(k) = O(log n)
Übung 3.4 — Halbierung vs. lineare Reduktion (Falle!)
Warum ist while m > 0: m = m // 2 (nur Zählschritt) so viel schneller als while m > 0: m = m - 1? Geben Sie die Klassen für beide an.
Halbiert: m: 16, 8, 4, 2, 1 → log₂(16) = 4 + 1 Durchläufe
⇒ O(log n)
Minus 1: m: 16, 15, …, 1 → 16 Durchläufe
⇒ O(n)
Beispiel n = 1024: 11 gegen 1024 Durchläufe.
Deshalb ist die äußere Schleife von f3 "billig" —
aber Vorsicht: f3 hat INNEN noch die range(m)-Schleife,
dadurch wird aus O(log n) die Summe → O(n).
Übung 3.5 — verschachtelte Schleife mit wachsendem i
Wie viele Schritte macht diese Funktion in Abhängigkeit von n? Geben Sie eine O-Klasse an.
def h(n: int) -> int:
count = 0
for i in range(n):
for j in range(i):
count = count + 1
return count
✍ Lösung
T(n) = 0 + 1 + 2 + … + (n-1) = n·(n-1)/2 ∈ O(n²) (Versuchen Sie nicht, die Schleifen "anders" zu zählen — die Summe ist der sichere Weg.)
Übung 3.6 — k=3 für die Klausur-Funktionen (Selbstkontrolle)
Ordnen Sie den drei Funktionen f1, f2, f3 aus der Klausur ihre Ergebnisse bei k = 3 zu (ohne zu rechnen, auswendig):
f1 → ? f2 → ? f3 → ?✍ Lösung
f1 → 64 (8·8, fest verschachtelt) f2 → 36 (8+7+6+5+4+3+2+1, Gauß) f3 → 15 (8+4+2+1, geometrisch, 2n-1) Merksätze: 64 = 2^(2·3), 36 = 8·9/2, 15 = 2·8 - 1
XML: einen Baum als XML-Code modellieren
- Einen vorgegebenen Baum (Knoten mit Attributen) verschachtelt als XML schreiben.
- Element-Tags und Attribute korrekt verwenden; „self-closing“ Elemente ohne Kinder.
- Die Regeln wohlgeformten XMLs anwenden (ein Wurzel-Element, korrekt schließende Tags).
- Fehler in fremdem XML-Code finden.
1. Was ist XML?
XML ist ein Textformat, das Daten hierarchisch (baumartig) speichert. Die Bausteine:
- Element mit Start- und End-Tag:
<kreis> … </kreis>. Ein Element ohne Inhalt:<haus/>(self-closing). - Attribut am Start-Tag, Wert in Anführungszeichen:
<kreis name="A-Kreis">. - Text-Inhalt optional:
<strasse>Hauptstr.</strasse>. - Verschachtelung: Elemente enthalten Elemente (Kinder) — daraus entsteht der Baum.
2. Regeln für wohlgeformtes XML
- Genau ein Wurzelelement — alles andere liegt darin.
- Jedes geöffnete Tag wird geschlossen — und zwar umgekehrt zur Öffnungsreihenfolge (Stack-Prinzip, LIFO).
- Attributwerte stehen in Anführungszeichen (einfach oder doppelt).
- Tags sind case-sensitiv:
<Ort>und</ort>passen nicht zusammen. - Elemente dürfen sich nicht überkreuzen:
<a><b></a></b>ist verboten.
((…)) ja, (…(…))…) nein. Beim Lesen des Codes von außen nach innen: Was zuletzt geöffnet wurde, muss zuerst geschlossen werden.3. Baum → XML übersetzen (die Methode)
- Wurzel identifizieren → wird das äußerste Element.
- Jede Kante nach unten wird ein Kind-Element; jede Beschriftung am Knoten wird ein Attribut (hier:
name,nummer). - Knoten ohne Kinder (die Blätter) als self-closing schreiben:
<haus nummer="1"/>. - In jeder Ebene einrücken und gegenprüfen: hat jedes Element genau einen Eltern-Knoten? Das Zählen der Elemente pro Ebene ist die beste Selbstkontrolle.
Es soll ein Stück XML-Code zur Beschreibung von Adressen geschrieben werden. Dazu soll es Elemente für Landkreis, Ort, Straße und Haus geben, deren Tags kreis, ort, strasse und haus heißen.
Landkreis, Ort und Straße sollen jeweils ein Attribut name für ihren Namen haben, haus ein Attribut nummer für die Hausnummer.
Modellieren Sie mit diesen Elementen folgende Situation mit dem Landkreis A-Kreis, den beiden Orten A-Stadt und A-Dorf, drei Straßen und fünf Häusern (Baumstruktur siehe oben): A-Stadt enthält die Straßen Hauptstr. (Häuser 1, 2, 3) und Umgehungsstr.; A-Dorf enthält die Straße Hauptstr. (Häuser 1, 2).
<kreis name="A-Kreis">
<ort name="A-Stadt">
<strasse name="Hauptstr.">
<haus nummer="1"/>
<haus nummer="2"/>
<haus nummer="3"/>
</strasse>
<strasse name="Umgehungsstr."/>
</ort>
<ort name="A-Dorf">
<strasse name="Hauptstr.">
<haus nummer="1"/>
<haus nummer="2"/>
</strasse>
</ort>
</kreis>
Der Landkreis A-Kreis ist der oberste Knoten, also das Wurzelelement <kreis name="A-Kreis">. Sein Attribut ist name, der Wert steht in Anführungszeichen.
Direkt unter dem Kreis hängen die zwei Orte A-Stadt und A-Dorf — beide als Kinder des kreis-Elements, jede mit eigenem name-Attribut.
A-Stadt hat zwei Straßen (Hauptstr. und Umgehungsstr.), A-Dorf eine (Hauptstr.). Die Umgehungsstr. hat keine Häuser, daher wird sie als leeres Element mit self-closing geschrieben: <strasse name="Umgehungsstr."/>.
Die Häuser sind Blätter (keine Kinder) und bekommen das Attribut nummer: <haus nummer="1"/>. Achtung: Die Häuser 1 und 2 der zweiten Hauptstr. gehören zu A-Dorf — nicht zu A-Stadt!
Zähle die Elemente: 1 kreis + 2 ort + 3 strasse + 5 haus = die geforderten 3 Straßen und 5 Häuser. Und jedes Element steht korrekt geschachtelt zwischen seinen Eltern-Tags.
- Tags auf Deutsch mit Umlaut oder anders benennen (
strassestattstraße!). - Schließende Tags vergessen oder überkreuz verschachteln.
- Häuser erfinden (z. B. auch der Umgehungsstr. Häuser geben).
- Attributwerte ohne Anführungszeichen oder Hausnummer als Element statt Attribut.
- Zwei Wurzeln schreiben — das
kreis-Element muss alles enthalten.
Übung 4A.1 — Baum → XML (Universität)
Eine Universität mit Fakultät Informatik. Die Fakultät hat die Institute institut (Attribut name): Theorie und Systeme. Das Institut „Theorie“ hat die Professoren (Element prof, Attribut name) Huber und Meier. Schreiben Sie das XML mit Wurzel universitaet.
<universitaet>
<fakultaet name="Informatik">
<institut name="Theorie">
<prof name="Huber"/>
<prof name="Meier"/>
</institut>
<institut name="Systeme"/>
</fakultaet>
</universitaet>
Übung 4A.2 — Baum → XML (Bibliothek)
Eine Bibliothek mit zwei Regalen (Attribut regal-Nummer). Regal A enthält die Bücher „Python“ und „Java“, Regal B das Buch „XML“. Elemente: bibliothek, regal (Attribut nr), buch (Attribut titel).
<bibliothek>
<regal nr="A">
<buch titel="Python"/>
<buch titel="Java"/>
</regal>
<regal nr="B">
<buch titel="XML"/>
</regal>
</bibliothek>
Übung 4A.3 — Fehler finden I
Was ist an diesem XML-Code falsch?
<kreis name=A-Kreis>
<ort name="A-Stadt">
<strasse name="Hauptstr."/>
<ort name="A-Dorf">
<strasse name="Umgehungsstr."/>
</kreis>
✍ Lösung
1) name=A-Kreis — Attributwert ohne Anführungszeichen. 2) Das zweite <ort> wird im ersten <ort> geöffnet, aber nie geschlossen: vor <ort name="A-Dorf"> fehlt </ort>. → überkreuzte Verschachtelung (Stack-Regel verletzt).
Übung 4A.4 — Fehler finden II
Ist dieses XML wohlgeformt? Begründen Sie.
<firma> <abteilung>Entwicklung</abteilung> <abteilung>Vertrieb </firma>✍ Lösung
Nein: die zweite <abteilung> wird nie geschlossen. Es fehlt </abteilung> vor </firma>. (Text-Inhalt ist erlaubt — aber jedes Element braucht sein End-Tag.)
Übung 4A.5 — Firma mit Teams (mittlere Tiefe)
Firma „Muster GmbH“ (Attribut name) mit Abteilung abteilung (Attribut name: „Entwicklung“), darin Team team (Attribut name: „Frontend“) mit den Mitarbeitern mitarbeiter (Attribut name: Anna, Ben). Schreiben Sie das XML mit Wurzel firma.
<firma name="Muster GmbH">
<abteilung name="Entwicklung">
<team name="Frontend">
<mitarbeiter name="Anna"/>
<mitarbeiter name="Ben"/>
</team>
</abteilung>
</firma>
Übung 4A.6 — Stammbaum (rekursive Tiefe)
Modellieren Sie einen Stammbaum: Wurzel familie mit Person „Maria“ (Element person, Attribut name), die das Kind „Lukas“ hat, welches wiederum das Kind „Ida“ hat. Hinweis: Die Person enthält ein Element kinder mit den person-Kindern.
<familie>
<person name="Maria">
<kinder>
<person name="Lukas">
<kinder>
<person name="Ida"/>
</kinder>
</person>
</kinder>
</person>
</familie>
(Man sieht hier schön die Stack-Regel: drei Ebenen tief
verschachteln und in exakt umgekehrter Reihenfolge schließen.)
Reguläre Ausdrücke & Sprachen
- Eine Sprachbedingung verstehen und daraus die Sprachmenge (hier: alle Wörter ≤ 3 Zeichen) systematisch aufschreiben.
- Einen regulären Ausdruck konstruieren, der genau die Sprache beschreibt.
- Die Kleenesche Hülle, Alternation und optionale Teile sicher einsetzen.
- Aus einem regulären Ausdruck die beschriebene Sprache ablesen können.
1. Grundbegriffe
- Alphabet
Σ: Menge erlaubter Zeichen, hier{a, b}. - Wort: endliche Folge von Zeichen; das leere Wort
εhat Länge 0. - Sprache
L: Menge von Wörtern überΣ. - Länge eines Wortes
|w|: Anzahl der Zeichen, z. B.|aba| = 3,|ε| = 0.
2. Operatoren regulärer Ausdrücke — die komplette Tabelle
| Operator | Schreibweise | Bedeutung | Beispiel |
|---|---|---|---|
| Verkettung | ab | erst a, dann b | ab erkennt nur „ab“ |
| Alternation | a|b | a oder b | a|b erkennt „a“ oder „b“ |
| Kleene-Stern | a* | beliebig viele a (auch 0 → ε) | a*: ε, a, aa, aaa, … |
| Plus | a+ | ein oder mehr a | a+: a, aa, aaa, … (ohne ε) |
| Optional | a? | a oder nichts (ε) | a?: ε, a |
r? heißt „entweder r oder ε“. a*(ba(a|b)*)? heißt: erst beliebig viele a's, dann optional ein Block, der mit ba beginnt und danach beliebig weitergeht.* bindet am stärksten, dann Verkettung, dann | am schwächsten. ab|c bedeutet also (ab)|c — mit Klammern ist man immer sicher.3. Sprachmengen systematisch aufzählen
Vorgehen bei „alle Wörter bis Länge n“:
- Nach Länge sortieren: erst Länge 0 (nur
ε), dann Länge 1, 2, 3, … - Innerhalb einer Länge alle Kombinationen über
{a, b}hinschreiben. Bei Länge 2:aa, ab, ba, bb; bei Länge 3:aaa, aab, aba, abb, baa, bab, bba, bbb(2³ = 8 Stück). - Jedes Wort gegen die Sprachbedingung prüfen — durchstreichen, was nicht passt.
4. Die Bedingung dieser Aufgabe genau lesen
„Wenn in dem Wort ein b vorkommt, muss auf das erste b ein a (unmittelbar) folgen.“ — Nur das erste b ist eingeschränkt! Nach diesem ersten ba darf alles folgen (auch weitere b's). Typische Fehlinterpretation: „auf jedes b folgt ein a“ — das ist eine andere (strengere) Sprache.
Alle Wörter bis Länge 3 als Baum
Ausgefüllte Kreise: in der Sprache. Graue, durchgestrichene Kreise: verletzen die Bedingung (das erste b hat kein unmittelbar folgendes a).
Der reguläre Ausdruck als endlicher Automat
Von q0 aus beliebig viele a's. Beim ersten b geht es nach q1, das verlangte a führt nach q2 — von dort ist alles erlaubt. Genau die Sprache a*(ba(a|b)*)?.
Wir betrachten über dem Alphabet Σ = {a, b} die Sprache L, die aus genau den Wörtern besteht, für die gilt: Wenn in dem Wort ein b vorkommt, muss auf das erste b ein a (unmittelbar) folgen.
a) Geben Sie alle Wörter aus L an, die höchstens drei Zeichen lang sind. Wie viele Wörter sind das?
b) Geben Sie einen regulären Ausdruck an, der genau die Wörter aus L akzeptiert.
Teil a) — alle Wörter ≤ 3 Zeichen (4 Punkte)
Länge 0: ε
Länge 1: a (b ✗ — auf das erste b folgt kein a)
Länge 2: aa, ba (ab ✗ — erstes b am Wortende; bb ✗)
Länge 3: aaa, aba, baa, bab
(aab ✗ — erstes b am Ende;
abb ✗, bba ✗, bbb ✗ — erstes b nicht von a gefolgt)
⇒ L_≤3 = { ε, a, aa, ba, aaa, aba, baa, bab }
Das sind 8 Wörter.
Das leere Wort enthält kein b, die Bedingung ist also leer erfüllt. Länge 0 liefert immer genau ein Wort: ε.
Regel: Markiere das erste b im Wort. Existiert danach kein Zeichen oder ein b, ist das Wort ungültig. Bei bab steht nach dem ersten b ein a — das Wort ist gültig, egal was später kommt. Genau das ist die Stelle, an der man aba und bab leicht vergisst oder fälschlich streicht.
1 (Länge 0) + 1 + 2 + 4 = 8 Wörter. Die Aufzählung pro Länge hinschreiben — dafür gibt es Teilpunkte, auch wenn das Zählen schiefgeht.
Teil b) — der reguläre Ausdruck (6 Punkte)
Fall 1: keine b's: a* Fall 2: es gibt ein erstes b: a* ba (a|b)* ⇒ r = a* (ba (a|b)*)? (äquivalent: a* | a*ba(a|b)*)
Wörter ganz ohne b sind beliebig viele a's: a*. Das deckt auch ε ab.
Vor dem ersten b dürfen beliebig viele a's stehen: a*. Dann kommt das erste b, und die Bedingung erzwingt ein unmittelbar folgendes a: ba. Danach ist alles erlaubt: (a|b)*.
Der Block ba(a|b)* kann auch fehlen (Fall 1) — also mit ? optional machen: a*(ba(a|b)*)?. Alternative Schreibweise mit |: a* | a*ba(a|b)*.
- Die Bedingung als „auf jedes b folgt ein a“ lesen → dann wären
babundabafälschlich außen vor. εvergessen — der Ausdruck beginnt nicht mita*.- Bei b) statt
(a|b)*fälschlicha*schreiben (dann fehlen Wörter mit mehreren b's). - Behaupten, es gäbe unendlich viele Wörter — bis Länge 3 sind es genau 8.
Übung 4B.1 — RegEx → Sprache aufzählen (≤ 3 Zeichen)
Geben Sie alle Wörter der Sprache bis Länge 3 an:
1) r = a* b (a's, dann genau ein b) 2) r = (ab)* (Wiederholungen von "ab")✍ Lösung
1) { b, ab, aab } → 3 Wörter
(a*b: beliebig viele a, dann genau ein b;
"aaab" wäre Länge 4 und fällt raus)
2) { ε, ab } → 2 Wörter
((ab)* = ε, ab, abab, ababab, …;
"abab" hat schon Länge 4)
Übung 4B.2 — Bedingung → RegEx I
Geben Sie einen regulären Ausdruck über {a, b} an für:
1) Wörter, die mit a beginnen und mit b enden 2) Wörter mit genau einem b 3) Wörter, die mit a enden 4) Wörter, die mit b beginnen✍ Lösung
1) a (a|b)* b 2) a* b a* 3) (a|b)* a 4) b (a|b)*
Übung 4B.3 — Bedingung → RegEx II (optionaler Teil)
Über {a, b}: Wörter, in denen nach dem ersten Zeichen (a) optional ein Block bb stehen darf, danach beliebig viele a's. RegEx?
r = a (bb)? a* (a, dann optional "bb", dann beliebig viele a) z. B. in der Sprache: a, aa, aaa, abb, abba, abbaa, … z. B. NICHT: ab, b, ba
Übung 4B.4 — Klausur-ähnlich: „mindestens ein a“
Über Σ = {a, b}: L = alle Wörter, die mindestens ein a enthalten.
a) Alle Wörter aus L bis Länge 3 (wie viele?) b) Ein regulärer Ausdruck für L.✍ Lösung
a) Länge 1: a
Länge 2: aa, ab, ba (bb ✗)
Länge 3: aaa, aab, aba, abb, baa, bab, bba (bbb ✗)
⇒ L_≤3 = { a, aa, ab, ba, aaa, aab, aba, abb,
baa, bab, bba } → 11 Wörter
b) r = (a|b)* a (a|b)*
(das a kann überall stehen — links und rechts beliebig)
Übung 4B.5 — Klausur-ähnlich: „nicht mit b enden“
Über Σ = {a, b}: L = alle Wörter, die nicht mit b enden.
a) Alle Wörter aus L bis Länge 3 (wie viele?) b) Ein regulärer Ausdruck für L.✍ Lösung
a) Länge 0: ε
Länge 1: a (b ✗)
Länge 2: aa, ba (ab ✗, bb ✗)
Länge 3: aaa, aba, baa, bba (aab, abb, bab, bbb ✗)
⇒ L_≤3 = { ε, a, aa, ba, aaa, aba, baa, bba } → 8 Wörter
b) r = (a|b)* a | ε
(alles, das mit a endet — oder das leere Wort)
Übung 4B.6 — strengere Bedingung (Vergleich zur Klausur)
Nehmen wir an, die Bedingung hieße: Auf jedes b folgt unmittelbar ein a. Geben Sie dann alle Wörter bis Länge 3 an — und vergleichen Sie mit der Klausur-Aufgabe.
✍ Lösungε, a, aa, ba, aaa, baa, aba → 7 Wörter (aba: auf beide b's folgt a ✓; bab ✗, abb ✗) Der Unterschied zur Klausur: hier wäre bab NICHT erlaubt (das zweite b hängt am Ende).
Kontextfreie Grammatik & check_word
- Die Sprache einer Grammatik verstehen: alle ableitbaren Wörter mit Längengrenze angeben.
- Ableitungen mit Pfeilen aufschreiben können (
S ⇒ aSb ⇒ …). - Eine Funktion
check_wordschreiben, die genau die Wörter der Grammatik erkennt — rekursiv oder mit Zählung.
1. Formale Definition
G = (Σ, V, S, P): Σ Terminale (das fertige Alphabet), V Variablen (Hilfssymbole), S Startsymbol, P Produktionen der Form Variable → Zeichenkette.In der Klausur: G = ({a, b, c}, {S}, S, {S → aSb, S → c}). Eine Variable S, drei Terminale, zwei Produktionen.
2. Ableiten — die Notation
Ein Wort ist in L(G), wenn man es vom Startsymbol aus durch Anwenden der Regeln erzeugen kann. Ein Ableitungsschritt ersetzt eine Variable durch die rechte Seite einer Produktion (geschrieben ⇒):
S ⇒ aSb ⇒ aaSbb ⇒ aacbb
Die dritte Produktionsanwendung ersetzt S durch c — damit sind alle Variablen verschwunden, das Wort aacbb ist fertig. ε wird genau dann abgeleitet, wenn eine Produktion S → ε existiert — hier nicht!
3. Die Struktur erkennen
Die Regel S → aSb „wickelt“ bei jeder Anwendung ein a links und ein b rechts um die noch offene Variable: S ⇒ aSb ⇒ aaSbb ⇒ …. Irgendwann wird S → c angewendet. Also:
L(G) = { a^n c b^n : n ≥ 0 } — gleich viele a's wie b's, dazwischen genau ein c.4. Wörter mit Längengrenze finden
Man zählt, wie oft S → aSb angewendet wurde (das ist das n): Die Wortlänge ist 2n + 1. Für Länge ≤ 5 heißt das: n = 0 (Länge 1), n = 1 (Länge 3), n = 2 (Länge 5). Mehr gibt es nicht — jede weitere Anwendung addiert 2 Zeichen.
5. Erkennung als Funktion
check_word(s) muss genau die Wörter a^n c b^n akzeptieren. Zwei klassische Ansätze:
- Rekursiv (passt zur Grammatik):
s == "c"ist der Basisfall; sonst muss s mitabeginnen, mitbenden und der innere Teil muss wieder gültig sein. - Zählend: Es darf genau ein
cgeben; links davon nura's, rechts nurb's, und die Anzahlen müssen gleich sein.
Der Ableitungsbaum für aacbb: innere Knoten sind die Variable S, die Blätter sind die Terminale. Die „Schalen“ a … b liegen von außen nach innen — das zeigt die Struktur a^n c b^n.
Wir betrachten die kontextfreie Grammatik G := (Σ, V, S, P) := ({a, b, c}, {S}, S, {S → aSb, S → c}).
a) Geben Sie alle Wörter aus L(G) an, die höchstens 5 Zeichen lang sind.
b) Schreiben Sie eine Funktion check_word mit einem String als Parameter und Ergebnistyp bool. Das Ergebnis soll genau dann True sein, wenn der übergebene String ein Wort aus L(G) darstellt.
Teil a) — Wörter ≤ 5 Zeichen (3 Punkte)
n = 0: c (Länge 1)
n = 1: a c b (Länge 3)
n = 2: a a c b b (Länge 5)
⇒ L(G)_≤5 = { c, acb, aacbb }
Das sind 3 Wörter.
Jede Anwendung von S → aSb wickelt ein a und ein b um die Variable. Wendet man sie n-mal an und dann S → c, erhält man a^n c b^n mit Länge 2n + 1.
2n + 1 ≤ 5 ⟹ n ≤ 2. Also n = 0, 1, 2 — mehr Anwendungen von aSb gehen nicht.
Die drei Wörter nach n sortiert aufschreiben. Häufige Fehlerwörter, die nicht dazugehören: cba (falsche Reihenfolge — erst a's, dann c, dann b's), aabbc (c muss in der Mitte stehen).
Teil b) — check_word (12 Punkte)
def check_word(s: str) -> bool:
if s == "c":
return True
if len(s) < 3:
return False
if s[0] != 'a' or s[-1] != 'b':
return False
return check_word(s[1:-1])
def check_word(s: str) -> bool:
if s.count('c') != 1:
return False
i = s.index('c')
return s[:i] == 'a' * i and s[i + 1:] == 'b' * i
Das kürzeste Wort ist c. Ist s == "c", akzeptieren.
Ein länger gültiges Wort muss mit a beginnen und mit b enden (genau so entsteht es aus S → aSb). Ist das nicht so → False. Sonst: das innere Stück s[1:-1] muss wieder ein gültiges Wort sein — rekursiv weiter.
In a^n c b^n gibt es genau ein c; links davon nur a's, rechts davon nur b's. s.count('c') != 1 fängt alle anderen Fälle ab.
Wenn das c an Position i steht, muss links genau i a's stehen und rechts genau i b's. Das prüft der Vergleich mit 'a' * i und 'b' * i — elegant und ohne Rekursion.
a^n c b^n erkennen und nichts anderes.- In Teil a) Wörter mit falscher Struktur wie
aabbc,abcodercbaufschreiben. check_wordprüft nur „beginnt mit a, endet mit b“, aber nicht die Gleichheit der Schalen — dann würdeaabbbfälschlich akzeptiert (die Rekursions[1:-1]würde es zwar verwerfen — aber nur, wenn man sie auch wirklich benutzt!).- Leeren String (
"") akzeptieren —εgehört nicht zuL(G), weil S immer ersetzt werden muss. - Rückgabetyp vergessen oder
printstattreturn.
Übung 5.1 — ähnliche Grammatik I
Gegeben G2 = ({a, b}, {S}, S, {S → aS, S → b}).
a) Alle Wörter bis Länge 4 b) Sprache in Mengenschreibweise✍ Lösung
a) b, ab, aab, aaab, aaaab → 5 Wörter
(n = 0..4 a's, dann b)
b) L(G2) = { a^n b : n ≥ 0 }
— beliebig viele a's, dann genau ein b.
Übung 5.2 — ähnliche Grammatik II
Gegeben G3 = ({a, b}, {S}, S, {S → aSb, S → ab}).
a) Alle Wörter bis Länge 6 b) Sprache in Mengenschreibweise✍ Lösung
a) ab, aabb, aaabbb → 3 Wörter
(n = 1: ab, n = 2: a²b², n = 3: a³b³)
b) L(G3) = { a^n b^n : n ≥ 1 }
— kein ε möglich, da S nie durch ε ersetzt wird!
Übung 5.3 — Ableitungsketten aufschreiben
Schreiben Sie die komplette Ableitung auf:
1) "aacbb" in G (S → aSb | c) 2) "aaabbb" in G3 (S → aSb | ab)✍ Lösung
1) S ⇒ aSb ⇒ aaSbb ⇒ aacbb
2) S ⇒ aSb ⇒ aaSbb ⇒ aaaSbbb ⇒ aaabbb
(zuletzt S → ab: aaa(ab)bbb = aaabbb)
Übung 5.4 — Sprache in Mengenschreibweise (ε-Fall)
Gegeben G4 = ({a, b}, {S}, S, {S → aSb, S → ε}). a) Sprache? b) Alle Wörter bis Länge 6?
a) L(G4) = { a^n b^n : n ≥ 0 } (n = 0 → ε)
b) ε, ab, aabb, aaabbb → 4 Wörter
(ε ist hier ERLAUBT — anders als in der Klausur-Grammatik!)
Übung 5.5 — check_word für G2 schreiben
Schreiben Sie check_word für G2 (beliebig viele a und genau ein b am Ende) — rekursiv und ohne Rekursion.
def check_word(s: str) -> bool:
if s == "b":
return True
if len(s) < 2 or s[0] != 'a':
return False
return check_word(s[1:])
# ohne Rekursion:
def check_word(s: str) -> bool:
return len(s) >= 1 and s[-1] == 'b' and s[:-1] == 'a' * (len(s) - 1)
Übung 5.6 — check_word für G3 schreiben
Schreiben Sie check_word für G3 (a^n b^n, n ≥ 1) — rekursiv.
def check_word(s: str) -> bool:
if s == "ab":
return True
if len(s) < 3:
return False
if s[0] != 'a' or s[-1] != 'b':
return False
return check_word(s[1:-1])
Rekursion: Typ erkennen, in Schleife umwandeln, Hilfsfunktionen
- Den Rekursionstyp einer Funktion begründet erkennen (hier: Endrekursion).
- Rekursion in eine äquivalente Schleife umwandeln — mit korrekter Reihenfolge der Updates.
- Hilfsfunktionen
cond,fa,fbentwerfen, sodass die Funktion eine gewünschte Summe/Produkt berechnet.
1. Rekursionstypen
- Lineare Rekursion: genau ein rekursiver Aufruf, aber danach wird noch weiter gerechnet, z. B.
return a + f(a - 1). Jeder offene Aufruf bleibt auf dem Aufruf-Stack, bis seine Kinder fertig sind. - Baum-/Kaskadenrekursion: mehrere rekursive Aufrufe (z. B. Fibonacci:
f(n-1) + f(n-2)). - Endrekursion (Tail Recursion): der rekursive Aufruf ist die allerletzte Aktion; nach der Rückkehr wird nichts mehr berechnet. Das Muster:
return f(neu_a, neu_b).
return und ist er der einzige noch ausstehende Schritt, liegt Endrekursion vor — der aktuelle Aufruf kann „vergessen“ werden, sein Stack-Frame wird nicht mehr gebraucht.2. Warum ist Endrekursion besonders?
Bei Endrekursion passiert nach dem rekursiven Aufruf nichts mehr: Das Ergebnis des Kind-Aufrufs wird direkt zurückgegeben. Deshalb braucht man den Stack-Frame des Eltern-Aufrufs nicht mehr — ideale Compiler können die Rekursion in eine Schleife umwandeln (Tail-Call-Optimization). Die Funktion läuft mit konstantem Zusatzspeicher O(1), während lineare Rekursion O(n) Stack verbraucht.
3. Das allgemeine Schema dieser Aufgabe
def f(a: int, b: int) -> int:
if cond(a):
return b
else:
return f(fa(a), fb(a, b))
cond(a)— Abbruchtest (Basisfall).fa(a)— neue Version von a („Schritt zum Ende hin“).fb(a, b)— neuer Akkumulator b, berechnet aus altem a und b.
Das ist das klassische Akkumulator-Muster: b sammelt das Zwischenergebnis, a schrumpft Richtung Basisfall. Genau dieses Schema muss man in Teil c) mit passenden Funktionen „füttern“.
4. Rekursion → Schleife
Endrekursion lässt sich mechanisch in eine Schleife übersetzen: Solange der Abbruchtest nicht erfüllt ist, werden die Parameter aktualisiert; danach b zurückgeben.
def f_loop(a: int, b: int) -> int:
while not cond(a):
b = fb(a, b) # erst b mit ALTEM a berechnen!
a = fa(a) # dann a aktualisieren
return b
a = fa(a) zuerst, würde fb(a, b) mit dem neuen a rechnen — ein anderes Ergebnis als die Rekursion! Erst b aktualisieren, dann a. (Oder in Python: a, b = fa(a), fb(a, b) — Tupel-Zuweisung, beide Werte werden gleichzeitig aus den alten berechnet.)5. Hilfsfunktionen entwerfen — die Methode
- Basisfall bestimmen: Bei welchem a soll f einfach b zurückgeben? (Für
s(n) = Σ iist dasa == 0.) - fa bestimmen: Wie kommt man Schritt für Schritt zum Basisfall? (
a - 1.) - fb bestimmen: Die Rekursionsformel aus der Aufgabe umformen:
s(a) = s(a-1) + a→ der neue Akkumulator istb + a. - Probe rechnen: Aufrufkette für ein Beispiel aufschreiben (siehe Musterlösung).
Lineare Rekursion — Stack wächst:
Endrekursion — Stack bleibt klein:
Wir betrachten die Funktion
def f(a: int, b: int) -> int:
if cond(a):
return b
else:
return f(fa(a), fb(a, b))
Die Hilfsfunktionen cond(a) -> bool, fa(a) -> int, fb(a, b) -> int seien definiert, aber unbekannt (und ihre Auswertung zieht auch keinen Aufruf von f nach sich).
a) Was für ein Typ von Rekursion liegt bei der Funktion f vor? (Begründung!)
b) Wandeln Sie die Funktion f um in eine äquivalente Funktion, in der die Rekursion durch eine Schleife ersetzt wird.
c) Geben Sie passende Hilfsfunktionen cond, fa, fb an, so dass ein Aufruf f(n, 0) die Summe
s(n) = Σ_{i=0}^{n} i mit s(n) = 0 für n = 0
und s(n) = s(n-1) + n sonst
berechnet; n darf dabei als nichtnegativer Wert vom Typ int vorausgesetzt werden.
Teil a) — Rekursionstyp (3 Punkte)
f ist ENDREKURSIV (tail recursion). Begründung: Im else-Zweig ist der rekursive Aufruf f(fa(a), fb(a, b)) die ALLERLETZTE Aktion — sein Ergebnis wird direkt zurückgegeben (return f(...)). Nach dem Aufruf findet KEINE weitere Berechnung statt.
Die Aufgabe gibt 3 Punkte nur für das Erkennen — und der Nachweis steckt in „letzte Aktion“ + „nichts mehr danach“. Ein Gegenbeispiel zur Abgrenzung: return a + f(a - 1) wäre nicht endrekursiv, weil nach der Rückkehr noch die Addition a + … ansteht.
Teil b) — Schleifen-Version (4 Punkte)
def f_loop(a: int, b: int) -> int:
while not cond(a):
b = fb(a, b)
a = fa(a)
return b
Rekursion: if cond(a): return b. Die Schleife läuft also solange not cond(a) und gibt danach b zurück.
In f(fa(a), fb(a, b)) wird fb mit dem alten a ausgewertet. Deshalb: erst b = fb(a, b), dann a = fa(a). Wer die Reihenfolge vertauscht, berechnet fb(fa(a), b) — falsch!
Teil c) — Hilfsfunktionen für die Summe (9 Punkte)
cond(a) = (a == 0) # Basisfall: Summe bis 0 ist 0 fa(a) = a - 1 # n → n-1, Schritt Richtung Basisfall fb(a, b) = b + a # akkumuliere: bisherige Summe + a Probe: f(4, 0) f(4, 0) → cond(4)? nein → f(3, 0+4) f(3, 4) → cond(3)? nein → f(2, 4+3) f(2, 7) → cond(2)? nein → f(1, 7+2) f(1, 9) → cond(1)? nein → f(0, 9+1) f(0, 10) → cond(0)? ja → 10 s(4) = 0 + 1 + 2 + 3 + 4 = 10 ✓
Wir brauchen f(n, 0) = s(n) = 0 + 1 + … + n. Das Akkumulator-Muster: a zählt die Summe herunter (Basisfall bei 0), b sammelt. Damit ist cond(a) = (a == 0) und fa(a) = a - 1 die offensichtliche Wahl.
Beim Übergang f(a, b) → f(a-1, fb(a, b)) muss gelten: s(a) = s(a-1) + a. Der neue Akkumulator muss also b + a sein. Genau die Formel aus der Aufgabenstellung — fb(a, b) = b + a.
Die Aufrufkette (siehe oben) zeigt: f(4, 0) = 10 und das ist wirklich 0+1+2+3+4. Eine komplette Probe auf dem Klausurbogen sichert die Punkte.
- Teil a) ohne Begründung — der Punkt liegt im Nachweis, dass nichts mehr nach dem Aufruf passiert.
- Teil b) Update-Reihenfolge vertauschen (a vor b) — ergibt eine andere Funktion.
- Teil c)
fbalsb + anicht erkennen, sondern z. B.b + fa(a)— das würde die Summe verschieben (Probe zeigt den Fehler). condals(a <= 0)— nicht falsch bei n ≥ 0, abera == 0ist die sauberste Antwort.
Übung 6.1 — Typ erkennen I
def g(a: int) -> int:
if a <= 1:
return 1
return a * g(a - 1)
Welcher Rekursionstyp liegt vor? (Begründung!)
✍ LösungLINEARE Rekursion, KEINE Endrekursion. Begründung: Nach dem Aufruf g(a-1) wird noch a * (…) multipliziert — der Aufruf ist nicht die letzte Aktion.
Übung 6.2 — Typ erkennen II
def fib(n: int) -> int:
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
Welcher Rekursionstyp liegt vor?
✍ LösungBAUM- bzw. Kaskadenrekursion. Begründung: Es gibt ZWEI rekursive Aufrufe in einem Ausdruck — die Aufrufe verzweigen sich wie ein Baum. (Weder endrekursiv noch linear.)
Übung 6.3 — Typ erkennen III (Knacknuss)
def h(a: int, acc: int) -> int:
if a == 0:
return acc
return h(a - 1, acc * a)
Welcher Rekursionstyp liegt vor?
✍ LösungENDREKURSION. Begründung: Der rekursive Aufruf h(a-1, acc·a) ist die letzte Aktion — sein Ergebnis wird direkt zurückgegeben. (acc ist der Akkumulator.)
Übung 6.4 — Rekursion → Schleife (Fakultät)
Wandeln Sie die Fakultätsfunktion aus Übung 6.1 in eine äquivalente Schleifen-Funktion um.
✍ Lösungdef g_loop(a: int) -> int:
acc = 1
while a > 1:
acc = acc * a
a = a - 1
return acc
# endrekursive Variante (Akkumulator-Muster):
def g_iter(a: int, acc: int = 1) -> int:
if a <= 1:
return acc
return g_iter(a - 1, acc * a)
Übung 6.5 — Rekursion → Schleife (Addition)
def p(a: int, b: int) -> int:
if b == 0:
return a
return p(a + 1, b - 1)
a) Rekursionstyp? b) Schleifen-Version?
✍ Lösunga) ENDREKURSIV — der Aufruf p(a+1, b-1) ist die letzte Aktion.
b) def p_loop(a: int, b: int) -> int:
while b != 0:
a = a + 1
b = b - 1
return a
(Hier ist die Reihenfolge egal — beide Updates
hängen nicht voneinander ab.)
Übung 6.6 — cond/fa/fb für n!
Geben Sie cond, fa, fb an, sodass f(n, 1) das Produkt n! = 1·2·…·n berechnet (für n ≥ 1).
cond(a) = (a == 0) # oder a <= 0 fa(a) = a - 1 fb(a, b) = b * a Probe: f(3, 1) → f(2, 3) → f(1, 6) → f(0, 6) → 6 = 3! ✓ Achtung: mit f(n, 0) würde 0·1·…·n = 0 herauskommen! Der Startwert b=1 ist entscheidend.
Übung 6.7 — cond/fa/fb für 2^n
Geben Sie cond, fa, fb an, sodass f(n, 1) die Potenz 2^n berechnet.
cond(a) = (a == 0) fa(a) = a - 1 fb(a, b) = b * 2 Probe: f(3, 1) → f(2, 2) → f(1, 4) → f(0, 8) → 8 = 2³ ✓
Übung 6.8 — Trace schreiben (Klausurformat)
Schreiben Sie die komplette Aufrufkette von f(5, 0) mit den Hilfsfunktionen aus Teil c) der Klausur (Summe) auf — und das Endergebnis.
f(5, 0) → cond(5)? nein → f(4, 0+5) f(4, 5) → cond(4)? nein → f(3, 5+4) f(3, 9) → cond(3)? nein → f(2, 9+3) f(2, 12) → cond(2)? nein → f(1, 12+2) f(1, 14) → cond(1)? nein → f(0, 14+1) f(0, 15) → cond(0)? ja → return 15 s(5) = 0+1+2+3+4+5 = 15 ✓
Objektorientierte Programmierung: StringNumber
- Eine Klasse mit Konstruktor, String-Repräsentation und Operator-Überladung schreiben.
- Verstehen, was
z = x + yundprint(z)hinter den Kulissen aufrufen. - Die wichtigsten „dunder“-Methoden und ihre Operatoren kennen.
1. Klasse, Objekt, Attribut, Methode
- Klasse: Bauplan (hier
StringNumber). - Objekt/Instanz: konkretes Exemplar, erzeugt mit
StringNumber("abcd"). - Attribut: Datenspeicher des Objekts, angelegt mit
self.val = …. - Methode: Funktion der Klasse; der erste Parameter ist immer
self(das Objekt selbst).
2. Die drei „dunder“-Methoden dieser Aufgabe
| Methode | Wann aufgerufen? | Zweck |
|---|---|---|
__init__(self, val) | bei StringNumber(...) | Konstruktor: Attribute setzen |
__str__(self) | bei print(z), str(z) | menschenlesbare Darstellung |
__add__(self, other) | bei x + y | Operator + überladen |
x + y wird von Python intern übersetzt in x.__add__(y). Wer __add__ definiert, entscheidet also, was + für seine Klasse bedeutet.3. Weitere dunder-Methoden (für Übungen und Verständnis)
| Operator / Funktion | dunder-Methode |
|---|---|
== | __eq__(self, other) |
- | __sub__(self, other) |
* | __mul__(self, other) |
len(obj) | __len__(self) |
obj[i] | __getitem__(self, i) |
str(obj) | __str__(self) |
4. Die Anforderungen der Aufgabe in Code übersetzt
- „Konstruktor bekommt den String als Parameter und speichert ihn als Attribut“ →
__init__(self, val)mitself.val = str(val)(sicherer Typ-Umbau). - „String-Repräsentation: String + in Klammern die Länge“ →
__str__mitf"{self.val} ({len(self.val)})". - „Addition zweier gespeicherter Zahlen“ →
__add__: verketten und eine neue StringNumber zurückgeben.
__add__ eine StringNumber zurückgeben? Damit das Ergebnis wieder + und print kann. Gäbe man einen reinen String zurück, wäre z + w zwar möglich (str + str), aber print(z) würde ohne Klammern-Länge ausgeben — und der Typ der Klasse ginge verloren. Die Aufgabenstellung erwartet StringNumber.5. f-Strings kurz erklärt
f"{self.val} ({len(self.val)})" setzt die Werte in geschweiften Klammern ein. Alles außerhalb der Klammern bleibt Text — hier ein Leerzeichen und zwei runde Klammern. Ergebnis für "abcd": abcd (4).
Schreiben Sie eine Klasse StringNumber, mit der nichtnegative ganze Zahlen als String gespeichert werden: Die Länge des Strings entspricht der Zahl, z. B. StringNumber("abcd") für die Zahl 4.
- Der Konstruktor bekommt den String als Parameter und speichert ihn als Attribut ab.
- Die Methode zur String-Repräsentation soll dem gespeicherten String in Klammern den Wert (also dessen Länge) anfügen, im obigen Beispiel wäre das Ergebnis
'abcd (4)'. - Weiter soll die Addition zweier so gespeicherter Zahlen möglich sein.
Ein Beispiel für die Verwendung der Klasse; es wird 4 + 3 = 7 berechnet:
x = StringNumber("abcd")
y = StringNumber("ABC")
z = x + y
print(z) # Ausgabe: abcdABC (7)
class StringNumber:
def __init__(self, val: str) -> None:
self.val = str(val)
def __str__(self) -> str:
return f"{self.val} ({len(self.val)})"
def __add__(self, other: 'StringNumber') -> 'StringNumber':
return StringNumber(self.val + other.val)
__init__Der Parameter val wird als Attribut gespeichert: self.val = str(val). Der str()-Umbau macht die Klasse robust gegen versehentlich übergebene Zahlen.
__str__print(z) ruft automatisch z.__str__() auf. Gewünschtes Format: Text (Länge). Mit f-String: f"{self.val} ({len(self.val)})" — für "abcd" wird das abcd (4).
__add__x + y wird zu x.__add__(y). Die Verkettung self.val + other.val ergibt "abcdABC" — und weil die Länge (7) der Summe 4 + 3 entspricht, passt alles. Wichtig: Ergebnis als neue StringNumber zurückgeben, nicht als rohen String.
print(z) muss abcdABC (7) ausgeben: len("abcdABC") = 7. Genau das fordert die Aufgabe. Die Typannotationen (-> str usw.) sind freiwillig, zeigen aber Sorgfalt.
__str__statt__repr__verwirren: gefordert ist die Darstellung beiprint— das ist__str__.- In
__add__einen String statt einerStringNumberzurückgeben. - Die Länge mit
len(val)stattlen(self.val)berechnen. - Vergessen, dass
x + yinternx.__add__(y)heißt — weradd(x, y)statt__add__schreibt, verliert die Punkte für die Überladung. - Im
__str__das Leerzeichen vergessen:"abcd(4)"statt"abcd (4)".
Übung 7.1 — ähnliche Klasse: Pair
Schreiben Sie eine Klasse Pair, die zwei Zahlen a und b speichert. + soll zwei Pair-Objekte komponentenweise addieren, print soll (a, b) ausgeben.
class Pair:
def __init__(self, a: int, b: int) -> None:
self.a = a
self.b = b
def __str__(self) -> str:
return f"({self.a}, {self.b})"
def __add__(self, other: 'Pair') -> 'Pair':
return Pair(self.a + other.a, self.b + other.b)
Übung 7.2 — Welche dunder-Methode?
Ordnen Sie zu: ==, -, *, len(obj), obj[i], str(obj)
== → __eq__(self, other) - → __sub__(self, other) * → __mul__(self, other) len(obj) → __len__(self) obj[i] → __getitem__(self, i) str(obj) → __str__(self)
Übung 7.3 — Fehler finden
Was ist an dieser Klasse falsch?
class StringNumber:
def __init__(val):
self.val = val
def __str__(self):
return self.val
✍ Lösung
1) __init__ braucht self als ersten Parameter. 2) __str__ fehlt das geforderte Format "val (Länge)". 3) Es gibt kein __add__ — x + y würde fehlschlagen.
Übung 7.4 — Klasse mit __len__ und __eq__
Schreiben Sie eine Klasse Word, die ein Wort (String) speichert. len(w) soll die Wortlänge liefern, w1 == w2 soll True sein, wenn beide Wörter gleich lang sind.
class Word:
def __init__(self, w: str) -> None:
self.w = str(w)
def __len__(self) -> int:
return len(self.w)
def __eq__(self, other: 'Word') -> bool:
return len(self.w) == len(other.w)
def __str__(self) -> str:
return self.w
Übung 7.5 — Multiplikation überladen
Erweitern Sie die Idee von StringNumber: Der Operator * soll eine Instanz mit einer Zahl n verketten (x * 3 → String 3-mal). Wie lautet die Methode?
def __mul__(self, n: int) -> 'StringNumber':
return StringNumber(self.val * n)
# x = StringNumber("ab"); x * 3 → "ababab" (Länge 6)
Übung 7.6 — Objektdiagramm zeichnen
Zeichnen Sie (auf Papier) das Objektdiagramm für:
x = StringNumber("a")
y = StringNumber("bc")
z = x + y
Tragen Sie für jedes Objekt das Attribut val und die daraus ablesbare Zahl ein.
x : StringNumber val = "a" → Zahl 1 y : StringNumber val = "bc" → Zahl 2 z : StringNumber val = "abc" → Zahl 3 print(z) → "abc (3)" (1 + 2 = 3 ✓)
Jede Klausur hat dieselbe Struktur wie die Grundklausur (7 Aufgaben, 92 Punkte) — nur mit anderen Zahlen, Bäumen und Sprachen. Erst selbst rechnen, dann die Lösung aufdecken und sich selbst Punkte geben. Im Klausurmodus läuft ein 90-Minuten-Timer.