Skip to content

Niveau 1 · Chapitre 1.2

Algorithmes et complexité (grand O)

Mesurer un algorithme avant de l’exécuter : compter des étapes plutôt que des secondes, vitesses de croissance et notation grand O, meilleur, pire et cas moyen, pourquoi n log n bat n², et où la vraie machine tord les règles — chaque chiffre tiré d’exécutions en direct.

Un algorithme est une recette précise et finie qui transforme une entrée en sortie : une liste de noms en liste triée, une carte en plus court chemin, un nombre en facteurs premiers. Avant même qu’un langage de programmation entre en jeu, on peut comparer des algorithmes sur une question : quelle quantité de travail font-ils quand l’entrée grandit ?

Compter des étapes, pas des secondes

Chronométrer un programme mesure à la fois le programme, le compilateur et la machine. Pour comparer des algorithmes, on compte plutôt leurs opérations de base — comparaisons, échanges, lectures du tableau — en fonction de la taille de l’entrée n. Ce décompte ne dépend pas du CPU, et il montre comment le coût croît. Pour les grandes entrées, c’est la croissance qui compte.

Voici deux façons de chercher une valeur dans une liste de 32 nombres. La recherche linéaire examine chaque élément tour à tour. La recherche dichotomique ne marche que sur une liste triée : elle regarde le milieu, écarte la moitié qui ne peut pas contenir la valeur, et recommence.

Algorithms · Recherche linéaire
target= 18
0
comparisons
of 8 total
…
result
≤ n = 32 probes
0
writes
array elements
0
reads
array elements
comparedswapped / writtenfoundrange lo..hi

Initial array.

step 0 / 9

Linear search — Checks every element left to right until it meets the target. Best O(1), average O(n), worst O(n).

Cost vs n· comparisons, random input, target present
01002008163264128256n →log₂ nnn = 8: 7 comparisonsn = 16: 4 comparisonsn = 32: 8 comparisonsn = 64: 62 comparisonsn = 128: 127 comparisonsn = 256: 174 comparisons
measuredreference curves (unscaled: log₂ n, n)
comparisons at n = 256 — click to switch input
Algorithms · Recherche dichotomique
target= 16
0
comparisons
of 2 total
…
result
≤ ⌊log₂ 32⌋ + 1 = 6 probes
0
writes
array elements
0
reads
array elements
comparedswapped / writtenfoundrange lo..hi

Initial array.

step 0 / 5

Binary search — Needs sorted input: each probe of the middle element halves the range lo..hi. Best O(1), average O(log n), worst O(log n). The generated array is sorted first.

Cost vs n· comparisons, random input, target present
02.557.58163264128256n →log₂ nnn = 8: 3 comparisonsn = 16: 2 comparisonsn = 32: 2 comparisonsn = 64: 5 comparisonsn = 128: 7 comparisonsn = 256: 7 comparisons
measuredreference curves (unscaled: log₂ n, n)
comparisons at n = 256 — click to switch input

Réglez la cible sur absent — le pire cas — et regardez le graphique cost vs n sous chaque démo. La recherche linéaire fait une comparaison par élément : 8, 16, 32 … 256. La recherche dichotomique divise l’intervalle par deux à chaque étape, et il lui faut 3, 4, 5 … 8 comparaisons. Doubler l’entrée ajoute une étape. Sur un million d’éléments, cela fait 20 comparaisons contre un million.

nrecherche linéairerecherche dichotomique
883
32325
2562568
1 000 0001 000 00020

La notation grand O

La notation grand O décrit la croissance d’un coût en ignorant les facteurs constants et les petites entrées. Formellement, un coût f(n) est en O(g(n)) s’il existe des constantes c et n₀ telles que f(n) ≤ c·g(n) pour tout n ≥ n₀. En pratique : on garde le terme qui croît le plus vite et on oublie son coefficient. Par exemple, 3n² + 5n + 20 est en O(n²).

Les classes les plus courantes, avec le temps qu’elles prendraient pour n = 1 000 000 à raison d’une nanoseconde par étape :

ClasseNomExemplen = 10⁶
O(1)constantelire a[i]1 ns
O(log n)logarithmiquerecherche dichotomique20 ns
O(n)linéairerecherche linéaire, somme d’un tableau1 ms
O(n log n)quasi linéairetri fusion, tri par tas20 ms
O(n²)quadratiquetri par insertion, comparer toutes les paires17 minutes
O(2ⁿ)exponentielleessayer tous les sous-ensemblesplus que l’âge de l’Univers

Chaque ligne du tableau est un autre monde. Aucune machine plus rapide ne sauve un algorithme en O(n²) sur une entrée assez grande, et un meilleur algorithme gagne souvent plus que n’importe quelle mise à niveau matérielle.

Meilleur cas, pire cas, cas moyen

Un même algorithme peut faire des quantités de travail très différentes sur des entrées de même taille. Le tri par insertion prend chaque élément et le fait glisser vers la gauche jusqu’à sa place :

Algorithms · Tri par insertion sur une entrée inversée
0
comparisons
of 120 total
0
swaps
of 120 total
0
writes
array elements
0
reads
array elements
3230282624222018161412108642
comparedswapped / writtenin final placeactive range

Initial array.

step 0 / 256

Insertion sort — Grows a sorted prefix; each new element sinks left until it meets a smaller one. Best O(n), average O(n²), worst O(n²).

