Analyse des algorithmes récursifsDans le dialogue d’impression, choisissez « Enregistrer au format PDF » comme destination.
Retour

Algorithmique 2 · C2 Diviser pour régner · Chapitre 2 · 4 h

Analyse des algorithmes récursifs

É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 nn donnent n2n^2, 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 logn\log n du chapitre précédent, et il démontre que ce nlognn \log n 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 nn, 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ù :

T(n)=2T ⁣(n2)+nT(1)=1T(n) = 2\,T\!\left(\frac{n}{2}\right) + n \qquad\qquad T(1) = 1

Trois quantités seulement gouvernent ce genre d'équation, et il faut les repérer avant de calculer : combien d'appels récursifs (aa), sur quelle fraction de l'entrée (n/bn/b), et combien coûte le travail non récursif (f(n)f(n)).

AlgorithmeÉquation
Recherche dichotomiqueT(n)=T(n/2)+1T(n) = T(n/2) + 1
Tri fusionT(n)=2T(n/2)+nT(n) = 2\,T(n/2) + n
Tri rapide, cas moyenT(n)=2T(n/2)+nT(n) = 2\,T(n/2) + n
Tri rapide, pire casT(n)=T(n1)+nT(n) = T(n-1) + n
FactorielleT(n)=T(n1)+1T(n) = T(n-1) + 1
Fibonacci naïfT(n)=T(n1)+T(n2)+1T(n) = T(n-1) + T(n-2) + 1

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 nlognn \log n et n2n^2 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 TT par sa définition, plusieurs fois, jusqu'à voir le motif.

T(n)=2T(n/2)+n=2[2T(n/4)+n/2]+n=4T(n/4)+2n=4[2T(n/8)+n/4]+2n=8T(n/8)+3n\begin{aligned} T(n) &= 2\,T(n/2) + n \\ &= 2\,[\,2\,T(n/4) + n/2\,] + n = 4\,T(n/4) + 2n \\ &= 4\,[\,2\,T(n/8) + n/4\,] + 2n = 8\,T(n/8) + 3n \end{aligned}

Le motif apparaît à la troisième ligne : après kk déroulements, T(n)=2kT(n/2k)+knT(n) = 2^k\,T(n/2^k) + k\,n.

Reste à savoir où s'arrêter : quand l'argument atteint le cas de base, c'est-à-dire quand n/2k=1n/2^k = 1, donc k=log2nk = \log_2 n. En substituant :

T(n)=nT(1)+nlog2n=n+nlog2n=O(nlogn)T(n) = n\,T(1) + n \log_2 n = n + n \log_2 n = O(n \log n)

Le log2n\log_2 n est le nombre de fois qu'on peut couper nn 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 = nniveau k   2^k appels de taille n/2^k               travail : nniveau log n    n appels de taille 1                travail : n

Chaque niveau coûte nn — 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 log2n\log_2 n niveaux. Total : nlog2nn \log_2 n.

Cette lecture rend immédiats les trois régimes possibles, qu'il suffit de reconnaître.

Le travail domine à la racine. Si f(n)f(n) décroît plus vite que le nombre d'appels ne croît, la somme est dominée par le premier niveau : T(n)=Θ(f(n))T(n) = \Theta(f(n)). Exemple : T(n)=2T(n/2)+n2T(n) = 2T(n/2) + n^2 donne Θ(n2)\Theta(n^2).

Le travail est également réparti. Chaque niveau coûte pareil, et l'on multiplie par le nombre de niveaux : T(n)=Θ(f(n)logn)T(n) = \Theta(f(n) \log n). C'est le tri fusion.

Le travail domine aux feuilles. Si le nombre d'appels croît plus vite que ff ne décroît, la somme est dominée par le dernier niveau, qui compte alogbn=nlogbaa^{\log_b n} = n^{\log_b a} feuilles. Exemple : T(n)=2T(n/2)+1T(n) = 2T(n/2) + 1 donne Θ(n)\Theta(n) — le travail se fait dans les nn feuilles.

Ces trois cas constituent, sous une forme informelle, le théorème principal (master theorem). Le comparant à retenir est simple : confronter f(n)f(n) à nlogban^{\log_b a}.

