Modo di andare dalla ricorsione all'iterazione


349

Ho usato molto la ricorsione nei miei molti anni di programmazione per risolvere problemi semplici, ma sono pienamente consapevole che a volte hai bisogno di iterazioni a causa di problemi di memoria / velocità.

Quindi, in un lontano passato, sono andato a cercare di scoprire se esistesse qualche "modello" o un modo da manuale per trasformare un approccio di ricorsione comune all'iterazione e non ho trovato nulla. O almeno nulla che io possa ricordare sarebbe di aiuto.

  • Ci sono regole generali?
  • C'è uno "schema"?

Risposte:


334

Di solito, sostituisco un algoritmo ricorsivo con un algoritmo iterativo spingendo su uno stack i parametri che normalmente verrebbero passati alla funzione ricorsiva. In effetti, stai sostituendo lo stack del programma con uno tuo.

Stack<Object> stack;
stack.push(first_object);
while( !stack.isEmpty() ) {
   // Do something
   my_object = stack.pop();

  // Push other objects on the stack.

}

Nota: se al suo interno sono presenti più chiamate ricorsive e si desidera conservare l'ordine delle chiamate, è necessario aggiungerle nell'ordine inverso allo stack:

foo(first);
foo(second);

deve essere sostituito da

stack.push(second);
stack.push(first);

Modifica: l'articolo Stacks and Recursion Elimination (o collegamento Backup articolo ) approfondisce questo argomento.


4
Se sostituisci il tuo stack con un figlio in coda, questo non risolve il problema di invertire l'ordine di aggiunta?
Samuel Warren,

2
L'ho elaborato su carta e sono due cose diverse. Se inverti l'ordine che li hai aggiunti, ti farà avanzare come al solito, ma il tuo attraversamento è ancora una ricerca approfondita. Ma se cambi l'intera cosa in una coda ora stai attraversando in senso ampio anziché in profondità.
pete,

1
Di recente l'ho fatto in modo generale, sostituendo la mia funzione di visita al nodo (node)->()con (node)->[actions]dove si trova l'azione () -> [actions]. Quindi all'esterno, fai semplicemente scattare un'azione / continuazione dallo stack, applicalo / eseguilo, spingi le azioni che ha restituito sullo stack in ordine inverso e ripeti. Traversate contingenti / complesse, catturi solo quelle che sarebbero state variabili dello stack locale in puntatori conteggio di riferimento che chiudi nei tuoi thunk, quindi i thunk successivi possono dipendere dai risultati dei precedenti sub-traversali ecc.
esperimento

6
A volte evitiamo la ricorsione per evitare lo stackoverflow. Ma mantenere il nostro stack causerà anche lo stackoverflow. Quindi perché vogliamo implementare la ricorsione con il nostro stack?
Zhu Li,

8
@ZhuLi Se utilizziamo new, possiamo creare un oggetto nell'heap anziché nello stack. A differenza dello stack, heap non ha restrizioni di memoria. Vedi gribblelab.org/CBootCamp/7_Memory_Stack_vs_Heap.html
yuqli,

77

Davvero, il modo più comune per farlo è mantenere il proprio stack. Ecco una funzione ricorsiva quicksort in C:

void quicksort(int* array, int left, int right)
{
    if(left >= right)
        return;

    int index = partition(array, left, right);
    quicksort(array, left, index - 1);
    quicksort(array, index + 1, right);
}

Ecco come potremmo renderlo iterativo mantenendo il nostro stack:

void quicksort(int *array, int left, int right)
{
    int stack[1024];
    int i=0;

    stack[i++] = left;
    stack[i++] = right;

    while (i > 0)
    {
        right = stack[--i];
        left = stack[--i];

        if (left >= right)
             continue;

        int index = partition(array, left, right);
        stack[i++] = left;
        stack[i++] = index - 1;
        stack[i++] = index + 1;
        stack[i++] = right;
    }
}

Ovviamente, questo esempio non controlla i limiti dello stack ... e davvero potresti ridimensionare lo stack in base al caso peggiore dato dai valori destro e sinistro. Ma hai avuto l'idea.


1
Qualche idea su come elaborare lo stack massimo da allocare per una ricorsione particolare?
lessicalscope

@lexicalscope supponiamo che tu abbia un algoritmo ricorsivo O(N) = O(R*L), dove Lè la somma della complessità "per il livello r", ad esempio in questo caso hai un O(N)lavoro ad ogni passo che fa i partizionamenti, la profondità ricorsiva è O(R), nel caso peggiore O(N), il caso medio O(logN)qui.
Caleth,

