Altklausur WS 2025/26 · Shady Tawfik Senior Data Sience

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

AufgabeThemaSeitePunkte
1Ausdrücke und Datentypen (Slicing, Indexierung)29
2Hoare-Kalkül: schwächste Vorbedingung + Schleifeninvariante312
3Komplexitäten: Ergebnis für k = 3 + Komplexitätsklasse (f1, f2, f3)418
4-AXML: Adressbaum als XML-Code modellieren (Wahl)510
4-BReguläre Ausdrücke: Wortaufzählung + RegEx (Wahl)610
5Kontextfreie Grammatik: Wörter + check_word715
6Rekursion: Typ erkennen, Schleife, Hilfsfunktionen816
7OOP: Klasse StringNumber912
Σmaximal erreichbare Punktzahl (inkl. Aufgabe 4)92

Lernplan — Heute Abend & Morgen (Klausur: Montag)

  1. Heute Abend (3–4 h): Thema 1 (Ausdrücke) + Thema 3 (Komplexität) — mechanische, schnelle Punkte, zusammen 27 Punkte.
  2. Heute Abend, danach: Thema 2 (Hoare-Kalkül) — wp-Ketten und Invarianten-Beweise üben.
  3. 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).
  4. Morgen Nachmittag (2–3 h): Thema 6 (Rekursion) + Thema 7 (OOP).
  5. Morgen Abend (2 h): Alle Selbsttests noch einmal, Musterlösungen auf Papier nachschreiben. Aufgaben 1 und 3 unter Zeitdruck komplett durchrechnen — zusammen 27 Punkte.
  6. 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.
Aufgabe 1 · Seite 29 Punkte · 6 Ausdrücke

Ausdrücke und Datentypen

Lernziele
  • 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).
Deep Dive · Grundlagen

1. Grundlegende Datentypen

Ein Datentyp legt fest, welche Werte ein Ausdruck annehmen kann und welche Operationen erlaubt sind. Wichtig für die Klausur:

TypBedeutungBeispieleVeränderlich?
intganze Zahlen6, -3, 2**10
floatGleitpunktzahlen6.0, 6., 0.5
strZeichenkette (Text)'a', "abc"nein (immutable)
listFolge in eckigen Klammern[1, 2, 3]ja (mutable)
tupleFolge in runden Klammern(1, 2, '3')nein (immutable)
boolWahrheitswertTrue, False
Der Punkt entscheidet: 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.
List vs. Tuple — warum wichtig? Beide sind Folgen mit identischer Index-/Slice-Syntax. Unterschied: Listen lassen sich verändern (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

OperatorNameBeispielErgebnisTyp
+Addition5 + 27int
-Subtraktion5 - 23int
*Multiplikation5 * 210int
/echte Division6 / 32.0immer float
//Ganzzahldivision (abrunden)7 // 23int
%Modulo (Rest)7 % 21int
**Potenz2**38int
Operator-Vorrang (hoch → tief): *** / // %+ -. 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'. Auch 3 * '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.
Merksatz: 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 stop ist NICHT enthalten. Weggelassen = Ende der Sequenz.
  • step — Schrittweite; -1 dreht 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 einen IndexError.
AusdruckErgebnisKommentar
'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).
Visualisierung · Indexierung & Slicing
1 2 3 („3“) a Index 0 Index 1 Index 2 Index 3 Index 4 -4 -3 -2 -1 (Ende) [1:2] → nur Index 1

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.

Klausuraufgabe · Originaltext
Aufgabe 1 — Ausdrücke und Datentypen (9 Punkte)

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]
Musterlösung · Schritt für Schritt (je Teilaufgabe ca. 1,5 P: Ergebnis + Typ)
✍ Handschriftlicher Klausurbogen-Aufschrieb
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
a) 6 * 318 · int

Zwei ganze Zahlen werden multipliziert — das Ergebnis ist die ganze Zahl 18. Es gibt keinen Grund für einen Punkt, also bleibt der Typ int.

b) 6. * 318.0 · float

Der Punkt nach der 6 macht das Literal 6. zu einer Gleitpunktzahl. Operationen zwischen float und int liefern float — der float „gewinnt“.

