Divide and conquer

video d'introduction sur le tri par fusion


A quoi ça sert?

Pourquoi un autre algorithme de tri ? Parce que les algorithmes de tri par sélection, par insertion ou par bulles sont de complexité quadratique( Lorsque n devient grand, le facteur en n^2 devient prépondérant, on dit que la complexité du tri est quadratique) et sont donc lents lorsque le nombre de données à trier (la taille de l’instance) est grand.

Nous allons voir un nouvel algorithme de tri très utilisé dans la résolution de problèmes courants : le merge sort. L'intérêt de cet algorithme est sa complexité exemplaire et sa stabilité. L’algorithme de tri par fusion est beaucoup plus rapide pour les instances de grande taille .
Vous serez, à l'issue du cours, capables d'implémenter une version du merge sort qui vous permettra de trier des listes.

La méthode « Diviser pour régner »

La méthode « Diviser pour régner » est une méthode de résolution de problèmes qui consiste à décomposer le problème en trois étapes :

  1. Diviser : Si la taille de l’ensemble des données à traiter est inférieure à une certaine limite (un ou deux éléments typiquement) résoudre le problème. Si la taille de l’ensemble de données à traiter est supérieur à la limite fixée, diviser cet ensemble en deux sous-ensembles (de tailles inférieures).

  2. Régner : Résoudre récursivement le problème pour les deux sous-ensembles de données.

  3. Combiner : Combiner les solutions des sous-problèmes afin de construire la solution du problème original.

Le tri fusion

Description du tri

Dans cette partie, nous allons essayer de comprendre les principes sur lesquels s’appuie ce tri. Son implémentation, pour des tableaux ou des listes chaînées, sera développée dans les prochaines sections.

Le tri fusion s’appuie sur la méthode Diviser pour régner pour trier les n éléments d’une séquence S :

  1. Diviser : Si la séquence S est composée de 0 ou un élément, retourner S immédiatement ; cette séquence est déjà triée. Si la séquence S est composée de plus de deux éléments, la diviser en deux sous-séquences S_1 et S_2 contenant chacune environ la moitié des éléments de S ; donc S_1 est formée des n/2 premiers éléments de S contient les n/2 derniers éléments de S.

  2. Régner : Trier récursivement S_1 et S_2.

  3. Combiner : Reformer la séquence S en combinant, dans l’ordre, les éléments des séquences triées S_1 et S_2.

  • n/2 est la notation mathématique pour l’opération en Python n // 2, c’est à dire le plus grand entier inférieur au résultat de la division de n par 2.
  • n/2 est la notation mathématique pour l’opération en Python n // 2 + 1, c’est à dire pour le plus petit entier supérieur au résultat de la division de n par 2.

Pour bien comprendre la méthode employée, le plus simple est de construire un arbre binaire dans lequel chaque nœud est le résultat d’un appel récursif.

flowchart TD S("85    24    63    45    17    31    96    50") --> S1("85    24    63    45") S --> S2("17    31    96    50") S1 --> S11("85    24") S1 --> S12("63    45") S11 --> S111(("85")) S11 --> S112(("24")) S12 --> S121(("63")) S12 --> S122(("45")) S2 --> S21("17    31") S21 --> S211((17)) S21 --> S212((31)) S2 --> S22("96    50") S22 --> S221((96)) S22 --> S222((50))

Résultats des différents appels récursifs (Partie Diviser).

flowchart BT S111((85)) --> S11("24    85") S112((24)) --> S11 S121((63)) --> S12("45    63") S122((45)) --> S12 S11 --> S1("24    45    63    85") S12 --> S1 S1 --> S("17    24    31    45    50    63    85    96") S211((17)) --> S21("17    31") S212((31)) --> S21 S221((96)) --> S22("50    96") S222((50)) --> S22 S21 --> S2("17    31    50    96") S22 --> S2 S2 --> S

Résultats progressifs après les étapes Régner et Fusionner.

Décomposition de l’exécution de l’algorithme


Légende

  • Chaque nœud représente un appel récursif ;
  • Nœud avec une bordure en pointilles : appels récursifs non encore effectués ;
  • Nœud avec une bordure en gras : appel récursif en cours ;
  • Nœud vide avec une bordure : partie déjà traitée ;
  • Nœud en partie vide (contenant tout de même des valeurs) : appels récursifs en attente.

