Zum Inhalt springen
Zurück zur Themenübersicht

Algorithmen

Algorithmen entwerfen: vom Problem zum Struktogramm

Schrittweise Verfeinerung, Struktogramme und der Nachweis, dass ein Entwurf wirklich das Richtige tut.

Benötigte Grundlagen

Dieses Vorwissen brauchst du für das Kapitel. Schau kurz nach, wenn dir etwas davon nicht mehr präsent ist, sonst leg direkt los.

Einführung

„Schreibe ein Programm, das die Klassenarbeit auswertet.“ So lautet die Aufgabe. Und jetzt?

Der häufigste Anfängerfehler an dieser Stelle ist, sofort zu tippen. Man kommt ein Stück weit, merkt dann, dass etwas fehlt, baut um, merkt wieder etwas, und nach zwei Stunden ist der Quelltext ein Flickwerk, das man selbst nicht mehr versteht.

Es gibt ein Verfahren, das genau das verhindert, und es kommt ohne Rechner aus. Man zerlegt das große Problem so lange in kleinere, bis jedes Stück offensichtlich lösbar ist. Das dauert am Anfang länger und am Ende deutlich kürzer.

Das kannst du nach diesem Kapitel

  • die schrittweise Verfeinerung anwenden und ein Problem in bearbeitbare Teilschritte zerlegen.

  • einen als darstellen und aus einem Struktogramm Pseudocode ableiten.

  • den Ablaufsinn eines Entwurfs an Beispielwerten prüfen.

  • mit einer Invariante begründen, dass ein Algorithmus das Richtige berechnet.

  • Randfälle systematisch bestimmen und im Entwurf berücksichtigen.

Kurz aufgefrischt

Vorausgesetzt werden die vier Eigenschaften eines und die drei Grundbausteine Folge, Auswahl und Wiederholung. Bei Unsicherheit lies Was ist ein Algorithmus? und Die drei Grundbausteine.

Neu ist hier die Frage, wie man auf einen Algorithmus kommt und wie man begründet, dass er stimmt.

Schrittweise Verfeinerung

Die Methode besteht darin, das Problem zunächst in groben Schritten zu beschreiben und jeden Schritt anschließend zu verfeinern, bis er unmittelbar umsetzbar ist.

Stufe 1, das ganze Problem in einem Satz:

Werte die Klassenarbeit aus.

Stufe 2, in drei Schritte zerlegt:

1. Punkte aller Schüler einlesen
2. Aus den Punkten Noten berechnen
3. Ergebnisse ausgeben

Stufe 3, Schritt 2 weiter verfeinert:

2.1 Für jeden Schüler:
2.2   Prozentsatz aus Punkten und Höchstpunktzahl berechnen
2.3   Aus dem Prozentsatz die Note bestimmen

Stufe 4, Schritt 2.3 weiter verfeinert:

2.3.1 Wenn Prozent >= 87 dann Note 1
2.3.2 sonst wenn Prozent >= 73 dann Note 2
2.3.3 sonst wenn Prozent >= 59 dann Note 3
2.3.4 ...

Jetzt ist jeder Schritt so einfach, dass man ihn ohne Nachdenken hinschreiben kann. Das ist das Abbruchkriterium: Verfeinert wird, bis ein Schritt offensichtlich umsetzbar ist.

🔴 Der Gewinn liegt nicht nur in der Übersicht. Auf jeder Stufe kann man prüfen, ob die Zerlegung vollständig ist, ohne sich um die tieferen Ebenen zu kümmern. Fehlt in Stufe 2 der Punkt „Ergebnisse ausgeben“, fällt das dort auf und nicht erst nach zweihundert Zeilen Quelltext.

1Stufe 1Werte die Klassenarbeit aus.2Stufe 21. Punkte einlesen · 2. Notenberechnen · 3. Ergebnisseausgeben3Stufe 3: nur Schritt 22.1 für jeden Schüler: 2.2Prozent berechnen, 2.3 Notebestimmen4Stufe 4: nur Schritt 2.3wenn Prozent ≥ 87 dann Note 1,sonst wenn ≥ 73 dann Note 2,sonst …