48

Sembra che nessuno abbia affrontato dove la funzione ricorsiva si chiama più di una volta nel corpo e gestisce il ritorno a un punto specifico della ricorsione (cioè non primitivo-ricorsivo). Si dice che ogni ricorsione può essere trasformata in iterazione , quindi sembra che ciò dovrebbe essere possibile.

Ho appena avuto un esempio in C # di come farlo. Supponiamo di avere la seguente funzione ricorsiva, che agisce come un attraversamento postordine e che AbcTreeNode è un albero a 3 assi con puntatori a, b, c.

public static void AbcRecursiveTraversal(this AbcTreeNode x, List<int> list) {
        if (x != null) {
            AbcRecursiveTraversal(x.a, list);
            AbcRecursiveTraversal(x.b, list);
            AbcRecursiveTraversal(x.c, list);
            list.Add(x.key);//finally visit root
        }
}

La soluzione iterativa:

        int? address = null;
        AbcTreeNode x = null;
        x = root;
        address = A;
        stack.Push(x);
        stack.Push(null)    

        while (stack.Count > 0) {
            bool @return = x == null;

            if (@return == false) {

                switch (address) {
                    case A://   
                        stack.Push(x);
                        stack.Push(B);
                        x = x.a;
                        address = A;
                        break;
                    case B:
                        stack.Push(x);
                        stack.Push(C);
                        x = x.b;
                        address = A;
                        break;
                    case C:
                        stack.Push(x);
                        stack.Push(null);
                        x = x.c;
                        address = A;
                        break;
                    case null:
                        list_iterative.Add(x.key);
                        @return = true;
                        break;
                }

            }


            if (@return == true) {
                address = (int?)stack.Pop();
                x = (AbcTreeNode)stack.Pop();
            }


        }

5
È davvero utile, ho dovuto scrivere una versione iterativa di reccurence che si rievoca n-volte, grazie al tuo post l'ho fatto.
Wojciech Kulik,

1
Questo deve essere il miglior esempio che io abbia mai visto di emulare la ricorsione dello stack di chiamate per situazioni in cui vengono effettuate più chiamate ricorsive all'interno del metodo. Bel lavoro.
CCS

1
Mi avevi in ​​"Sembra che nessuno abbia affrontato dove la funzione ricorsiva si chiama più di una volta nel corpo, e gestisce il ritorno a un punto specifico della ricorsione" e poi ho già votato. OK, ora leggerò il resto della tua risposta e vedrò se il mio voto prematuro era giustificato. (Perché ho un disperato bisogno di conoscere la risposta a questo).
mydoghasworms,

1
@mydoghasworms - Ritornando a questa domanda dopo così tanto tempo, mi ci è voluto anche un momento per ricordare cosa stavo pensando. Spero che la risposta abbia aiutato.
T. Webster,

1
Mi è piaciuta l'idea di questa soluzione, ma mi è sembrata confusa. Ho scritto una versione semplificata per l'albero binario in python, forse aiuterà qualcuno a capire l'idea: gist.github.com/azurkin/abb258a0e1a821cbb331f2696b37c3ac
azurkin

33

Sforzati di effettuare la tua chiamata ricorsiva Tail Recursion (ricorsione in cui l'ultima affermazione è la chiamata ricorsiva). Una volta che hai quello, convertirlo in iterazione è generalmente abbastanza facile.


2
Qualche ricorsione della coda della trasformazione di JIT: ibm.com/developerworks/java/library/j-diag8.html
Liran Orevi

Molti interpreti (ovvero lo Schema è il più noto) ottimizzeranno bene la ricorsione della coda. So che GCC, con una certa ottimizzazione, esegue la ricorsione della coda (anche se C è una scelta strana per tale ottimizzazione).
nuovo123456

19

Bene, in generale, la ricorsione può essere imitata come iterazione semplicemente usando una variabile di archiviazione. Si noti che ricorsione e iterazione sono generalmente equivalenti; uno può quasi sempre essere convertito nell'altro. Una funzione ricorsiva della coda viene convertita molto facilmente in una iterativa. Basta rendere la variabile accumulatore una locale e iterare invece di recurse. Ecco un esempio in C ++ (C se non fosse per l'uso di un argomento predefinito):

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

// iterative
int factorial (int n)
{
  int acc = 1;
  for (; n > 1; --n)
    acc *= n;
  return acc;
}

Conoscendomi, probabilmente ho fatto un errore nel codice, ma l'idea è lì.


