2013-03-21 22 views
14

Ich habe eine Frage darüber, wie 'Rekursion' in 'Tail-Rekursion' konvertieren. das ist keine Hausaufgabe, nur eine Frage taucht auf, als ich versuchte, das Rekursionstheorem vom Algorithmus Buch zu polieren. Ich bin vertraut mit den 2 typischen Beispielen für die Verwendung von Rekursion (faktorielle und Fibonacci-Sequenz) und weiß auch, wie man sie rekursiv und tail-rekursiv implementiert. Mein Code ist wie unten (I Perl benutzen Sie einfach, es einfach zu machen, kann aber leicht zu C/Java/C++ werden konvertieren)konvertieren Rekursion zu 'Tail-Rekursion'

#this is the recursive function 
sub recP { 
    my ($n) = @_; 
    if ($n==0 or $n==1 or $n==2) { 
     return 1; 
    } else { 
     return (recP($n-3)*recP($n-1))+1; 
    } 

} 
for (my $k=1;$k<10;$k++) { 
    print "*"x10,"\n"; 
    print "recP($k)=", recP($k), "\n"; 
} 

Bei der Ausführung des Codes, der Ausgang wie folgt:

recP(1)=1 
recP(2)=1 
recP(3)=2 
recP(4)=3 
recP(5)=4 
recP(6)=9 
recP(7)=28 
recP(8)=113 
recP(9)=1018 

Die rekursive Funktion ruft sich vor der Rückgabe zweimal mit einem anderen Parameter auf; Ich habe mehrere Möglichkeiten versucht, dies in einen tail rekursiven Weg zu konvertieren, aber alles wird falsch.

Kann jemand den Code ansehen und mir den richtigen Weg zeigen, ihn tailrekursiv zu machen? besonders glaube ich, dass es eine Routine für die Umwandlung für diese Baum-Rekursion gibt (rekursive Funktion mehrfach vor der Rückkehr aufrufen), kann irgendjemand etwas Licht darüber werfen? So kann ich die gleiche Logik verwenden, um später verschiedene Fragen zu behandeln. danke im voraus.

+1

wahre Endrekursion Sie erfordert entweder selbst den Stapel zu verarbeiten, schreiben Sie es in der Montage oder Compiler-Unterstützung haben. Ich glaube nicht, dass Perl es derzeit unterstützt. – Dai

+2

Ich denke @Dai sind Sie mit Tail-Rekursion _removal_ verwirrt, die eine Compiler-Optimierung ist. Er fragt nur, ob ich zu einer tailrekursiven Funktion übergehen soll, denke ich. – Gene

Antwort

17

Obwohl man oft die folgenden als Beispiel faktorielles zu Tail-Call der Umwandlung:

int factorial(int n, int acc=1) { 
    if (n <= 1) return acc; 
    else  return factorial(n-1, n*acc); 
} 

es ist nicht ganz richtig, da es Multiplikation erfordert sowohl assoziativ und kommutativ zu sein. (Multiplication ist assoziativ und kommutativ, aber die oben nicht für andere Operationen als Modell nicht dazu dienen, die diese Einschränkungen nicht erfüllen.) Eine bessere Lösung wäre:

int factorial(int n, int k=1, int acc=1) { 
    if (n == 0) return acc; 
    else  return factorial(n-1, k+1, acc*k); 
} 

Dies auch als Modell dient, für die Fibonacci-Transformation:

int fibonacci(int n, int a=1, int b=0) { 
    if (n == 0) return a; 
    else  return fibonacci(n-1, a+b, a); 
} 

beachten, dass diese die Sequenz berechnen am Anfang beginnen, wie zum Warteschlange anhängige Fortsetzungen in einer Aufrufliste entgegengesetzt. Sie sind strukturell eher der iterativen Lösung als der rekursiven Lösung. Im Gegensatz zum iterativen Programm ändern sie jedoch niemals eine Variable. Alle Bindungen sind konstant.Dies ist eine interessante und nützliche Eigenschaft; In diesen einfachen Fällen macht es keinen großen Unterschied, aber das Schreiben von Code ohne Neuzuweisung erleichtert einige Compiler-Optimierungen.

