Teil 14: Rekursive Programmierung. Prof. Dr. Herbert Fischer Fachhochschule Deggendorf Prof. Dr. Manfred Beham Fachhochschule Amberg-Weiden

Ähnliche Dokumente
Programmierung in C++

II.3.1 Rekursive Algorithmen - 1 -

Übung zu Algorithmen und Datenstrukturen (für ET/IT)

C++ - Kontrollstrukturen Teil 2

Vorkurs Informatik WiSe 17/18

Übung zu Algorithmen und Datenstrukturen (für ET/IT)

FHZ. K13 Rekursion. Lernziele. Hochschule Technik+Architektur Luzern Abteilung Informatik, Fach Programmieren. Inhalt

Vorkurs Informatik WiSe 16/17

Speicher und Adressraum

11. Rekursion, Komplexität von Algorithmen

Lösungen Übung 5. Programmieren in C++ 1. Aufgabe. #include <iostream.h> #include <stdarg.h>

Die Ausgangsposition. Der aus drei Scheiben bestehende Turm steht auf Platz 1.

Präzedenz von Operatoren

Einführung in die Programmierung I. 5. Prozeduren. Stefan Zimmer

Beim rekursiven Aufruf einer Funktion wird jeweils ein Duplikat der gesamten Funktion im Speicher abgelegt.

Kasparov versus Deep Blue. Till Tantau. Institut für Theoretische Informatik Universität zu Lübeck

C++ Teil 2. Sven Groß. 16. Apr IGPM, RWTH Aachen. Sven Groß (IGPM, RWTH Aachen) C++ Teil Apr / 22

Grundlagen der Programmierung in C Funktionen

Projekt 3 Variablen und Operatoren

Algorithmen & Programmierung. Rekursive Funktionen (1)

Java 8. Elmar Fuchs Grundlagen Programmierung. 1. Ausgabe, Oktober 2014 JAV8

Nachklausur: Grundlagen der Informatik I, am 02. April 2008 Dirk Seeber, h_da, Fb Informatik. Nachname: Vorname: Matr.-Nr.

Nachklausur: Grundlagen der Informatik I, am 02. April 2008 Dirk Seeber, h_da, Fb Informatik. Nachname: Vorname: Matr.-Nr.

Einführung in die Programmierung Wintersemester 2008/09

Programmieren lernen mit Groovy Rekursion Rekursion und Iteration

Rekursive Funktionen (1)

Übung zur Vorlesung Wissenschaftliches Rechnen Sommersemester 2012 Auffrischung zur Programmierung in C++, 1. Teil

Rekursive Funktionen (1)

Programmieren 1 C Überblick

Stand der Vorlesung Komplexität von Algorithmen (Kapitel 3)

Induktion und Rekursion

Informatik I: Einführung in die Programmierung

Algorithmen und Datenstrukturen

1. Teilklausur Gruppe A. Bitte in Druckschrift leserlich ausfüllen!

Nachklausur: Grundlagen der Informatik I, am 02. April 2008 Dirk Seeber, h_da, Fb Informatik

Das Kontrollfluss-Diagramm für Ò ¼ µ:

5. Elementare Befehle und Struktogramme

Grundlagen der Programmierung

Kapitel 7: Rekursion. Inhalt. Rekursion: Technik Rekursion vs. Iteration

Übung zur Vorlesung Wissenschaftliches Rechnen Sommersemester 2012 Auffrischung zur Programmierung in C++, 1. Teil

Grundlagen der Programmierung WS 15/16 (Vorlesung von Prof. Bothe)

Hackenbusch und Spieltheorie

4.4 Imperative Algorithmen Prozeduren

Klausur: Grundlagen der Informatik I, am 06. Februar 2009 Gruppe: B Dirk Seeber, h_da, Fb Informatik. Nachname: Vorname: Matr.-Nr.

1 Aufgaben 1.1 Objektorientiert: ("extended-hamster") Sammel-Hamster

Klausur: Grundlagen der Informatik I, am 06. Februar 2009 Gruppe: A Dirk Seeber, h_da, Fb Informatik. Nachname: Vorname: Matr.-Nr.

