Rekursion

aus Wikipedia, der freien Enzyklopädie
Wechseln zu: Navigation, Suche
Dieser Artikel erläutert die Technik der rekursiven Definition; zum Begriff rekursive Menge siehe Entscheidbar.
Rekursion in einem Bildschirm-Aufnahmeprogramm.

Als Rekursion (lateinisch recurrere ‚zurücklaufen‘) bezeichnet man die Technik in Mathematik, Logik und Informatik, eine Funktion durch sich selbst zu definieren (rekursive Definition). Wenn man mehrere Funktionen durch wechselseitige Verwendung voneinander definiert, spricht man von wechselseitiger Rekursion.

Nicht jede rekursive Definition ist eine Definition im eigentlichen Sinn, denn die zu definierende Funktion braucht nicht wohldefiniert zu sein. Jeder Aufruf der rekursiven Funktion muss sich durch Entfalten der rekursiven Definition in endlich vielen Schritten auflösen lassen. Ist dies nicht erfüllt, so spricht man von einem infiniten Regress (vulgo Endlosschleife).

Rekursion ist eine Problemlösungsstrategie, sie führt oft zu eleganten Darstellungen. Das Grundprinzip der Rekursion ist das Zurückführen einer allgemeinen Aufgabe auf eine einfachere Aufgabe derselben Klasse. In vielen Programmiersprachen sind rekursive Prozeduren oder Funktionen als Sprachmittel verfügbar. Rekursion und Iteration sind im Wesentlichen gleichmächtige Sprachmittel.

Definition[Bearbeiten]

(Hinweis vorab: Rekursionsverfahren und rekursive Definitionen sind nicht auf Funktionen natürlicher Zahlen beschränkt. Hier sei auf das verallgemeinerte Rekursionsschema verwiesen.)

Das Grundprinzip der rekursiven Definition einer Funktion f ist: Der Funktionswert f(n+1) einer Funktion f : N0N0 ergibt sich durch Verknüpfung bereits berechneter Werte f(n), f(n-1), ... Falls die Funktionswerte von f für hinreichend viele Startargumente bekannt sind, kann jeder Funktionswert von f berechnet werden. Bei einer rekursiven Definition einer Funktion f ruft sich die Funktion so oft selbst auf, bis eine durch den Aufruf der Funktion veränderte Variable einen vorgegebenen Zielwert erreicht oder Grenzwert überschritten hat (Terminierung, Abbruchbedingung).

Die Definition von rekursiv festgelegten Funktionen ist eine grundsätzliche Vorgehensweise in der funktionalen Programmierung. Ausgehend von einigen gegebenen Funktionen (wie z. B. der Summenfunktion) werden neue Funktionen definiert. Mit diesen können weitere Funktionen definiert werden.

Ein Spezialfall der Rekursion ist die primitive Rekursion, die stets durch eine Iteration ersetzt werden kann. Bei einer solchen Rekursion enthält der Aufrufbaum keine Verzweigungen, das heißt er ist eine Aufrufkette: das ist immer dann der Fall, wenn eine rekursive Funktion sich selbst jeweils nur einmal aufruft. Der Aufruf kann dabei am Anfang (Head Recursion, siehe Infiniter Regress) oder am Ende (Tail Recursion oder Endrekursion) der Funktion erfolgen. Umgekehrt kann jede Iteration durch eine primitive Rekursion ersetzt werden, ohne dass sich dabei die Komplexität des Algorithmus ändert.

Formen der Rekursion[Bearbeiten]

Die häufigste Rekursionsform ist die lineare Rekursion, bei der in jedem Fall der rekursiven Definition höchstens ein rekursiver Aufruf vorkommen darf. Die Berechnung verläuft dann entlang einer Kette von Aufrufen.

Die primitive Rekursion ist ein Spezialfall der linearen Rekursion. Hier definiert man Funktionen auf den natürlichen Zahlen, wobei in jedem rekursiven Aufruf dessen erster Parameter um Eins ab- oder zunimmt. Jede primitiv-rekursive Definition kann unter Zuhilfenahme eines Stapels durch eine Schleife (Programmierung) (z. B. For-Schleife oder While-Schleife) ersetzt werden.