c) 6 / 32.0 · float

Die echte Division / liefert immer eine Gleitpunktzahl — auch wenn sie „glatt“ aufgeht. 2.0, nicht 2! Das ist die häufigste Falle dieser Aufgabe.

d) [[1, 2][0], [3, 4][1]][1, 4] · list

Von 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].

e) (1, 2, '3')[[1, 2, 3][-2]]'3' · str

Zuerst 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!).

f) [1, 2, 3][1:2][2] · list

Slicing [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.

Häufige Fehler
  • 6 / 3 als 2 (int) notieren — richtig ist 2.0 (float).
  • Bei e) den Index-Ausdruck nicht zuerst auswerten.
  • Bei f) als Ergebnis 2 statt [2] schreiben — das Slicing „verpackt“ das Ergebnis in eine Liste.
  • Typ str und int verwechseln, wenn eine Zahl als String vorliegt.
Übungsaufgaben (alle aufgeklappt und mit kompletten Lösungen — erst selbst rechnen, dann vergleichen)
Ü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)
Selbsttest
Aufgabe 2 · Seite 312 Punkte · 6 + 6

Hoare-Kalkül: wp und Schleifeninvarianten

Lernziele
  • 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.
Deep Dive · Grundlagen

1. Hoare-Tripel

{ P } S { Q } bedeutet: Wenn vor der Ausführung von S die Vorbedingung P gilt, dann gilt nach der Ausführung die Nachbedingung Q (sofern S terminiert — „partielle Korrektheit“).
  • P und Q sind Prädikate (Bedingungen über Variablen), z. B. x > 0 oder y == 2·x.
  • Die schwächste Vorbedingung wp(S, Q) ist die größte Menge von Startzuständen, von denen aus Q nach S garantiert gilt.

2. Die Regeln des Hoare-Kalküls (komplett)

AnweisungRegel (wp)Bedeutung
Zuweisung x = ewp(x = e, Q) ≡ Q[x←e]jedes x in Q durch e ersetzen
Sequenz S1; S2wp(S1; S2, Q) ≡ wp(S1, wp(S2, Q))von unten nach oben rechnen
Bedingung if B: S1 else: S2wp(if B …, Q) ≡ (B ∧ wp(S1, Q)) ∨ (¬B ∧ wp(S2, Q))beide Zweige berücksichtigen
Schleife while B: SInvariante I zeigen (Anfang, Erhaltung, Abschluss)kein mechanisches wp
Zuweisungsregel — die Substitutionsrichtung: 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:

  1. Anfangsgültigkeit: { I } gilt vor dem ersten Schleifendurchlauf.
  2. Erhaltung: { I ∧ B } S { I } — nach jedem Rumpf-Durchlauf gilt I wieder.
  3. 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.

Warum „int“ wichtig ist: Beim Beweis von 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!
Visualisierung · wp-Kette & Invariante

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.

{ x > 1 } = P x = x - y { y + x > 1 } y = y + x { y > 1 } x = y - 1 (letzte Anweisung) Ergebnis der Kette: Schwächste Vorbedingung P ≡ { x > 1 }

Ableserichtung: ganz unten starten (Nachbedingung { x > 0 }), jede Zuweisung rückwärts „durchschieben“, oben ist die Vorbedingung erreicht.

Invariante als Schleifen-Flussdiagramm

I ∧ (q < b) (vor Rumpf) Rumpf: p = p + a q = q + 1 I (wieder) p' == q'·a ∧ q' ≤ b Zurückspringen, solange B = (q < b) gilt — I bleibt erhalten ¬B: q ≥ b, Ende der Schleife
Klausuraufgabe · Originaltext
Aufgabe 2 — Hoare-Kalkül (6 + 6 = 12 Punkte)

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.

Musterlösung · Schritt für Schritt

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.

✍ Handschriftlicher Klausurbogen-Aufschrieb
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 }
Schritt 1 — S3: 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.)

Schritt 2 — S2: 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.

Schritt 3 — S1: 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.

