algorithme de tri par fusion

09 Apr 2020 09 Apr 2020 34006 vues ESSADDOUKI Mostafa 9 min de lecture
Complexité algorithmique
1 Introduction à l'analyse des algorithmes 2 Notations asymptotiques 3 Opérations élémentaires et modèles de coût 4 Complexité temporelle et spatiale 5 Méthode de comptage des pas 6 Méthode de comptage des pas pour les boucles
Diviser pour régner & algorithmes de tri
7 Rappel sur l'approche récursive 8 Diviser pour régner 9 la recherche dichotomique 10 Tri par fusion 11 Tri rapide 12 Analyse des fonctions récursives
Les arbres binaires
13 Introduction aux arbres binaires 14 Définitions récursives des arbres 15 Propriétés des arbres binaires 16 Types d'arbre binaire 17 Parcours en profondeur des arbres binaires 18 Parcours en largeur des arbres binaires 19 Exercices corrigés sur les arbres - TD 1 20 Exercices corrigés sur les arbres - TD 2 21 Exercices corrigés sur les arbres - TD 3 22 DS - Arbres binaires de recherche équilibrés (AVL) 23 DS - Codage de Huffman - Compression de données
Algorithmes gloutons
24 Introduction aux algorithmes gloutons 25 Problème de la sélection d'activités 26 Problème de séquencement des tâches 27 Problème du Sac à Dos fraction
Programmation dynamique
28 Introduction à la programmation dynamique 29 Le concept de sous-structure optimale 30 Le concept de sous-problèmes superposés 31 Méthodes de la programmation dynamique 32 Différence entre la programmation dynamique, l'approche diviser pour régner, et les algorithmes gloutons 33 Calculer les nombres de catalan en C++ et Python 34 Calculer le coefficient binomial 35 Le nombre de façons pour construire un mur de dimension 4*N 36 Défi de conversion de mots 37 Décomposition de phrases à partir d'un dictionnaire 38 La collection de pièces dans un labyrinthe 39 Nombre de façons de regrouper les étudiants 40 Compter tous les chemins possibles dans une grille MxN
Méta heuristique
41 Algorithmes heuristiques et métaheuristiques 42 Algorithme de Recuit simulé 43 Algorithme Colonies de fourmis
Théorie des graphes
44 Introduction et notions fondamentales 45 Chemins, cycles et connexité 46 Représentations des graphes 47 Parcours de graphes 48 Algorithme de Dijkstra 49 Algorithme A* 50 Algorithme de Bellman-Ford 51 Algorithme Floyd-Warshall 52 Coloration des graphes
Base de données et SQL
53 Introduction au langage SQL 54 Concepts de base de SGBDR 55 Syntaxes de différentes instructions SQL 56 Création et suppression d'une base de données 57 Opérateurs SQL 58 Les contraintes en SQL 59 Création et suppression des tables en SQL 60 Insertion et modifications des enregistrements 61 Extraction des données - SELECT 62 Filtrer les données - WHERE 63 Recherche de motifs - LIKE 64 Trier les données - ORDER BY 65 Les jointures en SQL - JOIN 66 Fonctions d'agrégation en SQL - SUM, COUNT, AVG, MIN et MAX 67 Organiser des données identiques en groupes - GROUP BY et HAVING 68 Les sous-requêtes en SQL 69 Les fonctions SQL de manipulation de date 70 DS - langage SQL 71 Exercices corrigés de langage SQL
Introduction à l'intelligence artificielle
Introduction à la théorie des jeux
Préparation aux concours
72 Réseau de distribution d'eau 73 Arbre d'expression arithmétique 74 Exploration d'une grotte souterraine

Tri par fusion — Merge Sort

Définition — Tri par fusion Le tri par fusion est un algorithme de tri basé sur le paradigme Diviser pour régner (Divide and Conquer). Il divise récursivement le tableau en deux moitiés, trie chacune, puis les fusionne pour produire un tableau trié.

C'est l'un des algorithmes de tri les plus efficaces et les plus populaires, avec une complexité temporelle garantie de Θ(n log n) dans tous les cas.

Stratégie — Diviser pour régner

Les trois étapes Pour trier le sous-tableau T[debut..fin] avec milieu = (debut + fin) / 2:
  1. Diviser — Couper T[debut..fin] en deux moitiés : T[debut..milieu] et T[milieu+1..fin].
  2. Régner — Trier récursivement chaque moitié. Cas de base : un tableau d'un seul élément est déjà trié.
  3. Combiner — Fusionner les deux moitiés triées en un seul tableau trié.

  Exemple de division — tableau [38, 27, 43, 3, 9, 82, 10]

Arbre de récursion — phase de division
[38, 27, 43, 3, 9, 82, 10]
         ↙                ↘
[38, 27, 43, 3]      [9, 82, 10]
    ↙       ↘          ↙      ↘
[38, 27]  [43, 3]    [9, 82]  [10]
  ↙  ↘     ↙   ↘     ↙   ↘
[38] [27] [43]  [3]  [9] [82]

