web-dev-qa-db-fra.com

Comprendre la récursivité pour générer des permutations

Je trouve la récursivité, à part celles très simples comme factorielle, très difficile à comprendre. L'extrait de code suivant imprime toutes les permutations d'une chaîne. Quelqu'un peut-il m'aider à le comprendre. Quelle est la marche à suivre pour comprendre correctement la récursivité.

void permute(char a[], int i, int n)
{
   int j;
   if (i == n)
     cout << a << endl;
   else
   {
       for (j = i; j <= n; j++)
       {
          swap(a[i], a[j]);          
          permute(a, i+1, n);
          swap(a[i], a[j]);
       }
   }
} 

int main()
{
   char a[] = "ABCD";
   permute(a, 0, 3);
   getchar();
   return 0;
}
36
Nemo

PaulR a la bonne suggestion. Vous devez parcourir le code à la main (en utilisant les outils que vous voulez - débogueurs, papier, journalisation des appels de fonction et variables à certains moments) jusqu'à ce que vous le compreniez. Pour une explication du code, je vous renvoie à l'excellente réponse de quasiverse.

Peut-être que cette visualisation du graphe d'appel avec une chaîne légèrement plus petite rend plus évidente son fonctionnement: Call graph

Le graphique a été réalisé avec graphviz .

// x.dot
// dot x.dot -Tpng -o x.png
digraph x {
rankdir=LR
size="16,10"

node [label="permute(\"ABC\", 0, 2)"] n0;
 node [label="permute(\"ABC\", 1, 2)"] n1;
  node [label="permute(\"ABC\", 2, 2)"] n2;
  node [label="permute(\"ACB\", 2, 2)"] n3;
 node [label="permute(\"BAC\", 1, 2)"] n4;
  node [label="permute(\"BAC\", 2, 2)"] n5;
  node [label="permute(\"BCA\", 2, 2)"] n6;
 node [label="permute(\"CBA\", 1, 2)"] n7;
  node [label="permute(\"CBA\", 2, 2)"] n8;
  node [label="permute(\"CAB\", 2, 2)"] n9;

n0 -> n1 [label="swap(0, 0)"];
n0 -> n4 [label="swap(0, 1)"];
n0 -> n7 [label="swap(0, 2)"];

n1 -> n2 [label="swap(1, 1)"];
n1 -> n3 [label="swap(1, 2)"];

n4 -> n5 [label="swap(1, 1)"];
n4 -> n6 [label="swap(1, 2)"];

n7 -> n8 [label="swap(1, 1)"];
n7 -> n9 [label="swap(1, 2)"];
}
55
user786653

Il choisit chaque personnage parmi tous les caractères possibles restants:

void permute(char a[], int i, int n)
{
    int j;
    if (i == n)                  // If we've chosen all the characters then:
       cout << a << endl;        // we're done, so output it
    else
    {
        for (j = i; j <= n; j++) // Otherwise, we've chosen characters a[0] to a[j-1]
        {                        // so let's try all possible characters for a[j]
            swap(a[i], a[j]);    // Choose which one out of a[j] to a[n] you will choose
            permute(a, i+1, n);  // Choose the remaining letters
            swap(a[i], a[j]);    // Undo the previous swap so we can choose the next possibility for a[j]
        }
    }
} 
24
flight

Pour utiliser efficacement la récursivité dans la conception, vous résolvez le problème en supposant que vous l'avez déjà résol. Le tremplin mental pour le problème actuel est "si je pouvais calculer les permutations de n-1 caractères, alors je pourrais calculer les permutations de n caractères en choisissant chacun à son tour et en ajoutant les permutations des n-1 caractères restants, que je je fais semblant de savoir déjà comment faire ".

Ensuite, vous avez besoin d'un moyen de faire ce qui s'appelle "toucher le fond" de la récursivité. Étant donné que chaque nouveau sous-problème est plus petit que le précédent, vous finirez peut-être par un sous-sous-problème que vous saurez vraiment résoudre.

Dans ce cas, vous connaissez déjà toutes les permutations d'un seul caractère - c'est juste le caractère. Vous savez donc comment le résoudre pour n = 1 et pour chaque nombre qui est supérieur à un nombre, vous pouvez le résoudre, et vous avez terminé. Ceci est très étroitement lié à quelque chose appelé induction mathématique.

17
phunctor

enter image description here

Ce code et cette référence peuvent vous aider à le comprendre.