14

Anche l'uso dello stack non converte un algoritmo ricorsivo in iterativo. La ricorsione normale è ricorsione basata sulla funzione e se usiamo lo stack, diventa ricorsione basata sullo stack. Ma è ancora ricorsione.

Per gli algoritmi ricorsivi, la complessità dello spazio è O (N) e la complessità temporale è O (N). Per gli algoritmi iterativi, la complessità dello spazio è O (1) e la complessità temporale è O (N).

Ma se usiamo le cose dello stack in termini di complessità rimane la stessa. Penso che solo la ricorsione della coda possa essere convertita in iterazione.


1
Sono d'accordo con il tuo primo pezzo, ma penso di aver frainteso il secondo paragrafo. Considerare la clonazione di un array semplicemente copiando lo copy = new int[size]; for(int i=0; i<size; ++i) copy[i] = source[i];spazio di memoria e la complessità temporale sono entrambi O (N) basati sulla dimensione dei dati, ma è chiaramente un algoritmo iterativo.
Ponkadoodle,

13

L' articolo di eliminazione delle pile e della ricorsione cattura l'idea di esternalizzare il frame dello stack sull'heap, ma non fornisce un modo semplice e ripetibile per la conversione. Di seguito è uno.

Durante la conversione in codice iterativo, si deve essere consapevoli che la chiamata ricorsiva può avvenire da un blocco di codice arbitrariamente profondo. Non sono solo i parametri, ma anche il punto di tornare alla logica che resta da eseguire e allo stato delle variabili che partecipano ai condizionali successivi, che contano. Di seguito è riportato un modo molto semplice per convertire in codice iterativo con meno modifiche.

Considera questo codice ricorsivo:

struct tnode
{
    tnode(int n) : data(n), left(0), right(0) {}
    tnode *left, *right;
    int data;
};

void insertnode_recur(tnode *node, int num)
{
    if(node->data <= num)
    {
        if(node->right == NULL)
            node->right = new tnode(num);
        else
            insertnode(node->right, num);
    }
    else
    {
        if(node->left == NULL)
            node->left = new tnode(num);
        else
            insertnode(node->left, num);
    }    
}

Codice iterativo:

// Identify the stack variables that need to be preserved across stack 
// invocations, that is, across iterations and wrap them in an object
struct stackitem 
{ 
    stackitem(tnode *t, int n) : node(t), num(n), ra(0) {}
    tnode *node; int num;
    int ra; //to point of return
};

void insertnode_iter(tnode *node, int num) 
{
    vector<stackitem> v;
    //pushing a stackitem is equivalent to making a recursive call.
    v.push_back(stackitem(node, num));

    while(v.size()) 
    {
        // taking a modifiable reference to the stack item makes prepending 
        // 'si.' to auto variables in recursive logic suffice
        // e.g., instead of num, replace with si.num.
        stackitem &si = v.back(); 
        switch(si.ra)
        {
        // this jump simulates resuming execution after return from recursive 
        // call 
            case 1: goto ra1;
            case 2: goto ra2;
            default: break;
        } 

        if(si.node->data <= si.num)
        {
            if(si.node->right == NULL)
                si.node->right = new tnode(si.num);
            else
            {
                // replace a recursive call with below statements
                // (a) save return point, 
                // (b) push stack item with new stackitem, 
                // (c) continue statement to make loop pick up and start 
                //    processing new stack item, 
                // (d) a return point label
                // (e) optional semi-colon, if resume point is an end 
                // of a block.

                si.ra=1;
                v.push_back(stackitem(si.node->right, si.num));
                continue; 
ra1:            ;         
            }
        }
        else
        {
            if(si.node->left == NULL)
                si.node->left = new tnode(si.num);
            else
            {
                si.ra=2;                
                v.push_back(stackitem(si.node->left, si.num));
                continue;
ra2:            ;
            }
        }

        v.pop_back();
    }
}

Notare come la struttura del codice rimane fedele alla logica ricorsiva e le modifiche sono minime, con conseguente minor numero di bug. Per confronto, ho contrassegnato le modifiche con ++ e -. La maggior parte dei nuovi blocchi inseriti, ad eccezione di v.push_back, sono comuni a qualsiasi logica iterativa convertita