Lies die vier Stufen von oben nach unten und achte auf die Überschriften: Ab Stufe 3 wird nur noch ein einziger Schritt weiter aufgeklappt, alles andere bleibt stehen. Genau das ist der Gewinn der Methode. Auf jeder Stufe kannst du prüfen, ob die Zerlegung vollständig ist, ohne dich um die tieferen Ebenen zu kümmern, fehlte in Stufe 2 der Punkt „Ergebnisse ausgeben“, fiele es dort sofort auf und nicht erst nach zweihundert Zeilen Quelltext. Und das Abbruchkriterium liest du an der letzten Stufe ab: Verfeinert wird, bis ein Schritt so einfach ist, dass man ihn ohne Nachdenken hinschreiben kann.

Struktogramme

Ein stellt einen als ineinandergeschachtelte Kästen dar. Sein Vorteil gegenüber Pfeildiagrammen: Man kann keine unstrukturierten Sprünge zeichnen, weil ein Kasten immer vollständig in einem anderen liegt.

Die drei Grundbausteine sehen so aus:

Folge:                    Auswahl:                  Wiederholung:
+------------------+      +------------------+      +--------------------+
| Anweisung 1      |      |  \  Bedingung  / |      | solange Bedingung  |
+------------------+      |   \          /   |      | +----------------+ |
| Anweisung 2      |      | ja  \      /  nein|     | | Anweisung      | |
+------------------+      +-------+----------+      | +----------------+ |
                          | dann  | sonst    |      +--------------------+
                          +-------+----------+

Verschachteln ist ausdrücklich vorgesehen: In einem Wiederholungsrahmen darf eine Auswahl stehen und darin wieder eine Wiederholung. Genau daraus entsteht die Ausdruckskraft.

Vom Struktogramm zum Pseudocode ist es dann nur noch eine Übersetzung: Jeder Kasten wird eine Zeile, jede Schachtelung eine Einrückung.

Dieselbe Zerlegung als Struktogramm

Punkte einlesenfür jeden SchülerProzent berechnenNote bestimmenErgebnisseausgeben

Das ist der Baum von eben, nur in der anderen Darstellung, vergleiche Kasten für Kasten. Was der Baum als Ebenen zeigt, zeigt das als Schachtelung: Die beiden Schritte 2.2 und 2.3 liegen innerhalb des Wiederholungsrahmens, und man sieht auf einen Blick, was je Schüler passiert und was nur einmal. Der eigentliche Vorteil dieser Form steckt in dem, was du nicht zeichnen kannst: Ein Kasten liegt immer vollständig in einem anderen, also gibt es keine Sprünge quer durch den Ablauf. Und der Weg zum Programm ist danach nur noch eine Übersetzung, jeder Kasten wird eine Zeile, jede Schachtelung eine Einrückung.

Den Entwurf prüfen: Ablaufsinn

Ein Entwurf ist erst dann fertig, wenn man ihn geprüft hat, und geprüft heißt: mit konkreten Werten durchgespielt.

Das Werkzeug dafür kennst du, es ist die Wertetabelle: eine Spalte je , eine Zeile je Durchlauf. Man wählt Beispielwerte, deren Ergebnis man unabhängig kennt, und vergleicht.

Wichtig ist die Auswahl der Beispiele. Ein einziger typischer Fall genügt nie, denn die Fehler sitzen an den Rändern.

Randfälle systematisch bestimmen

Statt zu raten, geht man eine feste Liste durch. Für jede Größe im fragt man:

Fragetypischer Randfall
Was, wenn es nichts gibt?leere Liste, null Schüler
Was, wenn es genau eines gibt?ein Schüler, ein Durchlauf
Was am kleinsten Wert?0 Punkte
Was am größten Wert?volle Punktzahl
Was genau an der Grenze?exakt 87 Prozent
Was bei unerlaubten Werten?negative Punkte, Text statt Zahl

Besonders die Zeile „genau an der Grenze“ verdient Aufmerksamkeit. Bei einer Notengrenze von 87 Prozent muss festgelegt sein, ob 87,0 noch eine 1 ist oder schon eine 2. Diese Entscheidung trifft der Vergleichsoperator, und ein >> statt ≥\ge verschiebt sie.

