Cos'è la ricorsione della coda?


52

Conosco il concetto generale di ricorsione. Mi sono imbattuto nel concetto di ricorsione della coda mentre studiavo l'algoritmo quicksort. In questo video dell'algoritmo di ordinamento rapido del MIT alle 18:30 secondi il professore afferma che si tratta di un algoritmo ricorsivo di coda. Non mi è chiaro cosa significhi davvero la ricorsione della coda.

Qualcuno può spiegare il concetto con un esempio corretto?

Alcune risposte fornite dalla comunità SO qui .


Raccontaci di più sul contesto in cui hai incontrato il termine ricorsione della coda . Link? Citazione?
A. Schulz,

@ A.Schulz Ho inserito il link al contesto.
Geek,

5
Guarda " Cos'è la ricorsione della coda? " Su StackOverflow
Vor

2
@ajmartin La domanda è al limite su Stack Overflow ma saldamente sull'argomento in Informatica , quindi in linea di principio l' Informatica dovrebbe produrre risposte migliori. Non è successo qui, ma è ancora ok ri-chiedere qui nella speranza di una risposta migliore. Geek, avresti dovuto menzionare la tua precedente domanda su SO, in modo che le persone non ripetano ciò che è già stato detto.
Gilles 'SO- smetti di essere malvagio' il

1
Inoltre dovresti dire qual è la parte ambigua o perché non sei soddisfatto delle risposte precedenti, penso che su SO le persone forniscano buone risposte ma cosa ti ha spinto a chiedere di nuovo?

Risposte:


52

La ricorsione della coda è un caso speciale di ricorsione in cui la funzione chiamante non esegue più il calcolo dopo aver effettuato una chiamata ricorsiva. Ad esempio, la funzione

int f (int x, int y) {
  if (y == 0) {
    ritorna x;
  }

  restituisce f (x * y, y-1);
}

è ricorsivo di coda (poiché l'istruzione finale è una chiamata ricorsiva) mentre questa funzione non è ricorsiva di coda:

int g (int x) {
  if (x == 1) {
    ritorno 1;
  }

  int y = g (x-1);

  ritorna x * y;
}

poiché esegue alcuni calcoli dopo il ritorno della chiamata ricorsiva.

La ricorsione della coda è importante perché può essere implementata in modo più efficiente della ricorsione generale. Quando effettuiamo una normale chiamata ricorsiva, dobbiamo inserire l'indirizzo di ritorno nello stack di chiamate, quindi passare alla funzione chiamata. Ciò significa che abbiamo bisogno di uno stack di chiamate la cui dimensione è lineare nella profondità delle chiamate ricorsive. Quando abbiamo la ricorsione della coda sappiamo che non appena torneremo dalla chiamata ricorsiva, ritorneremo immediatamente anche noi, così possiamo saltare l'intera catena di funzioni ricorsive che ritornano e ritornano direttamente al chiamante originale. Ciò significa che non abbiamo bisogno di uno stack di chiamate per tutte le chiamate ricorsive e possiamo implementare la chiamata finale come un semplice salto, che ci fa risparmiare spazio.


2
hai scritto "Ciò significa che non abbiamo bisogno di uno stack di chiamate per tutte le chiamate ricorsive". Lo stack di chiamate sarà sempre lì, solo che l'indirizzo di ritorno non deve essere scritto nello stack di chiamate, giusto?
Geek

2
Dipende dal tuo modello di calcolo in una certa misura :) Ma sì, su un vero computer lo stack di chiamate è ancora lì, non lo stiamo usando.
Matt Lewis

E se fosse l'ultima chiamata ma dentro per il ciclo. Quindi fai tutti i tuoi calcoli sopra ma alcuni in un ciclo for similedef recurse(x): if x < 0 return 1; for i in range 100{ (do calculations) recurse(x)}
thed0ctor

13

Detto semplicemente, la ricorsione della coda è una ricorsione in cui il compilatore potrebbe sostituire la chiamata ricorsiva con un comando "goto", quindi la versione compilata non dovrà aumentare la profondità dello stack.

A volte la progettazione di una funzione ricorsiva della coda richiede la necessità di creare una funzione di supporto con parametri aggiuntivi.

Ad esempio, questa non è una funzione ricorsiva della coda:

