Zum Inhalt springen
Topos Uranos

Zusammenfassung

Diese Lektion enthält keinen Code, und das nicht aus Versehen: Der Algorithmus geht dem Programm voraus, und wer nicht genau beschreiben kann, was zu tun ist, wird es auch in C++ nicht sagen können. Zuerst lernt man, ein Problem zu spezifizieren, das heißt festzulegen, welche Daten es erhält, welches Ergebnis es liefern muss und welche Beziehung beide verbindet, und es wird gezeigt, dass ein und dieselbe Aufgabenstellung verschiedene Spezifikationen zulässt. Danach wird der Algorithmus als endliches, eindeutiges und allgemeines Verfahren definiert, und der Algorithmus der Division durch fortgesetzte Subtraktion wird in nummerierten Schritten aufgeschrieben. Man lernt, einen Algorithmus mit einer Ablauftabelle von Hand nachzuvollziehen; sie wird im ganzen Kurs das erste Werkzeug sein, um zu verstehen und um zu debuggen. Vorgestellt werden die drei Arten, Schritte zu kombinieren (Sequenz, Entscheidung und Wiederholung), mit denen sich jeder Algorithmus schreiben lässt, und man lernt zu begründen, dass eine Wiederholung terminiert, und zwar mit einer Größe, die in jedem Durchlauf fällt und nicht unbegrenzt sinken kann.

Lernziele

  1. Ein einfaches Problem spezifizieren, indem man seine Eingaben, seine Ausgaben und die Bedingungen angibt, die sie erfüllen müssen.
  2. Einen Algorithmus in nummerierten Schritten beschreiben, ohne Mehrdeutigkeit, mit ausdrücklichen Entscheidungen und Wiederholungen.
  3. Einen Algorithmus mit einer Ablauftabelle von Hand nachvollziehen und sein Ergebnis für eine gegebene Eingabe bestimmen.
  4. In einem gegebenen Verfahren Sequenz, Entscheidung und Wiederholung unterscheiden und begründen, dass eine Wiederholung terminiert.

Was ein Berechnungsproblem ist

Ein Berechnungsproblem ist eine Beziehung zwischen zwei Arten von Daten: denen, die man erhält und die Eingabe heißen, und denen, die geliefert werden müssen und die Ausgabe heißen. Es zu lösen bedeutet nicht, eine Antwort für einen Fall zu finden, sondern über ein Verfahren zu verfügen, das für jede zulässige Eingabe eine Ausgabe erzeugt, die die Beziehung erfüllt. „Wie viel ist 17 geteilt durch 5?“ ist eine Frage; „zu zwei ganzen Zahlen ihren Quotienten und ihren Rest bestimmen“ ist ein Problem, weil es von allen Paaren ganzer Zahlen zugleich spricht.

Nun ist die Umgangssprache ungenau, und dieselbe Aufgabenstellung kann auf mehrere Weisen verstanden werden. Deshalb wird vor dem Lösen spezifiziert: Man schreibt auf, was hineingeht, was herauskommt und welche Bedingungen jedes davon erfüllen muss. Die Spezifikation hat drei Teile. Die Eingabe zählt die Daten mit ihrer Art auf (ganze Zahlen, Dezimalzahlen, Wörter) und die Bedingungen, die an sie gestellt werden und die Vorbedingung heißen: Wer die Lösung benutzt, verpflichtet sich, sie einzuhalten. Die Ausgabe zählt die Ergebnisse auf. Die Beziehung, oder Nachbedingung, sagt, was die Ausgabe im Verhältnis zur Eingabe erfüllen muss: Wer die Lösung schreibt, verpflichtet sich, sie zu garantieren, sofern die Vorbedingung eingehalten wurde. Daraus folgt, dass die Spezifikation ein Vertrag zwischen dem ist, der das Problem stellt, und dem, der es löst, und dass die Lösung außerhalb der Vorbedingung nichts verspricht.

BeispielDas Maximum von drei Zahlen

Umgangssprachliche Aufgabenstellung: „die größte von drei Zahlen finden“. Spezifikation:

  • Eingabe: drei Zahlen xx, yy, zz (ganze Zahlen oder Dezimalzahlen, ohne weitere Bedingung).
  • Ausgabe: eine Zahl mm.
  • Beziehung: mm ist einer der drei Werte, und keiner von ihnen ist größer als mm; das heißt m≥xm \geq x, m≥ym \geq y und m≥zm \geq z.

Die Beziehung sagt nicht „die größte der drei“, denn genau das sollte ja präzisiert werden, sondern nennt zwei überprüfbare Bedingungen. Die erste verlangt, dass mm einer der gegebenen Werte ist (ohne sie würde die Zahl 1000 die zweite für fast jede Eingabe erfüllen); die zweite, dass keiner ihn übertrifft. Und die Spezifikation klärt nebenbei eine Frage, die die Aufgabenstellung offenließ: Wenn zwei Werte beim Maximum gleichauf liegen, wie in (4,9,9)(4, 9, 9), ist die Ausgabe 99, und darin liegt kein Fehler.

Ein und dieselbe Aufgabenstellung lässt verschiedene Spezifikationen zu, und zwischen ihnen zu wählen ist eine Entscheidung, kein Versehen. Der lehrreichste Fall ist die ganzzahlige Division. Für ganze Zahlen a≥0a \geq 0 und b>0b > 0 sind sich alle einig: Der Quotient qq und der Rest rr sind die einzigen ganzen Zahlen mit

a=b⋅q+r,0≤r<ba = b \cdot q + r, \quad 0 \leq r < b

So sind es für a=17a = 17 und b=5b = 5 die Werte q=3q = 3 und r=2r = 2, denn 17=5⋅3+217 = 5 \cdot 3 + 2. Aber die Aufgabenstellung „zwei ganze Zahlen dividieren“ sagt nicht, was zu tun ist, wenn aa negativ ist, und die Frage ist nicht müßig: Für a=−7a = -7 und b=2b = 2 gibt es zwei vernünftige Antworten.