void insertnode_iter(tnode *node, int num) 
{

+++++++++++++++++++++++++

    vector<stackitem> v;
    v.push_back(stackitem(node, num));

    while(v.size())
    {
        stackitem &si = v.back(); 
        switch(si.ra)
        {
            case 1: goto ra1;
            case 2: goto ra2;
            default: break;
        } 

------------------------

        if(si.node->data <= si.num)
        {
            if(si.node->right == NULL)
                si.node->right = new tnode(si.num);
            else
            {

+++++++++++++++++++++++++

                si.ra=1;
                v.push_back(stackitem(si.node->right, si.num));
                continue; 
ra1:            ;    

-------------------------

            }
        }
        else
        {
            if(si.node->left == NULL)
                si.node->left = new tnode(si.num);
            else
            {

+++++++++++++++++++++++++

                si.ra=2;                
                v.push_back(stackitem(si.node->left, si.num));
                continue;
ra2:            ;

-------------------------

            }
        }

+++++++++++++++++++++++++

        v.pop_back();
    }

-------------------------

}

Questo mi ha aiutato molto, ma non c'è un problema: gli stackitemoggetti sono assegnati con un valore di immondizia per ra. Tutto funziona ancora nel modo più simile, ma raper coincidenza dovrebbe essere 1 o 2 otterrai un comportamento errato. La soluzione è inizializzare raa 0.
JanX2

@ JanX2, stackitemnon deve essere inviato senza inizializzazione. Sì, l'inizializzazione su 0 catturerebbe errori.
Chethan,

Perché invece entrambi gli indirizzi di ritorno non sono impostati v.pop_back()sull'istruzione?
Is7s

7

Cerca "Google stile di passaggio di continuazione". Esiste una procedura generale per la conversione in uno stile ricorsivo della coda; esiste anche una procedura generale per trasformare le funzioni ricorsive della coda in anelli.


6

Basta ammazzare il tempo ... Una funzione ricorsiva

void foo(Node* node)
{
    if(node == NULL)
       return;
    // Do something with node...
    foo(node->left);
    foo(node->right);
}

può essere convertito in

void foo(Node* node)
{
    if(node == NULL)
       return;

    // Do something with node...

    stack.push(node->right);
    stack.push(node->left);

    while(!stack.empty()) {
         node1 = stack.pop();
         if(node1 == NULL)
            continue;
         // Do something with node1...
         stack.push(node1->right);             
         stack.push(node1->left);
    }

}

L'esempio sopra è un esempio di dfs iterativo ricorsivo sull'albero di ricerca binario :)
Amit

5

Generalmente la tecnica per evitare l'overflow dello stack è per le funzioni ricorsive è chiamata tecnica del trampolino che è ampiamente adottata dagli sviluppatori Java.

Tuttavia, per C # c'è un metodo di supporto po ' qui che trasforma il vostro funzione ricorsiva per iterativa senza richiedere alla logica modificare o rendere il codice in-comprensibili. C # è un linguaggio così carino che è possibile fare cose fantastiche.

Funziona avvolgendo parti del metodo con un metodo di supporto. Ad esempio la seguente funzione ricorsiva:

int Sum(int index, int[] array)
{
 //This is the termination condition
 if (int >= array.Length)
 //This is the returning value when termination condition is true
 return 0;

//This is the recursive call
 var sumofrest = Sum(index+1, array);

//This is the work to do with the current item and the
 //result of recursive call
 return array[index]+sumofrest;
}

Diventa:

int Sum(int[] ar)
{
 return RecursionHelper<int>.CreateSingular(i => i >= ar.Length, i => 0)
 .RecursiveCall((i, rv) => i + 1)
 .Do((i, rv) => ar[i] + rv)
 .Execute(0);
}

4

Pensando a cose che in realtà hanno bisogno di uno stack:

Se consideriamo il modello di ricorsione come:

if(task can be done directly) {
    return result of doing task directly
} else {
    split task into two or more parts
    solve for each part (possibly by recursing)
    return result constructed by combining these solutions
}

Ad esempio, la classica Torre di Hanoi

if(the number of discs to move is 1) {
    just move it
} else {
    move n-1 discs to the spare peg
    move the remaining disc to the target peg
    move n-1 discs from the spare peg to the target peg, using the current peg as a spare
}

Questo può essere tradotto in un ciclo che lavora su uno stack esplicito, riaffermandolo come:

place seed task on stack
while stack is not empty 
   take a task off the stack
   if(task can be done directly) {
      Do it
   } else {
      Split task into two or more parts
      Place task to consolidate results on stack
      Place each task on stack
   }
}

Per la Torre di Hanoi questo diventa:

