2013-03-21 22 views
14

Ho una domanda su come convertire la "ricorsione" in "ricorsione della coda". questo non è un compito a casa, solo una domanda si apre quando ho provato a lucidare il teorema di ricorsione dal libro dell'algoritmo. Ho familiarità con i 2 tipici esempi di utilizzo della ricorsione (sequenza fattoriale e Fibonacci) e so anche come implementarli in modo ricorsivo e in modo ricorsivo alla coda. Il mio codice è il seguente (io uso Perl solo per rendere semplice, ma può essere convertite facilmente C/Java/C++)convertire la ricorsione in "ricorsione della coda"

#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"; 
} 

Quando si esegue il codice, l'uscita come di seguito:

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 

la funzione ricorsiva si richiama due volte con parametri diversi prima del ritorno; Ho provato diversi modi per convertire questo in un modo ricorsivo in coda ma tutto si rivela sbagliato.

Qualcuno può dare un'occhiata al codice e mostrarmi il modo corretto per renderlo ricorsivo in coda? Soprattutto credo che ci sia una routine per la conversione per questa ricorsione dell'albero (invocando la funzione ricorsiva più volte prima del ritorno), può qualche accenno di luce su questo? Quindi posso usare la stessa logica per gestire domande diverse più tardi. grazie in anticipo.

+1

vero ricorsione di coda richiede di gestire sia la pila da soli, scriverlo in assemblea, o avere il supporto del compilatore. Non credo che Perl attualmente lo supporti. – Dai

+2

Penso che @Dai sei confuso con la ricorsione della coda _removal_, che è un'ottimizzazione del compilatore. Stavo solo chiedendo di convertire in una funzione ricorsiva della coda, penso. – Gene

risposta

17

Anche se spesso si vede il seguente come un esempio di conversione fattoriale alla coda-call:

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

non è del tutto corretto, in quanto richiede la moltiplicazione di essere sia associativa e commutativa. (Moltiplicazione è associativa e commutativa, ma quanto sopra non servire da modello per altre operazioni che non soddisfano tali vincoli.) Una soluzione migliore potrebbe essere:

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

Questo serve anche come un modello per il Fibonacci trasformare:

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

Si noti che questi calcolare la sequenza a partire dall'inizio, al contrario di fare la fila in attesa di continuazioni in uno stack di chiamate. Quindi sono strutturalmente più simili alla soluzione iterativa rispetto alla soluzione ricorsiva. A differenza del programma iterativo, però, non modificano mai alcuna variabile; tutti i collegamenti sono costanti.Questa è una proprietà interessante e utile; in questi casi semplici non fa molta differenza, ma la scrittura di codice senza riassegnazioni semplifica alcune ottimizzazioni del compilatore.

Ad ogni modo, l'ultimo fornisce un modello per la funzione ricorsiva; come la sequenza di Fibonacci, abbiamo bisogno di mantenere le relative valori del passato, ma abbiamo bisogno di tre di loro invece di due:

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

Nei commenti, due questioni sono state sollevate. Proverò a risponderle (e un'altra ancora) qui.

In primo luogo, dovrebbe essere chiaro (da una considerazione dell'architettura della macchina sottostante che non ha concetto di chiamata di funzione) che qualsiasi chiamata di funzione può essere riformulata come goto (possibilmente con memoria intermedia non limitata); inoltre, qualsiasi goto può essere espresso come un tail-call. Quindi è possibile (ma non necessariamente carino) riscrivere qualsiasi ricorsione come ricorsione in coda.

Il solito meccanismo è "stile di passaggio continuo" che è un modo elegante per dire che ogni volta che si desidera chiamare una funzione, si impacchetta invece il resto della funzione corrente come una nuova funzione (la "continuazione") e passare quella continuazione alla funzione chiamata. Poiché ogni funzione riceve una continuazione come argomento, deve terminare ogni continuazione creata con una chiamata alla continuazione ricevuta.

Questo è probabilmente sufficiente per far girare la testa, quindi lo metto in un altro modo: invece di spingere gli argomenti e una posizione di ritorno nello stack e chiamando una funzione (che poi tornerà), si spingono gli argomenti e una continuazione posizione sullo stack e goto una funzione, che successivamente andrà alla posizione di continuazione. In breve, devi semplicemente rendere lo stack un parametro esplicito, e quindi non hai mai bisogno di tornare. Questo stile di programmazione è comune nel codice event-driven (vedi Python Twisted), ed è un vero dolore scrivere (e leggere). Quindi consiglio vivamente di lasciare che i compilatori eseguano questa trasformazione per te, se puoi trovarne uno che lo farà.

@xxmouse mi ha suggerito di estrarre l'equazione di ricorsione da un cappello e di chiedere come è stata ricavata. E 'semplicemente la ricorsione originale, ma riformulato come una trasformazione di una singola tupla:

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

Non so se questo è più chiaro, ma è il meglio che posso fare. Guarda l'esempio di Fibonacci per un caso leggermente più semplice.

@j_random_hacker chiede quali sono i limiti di questa trasformazione. Funziona per una sequenza ricorsiva in cui ogni elemento può essere espresso da una formula degli elementi precedenti, dove k è una costante. Esistono altri modi per produrre una ricorsione di coda. Ad esempio:

// 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); 
} 

Quanto sopra non uguale al solito non tail-call ricorsione, che fa una sequenza diversa (ma equivalente e altrettanto lunga) di moltiplicazioni.

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)); 
} 

