Tri à bulles en Python : principe, code et complexité

Tri à bulles en Python : principe, code et complexité

Le tri à bulles, ou bubble sort, est l’un des algorithmes de tri les plus faciles à comprendre. Il parcourt une liste, compare les éléments voisins, puis échange ceux qui ne sont pas dans le bon ordre. À chaque passage, les grandes valeurs “remontent” progressivement vers la fin de la liste, comme des bulles.

En Python, on peut l’implémenter en quelques lignes.

def tri_a_bulles(valeurs):
    valeurs = valeurs.copy()
    n = len(valeurs)

    for i in range(n):
        for j in range(0, n - i - 1):
            if valeurs[j] > valeurs[j + 1]:
                valeurs[j], valeurs[j + 1] = valeurs[j + 1], valeurs[j]

    return valeurs


print(tri_a_bulles([5, 1, 4, 2, 8]))

Résultat :

[1, 2, 4, 5, 8]

En pratique, pour trier une liste dans un vrai programme Python, utilisez plutôt sorted() ou list.sort(). Le tri à bulles est surtout utile pour apprendre les boucles, les comparaisons, les échanges et la complexité algorithmique.

La réponse courte

Le tri à bulles répète cette règle :

si deux éléments voisins sont dans le mauvais ordre, on les échange

Après un passage complet, le plus grand élément restant est placé à sa position finale. On répète jusqu’à ce que la liste soit triée.

Version simple :

def tri_a_bulles(valeurs):
    valeurs = valeurs.copy()

    for i in range(len(valeurs)):
        for j in range(len(valeurs) - i - 1):
            if valeurs[j] > valeurs[j + 1]:
                valeurs[j], valeurs[j + 1] = valeurs[j + 1], valeurs[j]

    return valeurs

Complexité :

Cas Temps
Meilleur cas avec optimisation O(n)
Cas moyen O(n²)
Pire cas O(n²)
Mémoire supplémentaire O(1) hors copie

Sans optimisation, même une liste déjà triée reste parcourue inutilement.

Comment fonctionne le tri à bulles

Prenons cette liste :

[5, 1, 4, 2, 8]

Premier passage :

[5, 1, 4, 2, 8] -> échange 5 et 1
[1, 5, 4, 2, 8] -> échange 5 et 4
[1, 4, 5, 2, 8] -> échange 5 et 2
[1, 4, 2, 5, 8] -> 5 et 8 sont dans le bon ordre

À la fin du passage, 8 est déjà au bon endroit. Au passage suivant, il est inutile de le comparer à nouveau.

C’est pour cela que la boucle interne s’arrête à :

len(valeurs) - i - 1

À chaque passage, une valeur supplémentaire est fixée à la fin.

Version optimisée avec arrêt anticipé

Si la liste est déjà triée, le tri à bulles peut s’arrêter après un seul passage. Il suffit de vérifier si un échange a eu lieu.

def tri_a_bulles_optimise(valeurs):
    valeurs = valeurs.copy()
    n = len(valeurs)

    for i in range(n):
        echange = False

        for j in range(0, n - i - 1):
            if valeurs[j] > valeurs[j + 1]:
                valeurs[j], valeurs[j + 1] = valeurs[j + 1], valeurs[j]
                echange = True

        if not echange:
            break

    return valeurs

Test :

print(tri_a_bulles_optimise([1, 2, 3, 4, 5]))
print(tri_a_bulles_optimise([5, 4, 3, 2, 1]))

Résultat :

[1, 2, 3, 4, 5]
[1, 2, 3, 4, 5]

Cette optimisation change le meilleur cas : une liste déjà triée passe de O(n²) à O(n).

Trier en place ou renvoyer une copie

Deux styles sont possibles.

Version qui renvoie une nouvelle liste :

def tri_a_bulles(valeurs):
    valeurs = valeurs.copy()
    # tri...
    return valeurs

Version qui modifie directement la liste :

def tri_a_bulles_en_place(valeurs):
    n = len(valeurs)

    for i in range(n):
        echange = False

        for j in range(0, n - i - 1):
            if valeurs[j] > valeurs[j + 1]:
                valeurs[j], valeurs[j + 1] = valeurs[j + 1], valeurs[j]
                echange = True

        if not echange:
            break

Utilisation :

nombres = [5, 1, 4, 2, 8]
tri_a_bulles_en_place(nombres)
print(nombres)

La version en place économise une copie, mais elle modifie la liste donnée. Pour un tutoriel, renvoyer une copie est souvent plus sûr.

Pourquoi le tri à bulles est en O(n²)

Avec n éléments, le premier passage fait environ n - 1 comparaisons. Le deuxième en fait n - 2, puis n - 3, etc.

Le total ressemble à :

(n - 1) + (n - 2) + ... + 1

Ce qui donne environ :

n² / 2

En notation Big O, on ignore les constantes :

O(n²)

Cette complexité explique pourquoi le tri à bulles devient vite lent quand la liste grandit. Pour 100 éléments, ce n’est pas grave. Pour 100 000 éléments, c’est une mauvaise idée.

