C4 — ComplexitéDans le dialogue d’impression, choisissez « Enregistrer au format PDF » comme destination.
Retour

Licence 1 · Algorithmique 1

Cours 4Complexité

Compter les opérations d'un algorithme plutôt que le chronométrer, et le classer.

1 chapitre · 25 min de travail estimé

  1. 1. Complexité25 min

Chapitre 1 · 25 min

Complexité

Compter les opérations plutôt que chronométrer, et classer un algorithme.

Nous avons maintenant ce qui manquait aux sept premières leçons : deux algorithmes de recherche et deux algorithmes de tri, tous corrects, dont certains sont manifestement meilleurs que d'autres. Reste à dire en quoi, et à le dire autrement que par « il a l'air plus rapide ».

Compter, plutôt que chronométrer

Le réflexe naturel est de mesurer un temps d'exécution. C'est une mauvaise mesure, pour trois raisons.

Elle dépend de la machine : le même algorithme est dix fois plus rapide sur un ordinateur récent, sans avoir changé d'une ligne. Elle dépend du langage et du contexte : compilateur, autres programmes en cours, mémoire disponible. Et surtout, elle ne dit rien de ce qui compte vraiment — comment le coût évolue quand les données grandissent.

On compte donc les opérations élémentaires : comparaisons, affectations, accès à une case de tableau. Le résultat est un nombre en fonction de n, la taille des données. Il est indépendant de la machine, et il se calcule sur le papier, sans exécuter le programme.

Reprenons nos algorithmes.

AlgorithmeOpérations comptées (pire cas)
Lire T[i]1
Recherche séquentiellen comparaisons
Recherche dichotomiqueenviron log2n\log_2 n comparaisons
Tri par sélectionn(n1)/2n(n-1)/2 comparaisons

L'ordre de grandeur

Le tri par sélection fait exactement n(n1)/2n(n-1)/2 comparaisons, soit n2/2n/2n^2/2 - n/2. Pour n = 1000, cela fait 499 500. Le terme n2/2n^2/2 en vaut 500 000 : le second terme ne pèse que 0,1 % du total, et son poids relatif diminue encore quand n grandit.

D'où la convention centrale de cette leçon. On ne garde que le terme dominant, et on oublie les constantes multiplicatives. n2/2n/2n^2/2 - n/2 devient n2n^2, et on écrit :

n(n1)2=O(n2)\frac{n(n-1)}{2} = O(n^2)

Cela se lit « est en grand O de n carré » et signifie : quand n devient grand, le coût croît comme le carré de n. La notation ne prétend pas donner le nombre exact d'opérations — elle donne la forme de la croissance, qui est la seule chose qui survive au changement de machine.

Jeter les constantes peut sembler cavalier. C'est un choix assumé : un algorithme deux fois plus lent le reste toujours, alors qu'un algorithme d'une classe supérieure devient arbitrairement pire à mesure que les données grandissent. La classe l'emporte toujours, pourvu que n soit assez grand.

Quiz · 1 question

Un algorithme effectue 3n² + 500n + 2000 opérations. Quelle est sa classe ?

  • O(3n²)constante conservée
  • O(n²)terme dominant seul
  • O(n² + n)termes cumulés

Réponse : On garde le terme dominant, n², et on jette la constante 3 ainsi que les termes de degré inférieur. Pour n = 10 000, le terme en n² pèse 300 millions contre 5 millions pour les deux autres réunis : ils sont négligeables, même avec un coefficient de 500.

Les cinq classes à connaître

Animation · 6 étapes

Comment le coût grandit avec la taille des données

  1. O(1) — lire T[i]Une opération, quelle que soit la taille des données. La courbe est plate : doubler n ne coûte rien de plus.
  2. O(log n) — recherche dichotomiqueChaque doublement de n n'ajoute qu'une seule opération. C'est presque aussi bon que constant.
  3. O(n) — recherche séquentielleLe coût suit la taille : deux fois plus de données, deux fois plus de travail. Une droite.
  4. O(n log n) — tris efficacesÀ peine plus cher que linéaire, et c'est le meilleur coût possible pour un tri par comparaisons.
  5. O(n²) — tri par sélectionLa courbe quitte le cadre avant n = 20. C'est la classe qu'on cherche à éviter dès que n grandit.
  6. Comparaison finaleSur de petites données toutes ces courbes se valent. C'est en grandissant que l'écart devient décisif : le classement importe plus que le détail des opérations.