// C program to print all permutations with duplicates allowed
#include <stdio.h>
#include <string.h>

/* Function to swap values at two pointers */
void swap(char *x, char *y)
{
    char temp;
    temp = *x;
    *x = *y;
    *y = temp;
}

/* Function to print permutations of string
   This function takes three parameters:
   1. String
   2. Starting index of the string
   3. Ending index of the string. */
void permute(char *a, int l, int r)
{
   int i;
   if (l == r)
     printf("%s\n", a);
   else
   {
       for (i = l; i <= r; i++)
       {
          swap((a+l), (a+i));
          permute(a, l+1, r);
          swap((a+l), (a+i)); //backtrack
       }
   }
}

/* Driver program to test above functions */
int main()
{
    char str[] = "ABC";
    int n = strlen(str);
    permute(str, 0, n-1);
    return 0;
}

Référence: Geeksforgeeks.org

4

Bien que ce soit une petite question ancienne et déjà répondu, j'ai pensé à ajouter mes entrées pour aider les nouveaux visiteurs. Prévoyez également d'expliquer le temps d'exécution sans vous concentrer sur la réconciliation récursive.

J'ai écrit l'exemple en C # mais facile à comprendre pour la plupart des programmeurs.

static int noOfFunctionCalls = 0;
static int noOfCharDisplayCalls = 0;
static int noOfBaseCaseCalls = 0;
static int noOfRecursiveCaseCalls = 0; 
static int noOfSwapCalls = 0;
static int noOfForLoopCalls = 0;

static string Permute(char[] elementsList, int currentIndex)
{
    ++noOfFunctionCalls;

    if (currentIndex == elementsList.Length)
    {
        ++noOfBaseCaseCalls;        
        foreach (char element in elementsList)
        {
            ++noOfCharDisplayCalls;

            strBldr.Append(" " + element);
        }
        strBldr.AppendLine("");
    }
    else
    {
        ++noOfRecursiveCaseCalls;

        for (int lpIndex = currentIndex; lpIndex < elementsList.Length; lpIndex++)
        {
            ++noOfForLoopCalls;

            if (lpIndex != currentIndex)
            {
                ++noOfSwapCalls;
                Swap(ref elementsList[currentIndex], ref elementsList[lpIndex]);
            }

            Permute(elementsList, (currentIndex + 1));

            if (lpIndex != currentIndex)
            {
                Swap(ref elementsList[currentIndex], ref elementsList[lpIndex]);
            }
        }
    }
    return strBldr.ToString();
}

static void Swap(ref char Char1, ref char Char2)
{
    char tempElement = Char1;
    Char1 = Char2;
    Char2 = tempElement;
}      

public static void StringPermutationsTest()
{
    strBldr = new StringBuilder();
    Debug.Flush();

    noOfFunctionCalls = 0;
    noOfCharDisplayCalls = 0;
    noOfBaseCaseCalls = 0;
    noOfRecursiveCaseCalls = 0;
    noOfSwapCalls = 0;
    noOfForLoopCalls = 0;

    //string resultString = Permute("A".ToCharArray(), 0);
    //string resultString = Permute("AB".ToCharArray(), 0);
    string resultString = Permute("ABC".ToCharArray(), 0);
    //string resultString = Permute("ABCD".ToCharArray(), 0);
    //string resultString = Permute("ABCDE".ToCharArray(), 0);

    resultString += "\nNo of Function Calls : " + noOfFunctionCalls;
    resultString += "\nNo of Base Case Calls : " + noOfBaseCaseCalls;
    resultString += "\nNo of General Case Calls : " + noOfRecursiveCaseCalls;
    resultString += "\nNo of For Loop Calls : " + noOfForLoopCalls;
    resultString += "\nNo of Char Display Calls : " + noOfCharDisplayCalls;
    resultString += "\nNo of Swap Calls : " + noOfSwapCalls;

    Debug.WriteLine(resultString);
    MessageBox.Show(resultString);       
}

Étapes: Pour par exemple lorsque nous passons l'entrée comme "ABC".

  1. Méthode de permutations appelée depuis Main pour la première fois. Donc, appeler avec Index 0 et c'est le premier appel.
  2. Dans la partie else de la boucle for, nous répétons de 0 à 2 en faisant 1 appel à chaque fois.
  3. Sous chaque boucle, nous appelons récursivement avec LpCnt + 1. 4.1 Lorsque l'index est 1, alors 2 appels récursifs. 4.2 Lorsque l'index vaut 2, alors 1 appel récursif.