Grazie a Alexey Frunze per il seguente test: uscita (ideone): la risposta di

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

Risposta molto bella! Penso che la "doppia ricorsione" di Fibonacci possa essere trasformata in pura ricorsione di coda perché questo particolare problema può essere risolto in O (1) -space usando una soluzione iterativa, ma (correggimi se sbaglio) non tutti i problemi che sembrano simile alla ricorsione iniziale di Fibonacci può essere convertito in pura ricorsione di coda nello stesso modo - ad es sommare le foglie di un albero binario in realtà non può essere fatto senza uno stack (implicito o esplicito). È corretto? Se è così, c'è un modo carino per caratterizzare quali problemi sono come Fibonacci in quanto possono essere ridotti alla pura ricorsione di coda? –

+0

Grazie, @ rici, la tua risposta è molto concisa, mi piace. Spiegheresti per favore un po 'come' ti viene in mente la soluzione? per me, la linea in cui ricorsivamente si chiama 'return mouse (n-1, a * c + 1, a, b);' è come una magia, posso vederlo, ma non capisco molto bene come vieni ad arrivare dalla formula ricorsiva originale. Grazie in anticipo! – xxmouse

5

Utilizzando google, ho trovato questa pagina che descrive Tail Recursion. Fondamentalmente, devi dividere la funzione in almeno altre due funzioni: una che fa il lavoro, mantenendo un "accumulo" del valore corrente, e un'altra che è un driver per la tua funzione di workhouse. L'esempio del fattoriale in C è inferiore:

/* 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

Il punto chiave qui è che la chiamata ricorsiva è l'ultima cosa che accade nel driver e quindi la chiamata può essere sostituita con un salto al punto di ingresso della funzione. –

+0

I compilatori Do C ottimizzano questa chiamata di funzione? Io non la penso così È fatto automaticamente in altre lingue, ma in C devi farlo esplicitamente con un qualche tipo di ciclo – comocomocomocomo

+1

@comocomocomocomo: la maggior parte dei compilatori C effettua l'ottimizzazione delle chiamate tail, almeno per le chiamate tail tail. gcc lo fa abbastanza bene, nella mia esperienza. – rici

1

Si potrebbe fare qualcosa di simile:

#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; 
} 

uscita (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 

Il compilatore può trasformare fr() in qualcosa di simile :

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; 
} 

E questa sarebbe l'ottimizzazione della coda.

+0

Grazie, Alexey, finora questa è la migliore risposta che ho, almeno hai fornito un codice lavorabile e verificabile. – xxmouse

+1

La risposta di rici è più vicina a quello che stai cercando e funziona altrettanto bene. Gli ho dato +1. –

+0

La richiesta nel tuo secondo paragrafo è sbagliata, ho paura - vedi la risposta di Tosi per vedere un esempio di come convertire la ricorsione non-coda dell'OP alla ricorsione in coda con un parametro dell'accumulatore. –

3

@Alexey Frunze è male, ma non proprio giusto. È infatti possibile convertire qualsiasi programma in uno in cui tutta la ricorsione è la ricorsione in coda trasformandola in Continuation Passing Style.

Non ho tempo in questo momento ma proverò a ri-implementare il tuo programma in CPS se avrò qualche minuto.

+1

Vedi la risposta di rici. –

1

Il problema è che l'ultima operazione non è una delle chiamate ricorsive, ma l'aggiunta di 1 alla moltiplicazione. La funzione in C:

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

Se si modifica l'ordine in cui vengono richiesti i valori, è possibile attivare una delle chiamate in un ciclo:

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; 
} 

La chiave sta pensando l'ordine in quali valori sono effettivamente calcolati nella funzione originale, invece dell'ordine in cui sono richiesti.

Si presume che si desideri rimuovere una chiamata ricorsiva. Se vuoi scrivere la chiamata ricorsiva alla fine della funzione aspettando che il compilatore la ottimizzi, guarda le altre risposte.

Benchè, "La Cosa destro (TM)" fare qui è usare programmazione dinamica per evitare calcolare gli stessi valori molte volte:

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]; 
} 

La matrice A contiene una finestra scorrevole della sequenza: A [0] == f (i), A [1] == f (i-1), A [2] == f (i-2) e così via.

Il memmove potrebbe essere stato scritto come:

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