web-dev-qa-db-fra.com

Chemin à parcourir de la récursion à l'itération

Au cours de nombreuses années de programmation, j’ai beaucoup utilisé la récursivité pour résoudre des problèmes simples, mais je suis tout à fait conscient que vous avez parfois besoin d’itération à cause de problèmes de mémoire/vitesse.

Donc, à un moment très lointain, je suis allé chercher s'il existait un "modèle" ou une méthode textuelle permettant de transformer une approche de récursion courante en itération et je n'ai rien trouvé. Ou du moins rien dont je puisse me souvenir aurait pu aider.

  • Y a-t-il des règles générales?
  • Y a-t-il un "motif"?
292
Gustavo Carreno

En général, je remplace un algorithme récursif par un algorithme itératif en poussant les paramètres qui seraient normalement transmis à la fonction récursive sur une pile. En fait, vous remplacez la pile de programmes par l’une des vôtres.

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

  // Push other objects on the stack.

}

Remarque: si vous avez plusieurs appels récursifs à l'intérieur et que vous souhaitez conserver l'ordre des appels, vous devez les ajouter dans l'ordre inverse à la pile:

foo(first);
foo(second);

doit être remplacé par

stack.Push(second);
stack.Push(first);

Edit: L'article Élimination des piles et des récursions (ou Lien de sauvegarde d'article ) entre dans plus de détails sur ce sujet.

282
David S.

Vraiment, la façon la plus courante de le faire est de garder votre propre pile. Voici une fonction de tri rapide récursive en 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);
}

Voici comment nous pourrions le rendre itératif en gardant notre propre pile:

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

Évidemment, cet exemple ne vérifie pas les limites de la pile ... et vous pouvez réellement dimensionner la pile en fonction du cas le plus défavorable étant donné les valeurs gauche et droite. Mais vous avez l'idée.

70
bobwienholt

Il semble que personne n’a abordé le point où la fonction récursive s’appelle plus d’une fois dans le corps et gère le retour à un point spécifique de la récursion (c’est-à-dire non primitive-récursive). On dit que chaque récursion peut être transformée en itération , il semble donc que cela devrait être possible.

Je viens juste de trouver un exemple en C # sur la façon de procéder. Supposons que vous ayez la fonction récursive suivante, qui agit comme une traversée postérieure, et que AbcTreeNode est un arbre à trois branches avec des pointeurs 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 solution itérative:

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


        }
43
T. Webster

Efforcez-vous de faire votre appel récursif Queue Récursion (récursivité où la dernière instruction est l'appel récursif). Une fois que vous avez cela, la convertir en itération est généralement assez facile.

28
Chris Shaffer

Eh bien, en général, la récursivité peut être imitée comme une itération en utilisant simplement une variable de stockage. Notez que récursivité et itération sont généralement équivalentes; l'un peut presque toujours être converti à l'autre. Une fonction queue-récursive est très facilement convertie en une fonction itérative. Il suffit de faire de la variable accumulateur une variable locale et d’itérer au lieu de recurse. Voici un exemple en C++ (C sans l'utilisation d'un argument par défaut):

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

En me connaissant, j'ai probablement fait une erreur dans le code, mais l'idée est là.

18
coppro

Même en utilisant stack ne convertira pas un algorithme récursif en itératif. La récursivité normale est une récursivité basée sur les fonctions et si nous utilisons une pile, elle devient récursive. Mais c'est toujours la récursivité.

Pour les algorithmes récursifs, la complexité spatiale est O(N) et la complexité temporelle est O (N). Pour les algorithmes itératifs, la complexité de l’espace est de O(1) et la complexité du temps est de O (N). 

Mais si nous utilisons des éléments de pile, la complexité reste la même. Je pense que seule la récursion de la queue peut être convertie en itération.

13
ARC

L'article élimination des piles et de la récursion reprend l'idée d'extérioriser le cadre de pile sur le tas, mais ne fournit pas de méthode de conversion simple et répétable. Ci-dessous un.

Lors de la conversion en code itératif, vous devez être conscient du fait que l'appel récursif peut provenir d'un bloc de code arbitrairement profond. Ce ne sont pas seulement les paramètres, mais aussi le point de revenir à la logique qui reste à exécuter et à l'état des variables qui participent aux conditions ultérieures, ce qui compte. Voici un moyen très simple de convertir en code itératif avec le moins de changements possible. 

Considérons ce code récursif:

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

Code itératif:

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

Notez que la structure du code reste fidèle à la logique récursive et que les modifications sont minimes, ce qui réduit le nombre de bogues. Pour comparaison, j'ai marqué les changements avec ++ et -. La plupart des nouveaux blocs insérés, à l'exception de v.Push_back, sont communs à toute logique itérative convertie. 

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

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

}
10
Chethan