flowchart TD S("85    24    63    45    17    31    96    50") -.- S1("—    —    —    —") S -.- S2("—    —    —    —") S1 -.- S11("—    —") S1 -.- S12("—    —") S11 -.- S111(("—")) S11 -.- S112(("—")) S12 -.- S121(("—")) S12 -.- S122(("—")) S2 -.- S21("—    —") S21 -.- S211(("—")) S21 -.- S212(("—")) S2 -.- S22("—    —") S22 -.- S221(("—")) S22 -.- S222(("—")) classDef pointillet stroke-dasharray: 5 5; class S1,S2,S11,S12,S21,S22,S111,S112,S121,S122,S211,S212,S221,S222 pointillet; classDef gras stroke-width:3px; class S gras;
flowchart TD S("—    —    —    —    17    31    96    50") --> S1("85    24    63    45") S -.- S2("—    —    —    —") S1 -.- S11("—    —") S1 -.- S12("—    —") S11 -.- S111(("—")) S11 -.- S112(("—")) S12 -.- S121(("—")) S12 -.- S122(("—")) S2 -.- S21("—    —") S21 -.- S211(("—")) S21 -.- S212(("—")) S2 -.- S22("—    —") S22 -.- S221(("—")) S22 -.- S222(("—")) classDef pointillet stroke-dasharray: 5 5; class S2,S11,S12,S21,S22,S111,S112,S121,S122,S211,S212,S221,S222 pointillet; classDef gras stroke-width:3px; class S1 gras;
flowchart TD S("—    —    —    —    17    31    96    50") --> S1("—    —    63    45") S -.- S2("—    —    —    —") S1 --> S11("85    24") S1 -.- S12("—    —") S11 -.- S111(("—")) S11 -.- S112(("—")) S12 -.- S121(("—")) S12 -.- S122(("—")) S2 -.- S21("—    —") S21 -.- S211(("—")) S21 -.- S212(("—")) S2 -.- S22("—    —") S22 -.- S221(("—")) S22 -.- S222(("—")) classDef pointillet stroke-dasharray: 5 5; class S2,S12,S21,S22,S111,S112,S121,S122,S211,S212,S221,S222 pointillet; classDef gras stroke-width:3px; class S11 gras;
flowchart TD S("—    —    —    —    17    31    96    50") --> S1("—    —    63    45") S -.- S2("—    —    —    —") S1 --> S11("—    24") S1 -.- S12("—    —") S11 --> S111(("85")) S11 -.- S112(("—")) S12 -.- S121(("—")) S12 -.- S122(("—")) S2 -.- S21("—    —") S21 -.- S211(("—")) S21 -.- S212(("—")) S2 -.- S22("—    —") S22 -.- S221(("—")) S22 -.- S222(("—")) classDef pointillet stroke-dasharray: 5 5; class S2,S12,S21,S22,S112,S121,S122,S211,S212,S221,S222 pointillet; classDef gras stroke-width:3px; class S111 gras;
flowchart TD S("—    —    —    —    17    31    96    50") --> S1("—    —    63    45") S -.- S2("—    —    —    —") S1 --> S11("85    —") S1 -.- S12("—    —") S11 <--> S111(("—")) S11 --> S112(("24")) S12 -.- S121(("—")) S12 -.- S122(("—")) S2 -.- S21("—    —") S21 -.- S211(("—")) S21 -.- S212(("—")) S2 -.- S22("—    —") S22 -.- S221(("—")) S22 -.- S222(("—")) classDef pointillet stroke-dasharray: 5 5; class S2,S12,S21,S22,S121,S122,S211,S212,S221,S222 pointillet; classDef gras stroke-width:3px; class S112 gras;
flowchart TD S("—    —    —    —    17    31    96    50") --> S1("—    —    63    45") S -.- S2("—    —    —    —") S1 --> S11("24    85") S1 -.- S12("—    —") S11 <--> S111(("—")) S11 <--> S112(("—")) S12 -.- S121(("—")) S12 -.- S122(("—")) S2 -.- S21("—    —") S21 -.- S211(("—")) S21 -.- S212(("—")) S2 -.- S22("—    —") S22 -.- S221(("—")) S22 -.- S222(("—")) classDef pointillet stroke-dasharray: 5 5; class S2,S12,S21,S22,S121,S122,S211,S212,S221,S222 pointillet; classDef gras stroke-width:3px; class S11 gras;
flowchart TD S("—    —    —    —    17    31    96    50") --> S1("24    85    63    45") S -.- S2("—    —    —    —") S1 <--> S11("—    —") S1 -.- S12("—    —") S11 <--> S111(("—")) S11 <--> S112(("—")) S12 -.- S121(("—")) S12 -.- S122(("—")) S2 -.- S21("—    —") S21 -.- S211(("—")) S21 -.- S212(("—")) S2 -.- S22("—    —") S22 -.- S221(("—")) S22 -.- S222(("—")) classDef pointillet stroke-dasharray: 5 5; class S2,S12,S21,S22,S121,S122,S211,S212,S221,S222 pointillet; classDef gras stroke-width:3px; class S1 gras;
flowchart TD S("—    —    —    —    17    31    96    50") --> S1("24    85    —    —") S -.- S2("—    —    —    —") S1 <--> S11("—    —") S1 --> S12("63    45") S11 <--> S111(("—")) S11 <--> S112(("—")) S12 -.- S121(("—")) S12 -.- S122(("—")) S2 -.- S21("—    —") S21 -.- S211(("—")) S21 -.- S212(("—")) S2 -.- S22("—    —") S22 -.- S221(("—")) S22 -.- S222(("—")) classDef pointillet stroke-dasharray: 5 5; class S2,S21,S22,S121,S122,S211,S212,S221,S222 pointillet; classDef gras stroke-width:3px; class S12 gras;
flowchart TD S("—    —    —    —    17    31    96    50") --> S1("24    85    —    —") S -.- S2("—    —    —    —") S1 <--> S11("—    —") S1 --> S12("—    45") S11 <--> S111(("—")) S11 <--> S112(("—")) S12 --> S121(("63")) S12 -.- S122(("—")) S2 -.- S21("—    —") S21 -.- S211(("—")) S21 -.- S212(("—")) S2 -.- S22("—    —") S22 -.- S221(("—")) S22 -.- S222(("—")) classDef pointillet stroke-dasharray: 5 5; class S2,S21,S22,S122,S211,S212,S221,S222 pointillet; classDef gras stroke-width:3px; class S121 gras;
flowchart TD S("—    —    —    —    17    31    96    50") --> S1("24    85    —    —") S -.- S2("—    —    —    —") S1 <--> S11("—    —") S1 --> S12("63    —") S11 <--> S111(("—")) S11 <--> S112(("—")) S12 <--> S121(("—")) S12 --> S122(("45")) S2 -.- S21("—    —") S21 -.- S211(("—")) S21 -.- S212(("—")) S2 -.- S22("—    —") S22 -.- S221(("—")) S22 -.- S222(("—")) classDef pointillet stroke-dasharray: 5 5; class S2,S21,S22,S211,S212,S221,S222 pointillet; classDef gras stroke-width:3px; class S122 gras;
flowchart TD S("—    —    —    —    17    31    96    50") --> S1("24    85    —    —") S -.- S2("—    —    —    —") S1 <--> S11("—    —") S1 --> S12("45    63") S11 <--> S111(("—")) S11 <--> S112(("—")) S12 <--> S121(("—")) S12 <--> S122(("—")) S2 -.- S21("—    —") S21 -.- S211(("—")) S21 -.- S212(("—")) S2 -.- S22("—    —") S22 -.- S221(("—")) S22 -.- S222(("—")) classDef pointillet stroke-dasharray: 5 5; class S2,S21,S22,S211,S212,S221,S222 pointillet; classDef gras stroke-width:3px; class S12 gras;
flowchart TD S("—    —    —    —    17    31    96    50") --> S1("24    45    63    85") S -.- S2("—    —    —    —") S1 <--> S11("—    —") S1 <--> S12("—    —") S11 <--> S111(("—")) S11 <--> S112(("—")) S12 <--> S121(("—")) S12 <--> S122(("—")) S2 -.- S21("—    —") S21 -.- S211(("—")) S21 -.- S212(("—")) S2 -.- S22("—    —") S22 -.- S221(("—")) S22 -.- S222(("—")) classDef pointillet stroke-dasharray: 5 5; class S2,S21,S22,S211,S212,S221,S222 pointillet; classDef gras stroke-width:3px; class S1 gras;
flowchart TD S("24    45    63    85    —    —    —    —") <--> S1("—    —    —    —") S --> S2("17    31    96    50") S1 <--> S11("—    —") S1 <--> S12("—    —") S11 <--> S111(("—")) S11 <--> S112(("—")) S12 <--> S121(("—")) S12 <--> S122(("—")) S2 -.- S21("—    —") S21 -.- S211(("—")) S21 -.- S212(("—")) S2 -.- S22("—    —") S22 -.- S221(("—")) S22 -.- S222(("—")) classDef pointillet stroke-dasharray: 5 5; class S21,S22,S211,S212,S221,S222 pointillet; classDef gras stroke-width:3px; class S2 gras;
... et après quelques appels supplémentaires...
flowchart TD S("24    45    63    85    —    —    —    —") <--> S1("—    —    —    —") S --> S2("17    31    50    96") S1 <--> S11("—    —") S1 <--> S12("—    —") S11 <--> S111(("—")) S11 <--> S112(("—")) S12 <--> S121(("—")) S12 <--> S122(("—")) S2 <--> S21("—    —") S21 <--> S211(("—")) S21 <--> S212(("—")) S2 <--> S22("—    —") S22 <--> S221(("—")) S22 <--> S222(("—")) classDef gras stroke-width:3px; class S2 gras;
flowchart TD S("17    24    31    45    50    63    85    96") <--> S1("—    —    —    —") S <--> S2("—    —    —    —") S1 <--> S11("—    —") S1 <--> S12("—    —") S11 <--> S111(("—")) S11 <--> S112(("—")) S12 <--> S121(("—")) S12 <--> S122(("—")) S2 <--> S21("—    —") S21 <--> S211(("—")) S21 <--> S212(("—")) S2 <--> S22("—    —") S22 <--> S221(("—")) S22 <--> S222(("—")) classDef gras stroke-width:3px; class S gras;
On peut montrer que la hauteur de l’arbre des appels récursifs est voisine (en fonction de la définition choisie pour la hauteur) de log_2 (n) où n est le nombre d’éléments dans la séquence.