BeispielZwei Spezifikationen der Division mit negativem Dividenden

Erste Spezifikation: Der Rest ist nie negativ, 0≤r<b0 \leq r < b. Dann gilt −7=2⋅(−4)+1-7 = 2 \cdot (-4) + 1, und die Ausgabe ist q=−4q = -4, r=1r = 1. Diese wird in der Mathematik behandelt, in der Lektion „Teilbarkeit und Division mit Rest“ des Kurses Algebra und Geometrie I.

Zweite Spezifikation: Der Quotient ist das Ergebnis der Division mit Dezimalstellen, deren Nachkommateil verworfen wird, also zur Null hin gerundet; der Rest ist das, was fehlt, und hat das Vorzeichen des Dividenden. Dann wird −7/2=−3,5-7 / 2 = -3{,}5 auf −3-3 abgeschnitten, und es gilt −7=2⋅(−3)+(−1)-7 = 2 \cdot (-3) + (-1): Die Ausgabe ist q=−3q = -3, r=−1r = -1.

Beide erfüllen a=b⋅q+ra = b \cdot q + r; sie unterscheiden sich in der Bedingung, die dem Rest auferlegt wird. Keine ist ein Fehler, aber ein Programm, das die eine benutzt und glaubt, die andere zu benutzen, liefert bei negativen Zahlen falsche Ergebnisse. Vorweggenommen sei eine Tatsache: C++ übernimmt die zweite, wie die Lektion Arithmetik zeigen wird; wer die erste erwartet, wird eine Überraschung erleben.

Es gibt eine dritte Möglichkeit, die in der Praxis häufigste: den Fall durch die Vorbedingung auszuschließen. Verlangt die Spezifikation a≥0a \geq 0, so verschwindet das Problem der negativen Zahlen, nicht weil es gelöst wäre, sondern weil, wer die Lösung benutzt, sich verpflichtet hat, es nicht zu stellen. Was man nicht tun darf, ist, die Frage unentschieden zu lassen. Und aus demselben Grund verlangt die Spezifikation b>0b > 0: Mit b=0b = 0 gibt es keine qq und rr, die die Beziehung erfüllen, und der Vertrag muss das sagen.

Ein häufiger Fehler besteht darin, mit einem Beispiel zu spezifizieren: „die Ausgabe zu 17 und 5 ist 3 und 2“. Ein Beispiel veranschaulicht, aber es spezifiziert nicht, weil es nicht sagt, was mit den anderen Eingaben geschieht; unendlich viele verschiedene Beziehungen stimmen in einem Fall überein. Die Spezifikation wird als allgemeine Bedingung geschrieben, und die Beispiele dienen danach dazu, sie zu überprüfen.

Algorithmen

Ist das Problem spezifiziert, sucht man ein Verfahren, um es zu lösen. Ein Algorithmus ist eine Folge von Anweisungen, die, angewandt auf jede Eingabe, die die Vorbedingung erfüllt, in endlich vielen Schritten eine Ausgabe erzeugt, die die Nachbedingung erfüllt. Die Definition enthält drei Forderungen.

  • Endlichkeit. Der Algorithmus wird mit endlich vielen Anweisungen geschrieben und terminiert für jede zulässige Eingabe, nachdem er endlich viele davon ausgeführt hat. Ein Verfahren, das für irgendeine Eingabe endlos weiterläuft, ist für diese Eingabe kein Algorithmus.
  • Eindeutigkeit. Jede Anweisung hat eine einzige Bedeutung, und wer sie ausführt, weiß jederzeit, was zu tun ist und welche Anweisung als Nächstes kommt. Keine Entscheidung bleibt dem Ermessen dessen überlassen, der ausführt.
  • Allgemeinheit. Der Algorithmus löst das Problem für alle zulässigen Eingaben, nicht nur für einen Fall.

Mit einer gewissen Freiheit nennt man auch ein Verfahren einen Algorithmus, das eindeutig ist und terminiert, aber für irgendeine zulässige Eingabe die Nachbedingung nicht erfüllt: Streng genommen ist es ein Algorithmus für ein anderes Problem, und man wird sagen, dass es für das spezifizierte Problem inkorrekt ist. So sprechen die Aufgaben dieser Lektion, die inkorrekte Algorithmen mit einem Gegenbeispiel widerlegen.

Ein Kochrezept ähnelt einem Algorithmus, und deshalb ist es der übliche Vergleich; aber fast keines ist einer. „Eine Prise Salz hinzufügen“ ist nicht eindeutig, weil eine Prise keine Menge ist; „backen, bis es goldbraun ist“ hängt von einem Urteil ab, das zwei Menschen verschieden fällen würden; „mehrmals schlagen“ sagt nicht, wie oft. Ein Koch füllt diese Lücken mit Erfahrung; ein Computer hat keine. Daraus folgt die Forderung, die den ganzen Kurs bestimmt: Einem Algorithmus muss auch folgen können, wer das Problem nicht versteht, ohne etwas zu erraten. Wenn man wissen muss, wozu ein Schritt dient, um ihn auszuführen, ist der Schritt schlecht geschrieben.

Die Algorithmen dieses Kurses werden zuerst in nummerierten Schritten geschrieben, auf Deutsch, mit einigen Konventionen. Die Daten, die sich während des Algorithmus ändern, werden mit Buchstaben oder Wörtern benannt, etwa qq oder rr, und heißen Variablen. Man schreibt „rr erhält den Wert r−br - b“, um zu sagen, dass r−br - b mit dem aktuellen Wert von rr berechnet wird und das Ergebnis zum neuen Wert von rr wird; der vorige Wert geht verloren. In der Kurzform, die man beim Notieren verwendet, schreibt man dieselbe Anweisung mit einem Pfeil nach links, der auf der Tastatur r <- r - b lautet. Die Anweisungen werden in der Reihenfolge ihrer Nummern ausgeführt, sofern nicht eine von ihnen etwas anderes sagt.

BeispielDie Division durch fortgesetzte Subtraktion