Cost vs n· comparisons, reversed input
010k20k30k8163264128256n →nn log₂ nn²n = 8: 28 comparisonsn = 16: 120 comparisonsn = 32: 496 comparisonsn = 64: 2016 comparisonsn = 128: 8128 comparisonsn = 256: 32640 comparisons
measuredreference curves (unscaled: n, n log₂ n, n²)
comparisons at n = 256 — click to switch input

Sur 16 éléments en ordre inverse, chaque nouvel élément doit remonter jusqu’au début : 120 comparaisons, soit n(n − 1)/2. Passez l’entrée en sorted : 15 comparaisons, une par élément. Sur une entrée random, on est entre les deux : 88 ici. Pour n = 256, l’écart devient 255 comparaisons pour une entrée triée, 15 874 pour une entrée aléatoire et 32 640 pour une entrée inversée.

« Le tri par insertion est en O(n²) » est donc une affirmation sur le pire cas (et ici aussi le cas moyen). Son meilleur cas est en O(n), ce qui le rend excellent sur des données déjà presque triées.

Les pires cas se cachent là où on ne les attend pas. Le tri rapide de ce visualiseur prend le dernier élément comme pivot. Sur une entrée aléatoire, il lui faut 2 325 comparaisons pour 256 éléments, mais sur une entrée déjà triée, chaque partition est aussi déséquilibrée que possible, et il lui en faut 32 640, aussi mal que le tri par insertion. Les vraies bibliothèques s’en protègent : elles choisissent leurs pivots plus soigneusement et changent d’algorithme quand une partition tourne mal. Le std::sort du C++, par exemple, est en général un introsort, un tri rapide qui se replie sur le tri par tas.

n log n contre n²

Le tri montre l’écart entre les deux classes les plus courantes. Sur 256 éléments aléatoires :

AlgorithmeComparaisonsCroissance
tri par insertion15 874O(n²)
tri par sélection32 640O(n²), sur toute entrée
tri rapide2 325O(n log n) en moyenne
tri par tas3 323O(n log n)
tri fusion1 731O(n log n)
Algorithms · Tri fusion
0
comparisons
of 123 total
0
swaps
of 0 total
0
writes
array elements
0
reads
array elements
comparedswapped / writtenin final placeactive range

Initial array.

step 0 / 315

Merge sort — Top-down: sort each half, then merge the two sorted runs through an auxiliary buffer. Best O(n log n), average O(n log n), worst O(n log n).

Cost vs n· comparisons, random input
05001k1.5k8163264128256n →nn log₂ nn²n = 8: 15 comparisonsn = 16: 43 comparisonsn = 32: 123 comparisonsn = 64: 307 comparisonsn = 128: 736 comparisonsn = 256: 1731 comparisons
measuredreference curves (unscaled: n, n log₂ n, n²)
comparisons at n = 256 — click to switch input

Le tri fusion coupe la liste en deux jusqu’à des éléments isolés, puis refusionne des moitiés triées. Il y a log₂ n niveaux de découpage, et chaque niveau fait environ n travail : le total est en O(n log n).

Aucun tri par comparaisons ne peut faire fondamentalement mieux. Il existe n! ordres possibles, et chaque comparaison n’a que deux issues : les distinguer demande au moins log₂(n!) ≈ n log₂ n comparaisons dans le pire cas. Pour 256 éléments, cela fait environ 1 684 — et les 1 731 du tri fusion en sont proches.

Là où la vraie machine tord les règles

Le grand O est un point de départ, pas toute l’histoire :

  • Les constantes comptent pour les petits n. Un algorithme en O(n²) à petite constante bat un O(n log n) sur quelques dizaines d’éléments. C’est pourquoi les vrais tris — Timsort en Python et en Java, introsort en C++ — passent au tri par insertion sur les petits intervalles.
  • L’ordre des accès mémoire compte. Deux algorithmes au même décompte peuvent différer énormément en temps parce que l’un parcourt la mémoire dans l’ordre et l’autre saute partout. Le chapitre sur les caches montre 75 % contre 0 % de succès pour un travail identique.
  • Les branchements comptent. Une comparaison dont l’issue est aléatoire provoque une mauvaise prédiction environ une fois sur deux ; la même comparaison sur des données triées ne coûte presque rien.
  • La mémoire est aussi un coût. Le tri fusion a besoin de O(n) mémoire supplémentaire pour fusionner. Le tri par tas et le tri par insertion trient sur place, avec O(1) de mémoire en plus.

À retenir

  • On compare des algorithmes en comptant les étapes de base en fonction de la taille de l’entrée n, pas en les chronométrant.
  • Le grand O garde le terme qui croît le plus vite : O(1) < O(log n) < O(n) < O(n log n) < O(n²) < O(2ⁿ).
  • Un même algorithme a un meilleur cas, un pire cas et un cas moyen : le tri par insertion est en O(n) sur une entrée triée et en O(n²) sur une entrée inversée.
  • Le tri par comparaisons ne peut pas descendre sous environ n log₂ n comparaisons, et le tri fusion, le tri par tas et (en moyenne) le tri rapide atteignent cette borne.
  • Les constantes, les caches et la prédiction de branchement départagent les algorithmes d’une même classe.

Dans ce niveau

  1. 1.1Qu’est-ce qu’un calcul ?Prévu
  2. 1.2Algorithmes et complexité (grand O)
  3. 1.3Structures de données : tableaux, listes, arbres, tables de hachage
  4. 1.4Recherche et tri
  5. 1.5De l’algorithme au codePrévu