Die endständige oder repetitive Rekursion (Tail Recursion oder Endrekursion) bezeichnet den Spezialfall der linearen Rekursion, bei der jeder rekursive Aufruf die letzte Aktion des rekursiven Aufrufs ist. Endrekursionen lassen sich unmittelbar durch While-Schleifen ersetzen und umgekehrt.

Unter verschachtelter Rekursion versteht man eine Rekursion, bei welcher rekursive Aufrufe in Parameterausdrücken rekursiver Aufrufe vorkommen. Diese Rekursionsform gilt als außerordentlich schwer zu durchschauen.

Kaskadenförmige Rekursion bezeichnet den Fall, in dem mehrere rekursive Aufrufe nebeneinander stehen. Die rekursiven Aufrufe bilden dann einen Baum. Kaskadenförmige Rekursion gilt als elegant, kann aber ohne weitere Maßnahmen einen exponentiellen Berechnungsaufwand nach sich ziehen. Sie wird gerne als Ausgangspunkt für die Ableitung einer anderen effizienteren Formulierung gebraucht.

Die wechselseitige Rekursion bezeichnet die Definition mehrerer Funktionen durch wechselseitige Verwendung voneinander. Sie lässt sich auf die gewöhnliche Rekursion einer tupelwertigen Funktion zurückführen.

Anwendung[Bearbeiten]

Im Fall von primitiv-rekursiven Funktionen steht es dem Programmierer frei, eine iterative oder eine rekursive Implementierung zu wählen. Dabei ist die rekursive Umsetzung meist eleganter, während die iterative Umsetzung effizienter ist (insbesondere weil der Stack weniger beansprucht wird und der Overhead für den wiederholten Funktionsaufruf fehlt); siehe auch das Programmierbeispiel unten.

Manche Programmiersprachen (zum Beispiel in der Funktionalen Programmierung) erlauben keine Iteration, sodass immer die rekursive Umsetzung gewählt werden muss. Solche Sprachen setzen häufig zur Optimierung primitive Rekursionen intern als Iterationen um (einige Interpreter für LISP und Scheme verfahren so).

Es ist zu beachten, dass eine naive Implementierung bei manchen Funktionen (z. B. den Fibonacci-Zahlen) bedingt, dass Teillösungen mehrfach berechnet werden. Abhilfe schafft in diesem Beispiel die Memoisation, die auf der Wiederverwendung bereits berechneter Zwischenlösungen beruht. Die Rekursion ist ein wesentlicher Bestandteil einiger Entwurfsstrategien für effiziente Algorithmen, insbesondere der Teile-und-herrsche-Strategie (Divide and Conquer). Andere Ansätze (zum Beispiel sogenannte Greedy-Algorithmen) verlangen ein iteratives Vorgehen. Rekursion und primitiv-rekursive Funktionen spielen eine große Rolle in der theoretischen Informatik, insbesondere in der Komplexitätstheorie und Berechenbarkeitstheorie (siehe Lambda-Kalkül und Ackermannfunktion).

Im Compilerbau ist der rekursive Abstieg (Recursive Descent) eine Technik, bei der eine Sprache rekursiv geparst wird.

Beispiele[Bearbeiten]

Die Funktion \operatorname{sum}\colon \mathbb{N}_0 \to \mathbb{N}_0 soll zu jeder Zahl n die Summe der natürlichen Zahlen bis einschließlich n berechnen. Sie ist folgendermaßen definiert:

\operatorname{sum}(n) = \sum_{i=0}^n i.

Um eine gleichwertige rekursive Definition der Summenfunktion zu erhalten, bestimmen wir zunächst den einfachen Fall, den Rekursionsanfang. Im Beispiel handelt es sich um den Funktionswert für 0.

\operatorname{sum}(0) = 0

Übrig bleibt der schwierige Fall, also hier der Funktionswert für n>0. Den schwierigen Fall führen wir auf einen einfacheren Fall zurück, nämlich auf den Fall n-1. Dieser einfachere Fall wird unser rekursiver Aufruf. Die entsprechende Vorschrift heißt Rekursionsschritt. Beispielsweise lässt sich die Summe der natürlichen Zahlen bis einschließlich n berechnen, indem man die Summe der natürlichen Zahlen bis einschließlich n-1 berechnet und dazu die Zahl n addiert:

