web-dev-qa-db-fra.com

Comprendre la récursion de mergesort

La plupart des implémentations de mergesort que je vois sont similaires à celles-ci. intro au livre d'algorithmes avec implémentations en ligne que je recherche. Mes côtelettes de récursivité ne vont pas beaucoup plus loin que de jouer avec la génération Fibonacci (qui était assez simple), alors peut-être que ce sont les récursions multiples qui m'époustouflent, mais je ne peux même pas parcourir le code et comprendre ce qui se passe avant même d'avoir frappé la fonction de fusion.

Comment traverse-t-il cela? Y a-t-il une stratégie ou une lecture à suivre pour mieux comprendre le processus ici?

void mergesort(int *a, int*b, int low, int high)
{
    int pivot;
    if(low<high)
    {
        pivot=(low+high)/2;
        mergesort(a,b,low,pivot);
        mergesort(a,b,pivot+1,high);
        merge(a,b,low,pivot,high);
    }
}

et la fusion (même si franchement je suis coincé mentalement avant même d'arriver à cette partie)

void merge(int *a, int *b, int low, int pivot, int high)
{
    int h,i,j,k;
    h=low;
    i=low;
    j=pivot+1;

    while((h<=pivot)&&(j<=high))
    {
        if(a[h]<=a[j])
        {
            b[i]=a[h];
            h++;
        }
        else
        {
            b[i]=a[j];
            j++;
        }
        i++;
    }
    if(h>pivot)
    {
        for(k=j; k<=high; k++)
        {
            b[i]=a[k];
            i++;
        }
    }
    else
    {
        for(k=h; k<=pivot; k++)
        {
            b[i]=a[k];
            i++;
        }
    }
    for(k=low; k<=high; k++) a[k]=b[k];
}
19
2c2c

Je pense que le nom de la fonction "sort" dans MergeSort est un peu impropre, il faut vraiment l'appeler "diviser".

Voici une visualisation de l'algorithme en cours. 

enter image description here

Chaque fois que la fonction est récurrente, elle travaille sur une subdivision de plus en plus petite du tableau en entrée, en commençant par la moitié gauche. Chaque fois que la fonction revient de la récursivité, elle continue et commence soit à travailler sur la moitié droite, soit à rechute et à travailler sur une moitié plus grande.

Comme ça

[************************]mergesort
[************]mergesort(lo,mid)
[******]mergesort(lo,mid)
[***]mergesort(lo,mid)
[**]mergesort(lo,mid)
 [**]mergesort(mid+1,hi)
[***]merge
   [***]mergesort(mid+1,hi)
   [**]mergesort*(lo,mid)
    [**]mergesort(mid+1,hi)
   [***]merge
[******]merge
      [******]mergesort(mid+1,hi)
      [***]mergesort(lo,mid)
      [**]mergesort(lo,mid)
       [**]mergesort(mid+1,hi)
      [***]merge
         [***]mergesort(mid+1,hi)
         [**]mergesort(lo,mid)
           [**]mergesort(mid+1,hi)
         [***]merge
      [******]merge
[************]merge
            [************]mergesort(mid+1,hi)
            [******]mergesort(lo,mid)
            [***]mergesort(lo,mid)
            [**]mergesort(lo,mid)
             [**]mergesort(mid+1,hi)
            [***]merge
               [***]mergesort(mid+1,hi)
               [**]mergesort(lo,mid)
                 [**]mergesort(mid+1,hi)
               [***]merge
            [******]merge
                  [******]mergesort(mid+1,hi)
                  [***]mergesort(lo,mid)
                  [**]mergesort*(lo,mid)
                    [**]mergesort(mid+1,hi)
                  [***]merge
                     [***]mergesort(mid+1,hi)    
                     [**]mergesort(lo,mid)
                      [**]mergesort(mid+1,hi)
                     [***]merge
                  [******]merge
            [************]merge
[************************]merge
14
slashdottir

TRI PAR FUSION:

1) diviser le tableau en deux
2) Trier la moitié gauche
3) Trier la moitié droite
4) Fusionner les deux moitiés ensemble

 enter image description here

 enter image description here