Das Problem ist das oben spezifizierte: Eingabe, zwei ganze Zahlen a≥0a \geq 0 und b>0b > 0; Ausgabe, zwei ganze Zahlen qq und rr mit a=b⋅q+ra = b \cdot q + r und 0≤r<b0 \leq r < b. Die Idee ist die des Verteilens: Von aa nimmt man Gruppen zu bb weg, solange es geht, zählt die weggenommenen Gruppen, und was übrig bleibt, ist der Rest.

  1. qq erhält den Wert 00, und rr erhält den Wert aa.
  2. Solange r≥br \geq b gilt, wiederhole die Schritte 3 und 4; sobald es nicht mehr gilt, fahre mit Schritt 5 fort.
  3. rr erhält den Wert r−br - b.
  4. qq erhält den Wert q+1q + 1; kehre zu Schritt 2 zurück.
  5. Liefere qq und rr und beende den Algorithmus.

Schritt 2 ist eine Frage, die jedes Mal gestellt wird: vor dem ersten Durchlauf und nach jedem weiteren. Die Schritte 3 und 4 bilden den Rumpf der Wiederholung, das, was wiederholt wird. Nichts bleibt dem Ermessen dessen überlassen, der ausführt: In jedem Moment gibt es eine einzige nächste Anweisung.

Der Algorithmus ist langsam (um eine Million durch drei zu teilen, braucht er mehr als dreihunderttausend Durchläufe), und das Schulverfahren der Division ist viel schneller; aber er ist korrekt, und seine Einfachheit macht ihn ideal, um das Nachdenken über Algorithmen zu lernen. Auf den Unterschied zwischen einem korrekten und einem effizienten Algorithmus kommt der Kurs in der Lektion Die Kosten eines Algorithmus zurück.

Schließlich empfiehlt es sich, den Algorithmus von seiner Niederschrift zu trennen. Der Algorithmus der Division durch Subtraktion ist derselbe, ob man ihn auf Deutsch, in einem Diagramm, in C++ oder in irgendeiner anderen Programmiersprache aufschreibt: Was ihn bestimmt, ist die Folge der Operationen, nicht die Wörter. Ein Programm ist die Niederschrift eines Algorithmus in einer Sprache, die eine Maschine ausführen kann, und die Lektion Programme, Compiler und C++ wird erklären, wie diese Ausführung geschieht. Wenn das so ist, kann ein Programm auf zwei sehr verschiedene Arten falsch sein: weil der Algorithmus inkorrekt ist, und dann rettet ihn keine Sprache, oder weil der Algorithmus korrekt ist, aber schlecht aufgeschrieben wurde. Beide Fälle zu unterscheiden ist die halbe Arbeit des Debuggens, und deshalb schreibt der Kurs immer zuerst den Algorithmus.

Einen Algorithmus von Hand nachvollziehen

Einen Algorithmus zu lesen genügt nicht, um zu wissen, was er tut; man muss ihn ausführen. Ihn von Hand nachzuvollziehen, wie es die Maschine täte, ohne Schritte zu überspringen oder Ergebnisse zu erraten, ist die nützlichste Fähigkeit eines Programmierers: Mit ihr prüft man einen Algorithmus, bevor man ihn schreibt, sagt die Ausgabe eines Programms voraus, bevor man es ausführt, und findet den Fehler in einem, das versagt.

Das Werkzeug ist die Ablauftabelle (Trace-Tabelle): eine Spalte für die Nummer des ausgeführten Schritts, eine Spalte je Variable und, falls nützlich, eine für die ausgewerteten Bedingungen; und eine Zeile für jeden ausgeführten Schritt. In jede Zeile trägt man den neuen Wert der Variable ein, die sich in diesem Schritt geändert hat, und lässt leer, was sich nicht geändert hat, sodass der aktuelle Wert einer Variable der letzte in ihrer Spalte eingetragene ist. Bedingungen werden mit ihrem Ergebnis eingetragen.

BeispielDie Ablaufverfolgung der Division von 17 durch 5

Wir verfolgen die Division durch fortgesetzte Subtraktion mit a=17a = 17 und b=5b = 5. Der Pfeil der Figur zeigt auf den Schritt, der gerade ausgeführt wurde; die gepunktete Linie erinnert daran, dass Schritt 4 zu Schritt 2 zurückkehrt.

Beweis

  1. (q,r)=(0,17)(q, r) = (0, 17)

    Schritt 1: Die Anfangswerte werden gesetzt. Die Eingabe erfüllt die Vorbedingung, denn es gilt 17≥017 \geq 0 und 5>05 > 0.

  2. (q,r)=(1,12)(q, r) = (1, 12)

    Erster Durchlauf. In Schritt 2 wird geprüft, dass 17≥517 \geq 5 gilt, also wird der Rumpf ausgeführt: Schritt 3 nimmt eine Gruppe von fünf weg, und Schritt 4 zählt sie.

  3. (q,r)=(2,7)(q, r) = (2, 7)

    Zweiter Durchlauf: 12≥512 \geq 5, und der Rumpf wird erneut ausgeführt.

  4. (q,r)=(3,2)(q, r) = (3, 2)

    Dritter Durchlauf: 7≥57 \geq 5, und der Rumpf wird erneut ausgeführt.

  5. 17=5⋅3+2,0≤2<517 = 5 \cdot 3 + 2, \quad 0 \leq 2 < 5

    In Schritt 2 schlägt die Bedingung fehl, weil 2<52 < 5 gilt: Der Algorithmus fährt mit Schritt 5 fort und liefert q=3q = 3 und r=2r = 2. Die Ausgabe erfüllt die Nachbedingung.

Die vollständige Tabelle hat zwölf Zeilen: Schritt 1, drei Durchläufe zu je drei Zeilen (Schritte 2, 3 und 4), die fehlschlagende Prüfung und die Rückgabe.