2. Programmierung in C

12. Rekursion Grundlagen der Programmierung 1 (Java)

Klausur: Grundlagen der Informatik I, am 06. Februar 2009 Gruppe: A Dirk Seeber, h_da, Fb Informatik. Nachname: Vorname: Matr.-Nr.

4. Fortgeschrittene Algorithmen 4.1 Rekursion 4.2 Daten und Datenstrukturen 4.3 Bäume

Abschlußtest Programmieren 30. Juni 2017 Name: Punkte von 32: Gruppe: Haase-Di Haase-Do Stanek-Di Stanek-Do

( )= c+t(n-1) n>1. Stand der Vorlesung Komplexität von Algorithmen (Kapitel 3)

Einschub: Anweisungen und Bedingungen für PAP und Struktogramme (1)

7 Funktionen. 7.1 Definition. Prototyp-Syntax: {Speicherklasse} {Typ} Name ({formale Parameter});

C++ Teil 5. Sven Groß. 12. Nov IGPM, RWTH Aachen. Sven Groß (IGPM, RWTH Aachen) C++ Teil Nov / 16

5.3 Auswertung von Ausdrücken

Einstieg in die Informatik mit Java

public static void main(string[] args) {

Programm heute. Algorithmen und Datenstrukturen (für ET/IT) Definition Algorithmus. Wie beschreibt man Algorithmen?

Inhalt. 3. Spezielle Algorithmen

Einführung in die Programmierung für NF. Übung

Algorithmen und Datenstrukturen (für ET/IT)

Programmieren I. Methoden-Spezial Heusch --- Ratz 6.1, Institut für Angewandte Informatik

Einführung in die Informatik I

Die Türme von Hanoi. Wollen

C++ Teil 4. Sven Groß. 30. Apr IGPM, RWTH Aachen. Sven Groß (IGPM, RWTH Aachen) C++ Teil Apr / 16

Programmieren I. Methoden-Special Heusch --- Ratz 6.1, Institut für Angewandte Informatik

Übung zu Algorithmen und Datenstrukturen (für ET/IT)

Einführung in die Programmierung Wintersemester 2010/11

Verhalten. Def. und Nutzen von Verhalten. Pseudocode Schreibtischtest. Algorithmen

Einführung in die Programmierung Wintersemester 2011/12

hue06 December 2, 2016

Abschnitt 11: Korrektheit von imperativen Programmen

4 Rekursionen. 4.1 Erstes Beispiel

Was der Mathematiker Gauß nicht konnte, das können wir Prof. R. Zavodnik/C++ Vorlesung/Kapitel IX 1

Prof. Dr. Oliver Haase Karl Martin Kern Achim Bitzer. Programmiertechnik Klassenmethoden Teil 2

Verwendung Vereinbarung Wert einer Funktion Aufruf einer Funktion Parameter Rekursion. Programmieren in C

Klausur: Grundlagen der Informatik I, am 05. Februar 2008 Dirk Seeber, h_da, Fb Informatik

Gliederung. Algorithmen und Datenstrukturen I. Eine wichtige Frage. Algorithmus. Materialien zur Vorlesung. Begriffsbestimmung EUKLID Primzahltest

Funktionen. mehrfach benötigte Programmteile nur einmal zu schreiben und mehrfach aufzurufen

Informatik I (D-ITET)

Rekursion. Dr. Philipp Wendler. Zentralübung zur Vorlesung Einführung in die Informatik: Programmierung und Softwareentwicklung

Rekursion. Philipp Wendler. Zentralübung zur Vorlesung Einführung in die Informatik: Programmierung und Softwareentwicklung

Einführung in die Programmierung Wintersemester 2017/18

Einführung in die Informatik I

Inhalt. 3. Spezielle Algorithmen

2. Programmierung in C

Einführung in die Programmierung

Erwin Grüner

Einführung in die Programmierung WS 2009/10. Übungsblatt 7: Imperative Programmierung, Parameterübergabe

15. Rekursion. Rekursive Funktionen, Korrektheit, Terminierung, Aufrufstapel, Bau eines Taschenrechners, BNF, Parsen

Ein kleiner Blick auf die generische Programmierung

PIC16 Programmierung in HITECH-C

1 Bedingte Anweisungen. 2 Vergleiche und logische Operatoren. 3 Fallunterscheidungen. 4 Zeichen und Zeichenketten. 5 Schleifen.

Informatik I: Einführung in die Programmierung

Beispiel 1: Fakultät

Transkript:

Teil 14: Rekursive Programmierung Prof. Dr. Herbert Fischer Fachhochschule Deggendorf Prof. Dr. Manfred Beham Fachhochschule Amberg-Weiden

Inhaltsverzeichnis 14 Rekursive Programmierung... 3 14.1 Die Fakultätsfunktion... 3 14.2 Die Türme von Hanoi... 5 14.3 Ein einfaches Spiel... 7 Prof. Dr. Manfred Beham, FH Amberg-Weiden Seite 2

14 Rekursive Programmierung Wir wollen versuchen, zu verstehen, durch welches Prinzip es dem Rechner z.b. möglich wird, eine nur schwer zu übertreffende Spielstrategie zu entwickeln. Dieses Prinzip, das uns die Lösung vieler verwandter Fragestellungen erschließt, ist die rekursive Programmierung. An Hand von drei Beispielen wird gezeigt, wie mit Hilfe rekursiver Prozeduren sehr einfache und elegante Lösungen erstellt werden können: Ein anschauliches einführendes Beispiel findet sich in der Mathematik zur Berechnung der Fakultät. Eine Denksportaufgabe die Türme von Hanoi zeigt ganz deutlich, wie eine Fragestellung auf ein einfacheres Problem zurückgeführt werden kann. Letztlich wollen wir ein einfaches Spiel realisieren, um gegen den Computer anzutreten. 14.1 Die Fakultätsfunktion Die Fakultät einer positiven ganzen Zahl n > 0 wird definiert als Produkt n! = fact( n) = n ( n 1) L 2 1 der ganzen Zahlen von 1 bis n. Mit dieser Definition könnten wir sicher eine C++ Funktion realisieren, die dieses Produkt berechnet. Iterative Berechnung der Fakultät: int fact(int n) int prod = 1; while (n > 0) prod *= n--; return prod; Die Fakultät einer Zahl n könnte aber auch wie folgt definiert werden. 1, falls fact( n) = n fact( n 1), n = 0 sonst Das besondere daran ist, daß in der Definition von fact bereits die zu definierende Funktion verwendet wird. Eine solche Definition heißt rekursiv. Die Berechnung der Fakultät der Zahl n wird auf die Berechnung der Fakultät von n-1 zurückgeführt. Neu für uns ist, daß auch C++ die rekursive Definition von Funktionen zuläßt. Das bedeutet, daß man in dem Definitionsblock einer Funktion diese Funktion bereits aufrufen kann. Damit lassen sich derartige rekursive Definitionen eins zu eins in C++ Programmcode umsetzen. Und wir können die Fakultät nun auch ohne die Verwendung einer while-schleife programmieren. Die Funktion fact besteht aus einer bedingten Anweisung, die im Falle von n größer als 0, wieder die Funktion fact, nun mit n-1, aufruft. Prof. Dr. Manfred Beham, FH Amberg-Weiden Seite 3

Rekursive Berechnung der Fakultät: int fact(int n) if (n > 0) return n * fact(n-1); else return 1; Betrachten wir zum Verständnis dieser rekursiven Prozedur die Berechnung der Fakultät von 4. Unsere Funktion fact wird also mit dem Wert n = 4 aufgerufen. Die Berechnung des Funktionswertes, führt im if-zweig der bedingten Anweisung auf den Ausdruck: 4 * fact(3) Bevor dieser Ausdruck berechnet werden kann, muß das Ergebnis von fact(3) berechnet werden. Dies führt zu einem erneuten Aufruf der Funktion, diesmal mit dem Argument 3. Das Ergebnis dieses Aufrufs muß dann noch mit 4 multipliziert werden. Ebenso führt der Aufruf von fact(3) zu einem erneuten Aufruf der Funktion fact, diesmal mit dem Argument 2. Dieser Ablauf wird fortgesetzt, bis man beim Aufruf von fact(0) angelangt ist und das Ergebnis von 1 sofort ohne weitere Berechnungen im else-zweig erhält, da nun n nicht mehr größer als 0 ist. Das Ergebnis von fact(0) wird nun an den Aufrufenden fact(1) weitergereicht. Um das Ergebnis für fact(1) zu berechnen, muß noch mit 1 multipliziert werden; und so ergibt fact(1) = 1 * 1 = 1. So geht es rückwärts, bis fact(4) schließlich zu 4 * 6 = 24 berechnet wird. Folgende Abbildung stellt diese Aufrufkette dar. Die richtige Durchführung dieser Berechnung organisiert der Compiler mit Hilfe unserer rekursiven Definition vollkommen automatisch. Damit die Rekursion richtig funktioniert, muß sich der Programmierer nur um zwei Dinge kümmern: Grundprinzip rekursiver Prozeduren: Die entstehenden rekursiven Aufrufe müssen immer einfacher werden. Prof. Dr. Manfred Beham, FH Amberg-Weiden Seite 4

Und schließlich auf einen nicht rekursiven Fall führen, der die Aufrufkette abbricht. Rekursive Algorithmen können daher immer auf Probleme angewandt werden, die sich auf ein gleichartiges, aber einfacheres Problem zurückführen lassen. Und es muß eine Lösung für ein elementares Problem existieren. Das gilt auch für unser folgendes Beispiel. 14.2 Die Türme von Hanoi Bei dieser Knobelaufgabe müssen N Scheiben von einer Ausgangsposition zu einer Zielposition bewegt werden. Dazu dürfen die Scheiben auf einer Zwischenposition abgelegt werden. Der Durchmesser der Scheiben nimmt von unten nach oben hin kontinuierlich ab. Beim Transport der Scheiben müssen folgende 2 Regeln eingehalten werden: 1. Es darf nie eine größere Scheibe auf einer kleineren abgelegt werden. 2. Es darf in einem Zug immer nur eine Scheibe bewegt werden. Beim ersten Probieren werden wir bereits feststellen, daß kleinere Türme wiederholt umgesetzt werden müssen. Das Problem 5 Scheiben umzusetzen läßt sich also auf kleinere Probleme mit weniger Scheiben zurückführen und damit haben wir bereits das Prinzip für eine systematische Lösung mit Hilfe der Rekursion erkannt. Wir wollen uns jetzt vorstellen, wir hätten bereits eine Lösung gefunden, wenn die Höhe des Turmes nur um 1 kleiner wäre. Wir kennen also eine Lösung, um einen Turm von 4 Scheiben von einem Ort zu einem anderen Platz zu bewegen und wir könnten sicher auch die Rollen der Plätze als Zwischenlager bzw. Zielposition vertauschen. Damit gestaltet sich das Problem der 5 Scheiben sehr einfach: Transportiere die oberen 4 Scheiben zur Zwischenablage. Die unterste, größte Scheibe würde dabei sicher nicht stören. Die übriggebliebene größte Scheibe können wir jetzt ungehindert auf ihre Zielposition bewegen. Schließlich müssen wir noch den 4er Turm von der Zwischenablage auf die Zielposition transportieren; aber dafür kennen wir ja bereits wie angenommen eine Lösung, die sich nicht wesentlich von unserem ersten Transport des 4er Turms unterscheiden wird. Lediglich Ausgangsposition und Zielposition haben gewechselt. Prof. Dr. Manfred Beham, FH Amberg-Weiden Seite 5

Ich behaupte nun, damit ist die Knobelaufgabe erschöpfend gelöst. Wir haben also die Aufgabe, einen Turm der Höhe N zu bewegen, auf eine einfachere aber gleichartige Aufgabe zurückgeführt. Und es gibt auch eine elementare Lösung für Türme, die aus nur einer Scheibe bestehen. Prinzip der Rekursion Die rekursiv formulierte Lösung des Problems können wir sehr einfach in C++ umsetzen. Wir benötigen eine Funktion die folgende Parameter hat: Die Höhe, d.h. die Anzahl N an Scheiben, des umzusetzenden Turms. Drei Parameter, die die Belegung der drei Plätze beschreiben: - Ausgangsposition - Zwischenablage - Zielposition Rekursive Prozedur Hanoi: // Die Türme von Hanoi void Hanoi (int Hoehe, char Ausgang, char Zwischen, char Ziel) if (n > 0) Hanoi(Hoehe-1, Ausgang, Ziel, Zwischen); ziehe(n, Ausgang, Ziel); Hanoi(Hoehe-1, Zwischen, Ausgang, Ziel); void ziehe (int i, char Von, char Nach) cout << "ziehe Scheibe " << i << ", " << Von << " -> " << Nach << endl; Diese wenigen Programmzeilen bewegen nun jede Turmhöhe an die richtige Position. Dazu müssen wir die Funktion Hanoi nur mit der Turmhöhe, der Ausgangsposition und dem gewünschten Zielplatz / Zwischenplatz aufrufen. Hier soll ein Turm von 5 Scheiben von A nach C bewegt werden, wobei B als Zwischenlager genutzt wird. Aufruf der Prozedur Hanoi: // Lösung für die Türme von Hanoi der Höhe 5 int main () Hanoi (5, 'A', 'B', 'C'); return 0; Wenn sie nun den Anweisungen des Computers folgen, haben sie obendrein eine Lösung, die mit einer minimalen Anzahl an Scheibenbewegungen das gewünschte Ziel erreicht. Prof. Dr. Manfred Beham, FH Amberg-Weiden Seite 6

14.3 Ein einfaches Spiel Anhand eines einfachen Spiels für zwei Spieler, das dem NIM-Spiel verwandt ist, wollen wir zeigen, wie der Computer Spielstellungen bewerten kann, um einen möglichst optimalen Spielzug zu finden. Das Spiel: Zwei Spieler müssen abwechselnd Münzen aus einer Menge von Münzen nehmen. In jedem Zug darf ein Spieler entweder 3, 5, oder 7 Münzen entfernen. Kann ein Spieler nicht mehr ziehen, so hat er verloren. Wir starten mit einer zufälligen Zahl an Münzen. Dieses Spiel zu gewinnen, ist nicht schwer, da es eine einfache Gewinn-Strategie gibt. Wenn der Spieler die Strategie kennt und konsequent verfolgt, hat der Gegner von vornherein keine Chance. Für gewisse Anfangswerte von Münzen hat der Spieler, der beginnt, eine sichere Gewinnstrategie, für andere Anfangswerte gewinnt der zweite Spieler. Wenn beide die Strategie kennen, kann man nur noch hoffen, dass einer einen Fehler macht und das ist natürlich langweilig. Um nun unseren Computer in die Lage zu versetzen, dieses Spiel optimal zu spielen, wollen wir eine Funktion (Prädikat) zur Bewertung einer gegebenen Spielsituation entwickeln, welche uns sagen soll, ob der Spieler, der am Zug ist, eine sichere Gewinnmöglichkeit hat. Das Argument dieser Funktion ist die gegebene Spielsituation, die bewertet werden soll. Die Beschreibung einer gegebenen Spielsituation ist einfach die Anzahl der vorliegenden Münzen. Also eine ganze Zahl beschreibt den aktuellen Stand des Spiels ausreichend. Wir nennen unsere boolsche Funktion 'goodposition' und sie soll wahr liefern, wenn eine günstige Situation vorliegt und falsch wenn wir in einer Verlustsituation sind. Das Prädikat goodposition: // Spielstellung mit Gewinnmöglichkeit bool goodpos (int n) if (n < 3) return false; // schon verloren else if (n <= 9) return true; // klar wie ich gewinne else // ich gewinne, falls die Folgesituation schlecht ist return!goodpos(n-7)!goodpos(n-5)!goodpos(n-3); Für einige wenige Münzen ist ganz klar wer gewinnen oder verlieren wird. Bei weniger als 3 Münzen haben wir in jedem Fall verloren, da kein weiterer Zug mehr möglich ist. goodpos ergibt false. Zwischen 3 und 9 Münzen können wir durch Wegnahme von 3, 5 oder 7 Münzen die Anzahl immer soweit reduzieren, das der Gegner nicht mehr ziehen kann. Das ist also für den Spieler der am Zug ist eine höchst erfreuliche Situation, die in jedem Fall zum Gewinn führt also goodpos liefert true. Alle anderen Spielsituation können wir dann als für uns günstig bewerten, wenn wir durch irgend einen erlaubten Spielzug den Gegner in eine schlechte Situation zwingen können. Genau das versucht die Oder-Verknüpfung der möglichen Folge-Situationen, zu denen der Spieler, der am Zug ist, ziehen darf. Wenn irgend eine der Folgesituationen als "schlechte Position" (goodpos = false) zu bewerten ist, werden wir gewinnen also unsere Position ist gut. Die Funktion goodpos ist ein weiteres Beispiel für eine rekursive Prozedur, da für mehr als 9 Münzen die Funktion sich selbst aufruft. Das besondere hierbei ist, das die Funktion alle möglichen Spielzüge Prof. Dr. Manfred Beham, FH Amberg-Weiden Seite 7

ausprobieren muß, falls die Aufrufe nicht eine "schlechte" Position finden. Zur Bewertung einer beliebigen Spielsituation muß das Spiel also durch die rekursiven Aufrufe bis zum Spielende verfolgt werden, um zu entscheiden, ob ein Spielzug zum Erfolg führt. Zur Bewertung einer Spielsituation werden durch diese Oder- Verknüpfung nacheinander alle in Frage kommenden Spielzüge getestet. Bei Mißerfolg wird der Versuch zurückgezogen und die nächste Möglichkeit getestet. Dieses Verfahren setzt sich in den rekursiven Aufrufen von goodposition fort. Derartige rekursive Suchverfahren nennt man Backtracking-Verfahren Falls es keine weiteren alternativen Zugmöglichkeiten gibt, ist die aktuelle Spielsituation als "schlecht" zu bewerten. Die Auswertung der Oder-Verknüpfung kann allerdings abgebrochen werden, wenn ein passender Zug gefunden wurde; d.h.!goodpos() = true. Die anderen Alternativen müssen dann nicht mehr getestet werden. Der Operator wertet die Operanden von links nach rechts aus, solange nicht einer bereits true liefert. Beim Schachspiel explodiert der nötige Suchaufwand allerdings relativ schnell, da es in jeder Spielsituation eine große Menge möglicher Zug-Alternativen gibt. Deshalb können Schachprogramme die Suche nach einem optimalen Spielzug, nur bis zu einer gewissen Tiefe und nicht bis zum Ende des Spieles verfolgen. Daher kann ein Schachprogramm für eine beliebige Spielstellung auch nicht entscheiden ob es sich um eine Gewinn- oder Verlustsituation handelt. Statt dessen versucht man im Schach die Spielstellungen zu bewerten. Das bedeutet, das die Funktion zur Beurteilung der Spielsituation nicht mehr nur wahr oder falsch ausspuckt, sondern eine Zahl, die angibt wie gut oder schlecht eine bestimmte Spielstellung ist. Die Aufgabe der Suche nach einem optimalen Zug besteht nun darin, von allen Alternativen, diejenige zu wählen, die die beste Bewertung bekommt. Ein vollständiges Spielprogramm Unsere Funktion goodpos bildet den Kern eines Programms, das auch selber spielen kann. Wenn der Computer am Zug ist, wählt er aus allen möglichen Zügen einfach denjenigen aus, der in eine Spielsituation führt, für die goodpos falsch ist. Wenn eine solche Situation existiert, erhält der Gegner, der ja nun am Zug ist, eine "schlechte" Situation und ihm bleibt keine Chance. Existiert eine solche Stellung nicht, dann muß der Computer irgendeinen Spielzug zufällig wählen und ausführen. Er hat nur dann noch eine Chance, wenn der Gegner einen Fehler macht. Prof. Dr. Manfred Beham, FH Amberg-Weiden Seite 8