\operatorname{sum}(n) = \operatorname{sum}(n - 1) + n

Diese beiden Gleichungen lassen sich zu einer rekursiven Definition der Summenfunktion zusammenfassen:

\operatorname{sum}(n) = \left\{\begin{matrix}
0                         && \mbox{falls } n = 0   && \mbox{(Rekursionsanfang)} \\
\operatorname{sum}(n-1)+n && \mbox{sonst} && \mbox{(Rekursionsschritt)}
\end{matrix}\right.

Es handelt sich hierbei um eine lineare Rekursion, denn in jedem der beiden Fälle (Rekursionsanfang und Rekursionsschritt) gibt es höchstens einen sum-Aufruf. Es ist sogar eine primitive Rekursion. Bei der dargestellten Definition handelt es sich um keine Endrekursion, denn nach dem rekursiven Aufruf \operatorname{sum}(n-1) muss noch n addiert werden.

Die Summe der Zahlen von 0 bis 3 berechnet sich dann wie folgt:

\begin{matrix}
\operatorname{sum}(3) & = & \operatorname{sum}(2)+3     & \mbox{(Rekursionsschritt)} \\
                      & = & \operatorname{sum}(1)+2+3   & \mbox{(Rekursionsschritt)} \\
                      & = & \operatorname{sum}(0)+1+2+3 & \mbox{(Rekursionsschritt)} \\
                      & = & 0+1+2+3                     & \mbox{(Rekursionsanfang)} \\
                      & = & 6
\end{matrix}

Die Aufruf-Kette dazu sieht so aus:

\operatorname{sum}(3) \to \operatorname{sum}(2) \to \operatorname{sum}(1) \to \operatorname{sum}(0).

Es gibt auch eine Charakterisierung der Summenfunktion ohne Rekursion: die Gaußsche Summenformel besagt, dass \operatorname{sum}(n) = \frac{n(n+1)}{2}.

Ein anderes klassisches Beispiel für eine rekursive Funktion ist die Fibonacci-Folge

0, 1, 1, 2, 3, 5, 8, 13, 21, 34, \dotsc

Die Fibonacci-Funktion \operatorname{fib}\colon \mathbb{N}_0 \to \mathbb{N}_0, die jedem n die n-te Fibonacci-Zahl zuordnet, hat die einfachen Fälle \operatorname{fib}(0) = 0 und \operatorname{fib}(1) = 1 und genügt der Rekursionsgleichung

\operatorname{fib}(n) = \operatorname{fib}(n-1) + \operatorname{fib}(n-2) für n>1.

Es ergibt sich die rekursive Definition:

\operatorname{fib}(n) = \left\{\begin{matrix}
0                         && \mbox{falls } n = 0   && \mbox{(Rekursionsanfang)} \\
1                         && \mbox{falls } n = 1   && \mbox{(Rekursionsanfang)} \\
\operatorname{fib}(n-1)+\operatorname{fib}(n-2) && \mbox{sonst} && \mbox{(Rekursionsschritt)}
\end{matrix}\right.

Diese rekursive Definition ist kaskadenförmig. Die dritte Fibonacci-Zahl wird folgendermaßen berechnet:

\begin{matrix}
\operatorname{fib}(3) & = & \operatorname{fib}(2)+\operatorname{fib}(1)    & \mbox{(Rekursionsschritt)} \\
                      & = & \operatorname{fib}(1)+\operatorname{fib}(0)+\operatorname{fib}(1)    & \mbox{(Rekursionsschritt)} \\
                      & = & 1+\operatorname{fib}(0)+\operatorname{fib}(1) & \mbox{(Rekursionsanfang)} \\
                      & = & 1+0+\operatorname{fib}(1)                     & \mbox{(Rekursionsanfang)} \\
                      & = & 1+0+1                     & \mbox{(Rekursionsanfang)} \\
                      & = & 2
\end{matrix}

Die Berechnung für \operatorname{fib}(1) wird hier mehrfach durchgeführt. Das deutet an, dass es Potential für Optimierungen gibt. Auch für die Fibonacci-Funktion gibt es einen gleichwertigen geschlossenen Ausdruck.

Programmierbeispiel I, Methodik rekursiver Implementierung[Bearbeiten]

iterative Implementierung[Bearbeiten]

Im folgenden Beispiel wird eine Zeichenkette von Offset 0 bis Offset n implementativ iterativ durchlaufen. Die Abbruchbedingung ist erfüllt, wenn der Iterator auf den Nullterminator der Zeichenkette stößt.

 char* strKette = "foobar",
     * pTemp    = strKette;
 
 while( *pTemp != 0x0 )
 {
   // z. B. Buchstabe für Buchstabe ausgeben
   putchar(*pTemp);
   ++pTemp;
 };

rekursive Implementierung[Bearbeiten]

Jedoch lässt sich die Iteration einer Zeichenkette auch implementativ rekursiv darstellen, auch wenn die Aufgabenstellung nicht implizit rechnerisch rekursiv ist.

 void fnTraverse( char* pString )
 {
   if( *pString == 0x0 )
     return;
   else
     // z. B. Buchstabe für Buchst. ausgeben
     putchar(*pString);
     fnTraverse( ++pString );
 }

Programmierbeispiel II, Fakultätsberechnung[Bearbeiten]

Das Beispiel zeigt eine beliebte und einfache Implementierung der Fakultätsberechnung mittels Pseudocode. Der rekursiven Variante wird hier zur Verdeutlichung eine iterative Variante gegenübergestellt. Dabei ist n die Zahl, deren Fakultät berechnet werden soll.

fakultät_rekursiv(n)
{
  wenn n <= 1
      dann return 1
      sonst return ( n * fakultät_rekursiv(n-1) )
}

Die Rekursion kommt in der vorletzten Zeile zum Ausdruck, wo die Funktion sich selbst mit einem um 1 verringerten Argument aufruft. Im nächsten Beispiel wird die Funktion nur einmal aufgerufen und arbeitet dann linear den gegebenen Algorithmus ab:

fakultät_iterativ(n)
{
    fakultät = 1
    faktor = 2
    solange faktor <= n
    {
        fakultät = fakultät * faktor
        faktor = faktor + 1
    }
    return fakultät
}

Rekursive Grafiken[Bearbeiten]

Nicht nur Funktionen, sondern auch Punktmengen können rekursiv definiert werden (Fraktale). Deren graphische Darstellung liefert ästhetisch ansprechende, natürlich aussehende Gebilde. Ein Beispiel ist der rekursive Pythagoras-Baum. Der dazugehörige Algorithmus sieht folgendermaßen aus:

  • Errichte über zwei gegebenen Punkten ein Quadrat.
  • Auf der Oberseite zeichne ein Dreieck mit definierten Winkeln bzw. Höhe.
  • Rufe diese Funktion für die beiden Schenkel dieses Dreieckes auf.

Der Algorithmus wird dann bis zu einer vorgegebenen Rekursionstiefe entfaltet. Bei Rekursionstiefe eins entsteht ein Dreieck mit je einem Quadrat über den drei Seiten. Das sieht wie die Illustration zum Satz des Pythagoras aus - daher der Name. Je höher die Rekursionstiefe, desto mehr ähnelt das Gebilde einem Baum.

Lösen von Rekursionen[Bearbeiten]

Beim Lösen einer Rekursion sucht man zum einen den Laufzeitaufwand, zum anderen die explizite Form der Rekursion.

Der Aufwand kann als asymptotische Θ- bzw. Ο-Schranke mittels Mastertheorem bzw. Substitutionsmethode bestimmt werden. Auch das geschickte Raten mit anschließender Induktion bietet eine Möglichkeit, eine Oberschranke der Laufzeit zu ermitteln.

Die explizite Form (oder auch geschlossene Form genannt) der Rekursionsgleichung lässt sich beispielsweise durch die Erzeugende-Funktion finden. Eine zweite Möglichkeit bietet das Ableiten durch Differenzenbildung aufeinanderfolgender Funktionswerte der Rekurrenz.

Siehe auch[Bearbeiten]

Weblinks[Bearbeiten]

 Wiktionary: Rekursion – Bedeutungserklärungen, Wortherkunft, Synonyme, Übersetzungen
 Wiktionary: rekursiv – Bedeutungserklärungen, Wortherkunft, Synonyme, Übersetzungen