18
abe312

Une chose évidente à faire serait d’essayer ce type de fusion sur un petit tableau, disons en taille 8 (la puissance de 2 est pratique ici), sur papier. Imaginez que vous êtes un ordinateur exécutant le code et voyez si cela commence à devenir un peu plus clair. 

Votre question est un peu ambiguë car vous n'expliquez pas ce que vous trouvez déroutant, mais vous semblez vouloir essayer de dérouler les appels récursifs dans votre tête. Ce qui peut ou peut ne pas être une bonne chose, mais je pense que cela peut facilement conduire à avoir trop dans la tête à la fois. Au lieu d'essayer de tracer le code du début à la fin, voyez si vous pouvez comprendre le concept de manière abstraite. Tri par fusion:

  1. Divise le tableau en deux
  2. Trie la moitié gauche
  3. Trie la moitié droite
  4. Fusionne les deux moitiés ensemble

(1) devrait être assez évident et intuitif pour vous. Pour l'étape (2), la clé est la suivante: la moitié gauche d'un tableau ... est un tableau. En supposant que votre tri par fusion fonctionne , il devrait pouvoir trier la moitié gauche du tableau. Droite? L'étape (4) est en fait une partie assez intuitive de l'algorithme. Un exemple devrait rendre trivial:

at the start
left: [1, 3, 5], right: [2, 4, 6, 7], out: []

after step 1
left: [3, 5], right: [2, 4, 6, 7], out: [1]

after step 2
left: [3, 5], right: [4, 6, 7], out: [1, 2]

after step 3
left: [5], right: [4, 6, 7], out: [1, 2, 3]

after step 4
left: [5], right: [6, 7], out: [1, 2, 3, 4]

after step 5
left: [], right: [6, 7], out: [1, 2, 3, 4, 5]

after step 6
left: [], right: [7], out: [1, 2, 3, 4, 5, 6]

at the end
left: [], right: [], out: [1, 2, 3, 4, 5, 6, 7]

Donc, en supposant que vous compreniez (1) et (4), une autre façon de penser au type de fusion serait la suivante. Imaginez que quelqu'un d'autre ait écrit mergesort() et que vous soyez certain que cela fonctionne. Ensuite, vous pouvez utiliser cette implémentation de mergesort() pour écrire:

sort(myArray)
{
    leftHalf = myArray.subArray(0, myArray.Length/2);
    rightHalf = myArray.subArray(myArray.Length/2 + 1, myArray.Length - 1);

    sortedLeftHalf = mergesort(leftHalf);
    sortedRightHalf = mergesort(rightHalf);

    sortedArray = merge(sortedLeftHalf, sortedRightHalf);
}

Notez que sort n'utilise pas la récursivité. Il dit simplement "trier les deux moitiés puis les fusionner". Si vous avez compris l'exemple de fusion ci-dessus, alors espérons que vous verrez intuitivement que cette fonction sort semble faire ce qu'elle dit ... sort.

Maintenant, si vous le regardez plus attentivement ... sort() ressemble assez exactement à mergesort()! C'est parce que c'est mergesort() (sauf qu'il n'y a pas de cas de base parce que ce n'est pas récursif!).

Mais c’est comme ça que j’aime penser aux fonctions récursives - supposons que la fonction fonctionne quand vous l’appelez Traitez-le comme une boîte noire qui fait ce dont vous avez besoin. Lorsque vous faites cette hypothèse, il est souvent facile de savoir comment remplir cette case noire. Pour une entrée donnée, pouvez-vous la décomposer en entrées plus petites pour alimenter votre boîte noire? Une fois que vous avez résolu ce problème, il ne vous reste plus qu'à gérer les cas de base au début de votre fonction (c’est-à-dire les cas dans lesquels vous n’avez pas besoin de passer d’appel récursif. Par exemple, mergesort([]) renvoie simplement un tableau vide; 'pas faire un appel récursif à mergesort()).