Phase fusion (remontée) :
[27,38]  [3,43]     [9,82]  [10]
    ↘       ↙           ↘   ↙
  [3,27,38,43]        [9,10,82]
         ↘               ↙
       [3, 9, 10, 27, 38, 43, 82]

Algorithme


Pseudocode
fonction triFusion(T, debut, fin) :
    Si debut >= fin alors
        retourner          ← cas de base : sous-tableau de taille 1

    milieu ← (debut + fin) // 2

    triFusion(T, debut, milieu)       ← trier la moitié gauche
    triFusion(T, milieu + 1, fin)     ← trier la moitié droite

    fusion(T, debut, milieu, fin)     ← combiner les deux moitiés triées
Cas de base et condition d'arrêt La récursion s'arrête quand debut >= fin (sous-tableau d'un seul élément ou vide). Un tableau d'un élément est trivialement trié — c'est le cas de base.

Fonction fusion — le cœur de l'algorithme

La fusion est l'étape clé : elle prend deux sous-tableaux triés T[debut..milieu] et T[milieu+1..fin] et les combine en un seul tableau trié en place.


Pseudocode — fusion
fonction fusion(T, debut, milieu, fin) :
    Copier T[debut..milieu] dans G[]
    Copier T[milieu+1..fin] dans D[]

    i ← 0, j ← 0, k ← debut

    Tant que i < |G| ET j < |D| :
        Si G[i] ≤ D[j] alors
            T[k] ← G[i];  i ← i + 1
        Sinon
            T[k] ← D[j];  j ← j + 1
        k ← k + 1

    Copier les éléments restants de G dans T (si i < |G|)
    Copier les éléments restants de D dans T (si j < |D|)

  Trace de fusion — G = [3, 27, 38, 43] et D = [9, 10, 82]

ÉtapeG[i]D[j]ChoixT résultat partiel
139G[i]=3 ≤ D[j]=9 → G[3, ...]
2279D[j]=9 < G[i]=27 → D[3, 9, ...]
32710D[j]=10 < G[i]=27 → D[3, 9, 10, ...]
42782G[i]=27 ≤ D[j]=82 → G[3, 9, 10, 27, ...]
53882G[i]=38 → G[3, 9, 10, 27, 38, ...]
64382G[i]=43 → G[3, 9, 10, 27, 38, 43, ...]
782G épuisé → copier D restant[3, 9, 10, 27, 38, 43, 82]

Implémentations

def fusion(tab, debut, milieu, fin):
    """Fusionne deux sous-tableaux triés tab[debut..milieu] et tab[milieu+1..fin]."""
    G = tab[debut : milieu + 1]   # copie de la moitié gauche
    D = tab[milieu + 1 : fin + 1] # copie de la moitié droite

    i = j = 0
    k = debut

    # Comparer et placer le plus petit élément
    while i < len(G) and j < len(D):
        if G[i] <= D[j]:
            tab[k] = G[i]; i += 1
        else:
            tab[k] = D[j]; j += 1
        k += 1

    # Copier les éléments restants
    while i < len(G):
        tab[k] = G[i]; i += 1; k += 1
    while j < len(D):
        tab[k] = D[j]; j += 1; k += 1


def triFusion(tab, debut=0, fin=None):
    """Trie tab[debut..fin] par fusion (en place)."""
    if fin is None:
        fin = len(tab) - 1
    if debut < fin:
        milieu = (debut + fin) // 2
        triFusion(tab, debut, milieu)       # trier moitié gauche
        triFusion(tab, milieu + 1, fin)     # trier moitié droite
        fusion(tab, debut, milieu, fin)     # fusionner


# Test
A = [64, 25, 12, 22, 11]
print("Avant :", A)
triFusion(A)
print("Après :", A)
#include <stdio.h>
#include <stdlib.h>

void fusion(int tab[], int debut, int milieu, int fin) {
    int n1 = milieu - debut + 1;
    int n2 = fin - milieu;

    /* Tableaux temporaires */
    int *G = malloc(n1 * sizeof(int));
    int *D = malloc(n2 * sizeof(int));

    for (int i = 0; i < n1; i++) G[i] = tab[debut + i];
    for (int j = 0; j < n2; j++) D[j] = tab[milieu + 1 + j];

    int i = 0, j = 0, k = debut;

    while (i < n1 && j < n2) {
        if (G[i] <= D[j]) tab[k++] = G[i++];
        else               tab[k++] = D[j++];
    }
    while (i < n1) tab[k++] = G[i++];
    while (j < n2) tab[k++] = D[j++];

    free(G); free(D);
}

void triFusion(int tab[], int debut, int fin) {
    if (debut < fin) {
        int milieu = (debut + fin) / 2;
        triFusion(tab, debut, milieu);
        triFusion(tab, milieu + 1, fin);
        fusion(tab, debut, milieu, fin);
    }
}

void afficher(int tab[], int n) {
    for (int i = 0; i < n; i++) printf("%d ", tab[i]);
    printf("\n");
}

int main() {
    int tab[] = {64, 25, 12, 22, 11};
    int n = 5;
    printf("Avant : "); afficher(tab, n);
    triFusion(tab, 0, n - 1);
    printf("Après : "); afficher(tab, n);
    return 0;
}
import java.util.Arrays;

public class TriFusion {

    static void fusion(int[] tab, int debut, int milieu, int fin) {
        int n1 = milieu - debut + 1;
        int n2 = fin - milieu;

        int[] G = new int[n1];
        int[] D = new int[n2];

        for (int i = 0; i < n1; i++) G[i] = tab[debut + i];
        for (int j = 0; j < n2; j++) D[j] = tab[milieu + 1 + j];

        int i = 0, j = 0, k = debut;

        while (i < n1 && j < n2) {
            if (G[i] <= D[j]) tab[k++] = G[i++];
            else               tab[k++] = D[j++];
        }
        while (i < n1) tab[k++] = G[i++];
        while (j < n2) tab[k++] = D[j++];
    }

    static void triFusion(int[] tab, int debut, int fin) {
        if (debut < fin) {
            int milieu = (debut + fin) / 2;
            triFusion(tab, debut, milieu);
            triFusion(tab, milieu + 1, fin);
            fusion(tab, debut, milieu, fin);
        }
    }

    public static void main(String[] args) {
        int[] tab = {64, 25, 12, 22, 11};
        System.out.println("Avant : " + Arrays.toString(tab));
        triFusion(tab, 0, tab.length - 1);
        System.out.println("Après : " + Arrays.toString(tab));
    }
}
def tri_fusion(lst):
    """
    Version fonctionnelle (non en place) : retourne une nouvelle liste triée.
    Plus lisible, mais utilise plus de mémoire (O(n) supplémentaire).
    """
    if len(lst) <= 1:
        return lst

    milieu = len(lst) // 2
    G = tri_fusion(lst[:milieu])
    D = tri_fusion(lst[milieu:])
    return fusionner(G, D)


def fusionner(G, D):
    """Fusionne deux listes triées en une seule liste triée."""
    resultat = []
    i = j = 0
    while i < len(G) and j < len(D):
        if G[i] <= D[j]:
            resultat.append(G[i]); i += 1
        else:
            resultat.append(D[j]); j += 1
    return resultat + G[i:] + D[j:]   # concaténer les restes


# Test avec comptage des comparaisons
comparaisons = 0

def fusionner_compter(G, D):
    global comparaisons
    resultat = []
    i = j = 0
    while i < len(G) and j < len(D):
        comparaisons += 1
        if G[i] <= D[j]:
            resultat.append(G[i]); i += 1
        else:
            resultat.append(D[j]); j += 1
    return resultat + G[i:] + D[j:]


import random
n = 100
lst = random.sample(range(1000), n)
trie = tri_fusion(lst)
print(f"n = {n} → {comparaisons} comparaisons (≈ n·log₂n = {n * 7:.0f})")
print(f"Trié correctement : {trie == sorted(lst)}")
Sortie
Avant : [64, 25, 12, 22, 11]
Après : [11, 12, 22, 25, 64]

Analyse de la complexité

Théorème — Complexité du tri par fusion La relation de récurrence du tri par fusion est : $$T(n) = 2T\!\left(\frac{n}{2}\right) + \Theta(n)$$ Par le théorème maître (cas 2 : \(f(n) = \Theta(n^{\log_2 2}) = \Theta(n)\)) : $$T(n) = \Theta(n \log n)$$
CasComplexité temporelleRaison
MeilleurΘ(n log n)Division toujours en deux moitiés égales
MoyenΘ(n log n)Idem — indépendant de l'ordre initial
PireΘ(n log n)Idem — pas de cas dégénéré
EspaceO(n)Tableaux temporaires G et D dans fusion()
Explication intuitive — Θ(n log n) À chaque niveau de la récursion, on effectue Θ(n) opérations de fusion au total (chaque élément est copié exactement une fois). Il y a log₂ n niveaux (profondeur de l'arbre de récursion), d'où le total Θ(n log n).

Comparaison avec d'autres algorithmes de tri

AlgorithmeMeilleurMoyenPireEspaceStable
Tri fusionΘ(n log n)Θ(n log n)Θ(n log n)O(n)Oui
Tri rapideΘ(n log n)Θ(n log n)Θ(n²)O(log n)Non
Tri bullesΘ(n)Θ(n²)Θ(n²)O(1)Oui
Tri insertionΘ(n)Θ(n²)Θ(n²)O(1)Oui
Tri tasΘ(n log n)Θ(n log n)Θ(n log n)O(1)Non
Quand utiliser le tri par fusion ?Le tri par fusion est idéal quand :
  • On a besoin d'un tri stable (conserve l'ordre relatif des éléments égaux).
  • On trie des listes chaînées (la fusion est O(1) en espace pour les listes).
  • On travaille avec de grands ensembles de données stockés sur disque (tri externe).
  • On veut garantir Θ(n log n) dans tous les cas (contrairement au tri rapide).
Inconvénient : il nécessite O(n) mémoire supplémentaire (vs O(1) pour le tri en place).
Sortie
// La sortie apparaîtra ici…
Prêt · Ctrl+Entrée pour exécuter

Discussion (0)

Soyez le premier à laisser un commentaire !

Laisser un commentaire

Votre commentaire sera visible après modération.