Zwei Beobachtungen zur Tabelle. Erstens: Die Zahl der Durchläufe ist gleich dem Quotienten, 33, und Schritt 2 wurde einmal öfter ausgeführt als der Rumpf, nämlich 44-mal; das geschieht bei einer Wiederholung dieser Form immer, weil die letzte Prüfung diejenige ist, die fehlschlägt. Zweitens: Die Ablaufverfolgung zeigt etwas, das der Text des Algorithmus auf den ersten Blick nicht sagt; gilt a<ba < b, zum Beispiel a=3a = 3 und b=5b = 5, so schlägt die Bedingung gleich beim ersten Mal fehl, der Rumpf wird kein einziges Mal ausgeführt, und die Ausgabe ist q=0q = 0, r=3r = 3, was korrekt ist, denn 3=5⋅0+33 = 5 \cdot 0 + 3. Den Fall zu vergessen, in dem die Wiederholung nicht ausgeführt wird, ist einer der häufigsten Fehler beim Entwurf von Algorithmen, und die Ablaufverfolgung mit kleinen Eingaben deckt ihn auf.

Die Ablaufverfolgung eines Falls beweist nicht, dass der Algorithmus korrekt ist: Sie prüft einen Fall. Aber eine gut gewählte Ablaufverfolgung (ein gewöhnlicher Fall, ein Grenzfall, der Fall, in dem die Wiederholung nicht ausgeführt wird) deckt die meisten Fehler auf, und die, die fehlschlägt, zeigt, wo sie liegen. Die Lektion Korrekte Schleifen entwerfen wird lehren, von den Fällen zu einem Beweis überzugehen.

Sequenz, Entscheidung und Wiederholung

Aus der Nähe betrachtet, sind Algorithmen aus sehr wenigen Bausteinen gebaut. Es gibt elementare Anweisungen (einer Variable einen Wert geben, ein Ergebnis liefern) und drei Arten, sie zu kombinieren.

  • Sequenz: eine Anweisung nach der anderen, der Reihe nach. Die Schritte 3 und 4 der Division sind eine Sequenz: Zuerst wird subtrahiert, dann gezählt.
  • Entscheidung: Je nachdem, ob eine Bedingung erfüllt ist oder nicht, wird der eine oder der andere Teil ausgeführt (oder ein Teil oder nichts). „Wenn y>my > m, erhält mm den Wert yy“ ist eine Entscheidung.
  • Wiederholung: Solange eine Bedingung erfüllt ist, wird ein Teil immer wieder ausgeführt. Schritt 2 der Division ist mit seinem Rumpf eine Wiederholung.

Jede Form hat einen einzigen Eingang und einen einzigen Ausgang, und ihr Inneres kann seinerseits jede der drei sein: Der Rumpf einer Wiederholung kann Entscheidungen enthalten, ein Zweig einer Entscheidung kann eine Wiederholung enthalten, und so fort. Wenn das so ist, baut man mit den drei Formen Algorithmen jeder Komplexität, so wie man mit wenigen Bausteinen, die ineinandergreifen, jedes Gebäude errichtet. Dass sie genügen, um jeden Algorithmus auszudrücken, ist ein bekanntes Ergebnis der Theorie der Programmierung (Corrado Böhm und Giuseppe Jacopini haben es 1966 bewiesen); hier wird es nicht bewiesen, aber der ganze Kurs bestätigt es: Die Programme, die in ihm geschrieben werden, wie lang sie auch sein mögen, verwenden nichts anderes als diese drei Formen, in C++ als if für die Entscheidung und while oder for für die Wiederholung geschrieben, in den Lektionen Entscheidungen und Schleifen.

Eine nützliche Notation, um die Gestalt eines Algorithmus zu sehen, ist das Flussdiagramm (der Programmablaufplan): Jede Anweisung ist ein Rechteck, jede Bedingung eine Raute mit zwei Ausgängen (erfüllt, von der Figur mit 11 beschriftet, und nicht erfüllt, 00), und die Pfeile sagen, welcher Schritt als Nächstes kommt. Es ist eine Notation, kein Selbstzweck: Sie dient dazu, einen kleinen Algorithmus auf einen Blick zu erfassen, aber sie skaliert nicht, und niemand schreibt ein großes Programm, indem er es zeichnet.

BeispielDrei Formen und ihre Zusammensetzung

Die Figur zeichnet die drei Formen mit allgemeinen Anweisungen AA und BB und einer Bedingung cc und setzt sie danach im Diagramm der Division durch fortgesetzte Subtraktion zusammen.

Beweis

  1. A;BA ; B

    Sequenz: Es wird AA ausgeführt und danach BB; es gibt einen einzigen Eingang und einen einzigen Ausgang.

  2. c=1⇒A,c=0⇒Bc = 1 \Rightarrow A, \quad c = 0 \Rightarrow B

    Entscheidung: Ist cc erfüllt, wird AA ausgeführt; wenn nicht, BB. Beide Zweige laufen in einem einzigen Ausgang zusammen.

  3. c,A,c,A,…,cc, A, c, A, \ldots, c

    Wiederholung: Es wird cc ausgewertet; ist sie erfüllt, wird AA ausgeführt und erneut cc ausgewertet; wenn nicht, wird die Wiederholung verlassen. Die Bedingung wird einmal öfter ausgewertet als der Rumpf.

  4. a=b⋅q+r,0≤r<ba = b \cdot q + r, \quad 0 \leq r < b

    Zusammensetzung: Die Division durch fortgesetzte Subtraktion ist eine Sequenz (die Anfangswerte, die Wiederholung, die Rückgabe), deren Wiederholung als Rumpf eine weitere Sequenz hat (subtrahieren, zählen).

Die drei Formen in einem umgangssprachlich geschriebenen Verfahren zu erkennen ist der erste Schritt, um es zu übersetzen. Das Zeichen einer Entscheidung ist ein „wenn“, ein „falls“ oder ein „je nachdem“; das einer Wiederholung ein „solange“, ein „bis“, ein „für jedes“ oder ein „so oft wie“. Ein Verfahren ohne eines dieser Wörter ist eine Sequenz, und wenn es Eingaben variabler Größe verarbeiten muss (eine Liste von Zahlen, einen Text), fehlt ihm eine Wiederholung.