Enfin, ceci est un peu abstrait, mais un bon moyen de comprendre la récursivité consiste à écrire des preuves mathématiques en utilisant l’induction. La même stratégie utilisée pour écrire une preuve par induction est utilisée pour écrire une fonction récursive:

Preuve mathématique:

  • Montrer que la demande est vraie pour les cas de base
  • Supposons que cela soit vrai pour les entrées plus petites que certaines n
  • Utilisez cette hypothèse pour montrer que cela est toujours vrai pour une entrée de taille n

Fonction récursive:

  • Manipuler les cas de base
  • Supposons que votre fonction récursive fonctionne sur des entrées plus petites que certaines n
  • Utilisez cette hypothèse pour gérer une entrée de taille n
8
rliu

En ce qui concerne la partie récursion du type de fusion, j'ai trouvé cette page très utile. Vous pouvez suivre le code pendant son exécution. Il vous montre ce qui est exécuté en premier et ce qui suit ensuite.

À M

6
Tomislav Mikulin

Mes excuses si cela a été répondu de cette façon. Je reconnais que ceci est juste un croquis, plutôt qu'une explication profonde. 

Bien qu'il ne soit pas évident de voir comment le code réel correspond à la récursivité, j'ai pu comprendre la récursion dans un sens général de cette façon.

Prenons l'exemple de l'ensemble non trié {2,9,7,5} en entrée. L'algorithme merge_sort est désigné par "ms" pour la brièveté ci-dessous. Ensuite, nous pouvons esquisser l'opération comme suit:

étape 1: ms (ms (ms (2), ms (9)), ms (ms (7), ms (5)))

étape 2: ms (ms ({2}, {9}), ms ({7}, {5}))

étape 3: ms ({2,9}, {5,7})

étape 4: {2,5,7,9}

Il est important de noter que le fusionnement d'un singulet (comme {2}) est simplement le singlet (ms (2) = {2}), de sorte qu'au premier niveau de récursivité, nous obtenons notre première réponse. Les réponses restantes dégringolent comme des dominos au fur et à mesure que les récursions intérieures se terminent et se confondent.

Une partie du génie de l'algorithme réside dans la manière dont il construit automatiquement la formule récursive de l'étape 1 tout au long de sa construction. Ce qui m’a aidé, c’est l’exercice consistant à penser comment transformer l’étape 1 ci-dessus d’une formule statique en une récursion générale.

3
Alan G.

la mergesort() divise simplement le tableau en deux moitiés jusqu'à ce que la condition if échoue, c'est-à-dire low < high. Lorsque vous appelez deux fois mergesort(): un avec low à pivot et deuxième avec pivot+1 à high, les sous-tableaux seront divisés encore plus loin.

Prenons un exemple:

a[] = {9,7,2,5,6,3,4}
pivot = 0+6/2 (which will be 3)
=> first mergesort will recurse with array {9,7,2} : Left Array
=> second will pass the array {5,6,3,4} : Right Array

Il se répète jusqu'à ce que vous ayez 1 élément dans chaque left ainsi que le tableau right . Au final, vous aurez quelque chose de similaire à ceci:

L : {9} {7} {2} R : {5} {6} {3} {4} (each L and R will have further sub L and R)
=> which on call to merge will become

L(L{7,9} R{2}) : R(L{5,6} R{3,4})
As you can see that each sub array are getting sorted in the merge function.

=> on next call to merge the next L and R sub arrays will get in order
L{2,7,9} : R{3,4,5,6}

Now both L and R sub array are sorted within
On last call to merge they'll be merged in order

Final Array would be sorted => {2,3,4,5,6,7,9}

Voir les étapes de fusion en réponse données par @roliu

3
Dangling Cruze

Lorsque vous appelez la méthode récursive, elle n'exécute pas la fonction réelle en même temps que sa pile dans la mémoire de pile. Et lorsque la condition n'est pas satisfaite, elle passe à la ligne suivante.