Wie auch immer, die letzte bietet ein Modell für Ihre rekursive Funktion; wie die Fibonacci-Sequenz, müssen wir die relevanten Vergangenheitswerte halten, aber wir müssen drei von ihnen statt zwei:

int mouse(int n, int a=1, int b=1, int c=1) { 
    if (n <=2) return a; 
    else  return mouse(n-1, a*c+1, a, b); 
} 

Addenda

In Kommentaren wurden zwei Fragen aufgeworfen. Ich werde versuchen, sie (und einen weiteren) hier zu beantworten.

Zunächst sollte klar sein (aus einer Betrachtung der zugrunde liegenden Maschinenarchitektur, die kein Konzept des Funktionsaufrufs hat), dass jeder Funktionsaufruf als ein Goto umformuliert werden kann (möglicherweise mit nicht beschränktem Zwischenspeicher); Darüber hinaus kann jeder Goto als Tail-Call ausgedrückt werden. Es ist also möglich (aber nicht unbedingt schön), Rekursionen als Tail-Rekursion neu zu schreiben.

Der übliche Mechanismus ist "Continuation-Passing-Stil", die eine phantastische Art zu sagen, dass jedes Mal, wenn Sie eine Funktion aufrufen möchten, Sie stattdessen den Rest der aktuellen Funktion als eine neue Funktion (die "Fortsetzung") und übertrage diese Fortsetzung auf die aufgerufene Funktion. Da jede Funktion dann eine Fortsetzung als Argument erhält, muss sie jede Fortsetzung beenden, die sie mit einem Aufruf der Fortsetzung, die sie erhalten hat, erzeugt.

Das ist wahrscheinlich genug, um Ihren Kopf zu drehen, also werde ich es anders ausdrücken: Statt Argumente und eine Rückkehrposition auf den Stapel zu schieben und eine Funktion aufzurufen (die später zurückkehrt), drücken Sie Argumente und eine Fortsetzung Stelle auf den Stapel und gehe zu einer Funktion, die später zum Fortsetzungsort führt. Kurz gesagt, Sie machen den Stapel einfach zu einem expliziten Parameter, und dann müssen Sie nie mehr zurückkehren. Diese Art der Programmierung ist im ereignisgesteuerten Code üblich (siehe Python Twisted), und es ist ein echter Schmerz, zu schreiben (und zu lesen). Daher empfehle ich dringend, Compiler diese Transformation für Sie durchführen zu lassen, wenn Sie eine finden, die das tun wird.

@xxmouse vorgeschlagen, dass ich die Rekursion Gleichung aus einem Hut gezogen, und fragte, wie es abgeleitet wurde. Es ist einfach das Original Rekursion, sondern als Transformation eines einzelnen Tupel neu formuliert:

fn = fn-1*fn-3 + 1
=>
Fn = <Fn-11*Fn-13+1, Fn-11, Fn-12>

weiß ich nicht, ob das noch klarer, aber es ist das Beste, was ich tun kann. Sehen Sie sich das Beispiel mit den Fibonacci an, für einen etwas einfacheren Fall.

@j_random_hacker fragt, was die Grenzen dieser Transformation sind. Es funktioniert für eine rekursive Sequenz, in der jedes Element durch eine Formel der vorhergehenden k Elemente ausgedrückt werden kann, wobei k eine Konstante ist. Es gibt andere Möglichkeiten, eine Tail-Call-Rekursion zu erzeugen. Zum Beispiel:

// For didactic purposes only 
bool is_odd(int n) { return n%2 == 1; } 

int power(int x, int n, int acc=1) { 
    if (n == 0)   return acc; 
    else if (is_odd(n)) return power(x, n-1, acc*x); 
    else    return power(x*x, n/2, acc); 
} 

Obiges ist nicht das gleiche wie die übliche nicht-tail-Call-Rekursion, die ein anderes tut (aber gleichwertige und gleich lang) Folge von Multiplikationen.

int squared(n) { return n * n; } 