int factorial(int x) {
    if (x > 0) {
        return x * factorial(x - 1);
    }
    return 1;
}

Ma questa è una funzione ricorsiva della coda:

int factorial(int x) {
    return tailfactorial(x, 1);
}

int tailfactorial(int x, int multiplier) {
    if (x > 0) {
        return tailfactorial(x - 1, x * multiplier);
    }
    return multiplier;
}

perché il compilatore potrebbe riscrivere la funzione ricorsiva in una non ricorsiva, usando qualcosa del genere (uno pseudocodice):

int tailfactorial(int x, int multiplier) {
    start:
    if (x > 0) {
        multiplier = x * multiplier;
        x--;
        goto start;
    }
    return multiplier;
}

La regola per il compilatore è molto semplice: quando trovi " return thisfunction(newparameters);", sostituiscilo con " parameters = newparameters; goto start;". Ma questo può essere fatto solo se il valore restituito dalla chiamata ricorsiva viene restituito direttamente.

Se tutte le chiamate ricorsive in una funzione possono essere sostituite in questo modo, si tratta di una funzione ricorsiva della coda.


13

La mia risposta si basa sulla spiegazione fornita nel libro Struttura e interpretazione dei programmi per computer . Consiglio vivamente questo libro agli informatici.

Approccio A: processo ricorsivo lineare

(define (factorial n)
 (if (= n 1)
  1
  (* n (factorial (- n 1)))))

La forma del processo per l' approccio A è simile alla seguente:

(factorial 5)
(* 5 (factorial 4))
(* 5 (* 4 (factorial 3)))
(* 5 (* 4 (* 3 (factorial 2))))
(* 5 (* 4 (* 3 (* 2 (factorial 1)))))
(* 5 (* 4 (* 3 (* 2 (* 1)))))
(* 5 (* 4 (* 3 (* 2))))
(* 5 (* 4 (* 6)))
(* 5 (* 24))
120

Approccio B: processo iterativo lineare

(define (factorial n)
 (fact-iter 1 1 n))

(define (fact-iter product counter max-count)
 (if (> counter max-count)
  product
  (fact-iter (* counter product)
             (+ counter 1)
             max-count)))

La forma del processo per l' approccio B è simile alla seguente:

(factorial 5)
(fact-iter 1 1 5)
(fact-iter 1 2 5)
(fact-iter 2 3 5)
(fact-iter 6 4 5)
(fact-iter 24 5 5)
(fact-iter 120 6 5)
120

Il processo iterativo lineare (approccio B) viene eseguito in uno spazio costante anche se il processo è una procedura ricorsiva. Va anche notato che in questo approccio una serie di variabili definisce lo stato del processo in qualsiasi momento, vale a dire. {product, counter, max-count}. Questa è anche una tecnica con cui la ricorsione della coda consente l'ottimizzazione del compilatore.

Nell'approccio A ci sono più informazioni nascoste che l'interprete mantiene, che è sostanzialmente la catena delle operazioni differite.


5

La ricorsione della coda è una forma di ricorsione in cui le chiamate ricorsive sono le ultime istruzioni nella funzione (ecco da dove proviene la parte di coda). Inoltre, la chiamata ricorsiva non deve essere composta con riferimenti a celle di memoria che memorizzano valori precedenti (riferimenti diversi dai parametri della funzione). In questo modo, non ci importa dei valori precedenti e un frame stack è sufficiente per tutte le chiamate ricorsive; la ricorsione della coda è un modo per ottimizzare gli algoritmi ricorsivi. L'altro vantaggio / ottimizzazione è che esiste un modo semplice per trasformare un algoritmo ricorsivo di coda in un algoritmo equivalente che utilizza l'iterazione anziché la ricorsione. Quindi sì, l'algoritmo per quicksort è davvero ricorsivo alla coda.

QUICKSORT(A, p, r)
    if(p < r)
    then
        q = PARTITION(A, p, r)
        QUICKSORT(A, p, q–1)
        QUICKSORT(A, q+1, r)

Ecco la versione iterativa:

QUICKSORT(A)
    p = 0, r = len(A) - 1
    while(p < r)
        q = PARTITION(A, p, r)
        r = q - 1

    p = 0, r = len(A) - 1
    while(p < r)
        q = PARTITION(A, p, r)
        p = q + 1
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.