web-dev-qa-db-fra.com

Explication du tri par fusion pour les nuls

J'ai trouvé ce code en ligne:

def merge(left, right):
    result = []
    i ,j = 0, 0
    while i < len(left) and j < len(right):
        if left[i] <= right[j]:
            result.append(left[i])
            i += 1
        else:
            result.append(right[j])
            j += 1
    result += left[i:]
    result += right[j:]
    return result

def mergesort(list):
    if len(list) < 2:
        return list
    middle = len(list) / 2
    left = mergesort(list[:middle])
    right = mergesort(list[middle:])
    return merge(left, right)

Cela fonctionne à 100% quand je le lance. Je ne comprends tout simplement pas comment fonctionne le tri par fusion ni comment la fonction récursive est capable de commander correctement un gauche et un droite.

19
VPNTIME

Je crois que la clé pour comprendre le type de fusion consiste à comprendre le principe suivant - je l'appellerai le principe de fusion:

Étant donné deux listes distinctes A et B ordonnées du plus petit au plus grand, construisez une liste C en comparant de manière répétée la plus petite valeur de A à la plus petite valeur de B, en supprimant la plus petite valeur et en l'ajoutant à C. les éléments restants dans l'autre liste sur C dans l'ordre. La liste C est alors aussi une liste triée. 

Si vous travaillez cela à la main plusieurs fois, vous verrez que c'est correct. Par exemple:

A = 1, 3
B = 2, 4
C = 
min(min(A), min(B)) = 1

A = 3
B = 2, 4
C = 1
min(min(A), min(B)) = 2

A = 3
B = 4
C = 1, 2
min(min(A), min(B)) = 3

A = 
B = 4
C = 1, 2, 3

Maintenant, A est épuisé, alors prolongez C avec les valeurs restantes de B:

C = 1, 2, 3, 4

Le principe de fusion est également facile à prouver. La valeur minimale de A est inférieure à toutes les autres valeurs de A et la valeur minimale de B est inférieure à toutes les autres valeurs de B. Si la valeur minimale de A est inférieure à la valeur minimale de B, elle doit également être inférieure à que toutes les valeurs de B. Par conséquent, il est inférieur à toutes les valeurs de A et à toutes les valeurs de B.

Donc, tant que vous continuez d’ajouter la valeur correspondant à ces critères à C, vous obtenez une liste triée. C'est ce que fait la fonction merge ci-dessus. 

Maintenant, étant donné ce principe, il est très facile de comprendre une technique de tri qui trie une liste en la divisant en listes plus petites, en triant ces listes et en les fusionnant ensuite. La fonction merge_sort est simplement une fonction qui divise une liste en deux, trie ces deux listes, puis fusionne ces deux listes de la manière décrite ci-dessus. 

Le seul problème est que, parce qu'il est récursif, il trie les deux sous-listes en les passant à lui-même! Si vous avez du mal à comprendre la récursion ici, je vous suggère d’abord d’étudier des problèmes plus simples. Mais si vous avez déjà les bases de la récursivité, il ne vous reste plus qu'à comprendre qu'une liste à un élément est déjà triée. La fusion de deux listes à un élément génère une liste triée à deux éléments; la fusion de deux listes à deux éléments génère une liste triée à quatre éléments; etc. 

55
senderle

Lorsque je n'arrive pas à comprendre le fonctionnement de l'algorithme, j'ajoute une sortie de débogage pour vérifier ce qui se passe réellement dans l'algorithme.

Voici le code avec la sortie de débogage. Essayez de comprendre toutes les étapes avec les appels récursifs de mergesort et ce que merge fait avec la sortie:

def merge(left, right):
    result = []
    i ,j = 0, 0
    while i < len(left) and j < len(right):
        print('left[i]: {} right[j]: {}'.format(left[i],right[j]))
        if left[i] <= right[j]:
            print('Appending {} to the result'.format(left[i]))           
            result.append(left[i])
            print('result now is {}'.format(result))
            i += 1
            print('i now is {}'.format(i))
        else:
            print('Appending {} to the result'.format(right[j]))
            result.append(right[j])
            print('result now is {}'.format(result))
            j += 1
            print('j now is {}'.format(j))
    print('One of the list is exhausted. Adding the rest of one of the lists.')
    result += left[i:]
    result += right[j:]
    print('result now is {}'.format(result))
    return result

