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
- Ein einfaches Problem spezifizieren, indem man seine Eingaben, seine Ausgaben und die Bedingungen angibt, die sie erfüllen müssen.
- Einen Algorithmus in nummerierten Schritten beschreiben, ohne Mehrdeutigkeit, mit ausdrücklichen Entscheidungen und Wiederholungen.
- Einen Algorithmus mit einer Ablauftabelle von Hand nachvollziehen und sein Ergebnis für eine gegebene Eingabe bestimmen.
- 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.
Umgangssprachliche Aufgabenstellung: „die größte von drei Zahlen finden“. Spezifikation:
- Eingabe: drei Zahlen , , (ganze Zahlen oder Dezimalzahlen, ohne weitere Bedingung).
- Ausgabe: eine Zahl .
- Beziehung: ist einer der drei Werte, und keiner von ihnen ist größer als ; das heißt , und .
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 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 , ist die Ausgabe , 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 und sind sich alle einig: Der Quotient und der Rest sind die einzigen ganzen Zahlen mit
So sind es für und die Werte und , denn . Aber die Aufgabenstellung „zwei ganze Zahlen dividieren“ sagt nicht, was zu tun ist, wenn negativ ist, und die Frage ist nicht müßig: Für und gibt es zwei vernünftige Antworten.
Erste Spezifikation: Der Rest ist nie negativ, . Dann gilt , und die Ausgabe ist , . 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 auf abgeschnitten, und es gilt : Die Ausgabe ist , .
Beide erfüllen ; 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 , 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 : Mit gibt es keine und , 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 oder , und heißen Variablen. Man schreibt „ erhält den Wert “, um zu sagen, dass mit dem aktuellen Wert von berechnet wird und das Ergebnis zum neuen Wert von 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.
Das Problem ist das oben spezifizierte: Eingabe, zwei ganze Zahlen und ; Ausgabe, zwei ganze Zahlen und mit und . Die Idee ist die des Verteilens: Von nimmt man Gruppen zu weg, solange es geht, zählt die weggenommenen Gruppen, und was übrig bleibt, ist der Rest.
- erhält den Wert , und erhält den Wert .
- Solange gilt, wiederhole die Schritte 3 und 4; sobald es nicht mehr gilt, fahre mit Schritt 5 fort.
- erhält den Wert .
- erhält den Wert ; kehre zu Schritt 2 zurück.
- Liefere und 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.
Wir verfolgen die Division durch fortgesetzte Subtraktion mit und . 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
Schritt 1: Die Anfangswerte werden gesetzt. Die Eingabe erfüllt die Vorbedingung, denn es gilt und .
Erster Durchlauf. In Schritt 2 wird geprüft, dass gilt, also wird der Rumpf ausgeführt: Schritt 3 nimmt eine Gruppe von fünf weg, und Schritt 4 zählt sie.
Zweiter Durchlauf: , und der Rumpf wird erneut ausgeführt.
Dritter Durchlauf: , und der Rumpf wird erneut ausgeführt.
In Schritt 2 schlägt die Bedingung fehl, weil gilt: Der Algorithmus fährt mit Schritt 5 fort und liefert und . 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, , und Schritt 2 wurde einmal öfter ausgeführt als der Rumpf, nämlich -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 , zum Beispiel und , so schlägt die Bedingung gleich beim ersten Mal fehl, der Rumpf wird kein einziges Mal ausgeführt, und die Ausgabe ist , , was korrekt ist, denn . 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 , erhält den Wert “ 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 beschriftet, und nicht erfüllt, ), 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.
Die Figur zeichnet die drei Formen mit allgemeinen Anweisungen und und einer Bedingung und setzt sie danach im Diagramm der Division durch fortgesetzte Subtraktion zusammen.
Beweis
Sequenz: Es wird ausgeführt und danach ; es gibt einen einzigen Eingang und einen einzigen Ausgang.
Entscheidung: Ist erfüllt, wird ausgeführt; wenn nicht, . Beide Zweige laufen in einem einzigen Ausgang zusammen.
Wiederholung: Es wird ausgewertet; ist sie erfüllt, wird ausgeführt und erneut ausgewertet; wenn nicht, wird die Wiederholung verlassen. Die Bedingung wird einmal öfter ausgewertet als der Rumpf.
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 aus nicht mehr als Stufen bis zur Null gibt. Diese Größe heißt Variante oder Schrankenfunktion, und die Lektion Korrekte Schleifen entwerfen wird sie systematisch verwenden.
Bei der Division durch fortgesetzte Subtraktion ist die Variante . Solange die Wiederholung weiterläuft, gilt , also ist eine natürliche Zahl. In jedem Durchlauf erhält den Wert , und da gilt (Vorbedingung), ist der neue Wert streng kleiner. Folglich terminiert die Wiederholung, und zwar nach höchstens Durchläufen (genau genommen nach genau Durchläufen, dem Quotienten).
Man beachte, wo die Vorbedingung verwendet wurde. Führt man den Algorithmus mit aus, so lässt Schritt 3 den Wert unverändert, die Bedingung 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 negativ, so würde 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 gerade ist: „solange , erhält den Wert ; wird die Null erreicht, war gerade“. Die Größe fällt in jedem Durchlauf, und dennoch terminiert das Verfahren nicht, wenn ungerade ist: Mit durchläuft es , , , , und die Bedingung schlägt nie fehl. Was nicht erfüllt ist, ist die andere Bedingung: 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 gilt, und am Ende nach dem verbleibenden Wert zu entscheiden, oder ; jetzt ist in jedem Durchlauf eine natürliche Zahl, weil nur subtrahiert wird, wenn 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 wird, solange gilt, halbiert, wenn es gerade ist, und durch ersetzt, wenn es ungerade ist. Mit lautet die Folge , und sie endet. Niemand hat ein 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
Einen Algorithmus schreiben, der die Spezifikation des Maximums von drei Zahlen vom Anfang dieser Lektion erfüllt, und ihn für die Eingaben und von Hand nachvollziehen.
Lösung
Idee: Man geht die Eingabe durch und hält den bisher größten Wert in einer Variable fest. Erster Schritt: erhält den Wert . Danach ist das Maximum der gesehenen Werte, und das ist nur .
Zweiter Schritt: Wenn , erhält den Wert . Dritter Schritt: Wenn , erhält den Wert . Vierter Schritt: Liefere . Das sind drei Anweisungen in Sequenz, zwei davon Entscheidungen ohne alternativen Zweig.
Ablaufverfolgung für : Nach dem ersten Schritt gilt ; im zweiten ist falsch, und ändert sich nicht; im dritten ist falsch, und ändert sich nicht. Geliefert wird .
Ablaufverfolgung für : Nach dem ersten Schritt gilt ; im zweiten gilt , und erhält den Wert ; im dritten ist falsch, und ändert sich nicht. Geliefert wird , was korrekt ist: Der Gleichstand verlangt keine eigene Behandlung.
Begründung: Nach jedem Schritt ist 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 ; wenn nicht, übertrifft er nicht. Am Ende sind alle drei gesehen, und erfüllt die Nachbedingung.
Hätte man statt geschrieben, wäre das Ergebnis dasselbe, weil bei Gleichstand ein gleicher Wert zugewiesen wird. Die Wahl zwischen und wäre wichtig, wenn zusätzlich die Position des Maximums verlangt würde: Mit wird die erste der gleichauf liegenden Positionen geliefert, mit die letzte; die Formel nennt die Position .
Im gregorianischen Kalender ist ein Jahr ein Schaltjahr, wenn es durch teilbar ist, ausgenommen die durch teilbaren, die es nur sind, wenn sie außerdem durch teilbar sind. Jemand aus dem Kurs schlägt den Algorithmus vor: „Wenn das Jahr durch teilbar ist, liefere ja; sonst liefere nein.“ Ein Jahr finden, in dem er versagt, und einen korrekten Algorithmus mit seiner Spezifikation schreiben.
Lösung
Spezifikation. Eingabe: eine ganze Zahl (der gregorianische Kalender gilt seit Oktober 1582). Ausgabe: ja oder nein. Beziehung: Die Ausgabe ist genau dann ja, wenn durch und nicht durch teilbar ist oder durch teilbar ist.
Der vorgeschlagene Algorithmus trifft in den meisten Jahren das Richtige und wirkt deshalb korrekt. Um ihn zu widerlegen, genügt ein Jahr, das durch und nicht durch teilbar ist: ist durch teilbar, und der Algorithmus sagt ja; aber war kein Schaltjahr.
Korrigierter Algorithmus, in drei verketteten Entscheidungen. Schritt 1: Wenn durch teilbar ist, liefere ja und beende. Schritt 2: Wenn durch teilbar ist, liefere nein und beende. Schritt 3: Wenn durch teilbar ist, liefere ja; sonst liefere nein.
Die Reihenfolge der Fragen ist wesentlich: Sie gehen von der besondersten Regel zur allgemeinsten. Fragte man zuerst nach der , bekäme das Jahr ein ja, bevor die Ausnahme erreicht ist. Ablaufverfolgung für vier Jahre, eines je Fall: (Schritt 1: ja), (Schritt 2: nein), (Schritt 3: ja), (Schritt 3: nein).
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 , und zu testen hätte nichts aufgedeckt.
Die Division durch fortgesetzte Subtraktion mit und sowie mit und von Hand nachvollziehen. Danach begründen, dass der Algorithmus für jede zulässige Eingabe nach genau Durchläufen terminiert und dass seine Ausgabe die Nachbedingung erfüllt.
Lösung
Ablaufverfolgung mit , : durchläuft , , , , , und durchläuft , , , , . Die fünfte Prüfung, , schlägt fehl. Ausgabe: , . Hier ist der Grenzfall die aufgehende Division: Der vierte Durchlauf findet statt, weil gilt, und deshalb muss die Bedingung lauten und nicht .
Ablaufverfolgung mit , : Die erste Prüfung, , schlägt fehl; der Rumpf wird kein einziges Mal ausgeführt. Ausgabe: , , was korrekt ist.
Eine Beziehung, die erhalten bleibt. Vor dem ersten Durchlauf gilt . In jedem Durchlauf sinkt um und steigt um , sodass sich nicht ändert. Folglich gilt die Gleichung nach jedem Durchlauf.
Terminierung. Solange wiederholt wird, gilt , und ist eine natürliche Zahl; in jedem Durchlauf sinkt es um . Eine streng fallende Folge natürlicher Zahlen ist endlich: Die Wiederholung terminiert.
Beim Terminieren ist die Bedingung fehlgeschlagen: . Außerdem gilt , weil nur dann subtrahiert wird, wenn gilt. Zusammen mit der Beziehung, die erhalten bleibt, erfüllt die Ausgabe die Nachbedingung; und da die Durchläufe zählt, ist die Zahl der Durchläufe der Quotient.
Online-Übungen
Den folgenden Algorithmus von Hand nachvollziehen und den Wert, den er liefert, in das Feld schreiben, nur die Zahl.
- erhält den Wert , und erhält den Wert .
- Solange gilt, wiederhole die Schritte 3 und 4; sobald es nicht mehr gilt, fahre mit Schritt 5 fort.
- Wenn gerade ist, erhält den Wert ; wenn es ungerade ist, erhält den Wert .
- erhält den Wert ; kehre zu Schritt 2 zurück.
- Liefere .
Der Browser prüft deine Antwort sofort, ohne sie zu bewerten.
Hinweis
Lege die Tabelle an: geht im ersten Durchlauf von auf . Zähle einen Durchlauf für jede Änderung von , bis den Wert hat.
Lösung anzeigen
7Der 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 und von Hand nach: Der zweite Durchlauf muss stattfinden, obwohl der Rest genau beträgt.
Lösung anzeigen
Eingabe: a (ganze Zahl, a >= 0), b (ganze Zahl, b > 0)1. q <- 02. r <- a3. solange r >= b wiederhole: r <- r - b q <- q + 14. liefere q, rÜ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.
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 diese beiden Ausgaben ausschließt, und , und 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 , , . Ausgabe: drei Zahlen , , . Beziehung: , und ist eine Umordnung von , 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 ( und nicht , weil bei wiederholten Werten eine strenge Ordnung unmöglich ist); und dass die Ausgabe eine Umordnung sein muss. ist sortiert, enthält aber eine , die nicht vorkam; hat vier Werte; erfüllt beide Bedingungen. Ohne die Bedingung der Umordnung wäre für jede Eingabe eine korrekte Ausgabe.
Einen Algorithmus in nummerierten Schritten schreiben, der eine natürliche Zahl erhält und ihre Ziffern von der letzten bis zur ersten liefert (für liefert er , , , ), wobei nur Quotient und Rest der Division durch verwendet werden. Ihn für und für von Hand nachvollziehen und begründen, dass er für jedes terminiert.
Hinweis
Der Rest der Division durch 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 geschieht, einer Zahl mit einer Ziffer.
Lösung
Ein korrekter Algorithmus: (1) liefere den Rest der Division von durch ; (2) erhält den Wert des Quotienten der Division von durch ; (3) wenn , kehre zu Schritt 1 zurück; sonst beende. Die Frage steht nach dem Rumpf, sodass der Rumpf mindestens einmal ausgeführt wird: Für liefert er , seine einzige Ziffer. Fragte man am Anfang („solange “), würde für nichts geliefert; das ist der Fehler, den Fall zu vergessen, in dem die Wiederholung nicht ausgeführt wird. Ablaufverfolgung für : Geliefert werden , , , , und durchläuft , , , . Terminierung: Die Variante ist , eine natürliche Zahl; wenn zu Schritt 1 zurückgekehrt wird, dann weil gilt, und für ist der Quotient durch streng kleiner als , sodass in jedem Durchlauf streng fällt.
Es gibt Münzen zu , und Einheiten, in unbegrenzter Anzahl. Spezifikation: Eingabe, eine natürliche Zahl ; Ausgabe, drei natürliche Zahlen , , mit und der kleinstmöglichen Anzahl von Münzen, . Jemand schlägt den folgenden Algorithmus vor. Eine Eingabe finden, bei der er versagt, jeden Mangel erklären und ihn beheben.
- , und erhalten den Wert .
- Wenn , erhält den Wert und den Wert .
- Solange : erhält den Wert und den Wert .
- Solange : erhält den Wert und den Wert .
- Liefere , und .
Hinweis
Die Summe der Münzen ist immer ; was fehlschlagen kann, ist ihre Anzahl. Probiere ein genaues Vielfaches von und einen Betrag von mehr als , 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 : Mit liefert er keine Münze zu und zehn zu statt einer. Zweitens ist Schritt 2 eine Entscheidung, wo eine Wiederholung nötig ist: Mit liefert der Algorithmus, so wie er geschrieben ist, eine Münze zu , sechs zu und zehn zu (siebzehn Münzen), und selbst mit bereits korrigiertem Schritt 3 lieferte er eine zu und sieben zu (acht) statt zwei zu und zwei zu (vier). Die Eingabe zeigt beide Mängel: Der Algorithmus liefert eine zu , vier zu und zehn zu , fünfzehn Münzen, statt zwei. Korrektur: Schritt 2, „solange , …“; Schritt 3, „solange , …“. 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 kann nur mit Münzen zu bezahlt werden, und vom Rest zahlt man am besten so viel wie möglich mit Münzen zu , weil fünf zu so viel wert sind wie eine zu . Mit anderen Münzen, etwa zu , und für , ergibt die Wahl der jeweils größten nicht mehr das Minimum ( gegenüber ).
Abgeschlossen
Melde dich an, um deinen Fortschritt zu speichern