Ein Zeichen, eine Note Unterschied

mit ≥ 87mit > 87bei 87,1 ProzentNote 1Note 1bei genau 87,0 ProzentNote 1Note 2bei 86,9 ProzentNote 2Note 2Zwei von drei Zeilen sind gleich.Die mittlere entscheidet, und siesteht im Vergleichsoperator.

Drei Werte, dicht beieinander, und zwei Fassungen desselben , sie unterscheiden sich um ein einziges Zeichen. Geh die Zeilen von oben nach unten durch: Bei 87,1 und bei 86,9 Prozent sind sich beide einig. Genau auf der Grenze gehen sie auseinander, und für den Schüler mit exakt 87,0 Prozent ist das der Unterschied zwischen einer 1 und einer 2. Deshalb steht in der Randfallliste die Zeile „genau an der Grenze“: Sie ist der Fall, den man beim Testen fast immer übergeht, weil er so unwahrscheinlich aussieht und den ein Rechner regelmäßig trifft, weil Prozentwerte aus glatten Punktzahlen entstehen.

Begründen statt hoffen: die Invariante

Ein Durchspielen zeigt, dass der für diese Werte stimmt. Es zeigt nicht, dass er immer stimmt. Dafür braucht man ein Argument.

Das wichtigste Werkzeug dazu ist die Invariante: eine Aussage, die vor der gilt und die jeder Durchlauf erhält.

Beispiel, Summe der Zahlen von 1 bis nn:

summe := 0
i := 1
solange i <= n:
    summe := summe + i
    i := i + 1

Invariante: „summe\texttt{summe} enthält die Summe aller Zahlen von 1 bis i−1\texttt{i} - 1.“

Prüfen wir sie in drei Schritten, und genau diese drei gehören zu jeder Begründung:

Vor der Schleife gilt sie: summe=0\texttt{summe} = 0 und i=1\texttt{i} = 1, also die Summe von 1 bis 0, und die ist leer, also 0. Stimmt.

Jeder Durchlauf erhält sie: Galt sie vorher, so wird i\texttt{i} addiert und danach i\texttt{i} um eins erhöht. Danach enthält summe\texttt{summe} die Summe bis zum neuen i−1\texttt{i} - 1. Stimmt weiterhin.

Nach der Schleife ist die Bedingung falsch, also i>n\texttt{i} > n, und da i\texttt{i} genau um eins wächst, ist i=n+1\texttt{i} = n + 1. Die Invariante liefert dann: summe\texttt{summe} ist die Summe von 1 bis nn. Genau das war gesucht.

Damit ist die Richtigkeit für alle nn begründet und nicht nur für ausprobierte Werte. Das ist der Unterschied zwischen Testen und Begründen, und beides hat seinen Platz: Testen findet Fehler, Begründen zeigt ihre Abwesenheit.

Die Invariante Zeile für Zeile geprüft

Schrittisumme1 + … + (i−1)vor derSchleife10leer, also0 ✓nachDurchlauf 1211 ✓nachDurchlauf 2331+2 ✓nachDurchlauf 3461+2+3 ✓Bei n = 3 ist nach dem letztenDurchlauf i = 4 > n, die Schleifeendet und die Invariante liefertgenau die gesuchte Summe.

Die rechte Spalte ist die Invariante: „summe\texttt{summe} enthält die Summe aller Zahlen von 1 bis i−1\texttt{i}-1.“ Prüfe sie in jeder Zeile selbst nach, indem du die dritte und die vierte Spalte vergleichst. Sie stimmen überein, in jeder Zeile. Und jetzt der Schritt, der Testen von Begründen unterscheidet: Die erste Zeile zeigt, dass die Aussage vor der gilt. Jede weitere Zeile zeigt, dass ein Durchlauf sie erhält. Beides zusammen gilt dann für beliebig viele Durchläufe, also auch für n=1000n = 1000, ohne dass man tausend Zeilen schreiben müsste. Nach der Schleife ist i=n+1\texttt{i} = n+1, und die Invariante liefert die Summe von 1 bis nn, genau das Gesuchte. Testen findet Fehler; dieses Argument zeigt ihre Abwesenheit.