Une équation ne relève pas de ce schéma et mérite d'être traitée à part : T(n)=T(n1)+nT(n) = T(n-1) + n, le pire cas du tri rapide. Ici l'argument ne se divise pas, il décroît de 1 : il y a donc nn niveaux, et le travail total vaut n+(n1)++1=n(n+1)/2n + (n-1) + \dots + 1 = n(n+1)/2, soit Θ(n2)\Theta(n^2). Le contraste avec 2T(n/2)+n2T(n/2) + n tient tout entier dans la façon de réduire l'entrée.

Quiz · 1 question

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 ?

  • En log n, car il y a log n niveaux et le travail par niveau est constantlog n
  • En n : le nombre d'appels double à chaque niveau tandis que le travail par appel reste à 1, donc le dernier niveau coûte à lui seul n et domine tout le resten
  • En n log n, comme le tri fusion, puisque la structure de la récursion est la mêmen log n

Réponse : C'est le troisième régime : le travail domine aux feuilles. Le niveau k contient 2^k appels coûtant 1 chacun, donc 2^k au total — cette quantité DOUBLE à chaque niveau au lieu de rester constante. Le dernier niveau en compte n/2^0… soit n feuilles, et la somme 1 + 2 + 4 + … + n vaut 2n − 1, dominée par son dernier terme. Total Θ(n). La différence avec le tri fusion tient au seul f(n) : à n par appel, la décroissance de la taille compense exactement le doublement du nombre d'appels et chaque niveau coûte n ; à 1 par appel, elle ne compense plus rien. La première réponse compte les niveaux en oubliant que chacun contient de plus en plus d'appels.

La dichotomie, et pourquoi elle est si rapide

T(n)=T ⁣(n2)+1T(n)=log2nT(n) = T\!\left(\frac{n}{2}\right) + 1 \qquad\Longrightarrow\qquad T(n) = \log_2 n

Un seul appel par niveau, et un travail constant : l'arbre d'appels est une ligne de log2n\log_2 n é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.

Graphique

Nombre de comparaisons pour traiter 100 000 valeurs

  • Tri par insertion, n²/4 : 2 500 000 0002500000000
  • Tri fusion, n log₂ n : 1 700 0001700000
  • Recherche dichotomique, log₂ n : 1717
Les deux dernières barres sont invisibles, et c'est le propos : face aux deux milliards et demi de comparaisons du tri quadratique, le tri fusion en demande 1 700 000 — mille cinq cents fois moins — et une recherche dichotomique dix-sept. Ce ne sont pas des optimisations, ce sont des changements de régime.

Pourquoi nlognn \log n est une limite

Reste la question de fond : les deux tris du chapitre 3 s'arrêtent tous deux à nlognn \log n. 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 n!n! feuilles — une par permutation. Or un arbre binaire de hauteur hh a au plus 2h2^h feuilles. Il faut donc 2hn!2^h \ge n!, c'est-à-dire hlog2(n!)h \ge \log_2(n!).

La formule de Stirling donne log2(n!)=Θ(nlogn)\log_2(n!) = \Theta(n \log n). La hauteur de l'arbre étant le nombre de comparaisons dans le pire cas :

tout tri par comparaison exige Ω(nlogn) comparaisons.\text{tout tri par comparaison exige } \Omega(n \log n) \text{ comparaisons.}

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 nn entiers d'un intervalle borné en O(n)O(n), en les rangeant directement dans des cases. Il ne contredit pas le théorème, il n'entre pas dans son cadre.

Quiz · 1 question

Un étudiant annonce un tri par comparaison en O(n) dans le pire cas. Que peut-on affirmer sans même lire son algorithme ?

  • Qu'il faut l'examiner attentivement : rien n'interdit une telle découverteà examiner
  • Qu'il est faux, ou qu'il n'est pas un tri par comparaison : l'arbre de décision doit avoir n! feuilles, donc une hauteur d'au moins log₂(n!) = Θ(n log n)impossible par construction
  • Qu'il est correct mais seulement sur des données déjà presque triéescas particulier

