Cours 2 · Diviser pour régnerLeçon 2 sur 2
Analyse des algorithmes récursifs
4 h de lecture9 sections Version PDF
Équations de récurrence, résolution par déroulement et par arbre d'appels ; pourquoi n log n est la borne des tris par comparaison ; dichotomie récursive.
Algorithmique 1 comptait les opérations d'une boucle : deux boucles imbriquées sur donnent , et l'affaire est entendue. Cette méthode s'effondre devant une fonction qui s'appelle elle-même — pour compter ses opérations, il faudrait déjà connaître le coût de l'appel récursif, c'est-à-dire la réponse cherchée.
Ce chapitre donne l'outil qui débloque la situation : on écrit le coût en fonction de lui-même, puis on résout. Il explique enfin d'où sort le du chapitre précédent, et il démontre que ce n'est pas un hasard mais une limite infranchissable.
Poser l'équation
Le coût d'un algorithme récursif s'écrit comme une équation de récurrence : le coût pour une entrée de taille , exprimé à partir du coût pour des entrées plus petites.
La lecture est mécanique, et il faut prendre l'habitude de la faire ligne à ligne.
fonction triFusion(T) T(n) = ... si taille(T) ≤ 1 alors retourner T cas de base : T(1) = 1 couper T en G et D découpe : O(n), parfois O(1) G ← triFusion(G) un appel sur n/2 : T(n/2) D ← triFusion(D) un autre : T(n/2) retourner fusion(G, D) fusion linéaire : O(n)D'où :
Trois quantités seulement gouvernent ce genre d'équation, et il faut les repérer avant de calculer : combien d'appels récursifs (), sur quelle fraction de l'entrée (), et combien coûte le travail non récursif ().
| Algorithme | Équation |
|---|---|
| Recherche dichotomique | |
| Tri fusion | |
| Tri rapide, cas moyen | |
| Tri rapide, pire cas | |
| Factorielle | |
| Fibonacci naïf |
Remarquez la deuxième et la quatrième ligne : le même algorithme, deux équations différentes selon la qualité du pivot. C'est exactement l'écart entre et du chapitre 3, et il se lit ici avant tout calcul — diviser par deux contre retirer un.
Résoudre par déroulement
La première méthode consiste à remplacer par sa définition, plusieurs fois, jusqu'à voir le motif.
Le motif apparaît à la troisième ligne : après déroulements, .
Reste à savoir où s'arrêter : quand l'argument atteint le cas de base, c'est-à-dire quand , donc . En substituant :
Le est le nombre de fois qu'on peut couper en deux avant d'arriver à 1. Ce n'est pas une formule à retenir, c'est ce que la division par deux fait.
Résoudre par arbre d'appels
La seconde méthode est plus visuelle, et c'est celle qu'il faut avoir en tête pour raisonner vite. On dessine l'arbre des appels, on calcule le travail effectué à chaque niveau, puis on somme sur les niveaux.
niveau 0 n travail : n ┌─┴─┐niveau 1 n/2 n/2 travail : 2 × n/2 = n ┌─┴┐ ┌┴─┐niveau 2 n/4 n/4 n/4 n/4 travail : 4 × n/4 = n ⋮niveau k 2^k appels de taille n/2^k travail : n ⋮niveau log n n appels de taille 1 travail : nChaque niveau coûte — c'est le fait remarquable du tri fusion : le nombre d'appels double pendant que leur taille est divisée par deux, donc le produit reste constant. Il y a niveaux. Total : .
Cette lecture rend immédiats les trois régimes possibles, qu'il suffit de reconnaître.
Le travail domine à la racine. Si décroît plus vite que le nombre d'appels ne croît, la somme est dominée par le premier niveau : . Exemple : donne .
Le travail est également réparti. Chaque niveau coûte pareil, et l'on multiplie par le nombre de niveaux : . C'est le tri fusion.
Le travail domine aux feuilles. Si le nombre d'appels croît plus vite que ne décroît, la somme est dominée par le dernier niveau, qui compte feuilles. Exemple : donne — le travail se fait dans les feuilles.
Ces trois cas constituent, sous une forme informelle, le théorème principal (master theorem). Le comparant à retenir est simple : confronter à .
Une équation ne relève pas de ce schéma et mérite d'être traitée à part : , le pire cas du tri rapide. Ici l'argument ne se divise pas, il décroît de 1 : il y a donc niveaux, et le travail total vaut , soit . Le contraste avec tient tout entier dans la façon de réduire l'entrée.
Un algorithme récursif satisfait T(n) = 2 T(n/2) + 1 : deux appels sur des moitiés, et un travail constant hors récursion. Quel est son coût total ?
La dichotomie, et pourquoi elle est si rapide
Un seul appel par niveau, et un travail constant : l'arbre d'appels est une ligne de éléments. C'est le premier régime, dégénéré — il n'y a qu'un appel par niveau, donc rien ne s'accumule.
Le chiffre mérite d'être posé, parce qu'il est difficile à croire tant qu'on ne l'a pas écrit : sur un million de valeurs, la recherche séquentielle demande jusqu'à un million de comparaisons, la dichotomie vingt. Sur un milliard, trente.
Pourquoi est une limite
Reste la question de fond : les deux tris du chapitre 3 s'arrêtent tous deux à . Coïncidence, ou obstacle ?
C'est un obstacle, et il se démontre en trois lignes.
Un tri par comparaison ne peut faire qu'une chose : comparer deux éléments et se brancher selon le résultat. Son exécution se décrit donc par un arbre de décision binaire, dont chaque nœud est une comparaison et chaque feuille une permutation possible du tableau de départ.
Comme l'algorithme doit pouvoir trier n'importe quelle entrée, l'arbre doit posséder au moins feuilles — une par permutation. Or un arbre binaire de hauteur a au plus feuilles. Il faut donc , c'est-à-dire .
La formule de Stirling donne . La hauteur de l'arbre étant le nombre de comparaisons dans le pire cas :
Le tri fusion atteint donc l'optimum de sa catégorie, et aucun algorithme plus astucieux ne descendra sous cette barre.
L'hypothèse « par comparaison » est essentielle, et c'est elle qui laisse une porte ouverte. Un tri qui exploite la structure des clés au lieu de les comparer échappe au raisonnement : le tri par comptage trie entiers d'un intervalle borné en , en les rangeant directement dans des cases. Il ne contredit pas le théorème, il n'entre pas dans son cadre.
Un étudiant annonce un tri par comparaison en O(n) dans le pire cas. Que peut-on affirmer sans même lire son algorithme ?
À vous
L'exercice résout les récurrences numériquement, ce qui est le meilleur moyen de vérifier une résolution faite à la main : on programme la récurrence telle qu'elle est écrite, on la tabule, et on compare à la forme close conjecturée.
Trois choses à obtenir. Retrouver pour le tri fusion et pour le pire cas du tri rapide. Compter les niveaux et le travail par niveau d'un arbre d'appels, et voir lequel des trois régimes s'applique. Enfin, comparer à pour constater que la borne inférieure est bien du même ordre que ce que le tri fusion atteint.
Résolvez numériquement quatre récurrences, puis comparez log2(n!) à n log2 n.
// Une récurrence se programme littéralement, avec mémoïsation pour que la // tabulation reste possible (ch. 2 : ne jamais répondre deux fois). function resoudre(equation, base) { const table = new Map(); const T = (n) => { if (n <= 1) return base; if (table.has(n)) return table.get(n); const v = equation(n, T); table.set(n, v); return v; }; return T; } // T(n) = 2 T(n/2) + n — tri fusion const fusion = resoudre((n, T) => 2 * T(Math.floor(n / 2)) + n, 1); // T(n) = T(n-1) + n — tri rapide, pire cas const rapidePire = resoudre((n, T) => T(n - 1) + n, 1); // T(n) = T(n/2) + 1 — dichotomie const dicho = resoudre((n, T) => T(Math.floor(n / 2)) + 1, 1); // T(n) = 2 T(n/2) + 1 — ← à écrire : quel régime ? const feuilles = resoudre((n, T) => 0, 1); function comparer(nom, T, forme, formule) { console.log(nom); for (const n of [16, 64, 256, 1024, 4096]) { const mesure = T(n); const conjecture = forme(n); const ecart = (mesure / conjecture).toFixed(2); console.log(" n = " + String(n).padStart(5) + " | T(n) = " + String(mesure).padStart(10) + " | " + formule.padEnd(14) + " = " + String(Math.round(conjecture)).padStart(10) + " | rapport " + ecart); } } // ── À VOUS ──────────────────────────────────────────────────────────────── // 1. Écrivez l'équation de « feuilles » et devinez son ordre AVANT de lancer. // 2. Vérifiez que le rapport T(n) / conjecture tend vers une constante : // c'est ce que signifie « du même ordre ». // 3. Comparez log2(n!) à n·log2(n) : la borne inférieure des tris est-elle // bien du même ordre que ce que le tri fusion atteint ? comparer("T(n) = 2T(n/2) + n (tri fusion)", fusion, (n) => n * Math.log2(n), "n log2 n"); comparer("T(n) = T(n-1) + n (rapide, pire cas)", rapidePire, (n) => n * (n + 1) / 2, "n(n+1)/2");
En travaux pratiques
Confronter la théorie à la mesure
Prédire la complexité d'un algorithme récursif par le calcul, puis vérifier la prédiction sur la machine — et regarder ce qui se passe quand les deux divergent.
- Le TP 3 : les tris implémentés et mesurés
- De quoi tracer une courbe
- 1. Poser les récurrences
Écrivez la relation de récurrence de la recherche dichotomique, du tri fusion, du tri rapide en moyenne, et de Fibonacci naïf. Résolvez-les.
- 2. Prédire
Pour chacune, prédisez le facteur multiplicatif du temps quand n double. Notez vos quatre prédictions avant toute mesure.
- 3. Mesurer
Mesurez les quatre en doublant n plusieurs fois. Comparez au tableau de prédictions et calculez l'écart.
- 4. L'écart
Là où mesure et prédiction divergent, cherchez la cause. Vérifiez notamment ce que fait la courbe quand le tableau dépasse la taille du cache.
- 5. Compter au lieu de chronométrer
Instrumentez vos tris pour compter les comparaisons plutôt que le temps. Comparez les courbes obtenues, et dites laquelle valide la théorie.
- 6. Le pire, le meilleur, le moyen
Pour le tri rapide, mesurez les trois cas sur la même taille. Écrivez ce que chacun signifie et lequel doit figurer dans une garantie contractuelle.
- 7. Lire une courbe
Tracez vos mesures en échelle logarithmique sur les deux axes. Mesurez la pente de chaque droite et déduisez-en l'exposant.
- Vos quatre prédictions sont posées avant les mesures, et trois au moins se vérifient
- Vous expliquez l'écart restant sans invoquer le hasard
- Le comptage de comparaisons colle à la théorie mieux que le chronomètre
- Vous lisez l'exposant d'un algorithme sur la pente de sa droite
Ce que la suite en fait
Le bloc III change de sujet mais garde l'outil. Le coût d'une insertion en tête de liste chaînée, d'un empilement, d'un parcours d'arbre : tous s'écrivent en récurrences, et le chapitre 7 posera pour compter les nœuds d'un arbre binaire complet.
Le reviendra surtout comme promesse conditionnelle. Un arbre binaire de recherche donne des opérations en si sa hauteur reste logarithmique, et en s'il dégénère en liste — exactement le même écart qu'entre le bon et le mauvais pivot du chapitre 3. Le chapitre 8 montrera ce que coûte de le garantir.
À retenir
Vous avez parcouru les 9 sections.
Marquez-la terminée pour faire avancer votre parcours, ou revenez sur un point avant de passer à la suite.