ClasseNomExemple vu en coursSi n double…
O(1)O(1)constantelire T[i]le coût ne bouge pas
O(logn)O(\log n)logarithmiquerecherche dichotomiqueune opération de plus
O(n)O(n)linéairerecherche séquentielle, somme d'un tableaule coût double
O(nlogn)O(n \log n)quasi-linéairetris efficaces (fusion, rapide)un peu plus que double
O(n2)O(n^2)quadratiquetri par sélection, tri par insertionle coût quadruple

La colonne de droite est la plus utile en pratique. Elle donne un test mental immédiat : si je double mes données, qu'arrive-t-il à mon temps de calcul ?

Voici ce que ces classes donnent sur des données réelles, à raison d'un million d'opérations par seconde.

nO(logn)O(\log n)O(n)O(n)O(nlogn)O(n \log n)O(n2)O(n^2)
100710070010 000
10 0001310 000130 000100 millions — 1,7 min
1 000 000201 s20 s10¹² — 11 jours

Un million de valeurs à trier, ce n'est pas beaucoup : c'est un carnet d'adresses d'entreprise. Onze jours contre vingt secondes, c'est toute la différence entre les tris de la leçon 7 et ceux que vous rencontrerez en L2.

Deviner la classe en lisant le code

Pour les algorithmes de ce cours, quatre règles suffisent.

Une suite d'instructions simples, sans boucle, coûte O(1)O(1) — quel que soit leur nombre. Dix affectations restent une constante.

Une boucle qui parcourt les données une fois coûte O(n)O(n).

Deux boucles imbriquées parcourant chacune les données coûtent O(n2)O(n^2) : pour chacun des n tours de la boucle externe, la boucle interne en fait n. C'est la signature visuelle des deux tris de la leçon 7.

Une boucle qui divise par deux à chaque tour coûte O(logn)O(\log n) : c'est la dichotomie. Le critère n'est pas la forme de la boucle mais ce qu'elle fait à la quantité de travail restante — la réduire d'un élément donne O(n)O(n), la réduire de moitié donne O(logn)O(\log n).

Attention à une nuance : deux boucles imbriquées ne sont pas deux boucles successives. Deux parcours l'un après l'autre coûtent n+n=2nn + n = 2n, soit O(n)O(n) — pas O(n2)O(n^2). C'est l'imbrication qui multiplie, la succession additionne.

Quiz · 1 question

Quelle est la complexité de : Pour i de 0 à n−1 Faire ; Pour j de 0 à n−1 Faire ; c ← c + 1 ; FinPour ; FinPour ?

  • O(n)un seul parcours
  • O(n²)produit de deux parcours
  • O(2n)deux parcours additionnés

Réponse : La boucle externe fait n tours, et à CHACUN de ces tours la boucle interne en fait n : n × n incréments au total. Deux boucles imbriquées sur les mêmes données, c'est toujours O(n²).

Quiz · 1 question

Pour trouver une valeur dans un tableau trié d'un milliard d'éléments, combien de comparaisons demande la dichotomie ?

  • Environ 30un doublement à la fois
  • Environ 1000racine de n
  • Environ un millionun millième de n

Réponse : 2³⁰ dépasse le milliard, donc une trentaine de comparaisons suffisent. C'est le propre du logarithmique : la taille des données peut être multipliée par mille, le coût n'augmente que de dix. La recherche séquentielle, elle, ferait un milliard de comparaisons.

Mesurer pour vérifier

La théorie annonce O(n2)O(n^2) pour le tri par sélection. Vérifions-le en comptant réellement, plutôt qu'en le croyant sur parole.