def mergesort(L):
    print('---')
    print('mergesort on {}'.format(L))
    if len(L) < 2:
        print('length is 1: returning the list withouth changing')
        return L
    middle = len(L) / 2
    print('calling mergesort on {}'.format(L[:middle]))
    left = mergesort(L[:middle])
    print('calling mergesort on {}'.format(L[middle:]))
    right = mergesort(L[middle:])
    print('Merging left: {} and right: {}'.format(left,right))
    out = merge(left, right)
    print('exiting mergesort on {}'.format(L))
    print('#---')
    return out


mergesort([6,5,4,3,2,1])

Sortie:

---
mergesort on [6, 5, 4, 3, 2, 1]
calling mergesort on [6, 5, 4]
---
mergesort on [6, 5, 4]
calling mergesort on [6]
---
mergesort on [6]
length is 1: returning the list withouth changing
calling mergesort on [5, 4]
---
mergesort on [5, 4]
calling mergesort on [5]
---
mergesort on [5]
length is 1: returning the list withouth changing
calling mergesort on [4]
---
mergesort on [4]
length is 1: returning the list withouth changing
Merging left: [5] and right: [4]
left[i]: 5 right[j]: 4
Appending 4 to the result
result now is [4]
j now is 1
One of the list is exhausted. Adding the rest of one of the lists.
result now is [4, 5]
exiting mergesort on [5, 4]
#---
Merging left: [6] and right: [4, 5]
left[i]: 6 right[j]: 4
Appending 4 to the result
result now is [4]
j now is 1
left[i]: 6 right[j]: 5
Appending 5 to the result
result now is [4, 5]
j now is 2
One of the list is exhausted. Adding the rest of one of the lists.
result now is [4, 5, 6]
exiting mergesort on [6, 5, 4]
#---
calling mergesort on [3, 2, 1]
---
mergesort on [3, 2, 1]
calling mergesort on [3]
---
mergesort on [3]
length is 1: returning the list withouth changing
calling mergesort on [2, 1]
---
mergesort on [2, 1]
calling mergesort on [2]
---
mergesort on [2]
length is 1: returning the list withouth changing
calling mergesort on [1]
---
mergesort on [1]
length is 1: returning the list withouth changing
Merging left: [2] and right: [1]
left[i]: 2 right[j]: 1
Appending 1 to the result
result now is [1]
j now is 1
One of the list is exhausted. Adding the rest of one of the lists.
result now is [1, 2]
exiting mergesort on [2, 1]
#---
Merging left: [3] and right: [1, 2]
left[i]: 3 right[j]: 1
Appending 1 to the result
result now is [1]
j now is 1
left[i]: 3 right[j]: 2
Appending 2 to the result
result now is [1, 2]
j now is 2
One of the list is exhausted. Adding the rest of one of the lists.
result now is [1, 2, 3]
exiting mergesort on [3, 2, 1]
#---
Merging left: [4, 5, 6] and right: [1, 2, 3]
left[i]: 4 right[j]: 1
Appending 1 to the result
result now is [1]
j now is 1
left[i]: 4 right[j]: 2
Appending 2 to the result
result now is [1, 2]
j now is 2
left[i]: 4 right[j]: 3
Appending 3 to the result
result now is [1, 2, 3]
j now is 3
One of the list is exhausted. Adding the rest of one of the lists.
result now is [1, 2, 3, 4, 5, 6]
exiting mergesort on [6, 5, 4, 3, 2, 1]
#---
15
ovgolovin

Le tri par fusion a toujours été l'un de mes algorithmes préférés.

Vous commencez avec des séquences triées courtes et continuez à les fusionner, dans l'ordre, en séquences triées plus grandes. Si simple.