Probe (Warum ist das plausibel?): Starte mit x = 2, y = 5 (erfüllt x > 1). Rechne nach: x = 2-5 = -3; y = 5 + (-3) = 2; x = 2 - 1 = 11 > 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.

✍ Handschriftlicher Klausurbogen-Aufschrieb
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.
Schritt 1 — p-Anteil der Invariante

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.

Schritt 2 — q-Anteil der Invariante

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

Schritt 3 — Ergebnis formulieren

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

Häufige Fehler
  • 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) statt y + x - y sauber ausrechnen.
Übungsaufgaben (alle aufgeklappt, Lösungen in handschriftlicher Notation)
Ü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.

✍ Lösung
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?

✍ Lösung
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)
Selbsttest
Aufgabe 3 · Seite 418 Punkte · 6 + 6 + 6

Komplexitäten: Ergebnis & O-Klasse

Lernziele
  • Für eine Funktion mit Parameter k das Ergebnis des Aufrufs k = 3 bestimmen (exakt ausrechnen!).
  • Die Anzahl der Operationen als Summe aufstellen und schließen.
  • Eine möglichst kleine Komplexitätsklasse in Abhängigkeit von k angeben — und in Abhängigkeit von n = 2^k.
Deep Dive · Grundlagen

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).
Merksatz: In der O-Notation zählt nur der dominierende Term: 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 über k ist linear in k, aber logarithmisch in n.
  • 2^{2k} = (2^k)² = n².
  • Beispiele: k=3 → n=8, k=5 → n=32, k=10 → n=1024.

4. Analyse-Methodik für Schleifen

  1. Fest verschachtelt: for i in range(n): for j in range(m): mit festem mn · m Durchläufe (f1).
  2. m sinkt um 1: while m > 0: for j in range(m); m = m - 1 → Summe n + (n-1) + … + 1 = n(n+1)/2 (f2).
  3. m halbiert sich: while m > 0: for j in range(m); m = m // 2n + n/2 + … + 1 = 2n - 1 (f3) — die geometrische Reihe.
Der entscheidende Kniff dieser Aufgabe: Der Parameter ist 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!).

Visualisierung · Wachstum der Klassen
k (n = 2^k) Operationen 1 2 3 4 5 6 O(n) (f3: 2n) O(n²) (f1: n², f2: n(n+1)/2) O(1)

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
Klausuraufgabe · Originaltext
Aufgabe 3 — Komplexitäten (6 + 6 + 6 = 18 Punkte)

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
Musterlösung · Schritt für Schritt

a) f1 — fest verschachtelt (6 Punkte)

✍ Handschriftlicher Klausurbogen-Aufschrieb
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})
Warum?

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)

✍ Handschriftlicher Klausurbogen-Aufschrieb
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})
Warum?

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²/2O(n²).

c) f3 — m halbiert sich (6 Punkte)

✍ Handschriftlicher Klausurbogen-Aufschrieb
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)
Warum?

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.

Häufige Fehler
  • 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 ja m Schritte macht.
  • Ergebnis für k = 3 falsch, weil nicht mit n = 8 sondern mit k = 3 gerechnet wird (z. B. 3·3 = 9).
  • Klasse nur in n angeben — 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), nicht O(2^{k+1}) als Endergebnis schreiben.
Übungsaufgaben (alle aufgeklappt, Lösungen in handschriftlicher Notation)
Ü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.

✍ Lösung
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
Selbsttest
Aufgabe 4-A · Seite 510 Punkte · Wahl-Aufgabe

XML: einen Baum als XML-Code modellieren

Lernziele
  • 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.
Deep Dive · Grundlagen

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

  1. Genau ein Wurzelelement — alles andere liegt darin.
  2. Jedes geöffnete Tag wird geschlossen — und zwar umgekehrt zur Öffnungsreihenfolge (Stack-Prinzip, LIFO).
  3. Attributwerte stehen in Anführungszeichen (einfach oder doppelt).
  4. Tags sind case-sensitiv: <Ort> und </ort> passen nicht zusammen.
  5. Elemente dürfen sich nicht überkreuzen: <a><b></a></b> ist verboten.