Exercice de code

Placez le compteur, exécutez, et observez le rapport entre les trois lignes de résultats.

Point de départ

// Ne chronométrez pas : comptez.
// Placez l'incrément du compteur au bon endroit, puis lisez le tableau
// de résultats : quand n est multiplié par 10, par combien le nombre de
// comparaisons est-il multiplié ?
function comparaisonsTriSelection(n) {
  const a = Array.from({ length: n }, (_, k) => n - k); // tableau à l'envers
  let comparaisons = 0;

  for (let i = 0; i < a.length - 1; i++) {
    let indiceMin = i;
    for (let j = i + 1; j < a.length; j++) {
      // à compléter : une comparaison a lieu ici, à chaque tour
      if (a[j] < a[indiceMin]) indiceMin = j;
    }
    const tmp = a[i];
    a[i] = a[indiceMin];
    a[indiceMin] = tmp;
  }

  return comparaisons;
}

console.log("n\tmesuré\tn(n-1)/2");
for (const n of [10, 100, 1000]) {
  console.log(n, comparaisonsTriSelection(n), (n * (n - 1)) / 2);
}

Solution

function comparaisonsTriSelection(n) {
  const a = Array.from({ length: n }, (_, k) => n - k);
  let comparaisons = 0;

  for (let i = 0; i < a.length - 1; i++) {
    let indiceMin = i;
    for (let j = i + 1; j < a.length; j++) {
      // Le compteur est DANS la boucle interne : c'est elle qui compare.
      comparaisons++;
      if (a[j] < a[indiceMin]) indiceMin = j;
    }
    const tmp = a[i];
    a[i] = a[indiceMin];
    a[indiceMin] = tmp;
  }

  return comparaisons;
}

console.log("n\tmesuré\tn(n-1)/2");
for (const n of [10, 100, 1000]) {
  console.log(n, comparaisonsTriSelection(n), (n * (n - 1)) / 2);
}

// n × 10  →  comparaisons × 100 environ. C'est la signature de O(n²),
// et la mesure colle exactement à n(n-1)/2 : le compte est exact, pas
// approché — seul l'ordre de grandeur est arrondi.

Multipliez n par 10 et observez : le nombre de comparaisons est multiplié par environ 100. C'est la signature de O(n2)O(n^2), obtenue sans chronomètre et sans dépendre de votre machine.

Ce que vous savez faire maintenant

Le parcours de ces huit leçons a une logique. Les leçons 1 à 4 ont installé les trois briques dont tout algorithme est fait : un état (les variables), des choix (les conditions), des répétitions (les boucles). Les leçons 5 à 7 les ont appliquées à une vraie structure de données, le tableau, en résolvant deux problèmes concrets : chercher et trier. La leçon 8 est revenue sur ces algorithmes pour les mesurer.

Cet ordre était délibéré. La notation O(n)O(n) placée en ouverture d'un cours reste un formalisme creux ; placée ici, elle répond à une question que vous vous posiez déjà depuis la leçon 6, quand la dichotomie a écrasé la recherche séquentielle sans qu'on sache encore comment nommer cet écart.

Trois réflexes à emporter. Tracer avant de conclure : dérouler un algorithme à la main sur un petit exemple reste le seul moyen fiable de savoir ce qu'il fait vraiment. Compter avant de comparer : deux algorithmes corrects ne se valent pas, et l'intuition se trompe souvent sur lequel est le meilleur. Regarder les boucles imbriquées : c'est là que se cache le coût.

À retenir

Flashcards · 2 cartes

Pourquoi compter les opérations plutôt que chronométrer ?
Parce que le temps dépend de la machine, du langage et du contexte, alors que le compte d'opérations est stable et décrit comment le coût évolue quand les données grandissent.
Deux boucles imbriquées sur n éléments : quelle classe ?
O(n²). Deux boucles SUCCESSIVES, en revanche, donnent O(n) : l'imbrication multiplie, la succession additionne.