Auch die Endlichkeit gehört begründet

Zur Richtigkeit kommt die Frage, ob die überhaupt endet. Dafür genügt der Hinweis auf eine Größe, die sich in eine Richtung bewegt und eine Schranke erreichen muss:

i\texttt{i} startet bei 1, wächst in jedem Durchlauf um genau 1 und wird nie verkleinert; nn ändert sich nicht. Eine streng wachsende ganze Zahl überschreitet jede feste Schranke nach endlich vielen Schritten. Also endet die Schleife nach höchstens nn Durchläufen.

Ein vollständiger Entwurf beantwortet damit drei Fragen: Tut er das Richtige? Hört er auf? Was passiert an den Rändern?

Ein Problem schrittweise verfeinern

Entwirf durch schrittweise Verfeinerung einen , der aus einer Liste von Messwerten den größten, den kleinsten und den Mittelwert bestimmt.

  1. 1

    Stufe 1:

    Werte die Messreihe aus.
    
  2. 2

    Stufe 2, grobe Schritte:

    1. Messwerte einlesen
    2. Größten, kleinsten und Mittelwert bestimmen
    3. Ergebnisse ausgeben
    

    Hier lohnt schon die Prüfung: Fehlt etwas? Ja, der Fall einer leeren Liste ist nicht bedacht. Also ergänzen wir Schritt 0: „Wenn die Liste leer ist, Meldung ausgeben und beenden.“

  3. 3

    Stufe 3, Schritt 2 verfeinert. Alle drei Größen lassen sich in einem Durchlauf bestimmen:

    2.1 max := erster Wert
    2.2 min := erster Wert
    2.3 summe := erster Wert
    2.4 Für jeden weiteren Wert w:
    2.5   wenn w > max dann max := w
    2.6   wenn w < min dann min := w
    2.7   summe := summe + w
    2.8 mittel := summe / Anzahl
    
  4. 4

    Warum mit dem ersten Wert starten und nicht mit 0? Weil bei lauter negativen Messwerten ein Startwert 0 für max\texttt{max} falsch wäre: Er bliebe stehen, obwohl kein Messwert so groß ist. Der erste Wert ist dagegen immer ein tatsächlich vorkommender.

  5. 5

    Randfälle prüfen: Leere Liste ist durch Schritt 0 abgefangen. Genau ein Wert: Die läuft nullmal, max, min und Mittelwert sind dieser eine Wert. Richtig. Alle Werte gleich: max und min sind gleich, der Mittelwert ebenso. Richtig.

Vier Stufen, ein einziger Durchlauf für alle drei Größen, Startwerte aus dem ersten Element statt aus 0.

Eine Invariante aufstellen und prüfen

Ein zählt, wie viele Zahlen einer Liste größer als 100 sind. Stelle die Invariante auf und begründe die Richtigkeit.

  1. 1

    Der Algorithmus:

    anzahl := 0
    i := 1
    solange i <= laenge:
        wenn liste[i] > 100 dann anzahl := anzahl + 1
        i := i + 1
    
  2. 2

    Invariante formulieren. Man fragt: Was ist nach jedem Durchlauf wahr? Antwort: „anzahl\texttt{anzahl} enthält die Anzahl der Elemente unter den ersten i−1\texttt{i} - 1, die größer als 100 sind.“

  3. 3

    Gilt sie vorher? Vor der ist anzahl=0\texttt{anzahl} = 0 und i=1\texttt{i} = 1, also geht es um die ersten 0 Elemente. Unter keinem Element sind 0 größer als 100. Stimmt.

  4. 4

    Bleibt sie erhalten? Ein Durchlauf betrachtet Element i\texttt{i}. Ist es größer als 100, wird anzahl\texttt{anzahl} um eins erhöht, sonst nicht; danach wächst i\texttt{i} um eins. In beiden Fällen zählt anzahl\texttt{anzahl} danach genau die passenden unter den ersten i−1\texttt{i} - 1. Stimmt.

  5. 5

    Was folgt am Ende? Die Schleife endet, wenn i>laenge\texttt{i} > \texttt{laenge}, und da i\texttt{i} um genau eins wächst, ist i=laenge+1\texttt{i} = \texttt{laenge} + 1. Die Invariante liefert: anzahl\texttt{anzahl} zählt die passenden unter den ersten laenge\texttt{laenge} Elementen, also unter allen. Das war gesucht.