Réponse : La borne inférieure ne dépend d'aucun algorithme particulier : elle porte sur le MODÈLE. Un tri qui n'a que des comparaisons pour s'informer doit pouvoir aboutir à chacune des n! permutations, donc son arbre de décision a au moins n! feuilles ; un arbre binaire de hauteur h en ayant au plus 2^h, il faut h ≥ log₂(n!) = Θ(n log n). Aucune astuce ne franchit cela. Deux échappatoires seulement, et elles sortent du cadre : sortir du modèle, comme le tri par comptage qui range les clés dans des cases sans jamais les comparer et trie en O(n) sur un intervalle borné ; ou parler du MEILLEUR cas, où le tri par insertion est déjà en O(n) sur des données triées — mais ce n'est pas le pire cas.

À 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 nlog2nn \log_2 n pour le tri fusion et n(n+1)/2n(n+1)/2 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 log2(n!)\log_2(n!) à nlog2nn \log_2 n pour constater que la borne inférieure est bien du même ordre que ce que le tri fusion atteint.

Exercice de code

Résolvez numériquement quatre récurrences, puis comparez log2(n!) à n log2 n.

Point de départ

// 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");

Solution

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;
}

const fusion = resoudre((n, T) => 2 * T(Math.floor(n / 2)) + n, 1);
const rapidePire = resoudre((n, T) => T(n - 1) + n, 1);
const dicho = resoudre((n, T) => T(Math.floor(n / 2)) + 1, 1);
// Deux appels, travail constant : le nombre d'appels double sans que le
// travail par appel diminue. Le dernier niveau domine, et il compte n
// feuilles : c'est le troisième régime, donc Θ(n).
const feuilles = resoudre((n, T) => 2 * T(Math.floor(n / 2)) + 1, 1);
// Travail quadratique hors récursion : la racine domine, Θ(n²).
const racine = resoudre((n, T) => 2 * T(Math.floor(n / 2)) + n * n, 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);
    console.log("   n = " + String(n).padStart(5) +
      " | T(n) = " + String(mesure).padStart(12) +
      " | " + formule.padEnd(14) + " = " + String(Math.round(conjecture)).padStart(12) +
      " | rapport " + (mesure / conjecture).toFixed(2));
  }
  console.log("");
}

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");
comparer("T(n) = T(n/2) + 1      (dichotomie)", dicho, (n) => Math.log2(n), "log2 n");
comparer("T(n) = 2T(n/2) + 1     (travail aux feuilles)", feuilles, (n) => n, "n");
comparer("T(n) = 2T(n/2) + n^2   (travail à la racine)", racine, (n) => n * n, "n²");
// Dans les cinq cas le rapport tend vers une constante : c'est exactement ce
// que « du même ordre » veut dire. Le rapport ne tend PAS vers 1 — les
// constantes multiplicatives et les termes d'ordre inférieur subsistent — et
// c'est pourquoi on écrit Θ et non « égale ».

console.log("— la borne inférieure des tris par comparaison —");
// log2(n!) se calcule sans jamais construire n! : log2(a·b) = log2 a + log2 b.
function logFactorielle(n) {
  let s = 0;
  for (let k = 2; k <= n; k++) s += Math.log2(k);
  return s;
}
for (const n of [16, 64, 256, 1024, 4096]) {
  const borne = logFactorielle(n);
  const atteint = n * Math.log2(n);
  console.log("   n = " + String(n).padStart(5) +
    " | log2(n!) = " + String(Math.round(borne)).padStart(7) +
    " | n log2 n = " + String(Math.round(atteint)).padStart(7) +
    " | rapport " + (borne / atteint).toFixed(3));
}
// Le rapport se stabilise autour de 0,9 : log2(n!) et n log2 n sont du même
// ordre, comme l'annonce la formule de Stirling. Autrement dit la borne
// inférieure et ce que le tri fusion atteint ne diffèrent que d'une constante
// — le tri fusion est optimal, et il ne reste aucune marge asymptotique.

En travaux pratiques

Travaux pratiques 4 · 2 h

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.

Avant de commencer

  • Le TP 3 : les tris implémentés et mesurés
  • De quoi tracer une courbe

