Selbstaufrufende Funktionen |
[Summe über 1 bis n (reksumme.cpp)]
long summeRekursiv(long max)
{
if (0 < max)
{
return summeRekursiv(max - 1) + max;
}
return 0;
}
long summeIterativ(long max)
{
long Summe = 0;
while (0 < max)
{
Summe += max;
max--;
}
return Summe;
}
Die Einsparung zweier Zeilen wird Sie sicher nicht beeindrucken. Aber in dem Beispiel geht es weniger um die Frage besonderer Eleganz oder Effizienz, sondern um die Darstellung eines alternativen Ansatzes. An diesem einfachen Beispiel lässt sich eher verstehen, wie eine rekursive Funktion arbeitet. Der Rumpf der rekursiven Funktion entspricht einem Schleifenkörper. Er enthält nicht die Lösung des ganzen Problems, sondern implementiert einen Schritt. So wie bei der Schleife das Gesamtproblem erst durch die Wiederholung gelöst wird, so wird die Rekursion erst durch den Selbstaufruf zur Lösung kommen.
summeRekursiv(4) [=6]+4
summeRekursiv(3) [=3]+3
summeRekursiv(2) [=1]+2
summeRekursiv(1) [=0]+1
summeRekursiv(0)
Zunächst wird die Funktion summeRekursiv() mit dem Parameter 4 aufgerufen. Sofern dieser nicht 0 ist, ruft sie zunächst sich selbst mit einem um 1 reduzierten Parameter auf. Hier wäre das 3. So wiederholt sich Aufruf um Aufruf, bis der Parameter endlich 0 ist. Dann ruft die Funktion nicht mehr sich selbst auf, sondern läuft durch. Sie liefert 0 zurück. Sie springt in den vorigen Funktionsaufruf zurück, in dem der Parameter max noch 1 war. Dies wird zum Ergebnis des Aufrufs hinzuaddiert und als Ergebnis zurückgegeben. Die Rückkehr erfolgt in die Stufe, in der max 2 war. Und so geht es weiter, bis summeRekursiv(4) schließlich 10 zurückgibt.
[Druckroutine rekursiv]
DruckeRekursiv(char * DateiName)
{
OeffneDatei(DateiName)
while (!EndeDerDatei())
{
LiesZeile();
if (IncludeAnweisung(IncludeDateiName))
{
DruckeRekursiv(IncludeDateiName);
}
DruckeZeile();
}
}
Diese Funktion kommt mit beliebig verschachtelten #include-Befehlen zurecht und ist insgesamt recht übersichtlich. Sie können davon ausgehen, dass der C++-Präprozessor seine Dateien ebenfalls rekursiv einlesen wird.
An dieser Stelle steht im Buch die Abbildung "Binärer Baum" (binbaum)
Es zeigt sich wieder einmal, dass es um das biologische Wissen der meisten Informatiker betrüblich aussieht: Die Wurzel des Baums ist oben. Im vorliegenden Fall liegt in der Wurzel die 27. Nach links enthalten alle Knoten kleinere, nach rechts größere Werte. Das gilt in diesem Baum für alle Knoten. Sind die Informationen wie hier sortiert angeordnet, dann können sie extrem schnell gefunden werden. Um einen Baum in einem Programm nachzubilden, muss ein Knoten beschrieben werden und die Knoten untereinander in Baumform verbunden werden. Die Datenstruktur eines Knotens hat folgendes Aussehen:
[Knoten eines Binärbaums]
struct tBaum
{
int Inhalt;
tBaum * links;
tBaum * rechts;
};
Ein Baum entsteht, wenn mehrere Knoten durch die Zeiger links oder rechts miteinander verbunden werden und dabei kein Kreis entsteht. Zur Erinnerung: Ein Knoten wird mit dem Befehl new erzeugt. Der Rückgabewert ist der Zeiger auf den neuen Knoten. Um auf die Elemente des Knotens zuzugreifen, wird an den Zeigernamen die Zeichenkombination -> gefolgt vom Elementnamen angehängt
[Knoten eines Binärbaums erzeugen und füllen]
tBaum *NeuKnoten; NeuKnoten = new tBaum; NeuKnoten->Inhalt = 5;
[Ausgabe eines sortierten Binärbaums (binbaum.cpp)]
struct tBaum
{
int Inhalt;
tBaum * links;
tBaum * rechts;
};
void ZeigeBaum(tBaum *Blatt)
{
if (Blatt==0) return;
ZeigeBaum(Blatt->links);
cout << Blatt->Inhalt << endl;
ZeigeBaum(Blatt->rechts);
}
[Einfügen in einen sortierten Binärbaum (binbaum.cpp)]
tBaum * Einfuegen(tBaum *Blatt, int Inhalt)
{
if (Blatt==0) // Freie Position gefunden
{ // Neues Blatt einfügen
Blatt = new tBaum;
Blatt->links = Blatt->rechts = 0;
Blatt->Inhalt = Inhalt;
return Blatt; // Zeiger auf neues Blatt zurückgeben
}
if (Inhalt < Blatt->Inhalt)
{ // Zuweisung behält altes Blatt oder das neue
Blatt->links = Einfuegen(Blatt->links, Inhalt);
}
else if (Inhalt > Blatt->Inhalt)
{ // Zuweisung behält altes Blatt oder das neue
Blatt->rechts = Einfuegen(Blatt->rechts, Inhalt);
}
return Blatt; // gib das aktuelle Blatt zurück
}
Das Einhängen des neu angelegten Blatts im Baum erfolgt nicht in dem Durchlauf, in dem new aufgerufen wird, sondern in der davor aufgerufenen Funktion an der folgenden Stelle:
Blatt->links = Einfuegen(Blatt->links, Inhalt);
In den meisten Fällen wird diese Zuweisung das Blatt nicht verändern, weil das als erster Parameter übergebene Blatt exakt das ist, das vorher an dieser Stelle im Baum stand. Nur wenn Blatt->links 0 war, dann wird ein neues Blatt erzeugt. Der Zeiger auf das neue Blatt wird zurückgegeben und an dieser Stelle in den Baum eingehängt.
An dieser Stelle steht im Buch die Abbildung "Türme von Hanoi" (pichanoi1)
Die obersten n Scheiben werden auf den mittleren Stab gesetzt. Nach der Voraussetzung ist das ja möglich.
Auf dem linken Stab verbleibt eine Scheibe, die auf den dritten Stab verlegt wird.
Dann werden die n Scheiben vom mittleren Stab auf den rechten Stab gelegt. Anschließend liegen n+1 Scheiben auf dem rechten Stab.
Damit ist bewiesen, dass n+1 Scheiben umgesetzt werden können, wenn es möglich ist, n Scheiben regelkonform zu versetzen.
[Türme von Hanoi rekursiv (hanoi.cpp)]
#include <iostream>
using namespace std;
void Ziehe(int von, int nach, int ScheibenZahl)
{
int frei;
if (ScheibenZahl==0) return;
frei = 6 - von - nach; // bestimme den freien Platz
Ziehe(von, frei, ScheibenZahl-1);
cout << von << " - " << nach << endl;
Ziehe(frei, nach, ScheibenZahl-1);
}
int main()
{
Ziehe(1, 3, 3); // von 1 nach 3, 3 Scheiben
}
Der erste Aufruf zeigt, dass die Scheiben zunächst auf Stab 1 liegen, dass sie auf Stab 3 versetzt werden sollen und dass es drei Scheiben sind. Das Programm zeigt das folgende Ergebnis:
1 - 3 1 - 2 3 - 2 1 - 3 2 - 1 2 - 3 1 - 3Um also das Spiel für drei Scheiben zu lösen, legen Sie zunächst die Scheibe von Stab~1 auf Stab~3. Dann wird die nächste Scheibe von Stab~1 auf Stab~2 gelegt. Wenn Sie dieser Anleitung folgen, können Sie das Spiel auflösen.
[Lexikalische Analyse (calc.cpp)]
enum tToken
{
PLUS, MINUS, MUL, DIV, LPAR, RPAR, NUMBER, END, ERROR
};
// Globale Variablen:
tToken aktToken; // das zuletzt erkannte Token
double TokenZahlenWert; // der Wert bei Zahlenkonstanten
char *srcPos; // Programm-Position
tToken sucheToken()
// Sucht ab der aktuellen Stringposition das nächste Token.
// Hier werden auch die Zahlenkonstanten bestimmt und in
// der globalen Variablen TokenZahlenWert abgelegt.
{
aktToken = ERROR;
if (*srcPos==0)
{
aktToken = END;
}
else
{
switch (*srcPos)
{
case '(': aktToken=LPAR; break;
case ')': aktToken=RPAR; break;
case '*': aktToken=MUL; break;
case '/': aktToken=DIV; break;
case '+': aktToken=PLUS; break;
case '-': aktToken=MINUS; break;
}
if (*srcPos>='0' && *srcPos<'9')
{
aktToken=NUMBER;
TokenZahlenWert = 0.0;
}
while (*srcPos>='0' && *srcPos<'9')
{
TokenZahlenWert *= 10;
TokenZahlenWert += *srcPos-'0';
srcPos++;
}
if (aktToken != NUMBER)
{
srcPos++;
}
}
return aktToken;
}
tToken Error(char *s)
// Meldung ausgeben und Fehler-Token zurückliefern
{
cerr << s << endl;
return ERROR;
}
Die Rekursion wird verwendet, wenn das Programm beginnt, die Tokens als Anweisungen zu interpretieren und dabei Klammern und Prioritäten berücksichtigt. Zuerst wird die Funktion aufgerufen, die die Operation mit der niedrigsten Priorität auswertet. Das ist im Beispiel die Funktion PlusMinus(). Diese ruft sofort die Behandlung der Punktrechnung in der Funktion MulDiv() auf. Und auch diese Funktion wendet sich zuerst an die Funktion Klammern(). Die Klammern haben wie die Vorzeichen die höchste Priorität. Entsprechend prüft die Funktion Klammern(), ob ein Token vorliegt, das ihrer Prioritätsstufe entspricht. Dazu gehört die öffnende Klammer, eine Zahl oder ein Vorzeichen. Ist das vorliegende Token keines dieser Elemente, endet die Funktion und springt somit zur aufrufenden Funktion MulDiv() zurück. Diese schaut nach dem Stern oder dem Schrägstrich. Erst wenn auch das nicht passt, kehrt der Aufruf wieder zu PlusMinus() zurück und wertet die Addition oder Subtraktion aus.
[Parser (calc.cpp)]
double PlusMinus(); // Prototyp
double Klammern()
// wertet Klammern, Zahlen und Vorzeichen aus
// ruft Klammern() und PlusMinus() dadurch rekursiv auf!
{
double Wert;
switch(aktToken)
{
case NUMBER:
sucheToken();
return TokenZahlenWert;
case MINUS:
sucheToken();
return -Klammern();
case LPAR:
sucheToken();
Wert = PlusMinus();
if (aktToken != RPAR)
{
return Error(") expected");
}
sucheToken();
return Wert;
case END:
return 1;
}
return Error("primary expected");
}
double MulDiv()
// wertet Multiplikation und Division aus
// ruft Klammern() auf, dadurch rekursiv!
{
double Wert;
// rufe erst die Operation mit der nächsthöheren
// Priorität auf
Wert = Klammern();
while (aktToken==MUL || aktToken==DIV)
{
if (aktToken==MUL)
{
sucheToken();
Wert *= Klammern();
}
else if (aktToken==DIV)
{
sucheToken();
Wert /= Klammern();
}
}
return Wert;
}
double PlusMinus()
// wertet Summe und Differenz aus
// ruft MulDiv() auf, dadurch rekursiv
{
double Wert;
// rufe erst die Operation mit der nächsthöheren
// Priorität auf
Wert = MulDiv();
while (aktToken==PLUS || aktToken==MINUS)
{
if (aktToken==PLUS)
{
sucheToken();
Wert += MulDiv();
}
else if (aktToken==MINUS)
{
sucheToken();
Wert -= MulDiv();
}
}
return Wert;
}
double Auswertung(char *s)
{
srcPos = s;
sucheToken(); // bestimme das erste Token vorweg
return PlusMinus();
}
int main(int argc, char* argv[])
{
double Wert = Auswertung(argv[1]);
cout << Wert << endl;
return 0;
}
Der Aufruf der Funktion Auswertung() erfordert als Parameter nur die Zeichenkette, in der sich der auszuwertende Ausdruck befindet. Die Eingabe darf keine Leerzeichen enthalten und ist auf ganzzahlige Eingaben beschränkt, da die Funktion sucheToken() derzeit keine Nachkommastellen auswertet. An anderer Stelle finden Sie Lösungen, die auch dieses Problem beseitigen.
> calc "12*(5+2)" 84 > calc "4*(5+2)" 28 > calc "4*5+2" 22 > calc "(5-4)*5*6+(7-3)*((4+2))" 54 > calc "(5-4)*5*6+(7-3)*((4+5)/3)" 42 >
Der Rechner kommt mit der Punkt-vor-Strich-Regel klar, hat kein Problem mit Klammern und kann auch komplexere Ausdrücke ausrechnen.
|
Diese Seite basiert auf Inhalten aus dem Buch
Arnold Willemer: Einstieg in C++ Mit freundlicher Genehmigung und Unterstützung des Verlags galileo computing |
| Informatik-Ecke Einstieg in C++ |
(C) Copyright 2005 Arnold Willemer
|