Considérez que ceci est votre tableau:

int a[] = {10,12,9,13,8,7,11,5};

Donc, votre tri par fusion de méthode fonctionnera comme ci-dessous:

mergeSort(arr a, arr empty, 0 , 7);
mergeSort(arr a, arr empty, 0, 3);
mergeSort(arr a, arr empty,2,3);
mergeSort(arr a, arr empty, 0, 1);

after this `(low + high) / 2 == 0` so it will come out of first calling and going to next:

    mergeSort(arr a, arr empty, 0+1,1);

for this also `(low + high) / 2 == 0` so it will come out of 2nd calling also and call:

    merger(arr a, arr empty,0,0,1);
    merger(arr a, arr empty,0,3,1);
    .
    .
    So on

Ainsi, toutes les valeurs de tri sont stockées dans vide arr ..____. Cela pourrait aider à comprendre le fonctionnement de la fonction récursive

0
Milind

Je sais que c’est une vieille question, mais je voulais réfléchir à ce qui m’a aidée à comprendre le genre de fusion.

Il y a deux grandes parties à fusionner

  1. Division du tableau en plus petits morceaux (division)
  2. Fusionner le tableau ensemble (conquérir)

Le rôle de la recurison est simplement la partie qui se divise.

Je pense que ce qui déroute la plupart des gens, c'est qu'ils pensent qu'il y a beaucoup de logique dans la scission et la détermination de ce qu'il faut scinder, mais la plupart des logiques de tri se produisent lors du fusion . La récursion est simplement là pour diviser et faire la première moitié et ensuite la seconde moitié est juste en boucle, copiant les choses.

Je vois des réponses qui mentionnent des pivots mais Je recommanderais de ne pas associer le mot "pivot" au tri par fusion car c’est un moyen facile de confondre le tri par fusion avec quicksort (qui dépend fortement du choix du "pivot"). Ce sont deux algorithmes "diviser pour régner". Pour le tri par fusion, la division se passe toujours au milieu, tandis que pour le tri rapide, vous pouvez être intelligent avec la division lorsque vous choisissez un pivot optimal.

0
aug

processus pour diviser le problème en sous-problèmes L’exemple donné vous aidera à comprendre la récursion. int A [] = {numéro d'élément à court-circuiter}, int p = 0; (index amoureux). int r = A.length - 1; (indice supérieur).

class DivideConqure1 {
void devide(int A[], int p, int r) {
    if (p < r) {
        int q = (p + r) / 2; // divide problem into sub problems.
        devide(A, p, q);   //divide left problem into sub problems
        devide(A, q + 1, r); //divide right problem into sub problems
        merger(A, p, q, r);  //merger the sub problem
    }
}

void merger(int A[], int p, int q, int r) {
    int L[] = new int[q - p + 1];
    int R[] = new int[r - q + 0];

    int a1 = 0;
    int b1 = 0;
    for (int i = p; i <= q; i++) {  //store left sub problem in Left temp
        L[a1] = A[i];
        a1++;
    }
    for (int i = q + 1; i <= r; i++) { //store left sub problem in right temp
        R[b1] = A[i];
        b1++;
    }
    int a = 0;
    int b = 0; 
    int c = 0;
    for (int i = p; i < r; i++) {
        if (a < L.length && b < R.length) {
            c = i + 1;
            if (L[a] <= R[b]) { //compare left element<= right element
                A[i] = L[a];
                a++;
            } else {
                A[i] = R[b];
                b++;
            }
        }
    }
    if (a < L.length)
        for (int i = a; i < L.length; i++) {
            A[c] = L[i];  //store remaining element in Left temp into main problem 
            c++;
        }
    if (b < R.length)
        for (int i = b; i < R.length; i++) {
            A[c] = R[i];  //store remaining element in right temp into main problem 
            c++;
        }
}
0
Shravan Kumar