Warum die Stack-Regel? XML verlangt saubere Verschachtelung wie Klammern in der Mathematik: ((…)) 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)

  1. Wurzel identifizieren → wird das äußerste Element.
  2. Jede Kante nach unten wird ein Kind-Element; jede Beschriftung am Knoten wird ein Attribut (hier: name, nummer).
  3. Knoten ohne Kinder (die Blätter) als self-closing schreiben: <haus nummer="1"/>.
  4. 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.
Visualisierung · der Klausur-Baum
kreis name="A-Kreis" ort name="A-Stadt" ort name="A-Dorf" strasse name="Hauptstr." strasse name="Umgehungsstr." strasse name="Hauptstr." haus 1 haus 2 haus 3 haus 1 haus 2 Hinweis: Umgehungsstr. hat im Klausur-Baum keine Häuser — nicht erfinden!
Klausuraufgabe · Originaltext
Aufgabe 4-A — XML (10 Punkte)

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

Musterlösung · Schritt für Schritt
✍ Handschriftlicher Klausurbogen-Aufschrieb
<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>
Schritt 1 — Wurzel

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.

Schritt 2 — Ebene der Orte

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.

Schritt 3 — Ebene der Straßen

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."/>.

Schritt 4 — Ebene der Häuser

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!

Schritt 5 — Gegenprobe

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.

Häufige Fehler
  • Tags auf Deutsch mit Umlaut oder anders benennen (strasse statt straß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.
Übungsaufgaben (alle aufgeklappt, Lösungen in handschriftlicher Notation)
Ü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.

✍ Lösung
<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).

✍ Lösung
<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.

✍ Lösung
<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.

✍ Lösung
<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.)
Aufgabe 4-B · Seite 610 Punkte · 4 + 6

Reguläre Ausdrücke & Sprachen

Lernziele
  • 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.
Deep Dive · Grundlagen

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

OperatorSchreibweiseBedeutungBeispiel
Verkettungaberst a, dann bab erkennt nur „ab“
Alternationa|ba oder ba|b erkennt „a“ oder „b“
Kleene-Sterna*beliebig viele a (auch 0 → ε)a*: ε, a, aa, aaa, …
Plusa+ein oder mehr aa+: a, aa, aaa, … (ohne ε)
Optionala?a oder nichts (ε)a?: ε, a
Merksatz: 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.
Vorrang im RegEx: * 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“:

  1. Nach Länge sortieren: erst Länge 0 (nur ε), dann Länge 1, 2, 3, …
  2. 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).
  3. 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.

Visualisierung · Wortbaum & Automat

Alle Wörter bis Länge 3 als Baum

ε a b b aa ab bb ba aaa aab abb aba baa bab b ✗ bb ✗ aab ✗ abb ✗

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

q0 a q1 q2 b a a, b q0 und q2 akzeptieren

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)*)?.

Klausuraufgabe · Originaltext
Aufgabe 4-B — Reguläre Ausdrücke (4 + 6 = 10 Punkte)

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.

Musterlösung · Schritt für Schritt

Teil a) — alle Wörter ≤ 3 Zeichen (4 Punkte)

✍ Handschriftlicher Klausurbogen-Aufschrieb
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.
Schritt 1 — ε nicht vergessen

Das leere Wort enthält kein b, die Bedingung ist also leer erfüllt. Länge 0 liefert immer genau ein Wort: ε.

Schritt 2 — Wörter mit b systematisch prüfen

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.

Schritt 3 — zählen

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)

✍ Handschriftlicher Klausurbogen-Aufschrieb
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)*)
Schritt 1 — Fall ohne b

Wörter ganz ohne b sind beliebig viele a's: a*. Das deckt auch ε ab.

Schritt 2 — Fall mit erstem b

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)*.

Schritt 3 — beide Fälle zusammenführen

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)*.

Häufige Fehler
  • Die Bedingung als „auf jedes b folgt ein a“ lesen → dann wären bab und aba fälschlich außen vor.
  • ε vergessen — der Ausdruck beginnt nicht mit a*.
  • Bei b) statt (a|b)* fälschlich a* 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.