Rechercher sur Google pour "style de passage de continuation." Il existe une procédure générale pour convertir en style récursif; Il existe également une procédure générale permettant de transformer des fonctions récursives en boucles.

6
Marcin

Juste pour tuer le temps ... Une fonction récursive 

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

peut être converti en

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

}
5
Tae-Sung Shin

Généralement, la technique permettant d'éviter les débordements de pile pour les fonctions récursives est appelée technique de trampoline, qui est largement adoptée par les développeurs Java.

Cependant, pour C #, il existe une petite méthode helper here qui transforme votre fonction récursive en itérative sans nécessiter de changement de logique ni de rendre le code incompréhensible. C # est un langage tellement agréable que des choses incroyables sont possibles avec.

Cela fonctionne en encapsulant des parties de la méthode par une méthode d'assistance. Par exemple, la fonction récursive suivante:

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

Se transforme en:

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

Un modèle à rechercher est un appel de récursion à la fin de la fonction (appelée récursion de queue). Cela peut facilement être remplacé par un certain temps. Par exemple, la fonction toto:

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

se termine par un appel à foo. Ceci peut être remplacé par:

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

ce qui élimine le deuxième appel récursif.

3
Andrew Stein

Une question qui avait été fermée comme un duplicata de celui-ci avait une structure de données très spécifique:

 enter image description here

Le nœud avait la structure suivante:

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

La fonction de suppression récursive ressemblait à:

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

En général, il n’est pas toujours possible d’éviter une pile pour les fonctions récursives s’appelant plus d’une fois (voire une fois). Cependant, pour cette structure particulière, c'est possible. L'idée est de mettre à plat tous les nœuds dans une seule liste. Ceci est accompli en plaçant la variable child du nœud actuel à la fin de la liste de la rangée supérieure.

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

Cette technique peut être appliquée à toute structure liée aux données pouvant être réduite à un DAG avec un ordre topologique déterministe. Les nœuds actuels enfants sont réorganisés de sorte que le dernier enfant adopte tous les autres enfants. Ensuite, le nœud actuel peut être supprimé et la traversée peut ensuite être itérée sur l'enfant restant.

2
jxh

Je viens de voter pour la réponse suggérant d'utiliser une pile explicite qui, à mon avis, est la bonne solution et est applicable de manière générale.

Je veux dire que vous pouvez l'utiliser pour transformer n'importe quelle fonction récursive en une fonction itérative. Il suffit de vérifier quelles valeurs sont enregistrées dans les appels récursifs, ceux qui doivent être locaux à la fonction récursive et remplacer les appels par un cycle dans lequel vous les placerez dans une pile. Lorsque la pile est vide, la fonction récursive aurait été terminée.

Je ne peux m'empêcher de dire que la preuve que chaque fonction récursive est équivalente à une fonction itérative sur un type de données différent est l'un des souvenirs les plus chers de mon époque universitaire. C'est le cours (et le professeur) qui m'a vraiment fait comprendre en quoi consistait la programmation informatique.

2
Remo.D

En pensant aux choses qui nécessitent réellement une pile:

Si nous considérons le modèle de récursion comme:

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
}

Par exemple, la tour classique de 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
}

