Informatik 11 [email protected] Seite 35 8. Rekursion Nach einer großen Party muss das Geschirr gespült werden. Du läufst an der Küche vorbei und jemand sagt zu dir: „Spüle das Geschirr!“. Du bist faul, - spülst ein einziges Teil und sagst zu einem anderen: „Spüle das Geschirr!“ Wenn wir annehmen, dass derjenige genauso faul ist und die anderen auch, dann haben wir eine rekursive Lösung für das Geschirr-Spülen gefunden. Diese Methode ist sehr angenehm (besonders für den letzten), allerdings nicht unbedingt die effektivste. Manch schwieriges Problem lässt sich aber durch Rekursion so einfach und verständlich lösen wie dieses Geschirr-Problem. Der rekursive Ansatz kann so formuliert werden: „Spüle das Geschirr!“ - Wenn das Spülbecken leer ist, ist nichts zu tun. - Wenn es nicht leer ist, dann - spüle ein einziges Teil und - sage zu einem anderen: „Spüle das Geschirr!“ Prozedur Spüle das Geschirr ist Spülbecken leer wahr falsch Spüle ein Teil Spüle das Geschirr Eine Prozedur dieser Art, die einen Teil der Aufgabe löst und dann den Rest erledigt, indem sie sich selbst aufruft, wird rekursive Prozedur genannt. (das Spül-Beispiel nach Wolfram Burgard, http://ais.informatik.uni-freiburg.de/lehre/ws02/info1/material/recursion/10recurs.pdf) Die Fakultät In der Kombinatorik (Teil der Wahrscheinlichkeitsrechnung) braucht man häufig folgendes Produkt, das man Fakultät nennt: n! = n · (n-1) · (n-2) · . . . · 3 · 2 · 1 So berechnet man z.B. die Zahl der Möglichkeiten, wie man 6 Sprinter auf die 6 Bahnen verteilen kann: Für die erste Bahn hat man 6 Personen zur Auswahl, für die zweite jeweils noch 5, für die dritte jeweils 4 usw. Insgesamt gibt es 6! = 6 · 5 · 4 · 3 · 2 · 1 = 720 mögliche Aufstellungen. n! wächst unheimlich schnell, wenn man n größer macht. 20! hat bereits 19 Stellen, 30! hat 33 Stellen, und 100! ist wesentlich mehr, als es Elementarteilchen im gesamten Weltall (≈ 1070) gibt. Wenn das kein Grund ist, so wie beim Spülen nur einen Schritt zu berechnen und für den Rest einen Assistenten zu beauftragen! Wir wollen 10! berechnen. Wir sind faul und sagen zum Assistenten „Berechne 9!“ Wenn er das Ergebnis hat, multiplizieren wir es noch mit 10 und sind fertig. Der Assistent seinerseits durchschaut die List und beauftragt einen zweiten mit „Berechne 8!“, um dessen Ergebnis mit 9 zu multiplizieren. Du siehst, wie die Sache weiter läuft. Die Prozedur können wir folgendermaßen rekursiv angeben: „Berechne Fakultät(n)“ - Wenn n = 1 ist, dann ist das Ergebnis 1. - Wenn n > 1 ist, dann - sage einem Assistenten „Berechne Fakultät(n-1)“ - das Ergebnis ist n mal das des Assistenten. Prozedur Fakultät(n) ist n=1 wahr Ergebnis := 1 falsch Ergebnis := Fakultät(n-1) � n [email protected] Informatik 11 Seite 36 Diese Prozedur ist eine gültige Rekursion, weil sie - einen Aufruf zu sich selbst enthält das Problem bei jedem Aufruf verkleinert überprüft, ob die Arbeit erledigt ist. ► Schreibe dazu ein Delphi-Programm, das zur Berechnung von n! eine rekursive Funktion enthält mit dem Kopf: function Fakultaet(n:integer): integer; In einem Edit soll die Zahl n eingegeben werden, ein zweites Edit soll das Ergebnis anzeigen. Bedenke beim Test deines Programms, dass Integer-Zahlen in Computer-Programmen nur mit 32 Bit gespeichert werden. Deshalb ist die größte Integer-Zahl 2 147 483 647. Will man mit noch größeren ganzen Zahlen rechnen, dann muss man dem PC das „schrifliche“ Multiplizieren beibringen wie einem Grundschüler. ► Entwickle eine Rekursion zur Berechnung der Potenz xn , wobei x und n nur natürliche Zahlen sind. Wie bei der Fakultät soll diese Rekursion in einem Delphi-Programm angewandt werden. ► Euklid (300 v. Chr.) hat ein Verfahren zur ggT-Berechnung zweier natürlicher Zahlen a und b gefunden, das ohne Faktor-Zerlegung auskommt. Seine Überlegung: Jeder Teiler von a und b ist auch Teiler von der Differenz a-b. Deswegen kann man rechnen: ggT(48,30) = ggT(30,18) = ggT(18,12) = ggT(12,6) = ggT(6,6) = 6 Formuliere dafür einen rekursiven Algorithmus und setze ihn in ein Delphi-Programm um. Permutationen einer Menge (Permutation = Vertauschung) Bestimme alle Permutationen der Menge {A,B,C}. Das sind die Tupel (ABC), (ACB), (BAC), (BCA), (CAB) und (CBA). Es gibt 6 (= 3!) mögliche Permutationen der 3-Menge. In einer 4-Menge sind es 4! = 24 Permutationen. Bei den 6 Sprintern sind es bereits 720 mögliche Anordnungen. Wie man sie zählt, wissen wir also schon, aber wie kann man alle Permutationen anschreiben? ► Bevor wir das Problem allgemein lösen: Mache dir die kleine Mühe, alle Permutationen von {A,B,C,D} anzuschreiben. Überlege dir ein System. Wir versuchen wieder einen rekursiven Ansatz, indem wir einen Assistenten einsetzen, der ein reduziertes Problem zu lösen bekommt: Eine Idee wäre: Ich halte der Reihe nach jeden Buchstaben von ’ABCD’ fest, sage jeweils zu einem Assistenten „permutiere den Rest“ und hänge dann die Ergebnisse des Assistenten an meinen Buchstaben dran. Das ist leider nicht durchführbar, weil jetzt mein Assistent nicht ein Ergebnis liefert sondern viele, die er in einem Array speichern müsste, bevor ich sie an den festgehaltenen Buchstaben dranhängen kann. Ebenso jeder weitere Assistent. Ich kann aber dem Assistenten mitteilen, welchen Buchstaben ich festgehalten habe, damit er jedes seiner Ergebnisse gleich an meinen Buchstaben dranhängen kann, um das Endergebnis auszugeben. Der Assistent aber hält wieder der Reihe nach jeden Buchstaben fest, muss deshalb seinem Assistenten mitteilen, was ich und er festgehalten haben, damit dieser seine Permutationen dranhängen kann ... Das sieht dann so aus: Informatik 11 [email protected] Seite 37 Ich sage nacheinander zu meinem Assistenten: Permutiere (’A’, ’BCD’) (damit sich ergeben soll: ’A’+’BCD’ ’A’+’BDC’ ’A’+’CBD’ usw.) Permutiere (’B’,’ACD’) (damit sich ergeben soll: ’B’+’ACD’ ’B’+’ADC’ ’B’+’CAD’ usw.) Permutiere (’C’,’ABD’) (damit sich ergeben soll: ’C’+’ABD’ ’C’+’ADB’ ’C’+’BAD’ usw.) Permutiere (’D’,’ABC’) (damit sich ergeben soll: ’D’+’ABC’ ’D’+’ACB’ ’D’+’BAC’ usw.) Die Länge des String ’ABCD’ bestimmt also, wie oft ich meinen Assistenten bemühe. Was macht dieser bei meinem ersten Auftrag Permutiere (’A’, ’BCD’) ? Er gibt drei Aufträge an seinen Assistenten: Permutiere (’AB’, ’CD’) Permutiere (’AC’, ’BD’) Permutiere (’AD’, ’BC’) Er hält also nacheinander jeden der drei Buchstaben von ’BCD’ fest, hängt ihn an meinen Festteil dran und gibt den neuen Festteil und den neuen Rest weiter. Der Festteil wächst bei jedem Aufruf an, der Rest nimmt ab, bis er zuletzt ein Leerstring ist. Wie du siehst, brauchen wir folgende Prozedur, die sich rekursiv aufruft: procedure permutiere(festteil, rest: string); Diese stellt fest, ob der Rest bereits der Leerstring ist. Wenn ja, wird der Festteil als ein Ergebnis ausgegeben. Wenn nein, nimmt sie nacheinander jeden Buchstaben von Rest, hängt ihn an Festteil dran, bestimmt den neuen Rest und gibt den Auftrag rekursiv weiter. ► Erstelle das Struktogramm des Algorithmus. ► Realisiere deine Lösung in einem Delphi-Programm. Die Menge kannst du dir in einem Edit-Objekt als String ’ABCD’ vorgeben. Mit den Funktionen length() und copy() kannst du den String zerlegen, mit der Operation ’+’ kannst du den neuen Festteil wieder zusammensetzen (sh. S.11 oder die Delphi-Hilfe). Um alle Permutationen aufzulisten, eignet sich am besten eine Listbox. Beim allerersten Aufruf der Prozedur muss Festteil ein Leerstring sein. Quicksort Wenn du an diesen Beispielen Spass gefunden hast, dann kannst du dich jetzt mit Hilfe des Programms ’Sortieren Lernen’ an das Problem Quicksort wagen.