Énoncé

  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édirePour chacune, prédisez le facteur multiplicatif du temps quand n double. Notez vos quatre prédictions avant toute mesure. Indice : Un algorithme en n log n ne double pas exactement : il fait un peu plus.
  3. MesurerMesurez les quatre en doublant n plusieurs fois. Comparez au tableau de prédictions et calculez l'écart.
  4. L'écartLà 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étrerInstrumentez 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 moyenPour 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 courbeTracez vos mesures en échelle logarithmique sur les deux axes. Mesurez la pente de chaque droite et déduisez-en l'exposant.

C'est réussi quand

  • 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

Correction

Les quatre récurrences
dichotomie    T(n) = T(n/2) + O(1)      → O(log n)
tri fusion    T(n) = 2 T(n/2) + O(n)     → O(n log n)
tri rapide    T(n) = 2 T(n/2) + O(n)     → O(n log n)  en moyenne
 pire cas   T(n) = T(n-1)  + O(n)      → O(n²)
Fibonacci     T(n) = T(n-1) + T(n-2)     → O(1,618^n)

théorème général : T(n) = a T(n/b) + f(n)
compare n^(log_b a) à f(n) ; ici a=2, b=2 → n, égal à f(n)
→ O(n log n)

La forme de la récurrence se lit directement dans le code : combien d'appels récursifs (a), sur quelle fraction de l'entrée (b), plus quel travail local (f). Savoir écrire la récurrence en regardant une fonction est plus utile que savoir réciter le théorème.

Prédictions et mesures
quand n double :          prédit    mesuré
dichotomie   O(log n)      +1 étape   +1 étape      ✓
tri fusion   O(n log n)    ×2,1       ×2,3          ✓
insertion    O(n²)         ×4         ×4,1          ✓
Fibonacci    O(1,618^n)    ×2,6       ×2,6          ✓

/* mais entre n = 2^20 et n = 2^22, le tri fusion fait ×2,9 */

Trois prédictions sur quatre tombent juste immédiatement. La quatrième dérive au-delà d'une certaine taille — et ce n'est pas la théorie qui est fausse, c'est le modèle de coût qui suppose un accès mémoire à prix constant.

Ce que le modèle ignore
tri fusion, facteur mesuré quand n double :
n < 2^18  (< cache L2) : ×2,1
n = 2^20                : ×2,3
n > 2^22  (> cache L3) : ×2,9