int power(int x, int n) { 
    if (n == 0)   return 1; 
    else if (is_odd(n)) return x * power(x, n-1)); 
    else    return squared(power(x, n/2)); 
} 

Dank Alexey Frunse für den folgenden Test: Ausgang (ideone):

mouse(0) = 1 
mouse(1) = 1 
mouse(2) = 1 
mouse(3) = 2 
mouse(4) = 3 
mouse(5) = 4 
mouse(6) = 9 
mouse(7) = 28 
mouse(8) = 113 
mouse(9) = 1018 
+0

Sehr nette Antwort! Ich denke, die Fibonacci "Doppelrekursion" kann in reine Tailrekursion umgewandelt werden, da dieses spezielle Problem in O (1) -Raum gelöst werden kann, indem eine iterative Lösung verwendet wird, aber (korrigieren Sie mich, wenn ich falsch liege) nicht alle Probleme, die aussehen ähnlich wie die anfängliche Fibonacci-Rekursion kann auf die gleiche Weise in reine Tail-Rekursion umgewandelt werden - z das Summieren der Blätter eines Binärbaums kann wirklich nicht ohne einen (impliziten oder expliziten) Stapel erfolgen. Ist das richtig? Wenn ja, gibt es einen schönen Weg, um zu charakterisieren, welche Probleme wie Fibonacci sind, da sie auf reine Tail-Rekursion reduziert werden können? –

+0

Danke, @ rici, deine Antwort ist sehr kurz, ich mag es. Würden Sie bitte ein bisschen erklären, wie Sie die Lösung finden? zu mir, die Zeile, wo rekursiv aufrufen 'Maus zurück (n-1, a * c + 1, a, b);' ist wie eine Magie, ich kann es sehen, aber nicht sehr viel verstehen, wie bist du aus der ursprünglichen rekursiven Formel dazu gekommen. Danke im Voraus! – xxmouse

5

Mit Google fand ich diese Seite, die Tail Recursion beschreibt. Im Grunde müssen Sie die Funktion in mindestens zwei andere Funktionen aufteilen: eine, die die Arbeit erledigt, eine "Akkumulation" des aktuellen Wertes und eine andere, die ein Treiber für Ihre Workhouse-Funktion ist. Das Fakultäts Beispiel in C ist unter:

/* not tail recursive */ 
unsigned int 
factorial1(unsigned int n) 
{ 
    if(n == 0) 
     return 1; 
    return n * factorial1(n-1); 
} 

/* tail recursive version */ 
unsigned int 
factorial_driver(unsigned int n, unsigned int acc) 
{ 
    if(n == 0) 
     return acc; 

    /* notice that the multiplication happens in the function call */ 
    return factorial_driver(n - 1, n * acc); 
} 

/* driver function for tail recursive factorial */ 
unsigned int 
factorial2(unsigned int n) 
{ 
    return factorial_driver(n, 1); 
} 
+1

Der entscheidende Punkt hier ist, dass der rekursive Aufruf die allerletzte Sache ist, die im Treiber passiert, und so kann der Aufruf durch einen Sprung zum Eintrittspunkt der Funktion ersetzt werden. –

+0

Optimieren C-Compiler den Funktionsaufruf? Ich denke nicht. Es wird automatisch in anderen Sprachen gemacht, aber in C müssen Sie es explizit mit einer Art von Schleife – comocomocomocomo

+1

@comocomocomocomo tun: Die meisten C-Compiler tun Schwanz Anruf-Optimierung, zumindest für einfache Tail-Anrufe. gcc macht es ziemlich gut, nach meiner Erfahrung. – rici

1

Sie so etwas tun könnte:

#include <stdio.h> 

void fr(int n, int a[]) 
{ 
    int tmp; 

    if (n == 0) 
    return; 

    tmp = a[0] * a[2] + 1; 
    a[2] = a[1]; 
    a[1] = a[0]; 
    a[0] = tmp; 

    fr(n - 1, a); 
} 

int f(int n) 
{ 
    int a[3] = { 1, 1, 1 }; 

    if (n <= 2) 
    return 1; 

    fr(n - 2, a); 

    return a[0]; 
} 