Cela peut être traduit en une boucle fonctionnant sur une pile explicite, en la reformulant ainsi:

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

Pour la tour de Hanoi, cela devient:

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

Il y a une grande flexibilité dans la manière dont vous définissez votre pile. Vous pouvez créer dans votre pile une liste d'objets Command exécutant des tâches complexes. Vous pouvez également aller dans le sens opposé et en faire une liste de types plus simples (par exemple, une "tâche" peut être constitué de 4 éléments sur une pile de int, plutôt que d’un élément sur une pile de Task).

Cela signifie simplement que la mémoire de la pile se trouve dans le tas plutôt que dans la pile d'exécution Java, mais cela peut être utile car vous avez plus de contrôle sur celle-ci.

1
slim

La récursivité n’est rien d’autre que le processus d’appel d’une fonction à l’autre. Ce processus se fait uniquement par l’appel d’une fonction. Comme on le sait, lorsqu'une fonction appelle l'autre fonction, la première fonction enregistre son état (ses variables), puis passe le contrôle à la fonction appelée. La fonction appelée peut être appelée en utilisant le même nom de variables, ex fun1 (a) peut appeler fun2 (a). Lorsque nous faisons un appel récursif, rien de nouveau ne se produit. Une fonction s’appelle elle-même en passant le même type et des variables de nom similaires (mais les valeurs stockées dans les variables sont évidemment différentes, seul le nom reste identique.) À elle-même. Mais avant chaque appel, la fonction enregistre son état et le processus de sauvegarde se poursuit. Le SAVING IS est réalisé sur une pile.

MAINTENANT LA PILE ENTRE EN JEU.

Donc, si vous écrivez un programme itératif et sauvegardez l'état sur une pile à chaque fois, puis extrayez les valeurs de la pile en cas de besoin, vous avez converti avec succès un programme récursif en un programme itératif!

La preuve est simple et analytique.

En mode de récurrence, l’ordinateur gère une pile et en version itérative, vous devez gérer manuellement la pile.

Réfléchissez-y, convertissez simplement un programme récursif de recherche en profondeur (sur des graphiques) en programme itératif dfs.

Bonne chance!

1
Ajay Manas

Il existe un moyen général de convertir une traversée récursive en itérateur en utilisant un itérateur paresseux qui concatène plusieurs fournisseurs d'itérateurs (expression lambda qui renvoie un itérateur). Voir mon Conversion de la trajectoire récursive en itérateur .

0
Dagang

Un autre exemple simple et complet de transformation de la fonction récursive en une fonction itérative utilisant la pile.

#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
L_J

Une description approximative de la façon dont un système prend n'importe quelle fonction récursive et l'exécute en utilisant une pile:

Cela visait à montrer l'idée sans détails. Considérons cette fonction qui afficherait les nœuds d’un graphique:

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

Par exemple, le graphique: A-> B A-> C Show (A) afficherait B, A, C

Les appels de fonction signifient l’enregistrement de l’état local et du point de continuation afin que vous puissiez revenir, puis passer à la fonction que vous souhaitez appeler.

Par exemple, supposons que show (A) commence à s'exécuter. La fonction appelle sur la ligne 3. show (B) signifie - Ajouter un élément à la pile, ce qui signifie "vous devrez continuer à la ligne 2 avec la variable d'état noeud local = A" - Aller à la ligne 0 avec noeud = B.

Pour exécuter le code, le système exécute les instructions. Lorsqu'un appel de fonction est rencontré, le système transmet les informations dont il a besoin pour revenir là où il se trouvait, exécute le code de fonction et, lorsque la fonction est terminée, affiche les informations sur l'endroit où il doit se poursuivre.

0
Rick Giuly

Ce link fournit quelques explications et propose l’idée de garder "localisation" pour pouvoir se rendre à l’endroit exact entre plusieurs appels récursifs:

Cependant, tous ces exemples décrivent des scénarios dans lesquels un appel récursif est effectué un nombre de fois fixed. Les choses se compliquent lorsque vous avez quelque chose comme:

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