Implémentation du tri fusion pour un tableau

  1. Étudier le code suivant et expliquer comment s’effectue la fusion.
def fusion(S1: List[int], S2: List[int], S: List[int]) -> None:
"""
Combine les éléments des deux listes S1 et S2 dans la liste S (en place).
 i est le nombre d'élément(s) de S1 copié(s) dans S1. 
j est le nombre d'élément(s) de S2 copié(s) dans S2. 
On doit donc avoir i + j <= len(S).
 """
i = 0
j = 0
                            
while i + j < len(S):
    if j == len(S2) or (i < len(S1) and S1[i] < S2[j]):
        S[i + j] = S1[i]
        i = i + 1
    else:
        S[i + j] = S2[j]
        j = j + 1
  1. Étudier le code suivant et remplacer les … pour chaque numéro.
def tri_fusion(S: List[int]) -> None:
"""
Implémentation du tri fusion.
La liste S est modifiée en place.
"""
n = len(S)  # ... (0)

if n < 2:
    return None  # ... (1)

# Diviser, Régner, Combiner ? ... (2)
milieu = n // 2
S1 = S[:milieu]  # .... (3)
S2 = S[milieu:]  # .... (4)
                            
# Diviser, Régner, Combiner ? ... (5)
tri_fusion(S1)  # ... (6)
tri_fusion(S2)  # ... (7)
          
