Intermédiaire · Course de tris

Leçon 6 : Fonctions et récursivité

Construis en Python le motif « Course de tris » et apprends en chemin :

  • les fonctions, et passer une fonction comme valeur
  • la récursivité : une fonction qui s’appelle elle-même sur des parties plus petites
  • un compteur gardé dans un dictionnaire
  • assert pour vérifier un résultat
  • comment le travail augmente : n² face à n·log n

Regarde-le d’abord

Lecture depuis YouTube après un clic.

sorting-race.py
"""Sorting race: four algorithms on the same shuffled list, counting their comparisons.
Run: python sorting-race.py   (needs: pip install numpy matplotlib)
"""
import math

import numpy as np
import matplotlib.pyplot as plt

plt.style.use("dark_background")

n = 100
data = list(np.random.default_rng().permutation(n))
count = {"comparisons": 0}


def less(a, b):  # every algorithm compares only through this, so we can count
    count["comparisons"] += 1
    return a < b


def bubble(xs):
    xs = xs[:]
    for end in range(len(xs) - 1, 0, -1):
        for i in range(end):
            if less(xs[i + 1], xs[i]):
                xs[i], xs[i + 1] = xs[i + 1], xs[i]
    return xs


def insertion(xs):
    xs = xs[:]
    for i in range(1, len(xs)):
        j = i
        while j > 0 and less(xs[j], xs[j - 1]):
            xs[j], xs[j - 1] = xs[j - 1], xs[j]
            j -= 1
    return xs


def merge(xs):
    if len(xs) < 2:
        return xs
    left, right = merge(xs[: len(xs) // 2]), merge(xs[len(xs) // 2 :])
    out = []
    while left and right:
        out.append(left.pop(0) if not less(right[0], left[0]) else right.pop(0))
    return out + left + right


def quick(xs):
    if len(xs) < 2:
        return xs
    pivot, rest = xs[0], xs[1:]
    smaller = [x for x in rest if less(x, pivot)]
    return quick(smaller) + [pivot] + quick([x for x in rest if x not in smaller])


results = {}
for name, sort in [("bubble", bubble), ("insertion", insertion), ("merge", merge), ("quick", quick)]:
    count["comparisons"] = 0
    assert sort(data) == sorted(data)
    results[name] = count["comparisons"]
print(results, "| at least log2(n!) =", math.ceil(math.log2(math.factorial(n))))

plt.figure(figsize=(7, 4))
plt.bar(list(results), list(results.values()), color=["#f0abfc", "#c4b5fd", "#5eead4", "#5eead4"])
plt.axhline(math.log2(math.factorial(n)), color="white", linestyle="--", label="log2(n!): no comparison sort does better")
plt.ylabel(f"comparisons for n = {n}")
plt.legend()
plt.show()

# Try: n = 1000 (bubble sort needs about n*n/2, merge sort about n*log2(n))

Pas de Python sur cet ordinateur ? Le notebook exécute le même script, les exercices et les réponses dans ton navigateur, sur Google Colab. Le notebook est en anglais.

Ligne par ligne

  1. def less(a, b):

    Chaque algorithme compare à travers cette seule fonction, donc les comparaisons peuvent être comptées à un seul endroit.

  2. count["comparisons"] += 1

    Un dictionnaire garde le compte, donc la fonction peut le modifier sans le mot-clé global.

  3. def merge(xs):

    Le tri fusion : trier chaque moitié, puis fusionner les deux moitiés triées en prenant toujours le plus petit élément en tête.

  4. left, right = merge(xs[: len(xs) // 2]), merge(xs[len(xs) // 2 :])

    La récursivité : merge s’appelle elle-même sur chaque moitié, jusqu’à ce qu’une partie ait moins de deux éléments.

  5. assert sort(data) == sorted(data)

    assert arrête le script si un tri se trompe d’ordre : un test intégré au programme.

  6. math.ceil(math.log2(math.factorial(n)))

    La borne inférieure : log₂ du nombre d’ordres possibles, n!, arrondi à l’entier supérieur.

À toi de jouer

  1. Pour n = 100, quel tri demande le moins de comparaisons, et à quel point est-il proche de la borne ?

    Indice

    Lance le script ; il affiche les comptes et la borne.

    Réponse

    Le tri fusion, environ 540 face à une borne de 525. Le tri à bulles en demande exactement 4 950, c’est-à-dire 100 · 99 ÷ 2.

  2. Pourquoi ce tri à bulles demande-t-il toujours n·(n − 1) ÷ 2 comparaisons ?

    Indice

    Regarde ses deux boucles.

    Réponse

    Cette version fait toujours tourner les deux boucles jusqu’au bout : (n − 1) + (n − 2) + … + 1 = n·(n − 1) ÷ 2 comparaisons, quel que soit l’ordre.

  3. Donne au tri rapide une liste déjà triée. Qu’arrive-t-il à son compte ?

    Indice

    data = list(range(n)).

    Réponse

    Il bondit à 4 950, aussi mauvais que le tri à bulles : avec le premier élément comme pivot, chaque partage laisse un côté vide.

Quiz

1. Qu’est-ce que la récursivité ?
2. Pourquoi less garde-t-elle son compte dans un dictionnaire ?
3. Dans combien d’ordres 4 éléments peuvent-ils se présenter ?
4. Que fait assert quand sa condition vaut False ?
5. Pour 1 000 éléments, combien de comparaisons environ le tri fusion demande-t-il ?

Cartes mémoire

Lis le recto, dis-toi le verso, puis retourne la carte.

  • Récursivité

    Une fonction qui résout un problème en s’appelant elle-même sur des parties plus petites de ce problème.

  • assert condition

    Arrête le programme avec une erreur si la condition vaut False.

  • log₂(n!)

    Le plus petit nombre de comparaisons par oui ou non qui permet de distinguer n! ordres : la limite de tout tri par comparaisons.

  • n² face à n·log n

    Pour 1 000 éléments : environ 500 000 face à environ 10 000 étapes.

  • Passer une fonction

    for name, sort in [("merge", merge), …] : les fonctions sont des valeurs, comme les nombres.

Ces cartes mémoire sur une page à part →