Terminiert der Algorithmus?

Die Endlichkeit gehört zur Definition des Algorithmus, und eine Wiederholung ist der einzige Ort, an dem sie verloren gehen kann: Eine Sequenz von fünf Schritten führt fünf Schritte aus, und eine Entscheidung führt einen ihrer Zweige aus; aber eine Wiederholung führt ihren Rumpf so oft aus, wie die Bedingung erfüllt ist, und nichts garantiert im Voraus, dass sie irgendwann nicht mehr erfüllt ist.

Um zu begründen, dass eine Wiederholung terminiert, sucht man eine aus den Variablen berechnete Größe, die zwei Bedingungen erfüllt: Sie ist eine natürliche Zahl (ganz und nicht negativ), solange die Wiederholung weiterläuft, und sie fällt streng in jedem Durchlauf. Wenn das so ist, terminiert die Wiederholung: Eine streng fallende Folge natürlicher Zahlen kann nicht unendlich sein, weil jeder Durchlauf sie um mindestens eins senkt und es von einer natürlichen Zahl nn aus nicht mehr als nn Stufen bis zur Null gibt. Diese Größe heißt Variante oder Schrankenfunktion, und die Lektion Korrekte Schleifen entwerfen wird sie systematisch verwenden.

BeispielDie Variante der Division durch Subtraktion

Bei der Division durch fortgesetzte Subtraktion ist die Variante rr. Solange die Wiederholung weiterläuft, gilt r≥b>0r \geq b > 0, also ist rr eine natürliche Zahl. In jedem Durchlauf erhält rr den Wert r−br - b, und da b>0b > 0 gilt (Vorbedingung), ist der neue Wert streng kleiner. Folglich terminiert die Wiederholung, und zwar nach höchstens aa Durchläufen (genau genommen nach genau qq Durchläufen, dem Quotienten).

Man beachte, wo die Vorbedingung b>0b > 0 verwendet wurde. Führt man den Algorithmus mit b=0b = 0 aus, so lässt Schritt 3 den Wert rr unverändert, die Bedingung r≥0r \geq 0 ist immer erfüllt, und die Wiederholung terminiert nie: Der Algorithmus ist nicht falsch, sondern hat eine Eingabe erhalten, die der Vertrag ausschloss. Und wäre bb negativ, so würde rr in jedem Durchlauf wachsen. Die Vorbedingung ist kein Schmuck: Sie ist genau das, was nötig ist, damit die Überlegung funktioniert.

Beide Bedingungen der Variante sind notwendig, und man kann sich leicht bei jeder von ihnen irren. Man betrachte dieses Verfahren, das entscheiden soll, ob eine natürliche Zahl nn gerade ist: „solange n≠0n \neq 0, erhält nn den Wert n−2n - 2; wird die Null erreicht, war nn gerade“. Die Größe nn fällt in jedem Durchlauf, und dennoch terminiert das Verfahren nicht, wenn nn ungerade ist: Mit n=5n = 5 durchläuft es 33, 11, −1-1, −3-3, und die Bedingung n≠0n \neq 0 schlägt nie fehl. Was nicht erfüllt ist, ist die andere Bedingung: nn hört auf, eine natürliche Zahl zu sein, weil die Bedingung der Wiederholung nicht verhindert, dass es unter null sinkt. Die Korrektur besteht darin, zu wiederholen, solange n≥2n \geq 2 gilt, und am Ende nach dem verbleibenden Wert zu entscheiden, 00 oder 11; jetzt ist nn in jedem Durchlauf eine natürliche Zahl, weil nur subtrahiert wird, wenn n≥2n \geq 2 gilt, und es fällt: Das Verfahren terminiert.

Zu entscheiden, ob eine Wiederholung terminiert, ist nicht immer leicht. Ein berühmtes Beispiel: Ausgehend von einer natürlichen Zahl n≥1n \geq 1 wird, solange n≠1n \neq 1 gilt, nn halbiert, wenn es gerade ist, und durch 3n+13n + 1 ersetzt, wenn es ungerade ist. Mit n=6n = 6 lautet die Folge 6,3,10,5,16,8,4,2,16, 3, 10, 5, 16, 8, 4, 2, 1, und sie endet. Niemand hat ein nn gefunden, für das sie nicht endet, und man hat es mit Computern für ungeheuer viele Werte überprüft; aber niemand hat beweisen können, dass sie immer endet: Das ist die Collatz-Vermutung, ein offenes Problem der Mathematik. Keine bekannte Größe fällt in jedem Durchlauf. Die Moral für alle, die programmieren, ist bescheiden und fest: Ein Algorithmus, dessen Terminierung man nicht zu begründen weiß, ist nicht fertig.

Häufige Fehler

  • Mehrdeutige Schritte. „Mehrmals wiederholen“, „bis es passt“, „eine geeignete Zahl nehmen“. Jeder Schritt muss ausführbar sein, ohne das Problem zu verstehen; sonst ist er schlecht geschrieben.
  • Den Fall vergessen, in dem die Wiederholung nicht ausgeführt wird. Jede Wiederholung mit der Bedingung am Anfang kann ihren Rumpf null Mal ausführen. Man muss sich immer fragen, was der Algorithmus in diesem Fall liefert, und die Ablaufverfolgung mit einer minimalen Eingabe beantwortet das.
  • Mit einem Beispiel spezifizieren. „Für 17 und 5 ergibt sich 3 und 2“ ist keine Spezifikation; die Beziehung wird als allgemeine Bedingung geschrieben, und das Beispiel veranschaulicht sie.
  • Den Algorithmus mit seinem Programm verwechseln. Bei einem Programm, das versagt, kann der Algorithmus falsch sein oder seine Niederschrift. Wer den Algorithmus nicht vorher geschrieben hat, weiß nicht, welches von beiden er suchen soll.
  • Die Terminierung als selbstverständlich voraussetzen. Dass eine Größe fällt, genügt nicht: Sie muss außerdem eine natürliche Zahl sein, solange wiederholt wird.

Gelöste Aufgaben