stack.push(new Task(size, from, to, spare));
while(! stack.isEmpty()) {
    task = stack.pop();
    if(task.size() = 1) {
        just move it
    } else {
        stack.push(new Task(task.size() -1, task.spare(), task,to(), task,from()));
        stack.push(new Task(1, task.from(), task.to(), task.spare()));
        stack.push(new Task(task.size() -1, task.from(), task.spare(), task.to()));
    }
}

Vi è una notevole flessibilità qui su come definire il proprio stack. Puoi rendere la tua pila un elenco di Commandoggetti che fanno cose sofisticate. Oppure puoi andare nella direzione opposta e renderlo un elenco di tipi più semplici (ad esempio, un "compito" potrebbe essere composto da 4 elementi in una pila di int, anziché un elemento in una pila di Task).

Tutto ciò significa che la memoria per lo stack si trova nell'heap anziché nello stack di esecuzione Java, ma ciò può essere utile in quanto si ha un maggiore controllo su di esso.


3

Un modello da cercare è una chiamata di ricorsione alla fine della funzione (la cosiddetta ricorsione di coda). Questo può essere facilmente sostituito con un po 'di tempo. Ad esempio, la funzione foo:

void foo(Node* node)
{
    if(node == NULL)
       return;
    // Do something with node...
    foo(node->left);
    foo(node->right);
}

termina con una chiamata a foo. Questo può essere sostituito con:

void foo(Node* node)
{
    while(node != NULL)
    {
        // Do something with node...
        foo(node->left);
        node = node->right;
     }
}

che elimina la seconda chiamata ricorsiva.


3
Mi sembra ancora ricorsivo ... :)
Nathan,

2
Bene, sì - ma è metà ricorsivo. Sbarazzarsi dell'altra ricorsione richiederà un'altra tecnica ...
Mark Bessey,

2

Una domanda che era stata chiusa come duplicata di questa aveva una struttura di dati molto specifica:

inserisci qui la descrizione dell'immagine

Il nodo aveva la seguente struttura:

typedef struct {
    int32_t type;
    int32_t valueint;
    double  valuedouble;
    struct  cNODE *next;
    struct  cNODE *prev;
    struct  cNODE *child;
} cNODE;

La funzione di eliminazione ricorsiva sembrava:

void cNODE_Delete(cNODE *c) {
    cNODE*next;
    while (c) {
        next=c->next;
        if (c->child) { 
          cNODE_Delete(c->child)
        }
        free(c);
        c=next;
    }
}

In generale, non è sempre possibile evitare uno stack per funzioni ricorsive che si richiamano più di una volta (o anche una volta). Tuttavia, per questa particolare struttura, è possibile. L'idea è di appiattire tutti i nodi in un unico elenco. Ciò si ottiene inserendo i nodi correnti childalla fine dell'elenco della riga superiore.

void cNODE_Delete (cNODE *c) {
    cNODE *tmp, *last = c;
    while (c) {
        while (last->next) {
            last = last->next;   /* find last */
        }
        if ((tmp = c->child)) {
            c->child = NULL;     /* append child to last */
            last->next = tmp;
            tmp->prev = last;
        }
        tmp = c->next;           /* remove current */
        free(c);
        c = tmp;
    }
}

Questa tecnica può essere applicata a qualsiasi struttura collegata ai dati che può essere ridotta a un DAG con un ordinamento topologico deterministico. I figli dei nodi correnti vengono riorganizzati in modo che l'ultimo figlio adotti tutti gli altri bambini. Quindi il nodo corrente può essere eliminato e l'attraversamento può quindi iterare al figlio rimanente.


1

La ricorsione non è altro che il processo di chiamata di una funzione dall'altra, solo questo processo viene fatto chiamando una funzione da sola. Come sappiamo quando una funzione chiama l'altra funzione, la prima funzione salva il suo stato (le sue variabili) e quindi passa il controllo alla funzione chiamata. La funzione chiamata può essere chiamata usando lo stesso nome di variabili ex fun1 (a) può chiamare fun2 (a). Quando facciamo chiamate ricorsive non succede nulla di nuovo. Una funzione si chiama passando lo stesso tipo e simili nelle variabili dei nomi (ma ovviamente i valori memorizzati nelle variabili sono diversi, solo il nome rimane uguale) a se stesso. Ma prima di ogni chiamata la funzione salva il suo stato e questo processo di salvataggio continua. IL RISPARMIO È FATTO SU UNA PILE.

ORA LO STACK VIENE IN GIOCO.