Übungsaufgaben (alle aufgeklappt, Lösungen in handschriftlicher Notation)
Ü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?

✍ Lösung
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).
Selbsttest
Aufgabe 5 · Seite 715 Punkte · 3 + 12

Kontextfreie Grammatik & check_word

Lernziele
  • Die Sprache einer Grammatik verstehen: alle ableitbaren Wörter mit Längengrenze angeben.
  • Ableitungen mit Pfeilen aufschreiben können (S ⇒ aSb ⇒ …).
  • Eine Funktion check_word schreiben, die genau die Wörter der Grammatik erkennt — rekursiv oder mit Zählung.
Deep Dive · Grundlagen

1. Formale Definition

Kontextfreie Grammatik 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:

Sprache: 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 mit a beginnen, mit b enden und der innere Teil muss wieder gültig sein.
  • Zählend: Es darf genau ein c geben; links davon nur a's, rechts nur b's, und die Anzahlen müssen gleich sein.
Visualisierung · Ableitung
a b a b c S S S S ⇒ aSb ⇒ aaSbb ⇒ aacbb (n = 2 → a²c b²)

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.

Klausuraufgabe · Originaltext
Aufgabe 5 — Kontextfreie Grammatik (3 + 12 = 15 Punkte)

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.

Musterlösung · Schritt für Schritt

Teil a) — Wörter ≤ 5 Zeichen (3 Punkte)

✍ Handschriftlicher Klausurbogen-Aufschrieb
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.
Schritt 1 — Struktur erkennen

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.

Schritt 2 — Längengrenze übersetzen

2n + 1 ≤ 5n ≤ 2. Also n = 0, 1, 2 — mehr Anwendungen von aSb gehen nicht.

Schritt 3 — aufschreiben und zählen

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)

✍ Aufschrieb — Variante 1 (rekursiv, spiegelt die Grammatik)
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])
✍ Aufschrieb — Variante 2 (zählend)
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
Schritt 1 (Variante 1) — Basisfall

Das kürzeste Wort ist c. Ist s == "c", akzeptieren.

Schritt 2 (Variante 1) — Abziehen der äußeren Schale

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.

Schritt 1 (Variante 2) — die Position von c

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.

Schritt 2 (Variante 2) — Gleichheit der Schalen

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.

Warum zwei Varianten? In der Klausur zählt die Begründung: Die rekursive Variante spiegelt die Grammatik direkt wider („genau die Struktur der Produktionen“), die zählende ist kürzer. Beide sind richtig, solange sie exakt a^n c b^n erkennen und nichts anderes.
Häufige Fehler
  • In Teil a) Wörter mit falscher Struktur wie aabbc, abc oder cb aufschreiben.
  • check_word prüft nur „beginnt mit a, endet mit b“, aber nicht die Gleichheit der Schalen — dann würde aabbb fälschlich akzeptiert (die Rekursion s[1:-1] würde es zwar verwerfen — aber nur, wenn man sie auch wirklich benutzt!).
  • Leeren String ("") akzeptieren — ε gehört nicht zu L(G), weil S immer ersetzt werden muss.
  • Rückgabetyp vergessen oder print statt return.
Übungsaufgaben (alle aufgeklappt, Lösungen in handschriftlicher Notation)
Ü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?

✍ Lösung
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.

✍ Lösung
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.

✍ Lösung
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])
Selbsttest
Aufgabe 6 · Seite 816 Punkte · 3 + 4 + 9

Rekursion: Typ erkennen, in Schleife umwandeln, Hilfsfunktionen

Lernziele
  • Den Rekursionstyp einer Funktion begründet erkennen (hier: Endrekursion).
  • Rekursion in eine äquivalente Schleife umwandeln — mit korrekter Reihenfolge der Updates.
  • Hilfsfunktionen cond, fa, fb entwerfen, sodass die Funktion eine gewünschte Summe/Produkt berechnet.