Gelöste Aufgabe 1Die größte von drei Zahlen, mit Gleichstand

Einen Algorithmus schreiben, der die Spezifikation des Maximums von drei Zahlen vom Anfang dieser Lektion erfüllt, und ihn für die Eingaben (7,2,5)(7, 2, 5) und (4,9,9)(4, 9, 9) von Hand nachvollziehen.

Lösung

  1. m=xm = x

    Idee: Man geht die Eingabe durch und hält den bisher größten Wert in einer Variable mm fest. Erster Schritt: mm erhält den Wert xx. Danach ist mm das Maximum der gesehenen Werte, und das ist nur xx.

  2. m=max⁡(x,y,z)m = \max(x, y, z)

    Zweiter Schritt: Wenn y>my > m, erhält mm den Wert yy. Dritter Schritt: Wenn z>mz > m, erhält mm den Wert zz. Vierter Schritt: Liefere mm. Das sind drei Anweisungen in Sequenz, zwei davon Entscheidungen ohne alternativen Zweig.

  3. (x,y,z)=(7,2,5)⇒m=7(x, y, z) = (7, 2, 5) \Rightarrow m = 7

    Ablaufverfolgung für (7,2,5)(7, 2, 5): Nach dem ersten Schritt gilt m=7m = 7; im zweiten ist 2>72 > 7 falsch, und mm ändert sich nicht; im dritten ist 5>75 > 7 falsch, und mm ändert sich nicht. Geliefert wird 77.

  4. (x,y,z)=(4,9,9)⇒m=9(x, y, z) = (4, 9, 9) \Rightarrow m = 9

    Ablaufverfolgung für (4,9,9)(4, 9, 9): Nach dem ersten Schritt gilt m=4m = 4; im zweiten gilt 9>49 > 4, und mm erhält den Wert 99; im dritten ist 9>99 > 9 falsch, und mm ändert sich nicht. Geliefert wird 99, was korrekt ist: Der Gleichstand verlangt keine eigene Behandlung.

  5. m≥x,m≥y,m≥zm \geq x, \quad m \geq y, \quad m \geq z

    Begründung: Nach jedem Schritt ist mm einer der gesehenen Werte, und keiner von ihnen übertrifft ihn. Das stimmt nach dem ersten Schritt, und jede Entscheidung erhält es: Ist der neue Wert größer, wird er zu mm; wenn nicht, übertrifft er mm nicht. Am Ende sind alle drei gesehen, und mm erfüllt die Nachbedingung.

  6. (x,y,z)=(4,9,9)⇒p∈{2,3}(x, y, z) = (4, 9, 9) \Rightarrow p \in \{2, 3\}

    Hätte man y≥my \geq m statt y>my > m geschrieben, wäre das Ergebnis dasselbe, weil bei Gleichstand ein gleicher Wert zugewiesen wird. Die Wahl zwischen >> und ≥\geq wäre wichtig, wenn zusätzlich die Position des Maximums verlangt würde: Mit >> wird die erste der gleichauf liegenden Positionen geliefert, mit ≥\geq die letzte; die Formel nennt die Position pp.

Gelöste Aufgabe 2Das Schaltjahr, mit einem Gegenbeispiel korrigiert

Im gregorianischen Kalender ist ein Jahr ein Schaltjahr, wenn es durch 44 teilbar ist, ausgenommen die durch 100100 teilbaren, die es nur sind, wenn sie außerdem durch 400400 teilbar sind. Jemand aus dem Kurs schlägt den Algorithmus vor: „Wenn das Jahr durch 44 teilbar ist, liefere ja; sonst liefere nein.“ Ein Jahr finden, in dem er versagt, und einen korrekten Algorithmus mit seiner Spezifikation schreiben.

Lösung

  1. (4∣n∧¬(100∣n))∨400∣n(4 \mid n \wedge \neg (100 \mid n)) \vee 400 \mid n

    Spezifikation. Eingabe: eine ganze Zahl n≥1583n \geq 1583 (der gregorianische Kalender gilt seit Oktober 1582). Ausgabe: ja oder nein. Beziehung: Die Ausgabe ist genau dann ja, wenn nn durch 44 und nicht durch 100100 teilbar ist oder durch 400400 teilbar ist.

  2. 1900=4⋅475,1900=400⋅4+3001900 = 4 \cdot 475, \quad 1900 = 400 \cdot 4 + 300

    Der vorgeschlagene Algorithmus trifft in den meisten Jahren das Richtige und wirkt deshalb korrekt. Um ihn zu widerlegen, genügt ein Jahr, das durch 100100 und nicht durch 400400 teilbar ist: 19001900 ist durch 44 teilbar, und der Algorithmus sagt ja; aber 19001900 war kein Schaltjahr.

  3. 400∣n,100∣n,4∣n400 \mid n, \quad 100 \mid n, \quad 4 \mid n

    Korrigierter Algorithmus, in drei verketteten Entscheidungen. Schritt 1: Wenn nn durch 400400 teilbar ist, liefere ja und beende. Schritt 2: Wenn nn durch 100100 teilbar ist, liefere nein und beende. Schritt 3: Wenn nn durch 44 teilbar ist, liefere ja; sonst liefere nein.

  4. 2000,1900,2024,20232000, \quad 1900, \quad 2024, \quad 2023

    Die Reihenfolge der Fragen ist wesentlich: Sie gehen von der besondersten Regel zur allgemeinsten. Fragte man zuerst nach der 44, bekäme das Jahr 19001900 ein ja, bevor die Ausnahme erreicht ist. Ablaufverfolgung für vier Jahre, eines je Fall: 20002000 (Schritt 1: ja), 19001900 (Schritt 2: nein), 20242024 (Schritt 3: ja), 20232023 (Schritt 3: nein).

  5. 4∣1900∧¬(400∣1900)4 \mid 1900 \wedge \neg (400 \mid 1900)

    Die Lehre betrifft nicht nur Kalender: Ein Algorithmus wird mit einem einzigen Gegenbeispiel widerlegt, und Gegenbeispiele sucht man an den Grenzen der Regeln, nicht in den gewöhnlichen Fällen. Mit 20242024, 20202020 und 20162016 zu testen hätte nichts aufgedeckt.