Die Richtigkeit ist für jede Liste begründet, nicht nur für ausprobierte. Genau das leistet eine Invariante.

Typischer Fehler

„Ich habe es mit drei Beispielen getestet, es funktioniert. Also ist der richtig.“

Ein Test kann die Anwesenheit von Fehlern zeigen, niemals ihre Abwesenheit. Drei gelungene Läufe belegen genau drei Fälle.

Das Gegenbeispiel ist leicht gebaut. Ein Algorithmus, der das Maximum sucht und dafür max\texttt{max} mit 0 vorbelegt, arbeitet für alle positiven Zahlen einwandfrei. Testet man mit den Werten 5, 12 und 7, ist das Ergebnis dreimal richtig. Bei den Werten −5-5, −12-12, −7-7 liefert er 0, und 0 kommt in der Liste gar nicht vor.

Wer sollte diesen Fall testen? Genau darum geht es: Man findet ihn nicht durch mehr Beispiele, sondern durch systematisches Fragen nach den Rändern, hier „was, wenn alle Werte negativ sind?“.

Die drei Prüfungen ergänzen einander und ersetzen sich nicht:

Prüfungzeigtzeigt nicht
Durchspielen mit BeispielenFehler in diesen FällenRichtigkeit allgemein
Randfälle systematischdie typischen Lückenungewöhnliche Kombinationen
InvarianteRichtigkeit für alle EingabenTippfehler in der Umsetzung

Deshalb gehören zu einem fertigen Entwurf alle drei, und die Reihenfolge ist die des Aufwands: erst begründen, dann Ränder abfragen, dann testen.

Übung 1

leicht

Nenne für einen , der den Durchschnitt einer Zahlenliste berechnet, vier Randfälle und jeweils das erwartete Verhalten.

Tipp anzeigen

Gehe die sechs Fragen der Randfallliste durch.

Lösung anzeigen

Leere Liste: Der Durchschnitt ist nicht definiert, weil durch 0 geteilt würde. Erwartet wird eine Meldung, kein Absturz und kein erfundener Wert.

Genau ein Wert: Der Durchschnitt ist dieser Wert selbst.

Alle Werte gleich: Der Durchschnitt ist dieser Wert.

Negative Werte: Der Durchschnitt darf negativ sein; ein Algorithmus, der nur positive Werte annimmt, wäre falsch.

Weitere sinnvolle Fälle: sehr große Werte (Bereichsüberschreitung bei der Summe) und Text statt Zahl (unerlaubte Eingabe).

Detaillierte Schritterklärung anzeigen

Hier wird jeder Schritt einzeln erklärt, vor allem, warum er gemacht wird.

Erklärungstiefe

✦ Empfohlen: Standard – Die normale Erklärungstiefe passt zum Einstieg.

  1. 1

    Randfälle systematisch erzeugen statt sie zu erraten

    Randfälle findet man nicht durch Nachdenken über die Aufgabe, sondern durch eine feste Frageliste: Was ist der kleinste mögliche Fall? Der zweitkleinste? Was, wenn alle Werte gleich sind? Was liegt außerhalb des Erwarteten? Was ist sehr groß? Was hat den falschen Typ?

  2. 2

    Die leere Liste: kein Absturz und kein erfundener Wert

    Leere Liste: Der Durchschnitt ist nicht definiert, weil durch 0 geteilt würde. Erwartet wird eine Meldung, kein Absturz und vor allem kein erfundener Wert wie 0.

  3. 3

    Die Fälle mit bekanntem Ergebnis: genau ein Wert, alle gleich

    Genau ein Wert: Der Durchschnitt ist dieser Wert selbst. Alle Werte gleich: ebenfalls dieser Wert. Beide Fälle sind besonders wertvoll, weil man das richtige Ergebnis vorher kennt, ohne zu rechnen.

  4. 4

    Negative Werte und die weiteren Fälle

    Negative Werte: Der Durchschnitt darf negativ sein; ein Algorithmus, der nur positive Werte annimmt, wäre falsch. Weitere sinnvolle Fälle: sehr große Werte (Bereichsüberschreitung bei der Summe) und Text statt Zahl (unerlaubte Eingabe).