# Diviser, Régner, Combiner ? ... (8)
fusion(S1, S2, S)  # ... (9)
                            
Et voici la correction:

  1. Étudier le comportement du programme complet à l’aide de pythontutor.
    Construire la liste à l’aide de l’instruction :
liste = [randint(1, 400) for i in range(5)]
                            
  1. autre manière de coder peut être plus expressive: testez-le
    def tri_fusion(tableau):
        if  len(tableau) <= 1: 
            return tableau
        pivot = len(tableau)//2
        tableau1 = tableau[:pivot]
        tableau2 = tableau[pivot:]
        gauche = tri_fusion(tableau1)
        droite = tri_fusion(tableau2)
        fusionne = fusion(gauche,droite)
        return fusionne
    #Tri fusion fonction de fusion de 2 listes
    def fusion(tableau1,tableau2):
        indice_tableau1 = 0
        indice_tableau2 = 0    
        taille_tableau1 = len(tableau1)
        taille_tableau2 = len(tableau2)
        tableau_fusionne = []
        while indice_tableau1<taille_tableau1 and indice_tableau2<taille_tableau2:
            if tableau1[indice_tableau1] < tableau2[indice_tableau2]:
                tableau_fusionne.append(tableau1[indice_tableau1])
                indice_tableau1 += 1
            else:
                tableau_fusionne.append(tableau2[indice_tableau2])
                indice_tableau2 += 1
        while indice_tableau1<taille_tableau1:
            tableau_fusionne.append(tableau1[indice_tableau1])
            indice_tableau1+=1
        while indice_tableau2<taille_tableau2:
            tableau_fusionne.append(tableau2[indice_tableau2])
            indice_tableau2+=1
        return tableau_fusionne
  2. Quelle est la complexité de la fonction fusion ?

  3. Essayer d’évaluer la complexité de l’algorithme sans faire de calcul.

  4. Evaluer la complexité en temps avec time en utilisant le code suivant

    import time
    import numpy
                       
    def temps(N):
        n = 100
        t1 = time.clock()
        for k in range(n):
            liste = numpy.random.randint(0,N,size=N)
            tri_fusion(liste)
        t1 = time.clock()-t1

    comparer avec les deux tris que vous connaissez (selection et insertion) en modifiant la valeur de N. Vous pouvez "retourner" le quotient des temps

  5. Pour les plus rapides, vous pouvez coder le tri par fusion de manière itérative



voici une illustration visuelle de la méthode divide and conquer.

  • Voici un script qui décrit la rotation image avec l'algorithme divide and conquer, cliquez ici. Rappel: vous devez l'ouvrir avec Edupython et ajouter une image carrée comme celle-ci
  • Analysez-le, vous pouvez changer d'illustration. (sur les nouveaux PC ça devrait marcher:-)