Gelöste Aufgabe 3Wie viele Durchläufe die Division durch Subtraktion macht

Die Division durch fortgesetzte Subtraktion mit a=20a = 20 und b=5b = 5 sowie mit a=0a = 0 und b=7b = 7 von Hand nachvollziehen. Danach begründen, dass der Algorithmus für jede zulässige Eingabe nach genau qq Durchläufen terminiert und dass seine Ausgabe die Nachbedingung erfüllt.

Lösung

  1. 20=5⋅4+020 = 5 \cdot 4 + 0

    Ablaufverfolgung mit a=20a = 20, b=5b = 5: rr durchläuft 2020, 1515, 1010, 55, 00, und qq durchläuft 00, 11, 22, 33, 44. Die fünfte Prüfung, 0≥50 \geq 5, schlägt fehl. Ausgabe: q=4q = 4, r=0r = 0. Hier ist der Grenzfall die aufgehende Division: Der vierte Durchlauf findet statt, weil 5≥55 \geq 5 gilt, und deshalb muss die Bedingung ≥\geq lauten und nicht >>.

  2. 0=7⋅0+00 = 7 \cdot 0 + 0

    Ablaufverfolgung mit a=0a = 0, b=7b = 7: Die erste Prüfung, 0≥70 \geq 7, schlägt fehl; der Rumpf wird kein einziges Mal ausgeführt. Ausgabe: q=0q = 0, r=0r = 0, was korrekt ist.

  3. b⋅(q+1)+(r−b)=b⋅q+r=a\dato{conserva}{b \cdot (q + 1) + (r - b) = b \cdot q + r = a}

    Eine Beziehung, die erhalten bleibt. Vor dem ersten Durchlauf gilt b⋅q+r=b⋅0+a=ab \cdot q + r = b \cdot 0 + a = a. In jedem Durchlauf sinkt rr um bb und steigt qq um 11, sodass sich b⋅q+rb \cdot q + r nicht ändert. Folglich gilt die Gleichung nach jedem Durchlauf.

  4. r0>r1>r2>⋯≥0r_0 > r_1 > r_2 > \cdots \geq 0

    Terminierung. Solange wiederholt wird, gilt r≥b>0r \geq b > 0, und rr ist eine natürliche Zahl; in jedem Durchlauf sinkt es um b≥1b \geq 1. Eine streng fallende Folge natürlicher Zahlen ist endlich: Die Wiederholung terminiert.

  5. a=b⋅q+r,0≤r<ba = b \cdot q + r, \quad 0 \leq r < b

    Beim Terminieren ist die Bedingung fehlgeschlagen: r<br < b. Außerdem gilt r≥0r \geq 0, weil nur dann bb subtrahiert wird, wenn r≥br \geq b gilt. Zusammen mit der Beziehung, die erhalten bleibt, erfüllt die Ausgabe die Nachbedingung; und da qq die Durchläufe zählt, ist die Zahl der Durchläufe der Quotient.

Online-Übungen

Übung 1Die Durchläufe zählen

Den folgenden Algorithmus von Hand nachvollziehen und den Wert, den er liefert, in das Feld schreiben, nur die Zahl.

  1. nn erhält den Wert 2929, und kk erhält den Wert 00.
  2. Solange n>1n > 1 gilt, wiederhole die Schritte 3 und 4; sobald es nicht mehr gilt, fahre mit Schritt 5 fort.
  3. Wenn nn gerade ist, erhält nn den Wert n/2n / 2; wenn es ungerade ist, erhält nn den Wert n+1n + 1.
  4. kk erhält den Wert k+1k + 1; kehre zu Schritt 2 zurück.
  5. Liefere kk.

Der Browser prüft deine Antwort sofort, ohne sie zu bewerten.

Hinweis

Lege die Tabelle an: nn geht im ersten Durchlauf von 2929 auf 3030. Zähle einen Durchlauf für jede Änderung von nn, bis nn den Wert 11 hat.

Übung 2Die fehlende Bedingung

Der folgende Algorithmus, in der Kurzform geschrieben, soll Quotient und Rest der Division durch fortgesetzte Subtraktion berechnen, aber ihm fehlt die Bedingung der Wiederholung. Die Unterstriche durch die richtige Bedingung ersetzen, ohne den Rest zu ändern.

Der Browser prüft deine Antwort sofort, ohne sie zu bewerten.

Hinweis

Vollziehe den Algorithmus mit a=10a = 10 und b=5b = 5 von Hand nach: Der zweite Durchlauf muss stattfinden, obwohl der Rest genau 55 beträgt.

Übungsaufgaben

Jede Aufgabe hat einen Hinweis, den man erst nach einem eigenen Versuch öffnen sollte, und eine Lösung, die nach dem Öffnen des Hinweises erscheint.

Übungsaufgabe 1Die Ordnung von drei Zahlen spezifizieren

Die Spezifikation des Problems „drei Zahlen sortieren“ schreiben (Eingabe, Ausgabe und Beziehung) und angeben, welche Entscheidungen sie erzwingt, die die Aufgabenstellung nicht traf. Danach prüfen, dass die Spezifikation für die Eingabe (5,2,5)(5, 2, 5) diese beiden Ausgaben ausschließt, (2,5,6)(2, 5, 6) und (2,5,5,5)(2, 5, 5, 5), und (2,5,5)(2, 5, 5) zulässt.

Hinweis

Eine „sortierte“ Ausgabe kann mit der Eingabe nichts zu tun haben. Welche Bedingung verbindet die Zahlen der Ausgabe mit denen der Eingabe, die wiederholten eingeschlossen? Aufsteigend oder absteigend? Streng oder nicht?

Lösung