Übung 2

mittel

Entwirf einen , der prüft, ob eine Zahl eine Primzahl ist.

a) Zerlege das Problem in zwei Verfeinerungsstufen. b) Schreibe den Pseudocode. c) Stelle die Invariante der auf und begründe die Richtigkeit. d) Nenne drei Randfälle und prüfe deinen Entwurf daran.

Tipp anzeigen

Eine Zahl n>1n > 1 ist prim, wenn sie außer 1 und sich selbst keinen Teiler hat.

Lösung anzeigen

a) Stufe 1: „Prüfe, ob n eine Primzahl ist.“ Stufe 2:

  1. Sonderfälle behandeln (n kleiner als 2 ist keine Primzahl)
  2. Alle möglichen Teiler durchprobieren
  3. Ergebnis ausgeben

b) Pseudocode:

LIES n WENN n < 2 DANN SCHREIBE "keine Primzahl" SONST istPrim := wahr t := 2 SOLANGE t * t <= n UND istPrim WIEDERHOLE WENN n mod t = 0 DANN istPrim := falsch t := t + 1 ENDE SOLANGE SCHREIBE istPrim ENDE WENN

c) Invariante: „istPrim\texttt{istPrim} ist genau dann noch wahr, wenn keine der bisher geprüften Zahlen von 2 bis t−1\texttt{t} - 1 ein Teiler von nn ist.“

Gilt vorher: t=2\texttt{t} = 2, geprüft wurde nichts, istPrim\texttt{istPrim} ist wahr. Stimmt. Bleibt erhalten: Jeder Durchlauf prüft genau t\texttt{t}. Ist es ein Teiler, wird istPrim\texttt{istPrim} falsch; sonst bleibt es wahr. Danach wächst t\texttt{t} um eins. Stimmt weiterhin. Am Ende: Die Schleife endet, wenn t⋅t>n\texttt{t} \cdot \texttt{t} > n oder istPrim\texttt{istPrim} falsch ist. Im ersten Fall wurden alle Teiler bis n\sqrt{n} geprüft, und das genügt: Hätte nn einen Teiler größer als n\sqrt{n}, so hätte es auch einen kleineren, denn Teiler treten paarweise auf. Also ist nn prim. Im zweiten Fall wurde ein Teiler gefunden.

d) n = 1: kleiner als 2, wird als „keine Primzahl“ gemeldet. Richtig. n = 2: Die Schleifenbedingung 2⋅2≤22 \cdot 2 \le 2 ist von Anfang an falsch, die Schleife läuft nullmal, istPrim\texttt{istPrim} bleibt wahr. Richtig, 2 ist prim. n = 4: t=2t = 2, 2⋅2≤42 \cdot 2 \le 4 ist wahr, 4 mod 2=04 \bmod 2 = 0, also nicht prim. Richtig.

Detaillierte Schritterklärung anzeigen

Hier wird jeder Schritt einzeln erklärt, vor allem, warum er gemacht wird.

Erklärungstiefe