La partie récursive signifie que vous travaillez à l'envers - en commençant par la séquence complète et en triant les deux moitiés. Chaque moitié est également divisée, jusqu'à ce que le tri devienne trivial lorsqu'il n'y a que zéro ou un élément dans la séquence. Lorsque les fonctions récursives reviennent, les séquences triées deviennent plus grandes, comme je l’ai dit dans la description initiale.

4
Mark Ransom

Quelques façons de vous aider à comprendre ceci:

Parcourez le code dans un débogueur et observez ce qui se passe. Ou, parcourez-le sur papier (avec un très petit exemple) et observez ce qui se passe.

(personnellement, je trouve plus instructif de faire ce genre de chose sur papier)

Conceptuellement, cela fonctionne comme ceci: La liste de saisie continue à être découpée en morceaux de plus en plus petits en réduisant de moitié (par exemple, list[:middle] est la première moitié). Chaque moitié est réduite de moitié encore et encore jusqu'à ce qu'elle ait une longueur inférieure à 2. I.e. jusqu'à ce que ce ne soit rien du tout ou un seul élément. Ces éléments individuels sont ensuite rassemblés par la routine de fusion, en ajoutant ou en entrelaçant les 2 sous-listes à la liste result. Vous obtenez ainsi une liste triée. Comme les 2 sous-listes doivent être triées, l’ajout/l’entrelacement est une opération rapide ( O (n) ).