int main(void) 
{ 
    int k; 
    for (k = 0; k < 10; k++) 
    printf("f(%d) = %d\n", k, f(k)); 
    return 0; 
} 

Ausgang (ideone):

f(0) = 1 
f(1) = 1 
f(2) = 1 
f(3) = 2 
f(4) = 3 
f(5) = 4 
f(6) = 9 
f(7) = 28 
f(8) = 113 
f(9) = 1018 

Der Compiler kann fr() in so etwas wie diese Transformation :

void fr(int n, int a[]) 
{ 
    int tmp; 

label:  

    if (n == 0) 
    return; 

    tmp = a[0] * a[2] + 1; 
    a[2] = a[1]; 
    a[1] = a[0]; 
    a[0] = tmp; 

    n--; 

    goto label; 
} 

Und das wäre Tail-Call-Optimierung.

+0

Danke, Alexey, bis jetzt ist das die beste Antwort, die ich habe, zumindest hast du bearbeitbaren, überprüfbaren Code zur Verfügung gestellt. – xxmouse

+1

Ricis Antwort ist näher an dem, wonach Sie suchen und es funktioniert auch. Ich gab ihm +1. –

+0

Die Behauptung in Ihrem zweiten Absatz ist falsch Ich fürchte - siehe Tosis Antwort, um ein Beispiel zu sehen, wie man die Nicht-Tail-Rekursion des OPs in Tail-Rekursion mit einem Akkumulator-Parameter umwandelt. –

3

@Alexey Frunse Antwort in Ordnung, aber nicht genau richtig. Es ist tatsächlich möglich, ein beliebiges Programm in ein Programm umzuwandeln, in dem die gesamte Rekursion eine Tail-Rekursion ist, indem es in Continuation Passing Style umgewandelt wird.

Ich habe gerade keine Zeit, aber werde versuchen, Ihr Programm in CPS zu re-implementieren, wenn ich ein paar Minuten bekomme.

+1

Siehe Ricis Antwort. –

1

Das Problem ist, dass die letzte Operation nicht einer der rekursiven Aufrufe ist, sondern die Addition von 1 zu der Multiplikation. Ihre Funktion in C:

unsigned faa (int n) // Ordinary recursion 
{ 
    return n<3 ? 1 : 
       faa(n-3)*faa(n-1) + 1; // Call, call, multiply, add 
} 

Wenn Sie die Reihenfolge ändern, in der die Werte angefordert werden, können Sie einen der Anrufe in eine Schleife drehen:

unsigned foo (int n) // Similar to tail recursion 
{      // (reverse order) 
    int i; 
    unsigned f; 

    for (i=3, f=1; i<=n; i++) 
     f = f*foo(i-3) + 1; 

    return f; 
} 

Der Schlüssel über die Reihenfolge denkt in welche Werte tatsächlich in der ursprünglichen Funktion berechnet werden, anstatt in der Reihenfolge, in der sie angefordert werden.

Beachten Sie, dass ich davon ausgehe, dass Sie einen rekursiven Aufruf entfernen möchten. Wenn Sie den rekursiven Aufruf am Ende der Funktion schreiben möchten und erwarten, dass der Compiler ihn für Sie optimiert, lesen Sie die anderen Antworten.

Obwohl "The Right Thing (TM)" hier zu tun ist, die dynamische Programmierung zu verwenden, um die gleichen Werte oft zu vermeiden Berechnung:

unsigned fuu (int n) // Dynamic programming 
{ 
    int i; 
    unsigned A[4]={1,1,1,1}; 

    for (i=3; i<=n; i++) 
    { 
     memmove (A+1, A, 3*sizeof(int)); 
     A[0] = A[1]*A[3] + 1; 
    } 

    return A[0]; 
} 

Die Array A enthält ein Schiebefenster der Sequenz: A [0] == f (i), A [1] == f (i-1), A [2] == f (i-2) und so weiter.

The memmove geschrieben worden sein könnte als:

 A[3] = A[2]; 
     A[2] = A[1]; 
     A[1] = A[0];