Pour approfondir cette notion, voir la complexité algorithmique en Python.

Compter les comparaisons et les échanges

Pour comprendre le coût réel, on peut instrumenter l’algorithme.

def tri_a_bulles_stats(valeurs):
    valeurs = valeurs.copy()
    comparaisons = 0
    echanges = 0
    n = len(valeurs)

    for i in range(n):
        echange = False

        for j in range(0, n - i - 1):
            comparaisons += 1

            if valeurs[j] > valeurs[j + 1]:
                valeurs[j], valeurs[j + 1] = valeurs[j + 1], valeurs[j]
                echanges += 1
                echange = True

        if not echange:
            break

    return valeurs, comparaisons, echanges

Exemple :

resultat, comparaisons, echanges = tri_a_bulles_stats([5, 1, 4, 2, 8])

print(resultat)
print(comparaisons)
print(echanges)

Ce genre de mesure aide à comprendre pourquoi certains algorithmes simples sont pédagogiques mais pas performants.

Trier des objets avec une clé

Le tri à bulles peut aussi trier des objets selon une clé, comme sorted().

def tri_a_bulles_key(valeurs, key=lambda x: x):
    valeurs = valeurs.copy()
    n = len(valeurs)

    for i in range(n):
        echange = False

        for j in range(0, n - i - 1):
            if key(valeurs[j]) > key(valeurs[j + 1]):
                valeurs[j], valeurs[j + 1] = valeurs[j + 1], valeurs[j]
                echange = True

        if not echange:
            break

    return valeurs

Exemple :

personnes = [
    {"nom": "Amina", "age": 31},
    {"nom": "Yanis", "age": 24},
    {"nom": "Lea", "age": 28},
]

print(tri_a_bulles_key(personnes, key=lambda p: p["age"]))

Dans un vrai projet, vous écririez plutôt :

sorted(personnes, key=lambda p: p["age"])

Mais l’exercice montre comment une fonction de comparaison peut être intégrée dans un algorithme.

Tri à bulles ou sorted()

Python fournit deux outils de tri à utiliser en priorité :

nouvelle_liste = sorted(valeurs)

et :

valeurs.sort()

sorted() renvoie une nouvelle liste. list.sort() trie la liste en place. Ces fonctions sont stables, très optimisées et beaucoup plus adaptées à la production que le tri à bulles.

Le tri à bulles reste intéressant pour apprendre :

  • les boucles imbriquées ;
  • les échanges de valeurs ;
  • la notion de complexité ;
  • le meilleur cas, le pire cas et l’arrêt anticipé ;
  • la différence entre algorithme pédagogique et fonction de production.

Erreurs fréquentes

Oublier - 1 dans la boucle interne

Si vous écrivez :

for j in range(len(valeurs)):
    if valeurs[j] > valeurs[j + 1]:
        ...

vous finirez par accéder à valeurs[j + 1] hors de la liste.

La boucle correcte doit s’arrêter avant le dernier index :

for j in range(0, n - i - 1):

Modifier la liste sans le vouloir

Si vous voulez garder la liste d’origine, faites une copie :

valeurs = valeurs.copy()

Sinon, votre fonction triera directement l’objet passé en argument.

Croire que le tri à bulles est efficace

Le tri à bulles est facile à lire, mais il est rarement efficace. Pour trier de vraies données, utilisez sorted() ou list.sort().

Confondre stable et efficace

Le tri à bulles est stable si l’on échange seulement quand valeurs[j] > valeurs[j + 1], et non quand les valeurs sont égales. Mais stable ne veut pas dire rapide.

Exercices rapides

Exercice 1

Que renvoie ce code ?

print(tri_a_bulles([3, 2, 1]))

Correction :

[1, 2, 3]

Exercice 2

Ajoutez un paramètre reverse=True pour trier dans l’ordre décroissant.

Correction possible :

def tri_a_bulles_reverse(valeurs, reverse=False):
    valeurs = valeurs.copy()
    n = len(valeurs)

    for i in range(n):
        echange = False

        for j in range(0, n - i - 1):
            mauvais_ordre = valeurs[j] > valeurs[j + 1]

            if reverse:
                mauvais_ordre = valeurs[j] < valeurs[j + 1]

            if mauvais_ordre:
                valeurs[j], valeurs[j + 1] = valeurs[j + 1], valeurs[j]
                echange = True

        if not echange:
            break

    return valeurs

Exercice 3

Comparez le nombre de comparaisons entre :

[1, 2, 3, 4, 5]

et :

[5, 4, 3, 2, 1]

avec la fonction tri_a_bulles_stats.

Pour aller plus loin

Le tri à bulles est un bon premier algorithme de tri, mais il ne doit pas rester le seul. Après lui, les tris par insertion, sélection, fusion et rapide permettent de comprendre des stratégies différentes.

À lire ensuite :

La règle à retenir : le tri à bulles est excellent pour apprendre, mais sorted() et list.sort() sont les bons outils pour trier en production.

Références