Quindi, se scrivi un programma iterativo e salvi lo stato su uno stack ogni volta e poi fai uscire i valori dallo stack quando necessario, hai convertito con successo un programma ricorsivo in uno iterativo!

La prova è semplice e analitica.

Nella ricorsione il computer mantiene uno stack e nella versione iterativa dovrai mantenere lo stack manualmente.

Pensaci, basta convertire un programma ricorsivo di ricerca approfondita (su grafici) in un programma iterativo dfs.

Ti auguro il meglio!


1

Un altro esempio semplice e completo di trasformare la funzione ricorsiva in una funzione iterativa usando lo stack.

#include <iostream>
#include <stack>
using namespace std;

int GCD(int a, int b) { return b == 0 ? a : GCD(b, a % b); }

struct Par
{
    int a, b;
    Par() : Par(0, 0) {}
    Par(int _a, int _b) : a(_a), b(_b) {}
};

int GCDIter(int a, int b)
{
    stack<Par> rcstack;

    if (b == 0)
        return a;
    rcstack.push(Par(b, a % b));

    Par p;
    while (!rcstack.empty()) 
    {
        p = rcstack.top();
        rcstack.pop();
        if (p.b == 0)
            continue;
        rcstack.push(Par(p.b, p.a % p.b));
    }

    return p.a;
}

int main()
{
    //cout << GCD(24, 36) << endl;
    cout << GCDIter(81, 36) << endl;

    cin.get();
    return 0;
}

0

Una descrizione approssimativa di come un sistema accetta qualsiasi funzione ricorsiva e la esegue usando uno stack:

Ciò intendeva mostrare l'idea senza dettagli. Considera questa funzione che stamperebbe i nodi di un grafico:

function show(node)
0. if isleaf(node):
1.  print node.name
2. else:
3.  show(node.left)
4.  show(node)
5.  show(node.right)

Ad esempio il grafico: A-> B A-> C show (A) stampa B, A, C

Le chiamate di funzione significano salvare lo stato locale e il punto di continuazione in modo da poter tornare indietro, quindi saltare la funzione che si desidera chiamare.

Ad esempio, supponiamo che show (A) inizi a funzionare. La chiamata di funzione sulla linea 3. mostra (B) significa - Aggiungi elemento allo stack che significa "dovrai continuare alla linea 2 con nodo stato variabile locale = A" - Vai alla linea 0 con nodo = B.

Per eseguire il codice, il sistema esegue le istruzioni. Quando viene rilevata una chiamata di funzione, il sistema invia le informazioni necessarie per tornare al punto in cui si trovava, esegue il codice della funzione e, al termine della funzione, visualizza le informazioni su dove deve andare per continuare.


0

Questo link fornisce alcune spiegazioni e propone l'idea di mantenere la "posizione" per essere in grado di raggiungere il posto esatto tra diverse chiamate ricorsive:

Tuttavia, tutti questi esempi descrivono scenari in cui una chiamata ricorsiva viene effettuata un determinato numero di volte. Le cose si complicano quando hai qualcosa del tipo:

function rec(...) {
  for/while loop {
    var x = rec(...)
    // make a side effect involving return value x
  }
}


0

I miei esempi sono in Clojure, ma dovrebbero essere abbastanza facili da tradurre in qualsiasi lingua.

Data questa funzione che StackOverflowè per grandi valori di n:

(defn factorial [n]
  (if (< n 2)
    1
    (*' n (factorial (dec n)))))

possiamo definire una versione che utilizza il proprio stack nel modo seguente:

(defn factorial [n]
  (loop [n n
         stack []]
    (if (< n 2)
      (return 1 stack)
      ;; else loop with new values
      (recur (dec n)
             ;; push function onto stack
             (cons (fn [n-1!]
                     (*' n n-1!))
                   stack)))))

dove returnè definito come:

(defn return
  [v stack]
  (reduce (fn [acc f]
            (f acc))
          v
          stack))

Questo funziona anche per funzioni più complesse, ad esempio la funzione ackermann :

(defn ackermann [m n]
  (cond
    (zero? m)
    (inc n)

    (zero? n)
    (recur (dec m) 1)

    :else
    (recur (dec m)
           (ackermann m (dec n)))))

può essere trasformato in:

(defn ackermann [m n]
  (loop [m m
         n n
         stack []]
    (cond
      (zero? m)
      (return (inc n) stack)

      (zero? n)
      (recur (dec m) 1 stack)

      :else
      (recur m
             (dec n)
             (cons #(ackermann (dec m) %)
                   stack)))))
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.