Deep Dive · Grundlagen

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).
Erkennungsregel: Steht der rekursive Aufruf direkt hinter 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
Die Reihenfolge-Falle: Wäre die Zeile 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

  1. Basisfall bestimmen: Bei welchem a soll f einfach b zurückgeben? (Für s(n) = Σ i ist das a == 0.)
  2. fa bestimmen: Wie kommt man Schritt für Schritt zum Basisfall? (a - 1.)
  3. fb bestimmen: Die Rekursionsformel aus der Aufgabe umformen: s(a) = s(a-1) + a → der neue Akkumulator ist b + a.
  4. Probe rechnen: Aufrufkette für ein Beispiel aufschreiben (siehe Musterlösung).
Visualisierung · Stack-Verhalten

Lineare Rekursion — Stack wächst:

f(4): a + f(3)… f(3): a + f(2)… f(2): a + f(1)… f(1): a + f(0)… f(0): 0 danach rückwärts addieren → O(n) Stack-Speicher

Endrekursion — Stack bleibt klein:

f(4, 0) → f(3, 4) f(3, 4) → f(2, 7) f(2, 7) → f(1, 9) f(1, 9) → f(0, 10) f(0, 10) → 10 Rückgabe wird nur durchgereicht → O(1) Zusatzspeicher
Klausuraufgabe · Originaltext
Aufgabe 6 — Rekursion (3 + 4 + 9 = 16 Punkte)

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.

Musterlösung · Schritt für Schritt

Teil a) — Rekursionstyp (3 Punkte)

✍ Handschriftlicher Klausurbogen-Aufschrieb
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.
Warum ist die Begründung Pflicht?

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)

✍ Handschriftlicher Klausurbogen-Aufschrieb
def f_loop(a: int, b: int) -> int:
    while not cond(a):
        b = fb(a, b)
        a = fa(a)
    return b
Schritt 1 — Abbruch-Bedingung umdrehen

Rekursion: if cond(a): return b. Die Schleife läuft also solange not cond(a) und gibt danach b zurück.

Schritt 2 — Parameter-Update in der richtigen Reihenfolge

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)

✍ Handschriftlicher Klausurbogen-Aufschrieb
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 ✓
Schritt 1 — Das Schema in die Summe übersetzen

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.

Schritt 2 — fb so wählen, dass die Probe stimmt

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.

Schritt 3 — Probe mit den Zahlen

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.

Häufige Fehler
  • 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) fb als b + a nicht erkennen, sondern z. B. b + fa(a) — das würde die Summe verschieben (Probe zeigt den Fehler).
  • cond als (a <= 0) — nicht falsch bei n ≥ 0, aber a == 0 ist die sauberste Antwort.
Übungsaufgaben (alle aufgeklappt, Lösungen in handschriftlicher Notation)
Ü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ösung
LINEARE 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ösung
BAUM- 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ösung
ENDREKURSION.

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ösung
def 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ösung
a)  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).

✍ Lösung
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.

✍ Lösung
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.

✍ Lösung
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 ✓
Selbsttest
Aufgabe 7 · Seite 912 Punkte

Objektorientierte Programmierung: StringNumber

Lernziele
  • Eine Klasse mit Konstruktor, String-Repräsentation und Operator-Überladung schreiben.
  • Verstehen, was z = x + y und print(z) hinter den Kulissen aufrufen.
  • Die wichtigsten „dunder“-Methoden und ihre Operatoren kennen.
Deep Dive · Grundlagen

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

MethodeWann aufgerufen?Zweck
__init__(self, val)bei StringNumber(...)Konstruktor: Attribute setzen
__str__(self)bei print(z), str(z)menschenlesbare Darstellung
__add__(self, other)bei x + yOperator + überladen
Operator-Überladung: 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 / Funktiondunder-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

  1. „Konstruktor bekommt den String als Parameter und speichert ihn als Attribut“ → __init__(self, val) mit self.val = str(val) (sicherer Typ-Umbau).
  2. „String-Repräsentation: String + in Klammern die Länge“ → __str__ mit f"{self.val} ({len(self.val)})".
  3. „Addition zweier gespeicherter Zahlen“ → __add__: verketten und eine neue StringNumber zurückgeben.
Warum muss __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).