La clé de ceci (à mon avis) n'est pas la routine de fusion, c'est assez évident une fois que vous comprenez que ses entrées seront toujours triées. Le "truc" (j'utilise des guillemets parce que ce n’est pas un truc, c’est la science informatique :-)) est que, pour garantir que les entrées à fusionner soient triées, vous devez continuer à revenir jusqu'à ce que vous obteniez une liste qui doit être trié, raison pour laquelle vous continuez à appeler de manière récursive mergesort jusqu'à ce que la liste contienne moins de 2 éléments.

La récursion et le tri par fusion d'extension peuvent ne pas être évidents lorsque vous les rencontrez pour la première fois. Vous voudrez peut-être consulter un bon livre d'algorithmes (par exemple, DPV est disponible en ligne, légalement et gratuitement), mais vous pouvez parcourir un long chemin en parcourant le code que vous avez. Si vous voulez vraiment y entrer, le Stanford/Coursera algo course sera bientôt opérationnel à nouveau et il décrit en détail le type Merge.

Si vous voulez vraiment , lisez le chapitre 2 de la référence de ce livre, puis jetez le code ci-dessus et réécrivez à partir de zéro. Sérieusement.

4
Steve Haigh

Une image vaut mille mots et une animation vaut 10 000.

Consultez l'animation suivante tirée de Wikipedia qui vous aidera à visualiser le fonctionnement réel de l'algorithme de tri par fusion. 

 Merge Sort

Animation détaillée avec explication pour chaque étape du processus de tri des inquisiteurs.

Une autre animationintéressantede différents types d’algorithmes de tri.

1
Niket Pathak

Vous pouvez avoir une bonne visualisation sur le fonctionnement du tri par fusion ici:

http://www.ee.ryerson.ca/~courses/coe428/sorting/mergesort.html

J'espère que ça aide. 

0
Ricardo Parro

Comme l'explique l'article Wikipedia , il existe de nombreux moyens précieux de réaliser un type de fusion. La façon de réaliser une fusion dépend également de la collection d'éléments à fusionner, certaines collections permettant à certains outils dont elle dispose. 

Je ne vais pas répondre à cette question en Python, simplement parce que je ne peux pas l'écrire; Cependant, prendre une partie de l'algorithme de «fusion» semble être vraiment au cœur de la question, dans son ensemble. Une ressource qui m'a aidé est la page Web de l'algorithme (écrite par un professeur) plutôt obsolète de K.I.T.E, tout simplement parce que l'auteur du contenu élimine les identificateurs contextuels. 

Ma réponse est dérivée de cette ressource. 

Rappelez-vous, les algorithmes de tri de fusion fonctionnent en séparant la collection fournie, puis en rassemblant chaque élément individuel, en comparant les éléments au fur et à mesure que la collection est reconstruite. 

Voici le "code" (cherchez à la fin un "violon" Java):

public class MergeSort {

/**
 * @param a     the array to divide
 * @param low   the low INDEX of the array
 * @param high  the high INDEX of the array
 */
public void divide (int[] a, int low, int high, String hilo) {


    /* The if statement, here, determines whether the array has at least two elements (more than one element). The
     * "low" and "high" variables are derived from the bounds of the array "a". So, at the first call, this if 
     * statement will evaluate to true; however, as we continue to divide the array and derive our bounds from the 
     * continually divided array, our bounds will become smaller until we can no longer divide our array (the array 
     * has one element). At this point, the "low" (beginning) and "high" (end) will be the same. And further calls 
     * to the method will immediately return. 
     * 
     * Upon return of control, the call stack is traversed, upward, and the subsequent calls to merge are made as each 
     * merge-eligible call to divide() resolves
     */
    if (low < high) {
        String source = hilo;
        // We now know that we can further divide our array into two equal parts, so we continue to prepare for the division 
        // of the array. REMEMBER, as we progress in the divide function, we are dealing with indexes (positions)

        /* Though the next statement is simple arithmetic, understanding the logic of the statement is integral. Remember, 
         * at this juncture, we know that the array has more than one element; therefore, we want to find the middle of the 
         * array so that we can continue to "divide and conquer" the remaining elements. When two elements are left, the
         * result of the evaluation will be "1". And the element in the first position [0] will be taken as one array and the
         * element at the remaining position [1] will be taken as another, separate array.
         */
        int middle = (low + high) / 2;

        divide(a, low, middle, "low");
        divide(a, middle + 1, high, "high");


        /* Remember, this is only called by those recursive iterations where the if statement evaluated to true. 
         * The call to merge() is only resolved after program control has been handed back to the calling method. 
         */
        merge(a, low, middle, high, source);
    }
}


public void merge (int a[], int low, int middle, int high, String source) {
// Merge, here, is not driven by tiny, "instantiated" sub-arrays. Rather, merge is driven by the indexes of the 
// values in the starting array, itself. Remember, we are organizing the array, itself, and are (obviously
// using the values contained within it. These indexes, as you will see, are all we need to complete the sort.  

    /* Using the respective indexes, we figure out how many elements are contained in each half. In this 
     * implementation, we will always have a half as the only way that merge can be called is if two
     * or more elements of the array are in question. We also create to "temporary" arrays for the 
     * storage of the larger array's elements so we can "play" with them and not propogate our 
     * changes until we are done. 
     */
    int first_half_element_no       = middle - low + 1;
    int second_half_element_no      = high - middle;
    int[] first_half                = new int[first_half_element_no];
    int[] second_half               = new int[second_half_element_no];

    // Here, we extract the elements. 
    for (int i = 0; i < first_half_element_no; i++) {  
        first_half[i] = a[low + i]; 
    }

    for (int i = 0; i < second_half_element_no; i++) {  
        second_half[i] = a[middle + i + 1]; // extract the elements from a
    }

    int current_first_half_index = 0;
    int current_second_half_index = 0;
    int k = low;


    while (current_first_half_index < first_half_element_no || current_second_half_index < second_half_element_no) {

        if (current_first_half_index >= first_half_element_no) {
            a[k++] = second_half[current_second_half_index++];
            continue;
        }

        if (current_second_half_index >= second_half_element_no) {
            a[k++] = first_half[current_first_half_index++];
            continue;
        }

        if (first_half[current_first_half_index] < second_half[current_second_half_index]) {
            a[k++] = first_half[current_first_half_index++];
        } else {
            a[k++] = second_half[current_second_half_index++];
        }
    }
}

J'ai également une version, ici , qui imprimera des informations utiles et fournira une représentation plus visuelle de ce qui se passe ci-dessus. La coloration syntaxique est également préférable, si cela est utile.

0
Thomas

en gros, vous obtenez votre liste, puis vous la divisez, puis vous la triez, mais vous appliquez cette méthode de manière récursive pour la diviser à nouveau, jusqu'à ce que vous ayez un ensemble trivial que vous puissiez trier facilement, puis fusionner toutes les solutions simples. obtenir un tableau entièrement trié.

0
memo