✦ Empfohlen: Standard – Die normale Erklärungstiefe passt zum Einstieg.

  1. 1

    Erst die Sonderfälle abtrennen

    Bevor man an die Schleife geht, klärt man die Fälle, die gar nicht in das allgemeine Verfahren passen. Für Zahlen kleiner als 2 ist die Frage per Definition beantwortet, und man erspart sich später Sonderregeln in der Schleife.

    Zwischenergebnis

    n < 2 wird vorab abgefangen.

    Sonderfälle nach vorn zu ziehen macht den Hauptteil sauber. Wer sie in die Schleife einbaut, muss ihre Bedingung bei jedem Durchlauf mitprüfen, obwohl sie sich nie ändert.

  2. 2

    Die Schleife: was wird eigentlich durchprobiert?

    Geprüft wird, ob eine der Zahlen ab 2 ein Teiler ist. Ein Teiler liegt vor, wenn die Division ohne Rest aufgeht, also der Rest 0 ist.

    n \bmod t = 0 ;\Leftrightarrow; t \text{ ist Teiler von } n

    Zwischenergebnis

    Ein einziger gefundener Teiler genügt, um „keine Primzahl“ zu entscheiden.

  3. 3

    Warum bis zur Wurzel genügt

    Die Bedingung lautet t⋅t≤nt \cdot t \le n und nicht t≤nt \le n. Der Grund ist ein Argument über Teilerpaare, kein Sparbeschluss.

    n = a \cdot b ;\Rightarrow; \text{einer der beiden Faktoren} \le \sqrt{n}

    Zwischenergebnis

    Es genügt, bis n\sqrt{n} zu prüfen.

  4. 4

    Teil d): Randfälle einzeln durchspielen

    Geprüft wird nicht mit einer großen Zahl, sondern mit den kleinen und den Grenzfällen. Bei n = 2 ist entscheidend, ob die Schleife nullmal läuft; das prüft man, indem man die Bedingung mit den Startwerten einsetzt.

    t = 2,\quad t \cdot t = 4 \not\le 2 ;\Rightarrow; \text{null Durchläufe}

    Zwischenergebnis

    istPrim bleibt wahr, 2 wird korrekt als Primzahl erkannt.

    Genau hier zeigt sich der Nutzen der kopfgesteuerten Schleife: Sie darf nullmal laufen. Eine fußgesteuerte hätte bei n = 2 einmal geprüft und dabei nichts kaputtgemacht, bei anderer Formulierung aber leicht.

Übung 3

schwer

Ein soll aus einer Liste von Tagestemperaturen die längste Folge aufeinanderfolgender Tage mit Frost (Temperatur unter 0) bestimmen.

a) Zerlege das Problem und schreibe den Pseudocode. b) Welche zwei Zählvariablen brauchst du, und warum reicht eine nicht? c) Stelle die Invariante auf. d) Prüfe deinen Entwurf an den Listen [3, -1, -2, 4], [-1, -2, -3] und der leeren Liste.

Tipp anzeigen

Zu b): Eine Zählung läuft mit, die andere merkt sich das beste bisher gesehene Ergebnis.

Lösung anzeigen

a) Stufe 2: 1. Liste durchlaufen. 2. Frostfolgen mitzählen. 3. Längste merken. 4. Ausgeben.

Pseudocode:

aktuell := 0 laengste := 0 FÜR jeden Wert t IN liste WIEDERHOLE WENN t < 0 DANN aktuell := aktuell + 1 WENN aktuell > laengste DANN laengste := aktuell SONST aktuell := 0 ENDE WENN ENDE FÜR SCHREIBE laengste

b) Man braucht aktuell\texttt{aktuell} für die gerade laufende Frostfolge und laengste\texttt{laengste} für die beste bisher gesehene. Eine reicht nicht, weil beim Ende einer Folge der laufende Zähler zurückgesetzt werden muss; ohne die zweite Variable wäre das bisherige Ergebnis dann verloren. Der Fall [-1, -2, 5, -3] zeigt es: Nach dem 5 wird aktuell\texttt{aktuell} auf 0 gesetzt, und die 2 aus der ersten Folge muss anderswo aufbewahrt sein.

c) Invariante: „laengste\texttt{laengste} ist die Länge der längsten Frostfolge unter den bisher betrachteten Tagen, und aktuell\texttt{aktuell} ist die Länge der Frostfolge, die mit dem zuletzt betrachteten Tag endet.“

Sie gilt vor der (beide 0, keine Tage betrachtet), bleibt bei jedem Durchlauf erhalten (Frosttag verlängert die laufende Folge und aktualisiert gegebenenfalls das Beste; ein frostfreier Tag beendet die laufende Folge), und liefert nach dem letzten Tag genau das gesuchte Ergebnis.