Donc, du point 2 au point 4.2, le nombre total d'appels est de 5 pour chaque boucle et le total est de 15 appels + appel d'entrée principal = 16. Chaque fois que loopCnt est égal à 3, alors si la condition est exécutée.

Sur le diagramme, nous pouvons voir le nombre de boucles devenir 3 au total 6 fois, c'est-à-dire une valeur factorielle de 3, c'est-à-dire une longueur d'entrée "ABC".

Si l'instruction for boucle se répète 'n' fois pour afficher les caractères de l'exemple "ABC", c'est-à-dire 3. Total 6 fois (temps factoriels), nous entrons dans if pour afficher les permutations. Donc, la durée totale de fonctionnement = n X n !.

J'ai donné quelques variables CallCnt statiques et le tableau pour comprendre chaque exécution de ligne en détail.

Experts, n'hésitez pas à modifier ma réponse ou à commenter si mes coordonnées ne sont pas claires ou incorrectes, je suis heureux de les corriger.

enter image description hereenter image description here

Téléchargez l'exemple de code et d'autres exemples d'ici

3
Sai

Considérez la récursivité comme un simple nombre de niveaux. À chaque niveau, vous exécutez un morceau de code, ici vous exécutez une boucle for n-i fois à chaque niveau. cette fenêtre diminue à chaque niveau. n-i fois, n- (i + 1) fois, n- (i + 2) fois, .. 2,1,0 fois.

En ce qui concerne la manipulation et la permutation des chaînes, pensez à la chaîne comme simplement un "ensemble" de caractères. "abcd" comme {'a', 'b', 'c', 'd'}. La permutation consiste à réorganiser ces 4 éléments de toutes les manières possibles. Ou comme choisir 4 éléments parmi ces 4 éléments de différentes manières. Dans les permutations, l'ordre importe. abcd est différent de acbd. nous devons générer les deux.

Le code récursif fourni par vous fait exactement cela. Dans ma chaîne au-dessus de "abcd", votre code récursif exécute 4 itérations (niveaux). Dans la première itération, vous avez le choix entre 4 éléments. deuxième itération, vous avez le choix entre 3 éléments, le troisième 2 éléments, etc. donc votre code tourne 4! calculs. Ceci est expliqué ci-dessous

First iteration: choisissez un caractère parmi {a, b, c, d}

Second Iteration: choisissez un caractère dans l'ensemble soustrait {{a, b, c, d} - {x}} où x est le caractère choisi lors de la première itération. c'est-à-dire si 'a' a été choisi dans la première itération, cette itération a {b, c, d} parmi lesquelles choisir.

Third Iteration: choisissez un caractère dans l'ensemble soustrait {{a, b, c, d} - {x, y}} où x et y sont des caractères choisis dans les itérations précédentes. c'est-à-dire si 'a' est choisi à la première itération, et 'c' est choisi à partir de la 2ème, nous avons {b, d} pour jouer avec ici.

Cela se répète jusqu'à ce que nous choisissions 4 caractères au total. Une fois que nous avons choisi 4 caractères possibles, nous imprimons les caractères. Revenez ensuite en arrière et choisissez un caractère différent dans l'ensemble possible. c'est-à-dire que lorsque nous revenons à la troisième itération, nous choisissons ensuite parmi l'ensemble possible {b, d}. De cette façon, nous générons toutes les permutations possibles de la chaîne donnée.

Nous faisons ces manipulations d'ensemble afin de ne pas sélectionner deux fois les mêmes caractères. c'est-à-dire abcc, abbc, abbd, bbbb ne sont pas valides.

L'instruction swap dans votre code effectue cette construction d'ensemble. Il divise la chaîne en deux ensembles free set au choix used set déjà utilisés. Tous les caractères sur le côté gauche de i+1 est used set et à droite sont free set. Dans la première itération, vous choisissez parmi {a, b, c, d} puis passez {a}: {b, c, d} à l'itération suivante. L'itération suivante choisit l'un des {b, c, d} et passe {a, b}: {c, d} à l'itération suivante, et ainsi de suite. Lorsque le contrôle revient sur cette itération, vous choisirez alors c et construirez {a, c}, {b, d} à l'aide de l'échange.

Voilà le concept. Sinon, la récursivité est simple ici en exécutant n en profondeur et chaque niveau en exécutant une boucle pour n, n-1, n-2, n-3 ... 2,1 fois.

2
Sureshkumar T