Eingabe: drei Zahlen xx, yy, zz. Ausgabe: drei Zahlen uu, vv, ww. Beziehung: u≤v≤wu \leq v \leq w, und (u,v,w)(u, v, w) ist eine Umordnung von (x,y,z)(x, y, z), das heißt, es enthält dieselben Werte mit denselben Wiederholungen. Entscheidungen, die die Aufgabenstellung nicht traf: die Richtung (hier vom kleinsten zum größten); dass die Ordnung nicht streng ist (≤\leq und nicht <<, weil bei wiederholten Werten eine strenge Ordnung unmöglich ist); und dass die Ausgabe eine Umordnung sein muss. (2,5,6)(2, 5, 6) ist sortiert, enthält aber eine 66, die nicht vorkam; (2,5,5,5)(2, 5, 5, 5) hat vier Werte; (2,5,5)(2, 5, 5) erfüllt beide Bedingungen. Ohne die Bedingung der Umordnung wäre (0,0,0)(0, 0, 0) für jede Eingabe eine korrekte Ausgabe.

Übungsaufgabe 2Die Ziffern einer Zahl, von rechts nach links

Einen Algorithmus in nummerierten Schritten schreiben, der eine natürliche Zahl nn erhält und ihre Ziffern von der letzten bis zur ersten liefert (für 40724072 liefert er 22, 77, 00, 44), wobei nur Quotient und Rest der Division durch 1010 verwendet werden. Ihn für 40724072 und für 00 von Hand nachvollziehen und begründen, dass er für jedes nn terminiert.

Hinweis

Der Rest der Division durch 1010 ist die letzte Ziffer, und der Quotient ist die Zahl ohne sie. Entscheide, ob die Bedingung vor oder nach dem Liefern einer Ziffer gefragt wird, und sieh nach, was bei n=0n = 0 geschieht, einer Zahl mit einer Ziffer.

Lösung

Ein korrekter Algorithmus: (1) liefere den Rest der Division von nn durch 1010; (2) nn erhält den Wert des Quotienten der Division von nn durch 1010; (3) wenn n>0n > 0, kehre zu Schritt 1 zurück; sonst beende. Die Frage steht nach dem Rumpf, sodass der Rumpf mindestens einmal ausgeführt wird: Für n=0n = 0 liefert er 00, seine einzige Ziffer. Fragte man am Anfang („solange n>0n > 0“), würde für n=0n = 0 nichts geliefert; das ist der Fehler, den Fall zu vergessen, in dem die Wiederholung nicht ausgeführt wird. Ablaufverfolgung für 40724072: Geliefert werden 22, 77, 00, 44, und nn durchläuft 407407, 4040, 44, 00. Terminierung: Die Variante ist nn, eine natürliche Zahl; wenn zu Schritt 1 zurückgekehrt wird, dann weil n>0n > 0 gilt, und für n≥1n \geq 1 ist der Quotient durch 1010 streng kleiner als nn, sodass nn in jedem Durchlauf streng fällt.

Übungsaufgabe 3Wechselgeld in Münzen

Es gibt Münzen zu 5050, 1010 und 11 Einheiten, in unbegrenzter Anzahl. Spezifikation: Eingabe, eine natürliche Zahl mm; Ausgabe, drei natürliche Zahlen c50c_{50}, c10c_{10}, c1c_1 mit 50c50+10c10+c1=m50 c_{50} + 10 c_{10} + c_1 = m und der kleinstmöglichen Anzahl von Münzen, c50+c10+c1c_{50} + c_{10} + c_1. Jemand schlägt den folgenden Algorithmus vor. Eine Eingabe finden, bei der er versagt, jeden Mangel erklären und ihn beheben.

  1. c50c_{50}, c10c_{10} und c1c_1 erhalten den Wert 00.
  2. Wenn m≥50m \geq 50, erhält c50c_{50} den Wert c50+1c_{50} + 1 und mm den Wert m−50m - 50.
  3. Solange m>10m > 10: c10c_{10} erhält den Wert c10+1c_{10} + 1 und mm den Wert m−10m - 10.
  4. Solange m>0m > 0: c1c_1 erhält den Wert c1+1c_1 + 1 und mm den Wert m−1m - 1.
  5. Liefere c50c_{50}, c10c_{10} und c1c_1.
Hinweis

Die Summe der Münzen ist immer mm; was fehlschlagen kann, ist ihre Anzahl. Probiere ein genaues Vielfaches von 1010 und einen Betrag von mehr als 100100, und frage dich, welche Art, Schritte zu kombinieren, Schritt 2 verwendet und welche er verwenden sollte.

Lösung

Es gibt zwei Mängel. Erstens verwendet Schritt 3 m>10m > 10: Mit m=10m = 10 liefert er keine Münze zu 1010 und zehn zu 11 statt einer. Zweitens ist Schritt 2 eine Entscheidung, wo eine Wiederholung nötig ist: Mit m=120m = 120 liefert der Algorithmus, so wie er geschrieben ist, eine Münze zu 5050, sechs zu 1010 und zehn zu 11 (siebzehn Münzen), und selbst mit bereits korrigiertem Schritt 3 lieferte er eine zu 5050 und sieben zu 1010 (acht) statt zwei zu 5050 und zwei zu 1010 (vier). Die Eingabe m=100m = 100 zeigt beide Mängel: Der Algorithmus liefert eine zu 5050, vier zu 1010 und zehn zu 11, fünfzehn Münzen, statt zwei. Korrektur: Schritt 2, „solange m≥50m \geq 50, …“; Schritt 3, „solange m≥10m \geq 10, …“. So korrigiert, nimmt der Algorithmus immer die größte Münze, die passt, und mit diesen drei Münzen ergibt das die kleinste Anzahl: Die Einerziffer von mm kann nur mit Münzen zu 11 bezahlt werden, und vom Rest zahlt man am besten so viel wie möglich mit Münzen zu 5050, weil fünf zu 1010 so viel wert sind wie eine zu 5050. Mit anderen Münzen, etwa zu 11, 33 und 44 für m=6m = 6, ergibt die Wahl der jeweils größten nicht mehr das Minimum (4+1+14 + 1 + 1 gegenüber 3+33 + 3).