Visualisierung · Objektdiagramm
x : StringNumber val = "abcd" Wert: 4 (Länge) y : StringNumber val = "ABC" Wert: 3 (Länge) z : StringNumber val = "abcdABC" Wert: 7 (Länge) z = x + y ⟶ z.val = x.val + y.val ("abcd" + "ABC") print(z) ⟶ __str__ ⟶ "abcdABC (7)"
Klausuraufgabe · Originaltext
Aufgabe 7 — Objektorientierte Programmierung (12 Punkte)

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)
Musterlösung · Schritt für Schritt
✍ Handschriftlicher Klausurbogen-Aufschrieb
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)
Schritt 1 — Konstruktor __init__

Der Parameter val wird als Attribut gespeichert: self.val = str(val). Der str()-Umbau macht die Klasse robust gegen versehentlich übergebene Zahlen.

Schritt 2 — String-Repräsentation __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).

Schritt 3 — Addition __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.

Schritt 4 — Gegenprobe mit dem Beispiel

print(z) muss abcdABC (7) ausgeben: len("abcdABC") = 7. Genau das fordert die Aufgabe. Die Typannotationen (-> str usw.) sind freiwillig, zeigen aber Sorgfalt.

Häufige Fehler
  • __str__ statt __repr__ verwirren: gefordert ist die Darstellung bei print — das ist __str__.
  • In __add__ einen String statt einer StringNumber zurückgeben.
  • Die Länge mit len(val) statt len(self.val) berechnen.
  • Vergessen, dass x + y intern x.__add__(y) heißt — wer add(x, y) statt __add__ schreibt, verliert die Punkte für die Überladung.
  • Im __str__ das Leerzeichen vergessen: "abcd(4)" statt "abcd (4)".
Übungsaufgaben (alle aufgeklappt, Lösungen in handschriftlicher Notation)
Ü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.

✍ Lösung
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)

✍ Lösung
==       → __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.

✍ Lösung
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?

✍ Lösung
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.

✍ Lösung
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 ✓)
Selbsttest

Klausurtrainer Informatik I · Altklausur WS 2025/26 · Shady Tawfik Senior Data Sience · „Informatik für Ingenieurwissenschaften I“

Tipp: Über die Druckfunktion (Cmd/Ctrl-P) lässt sich die ganze Seite als schwarz-weißes PDF ausgeben — alle Themen werden nacheinander gedruckt.

Übungsklausuren — 10 Stück zum Durchrechnen

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.

Übungsklausur 1
8 Aufgaben · 92 Punkte · Neue Zahlen, neuer Baum: übe mit diesem fast-Original.
noch nicht versucht
Übungsklausur 2
8 Aufgaben · 92 Punkte · Variation mit Tausch-Trick und neuen Grenzen.
noch nicht versucht
Übungsklausur 3
8 Aufgaben · 92 Punkte · Neue Funktionen für Aufgabe 3 — ein ganz anderes Komplexitäts-Muster.
noch nicht versucht
Übungsklausur 4
8 Aufgaben · 92 Punkte · Tausch-Algorithmus und Summen der Quadrate.
noch nicht versucht
Übungsklausur 5
8 Aufgaben · 92 Punkte · Palindrome, Potenzen und ein Riesenschritt für n = 32.
noch nicht versucht
Übungsklausur 6
8 Aufgaben · 92 Punkte · Zwillings-Schleife und eine ungewöhnliche f3.
noch nicht versucht
Übungsklausur 7
8 Aufgaben · 92 Punkte · Kleines k, große Erkenntnisse.
noch nicht versucht
Übungsklausur 8
8 Aufgaben · 92 Punkte · Doppelschritt-Indexierung und Potenzen.
noch nicht versucht
Übungsklausur 9
8 Aufgaben · 92 Punkte · Zwei neue f1-Muster und schräge Indizes.
noch nicht versucht
Übungsklausur 10
8 Aufgaben · 92 Punkte · Die große Abschluss-Klausur: dreifach verschachtelt und alle Fälle.
noch nicht versucht
Klausurmodus
Mit 90-Minuten-Timer wie in der echten Klausur
Modus: Üben (ohne Zeit)