la complexité n'a pas changé ; le COÛT D'UN ACCÈS a changé
(TP 7 d'Architecture : 1 ns en L1, 100 ns en mémoire centrale)

La complexité compte les OPÉRATIONS en supposant qu'elles coûtent toutes pareil. La machine réelle ne respecte pas cette hypothèse. La théorie reste valable — elle prédit correctement l'ordre de grandeur — mais elle ne suffit pas à choisir entre deux algorithmes de même complexité. C'est pourquoi on mesure toujours.

Compter plutôt que chronométrer
tri fusion, comparaisons mesurées :
n        comparaisons   n log2 n
1 000        8 704        9 966
10 000     120 464      132 877
100 000  1 536 371    1 660 964

rapport comparaisons / (n log2 n) : 0,87, 0,91, 0,92 — STABLE

Le comptage d'opérations valide la théorie avec une précision que le chronomètre n'atteint jamais, parce qu'il ne dépend ni du cache, ni de l'ordonnanceur, ni de la fréquence du processeur. C'est la bonne façon de vérifier une analyse ; le chronomètre, lui, répond à une autre question — celle de la performance réelle.

Les trois cas, et lequel promettre
tri rapide sur 100 000 éléments :
meilleur cas (pivot toujours médian)  : 0,008 s
cas moyen    (entrée aléatoire)       : 0,009 s
pire cas     (entrée triée)           : 24 s

garantie contractuelle → le PIRE cas

Le cas moyen décrit ce qui se passe d'habitude, le pire cas ce qu'on peut PROMETTRE. Un service exposé sur le réseau ne raisonne jamais en moyenne, parce que l'entrée n'est pas tirée au hasard : elle est choisie par quelqu'un. C'est aussi pourquoi le tri rapide de la bibliothèque standard bascule vers le tri par tas quand la profondeur dérape — pour garantir un pire cas en n log n.

Lire l'exposant sur une pente
en échelle log-log, une complexité en n^k donne une DROITE
de pente k

insertion : pente 2,0    → O(n²)
fusion    : pente 1,05   → O(n log n), presque linéaire
dichotomie: plate         → O(log n)

C'est l'outil de diagnostic le plus rapide devant un programme lent : quelques mesures, une échelle logarithmique, et la pente donne la complexité réelle du code — celle qui s'exécute, pas celle qu'on croit avoir écrite. Une pente de 2 là où on attendait 1 signale presque toujours une boucle imbriquée oubliée, ou un appel à strlen dans une condition de boucle.

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 T(h)=2T(h1)+1T(h) = 2T(h-1) + 1 pour compter les nœuds d'un arbre binaire complet.

Le logn\log n reviendra surtout comme promesse conditionnelle. Un arbre binaire de recherche donne des opérations en O(logn)O(\log n) si sa hauteur reste logarithmique, et en O(n)O(n) 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

Flashcards · 5 cartes

Comment lit-on l'équation de récurrence d'un algorithme récursif ?
Ligne à ligne, en repérant trois quantités : COMBIEN d'appels récursifs (a), SUR QUELLE FRACTION de l'entrée (n/b), et le COÛT DU TRAVAIL NON RÉCURSIF f(n). Le tri fusion donne T(n) = 2T(n/2) + n ; le pire cas du tri rapide donne T(n) = T(n−1) + n. Même algorithme, deux équations selon le pivot — et tout l'écart entre n log n et n² se lit avant le moindre calcul : diviser par deux contre retirer un.
Décrivez la résolution par déroulement, sur T(n) = 2T(n/2) + n.
On remplace T par sa définition jusqu'à voir le motif : 2T(n/2)+n, puis 4T(n/4)+2n, puis 8T(n/8)+3n, d'où après k déroulements 2^k·T(n/2^k) + k·n. On s'arrête quand l'argument atteint le cas de base, soit n/2^k = 1 donc k = log₂ n, ce qui donne n·T(1) + n log₂ n = O(n log n). Le log₂ n est simplement le NOMBRE DE FOIS qu'on peut couper n en deux avant d'arriver à 1.
Quels sont les trois régimes que révèle l'arbre d'appels ?
On calcule le travail par NIVEAU, puis on somme. 1) Le travail domine À LA RACINE : la somme est dominée par le premier niveau, T = Θ(f(n)) — ex. 2T(n/2)+n² donne Θ(n²). 2) Le travail est ÉGALEMENT RÉPARTI : chaque niveau coûte pareil, T = Θ(f(n)·log n) — c'est le tri fusion. 3) Le travail domine AUX FEUILLES : le dernier niveau l'emporte, T = Θ(n^(log_b a)) — ex. 2T(n/2)+1 donne Θ(n). C'est le théorème principal, et le comparant est f(n) contre n^(log_b a).
Démontrez que tout tri par comparaison exige Ω(n log n) comparaisons.
Un tel tri ne peut que comparer et se brancher : son exécution est un ARBRE DE DÉCISION binaire dont chaque feuille est une permutation atteignable. Comme il doit trier n'importe quelle entrée, l'arbre a au moins n! feuilles. Or un arbre binaire de hauteur h en a au plus 2^h, donc h ≥ log₂(n!) = Θ(n log n) par Stirling. La hauteur étant le nombre de comparaisons au pire cas, la borne est démontrée — et le tri fusion l'atteint, donc il est optimal dans sa catégorie.
Comment le tri par comptage peut-il trier en O(n) sans contredire cette borne ?
Parce qu'il n'entre pas dans le modèle. La borne suppose que l'algorithme ne s'informe QUE par des comparaisons. Le tri par comptage exploite la structure des clés : il range directement chaque entier dans la case correspondante d'un tableau de compteurs, sans jamais comparer deux éléments entre eux. Il trie donc n entiers d'un intervalle borné en O(n) — il ne contredit pas le théorème, il en sort.