d) [3, -1, -2, 4]: 3 setzt aktuell auf 0. −1-1 ergibt aktuell 1, laengste 1. −2-2 ergibt aktuell 2, laengste 2. 4 setzt aktuell auf 0. Ergebnis 2. Richtig. [-1, -2, -3]: aktuell wächst auf 3, laengste ebenfalls 3. Ergebnis 3. Richtig, und dieser Fall prüft, ob das Ergebnis auch dann stimmt, wenn die längste Folge am Ende liegt und kein frostfreier Tag mehr folgt. Genau hier scheitern Entwürfe, die das Ergebnis erst beim Zurücksetzen aktualisieren. Leere Liste: Die Schleife läuft nullmal, laengste bleibt 0. Ergebnis 0. Richtig, denn ohne Tage gibt es keine Frostfolge.

Detaillierte Schritterklärung anzeigen

Hier wird jeder Schritt einzeln erklärt, vor allem, warum er gemacht wird.

Erklärungstiefe

✦ Empfohlen: Standard – Die normale Erklärungstiefe passt zum Einstieg.

  1. 1

    a) Das Problem zerlegen, bevor Pseudocode entsteht

    Die Zerlegung: 1. Liste durchlaufen. 2. Frostfolgen mitzählen. 3. Die längste merken. 4. Ausgeben. Erst danach der Pseudocode mit zwei Zählern, einer Schleife und einer Auswahl darin.

    aktuell := 0\texttt{aktuell := 0}; laengste := 0\texttt{laengste := 0}; für jeden Wert prüfen und zurücksetzen

  2. 2

    b) Warum eine Zählvariable nicht reicht

    Man braucht aktuell\texttt{aktuell} für die gerade laufende Frostfolge und laengste\texttt{laengste} für die beste bisher gesehene. Eine reicht nicht, weil beim Ende einer Folge der laufende Zähler zurückgesetzt werden muss, ohne die zweite Variable wäre das bisherige Ergebnis dann verloren.

  3. 3

    c) Die Invariante aufstellen und in drei Punkten prüfen

    Invariante: „laengste\texttt{laengste} ist die Länge der längsten Frostfolge unter den bisher betrachteten Tagen, und aktuell\texttt{aktuell} ist die Länge der Frostfolge, die mit dem zuletzt betrachteten Tag endet.“ Geprüft wird in drei Punkten: Gilt sie vor der Schleife (beide 0, keine Tage betrachtet, ja)? Bleibt sie bei jedem Durchlauf erhalten? Liefert sie nach dem letzten Tag das Gesuchte?

  4. 4

    d) An drei Listen prüfen, jede prüft etwas anderes

    [3,−1,−2,4][3, -1, -2, 4]: 3 setzt aktuell auf 0; −1-1 ergibt 1, −2-2 ergibt 2 (laengste 2); 4 setzt zurück. Ergebnis 2. [−1,−2,−3][-1, -2, -3]: aktuell wächst auf 3, laengste ebenso. Ergebnis 3. Leere Liste: Die Schleife läuft nullmal, laengste bleibt 0. Ergebnis 0.

Zusammenfassung

Ein entsteht nicht beim Tippen, sondern durch schrittweise Verfeinerung: Das Problem wird grob zerlegt, und jeder Schritt wird weiter zerlegt, bis er offensichtlich umsetzbar ist. Auf jeder Stufe lässt sich prüfen, ob die Zerlegung vollständig ist, und dort fällt Fehlendes auf, statt erst im Quelltext. stellen das Ergebnis als geschachtelte Kästen dar und schließen unstrukturierte Sprünge zeichnerisch aus; die Übersetzung in Pseudocode ist danach reine Schreibarbeit. Geprüft wird ein Entwurf auf drei Weisen, die einander ergänzen: Durchspielen mit Beispielwerten findet Fehler in genau diesen Fällen, eine systematische Randfallliste deckt die typischen Lücken auf, und eine Invariante begründet die Richtigkeit für alle Eingaben. Dazu gehört die Begründung der Endlichkeit über eine Größe, die sich streng auf eine Schranke zubewegt. Ein fertiger Entwurf beantwortet damit drei Fragen: Tut er das Richtige, hört er auf, und was passiert an den Rändern?