Aller au contenu principal

Complexité algorithmique et Green IT

Ce que ce chapitre apporte 8 points
  • Compter les opérations élémentaires d'un algorithme en fonction de la taille de l'entrée, boucle par boucle.
  • Définir et utiliser les notations de Landau O, Ω et Θ.
  • Situer les classes de complexité usuelles et comparer leurs croissances.
  • Distinguer meilleur cas, pire cas et cas moyen, temps et espace.
  • Expliquer le fonctionnement d'une table de hachage et le coût de ses opérations.
  • Distinguer la complexité d'un problème de celle d'un algorithme.
  • Confronter une complexité théorique à une mesure : lire un exposant, extrapoler une durée.
  • Chiffrer l'énergie et les émissions d'un traitement, consommation à vide comprise, sans surestimer l'effet d'une optimisation.

Le service comptable d'un grand groupe garde l'historique des paiements versés à ses fournisseurs : dix millions de lignes. Certaines sont des doublons, un même paiement saisi deux fois : une facture reçue par courrier puis par courriel, un virement relancé après une coupure du réseau. Chaque doublon est de l'argent versé deux fois. Avant chaque clôture mensuelle, un contrôle lancé la nuit doit donc les retrouver tous, et sa première version compare chaque paiement à tous les autres. Tiendra-t-il dans les huit heures de la nuit, et que coûtera-t-il en énergie ? Tout dépend moins de la machine que de la façon d'écrire la recherche, et ce chapitre donne l'outil qui permet de le prévoir avant d'écrire le code. La réponse chiffrée vient après le Green IT, dans Retour aux paiements : du temps de calcul économisé, c'est de l'énergie et du matériel économisés, mais pas toujours autant qu'on le croit.

Il fait suite aux paradigmes de programmation : on y a vu comment structurer un programme, on regarde ici ce qu'il en coûte. Chaque calcul, quiz ou exercice est précédé de la notion qu'il emploie, souvent dans un encadré « Rappel de cours » qui donne la définition, la formule et un exemple résolu : le chapitre se fait seul, dans l'ordre.

Mesurer un algorithme, pas un programme

Chronométrer un programme donne un nombre qui dépend de la machine, du langage, de la version de l'interpréteur, de la charge du système et du jeu d'essai. Il suffit de changer l'un de ces facteurs pour que le nombre change.

Ce qu'on cherche. Non pas « combien de temps ça prend », mais « comment le temps grandit quand les données grandissent ». Cette question-là a une réponse indépendante de la machine, et c'est la seule qui permette de décider avant d'avoir écrit le code.

Opération élémentaire et fonction de coût

Définition

Une opération élémentaire est une opération dont le coût ne dépend pas de la taille de l'entrée : une comparaison, une affectation, une addition, un accès à une case de tableau.

La fonction de coût T(n)T(n) est le nombre d'opérations élémentaires effectuées sur une entrée de taille nn.

Compter, sur un exemple. Rechercher une valeur dans une liste non triée de nn éléments, en comptant les comparaisons au passage. L'algorithme est écrit en pseudo-code, la notation du parcours Algorithmique. Le bouton de déroulement exécute l'algorithme ligne par ligne. La seule chose à suivre est la variable comparaisons : elle monte jusqu'à 4 quand la cible est en quatrième position, et jusqu'à n=6n = 6 quand elle est absente. C'est ce second nombre, celui de la recherche infructueuse, qui donne le coût à retenir.

Algorithme
pas 1 / 42
Fonction Rechercher(tableau, n, cible)
comparaisons ← 0
Pour i de 1 à n
comparaisons ← comparaisons + 1
Si tableau[i] = cible Alors
Écrire "trouvé en ", comparaisons, " comparaisons"
Retourner i
FinSi
FinPour
Écrire "absent, après ", comparaisons, " comparaisons"
Retourner -1
FinFonction
Début
t ← [42, 17, 8, 23, 4, 15]
Écrire Rechercher(t, 6, 23)
Écrire Rechercher(t, 6, 99)
Fin

programme principal

t[42, 17, 8, 23, 4, 15]

Dans le pire des cas (la valeur est absente), la boucle fait nn tours. La variable comparaisons n'est qu'un instrument de mesure : ses propres mises à jour ne comptent pas. Chaque tour coûte en revanche trois opérations de l'algorithme : la comparaison tableau[i] = cible, celle que compte la variable, et deux autres invisibles dans le code, le test « ii a-t-il dépassé nn ? » et l'incrémentation de ii. Trois opérations restent hors des tours : l'initialisation de ii, le dernier test, celui qui fait sortir de la boucle, et le retour final. On obtient T(n)=3n+3T(n) = 3n + 3 opérations.

Pourquoi on jette les constantes

Le coefficient 33 dépend de ce qu'on décide d'appeler « une opération », et le terme constant +3+ 3 devient négligeable dès que nn dépasse quelques dizaines. Ce qui survit à tous ces choix arbitraires, c'est que TT croît proportionnellement à nn. C'est cette information-là qu'on garde, et rien d'autre.

La notation de Landau

Il reste à écrire « TT croît proportionnellement à nn » d'une manière qui ne dépende d'aucune convention de comptage. Il faut donc un langage qui compare deux fonctions à un facteur constant près, et seulement pour les grandes valeurs de nn. C'est exactement ce que fournit la notation ci-dessous.

Définition

On dit que uu est dominée par vv, et on note u(n)=O(v(n))u(n) = O(v(n)), s'il existe un rang n0n_0 et un réel k>0k > 0 tels que :

∀n≥n0,∣u(n)∣≤k ∣v(n)∣\forall n \geq n_0, \quad |u(n)| \leq k\,|v(n)|

En clair : à partir d'un certain rang n0n_0, la courbe de uu reste sous celle de vv agrandie par un facteur fixe kk. Les deux paramètres portent chacun une permission : kk permet d'ignorer les constantes, n0n_0 permet d'ignorer le début de la courbe.

Ce que la définition écarte, du même coup : elle ne dit rien de T(10)T(10), rien de la machine, et rien du rapport exact entre deux algorithmes de même classe.

Trois notations, et il faut savoir laquelle on emploie :

  • O(v)O(v), la majoration : « ne croît pas plus vite que ». C'est la plus utilisée, parce qu'on veut garantir un plafond.
  • Ω(v)\Omega(v), la minoration : « croît au moins aussi vite que ». Formellement, u(n)=Ω(v(n))u(n) = \Omega(v(n)) s'il existe n0n_0 et k>0k > 0 tels que ∣u(n)∣≥k ∣v(n)∣|u(n)| \geq k\,|v(n)| pour tout n≥n0n \geq n_0 : la même définition que OO, avec l'inégalité retournée. C'est la notation des bornes inférieures, celles qui disent qu'aucun algorithme ne peut faire mieux.
  • Θ(v)\Theta(v), l'encadrement : O(v)O(v) et Ω(v)\Omega(v) à la fois, c'est-à-dire k1 ∣v(n)∣≤∣u(n)∣≤k2 ∣v(n)∣k_1\,|v(n)| \leq |u(n)| \leq k_2\,|v(n)| à partir d'un certain rang. C'est la plus précise, et souvent celle qu'on veut vraiment dire.
« Cet algorithme est en O(n²) » peut être vrai et inutile

Un algorithme en Θ(n)\Theta(n) est aussi, formellement, en O(n2)O(n^2), et en O(n100)O(n^{100}). La majoration reste vraie, elle est simplement mauvaise.

Quand on affirme une complexité, dire si c'est une majoration ou un encadrement. Écrire Θ\Theta quand on le sait est plus fort, plus précis, et signale qu'on a compris la différence.

Une preuve, sur l'exemple de la recherche

Il s'agit de montrer que T(n)=3n+3=O(n)T(n) = 3n + 3 = O(n), c'est-à-dire de trouver un rang n0n_0 et un facteur kk qui conviennent.

Le couple n0=1n_0 = 1 et k=6k = 6 convient. Pour tout n≥1n \geq 1 on a 3≤3n3 \leq 3n, donc 3n+3≤3n+3n=6n3n + 3 \leq 3n + 3n = 6n.

Donc ∀n≥1,∣3n+3∣≤6∣n∣\forall n \geq 1, |3n + 3| \leq 6|n| : la définition est vérifiée avec n0=1n_0 = 1 et k=6k = 6. ∎

Le couple (n0,k)(n_0, k) n'est pas unique : n'importe quel k>3k > 3 conviendrait, avec un n0n_0 adapté. Avec k=4k = 4, par exemple, 3n+3≤4n3n + 3 \leq 4n équivaut à n≥3n \geq 3, et le rang devient n0=3n_0 = 3. Exhiber un couple suffit, c'est tout ce que demande la définition.

La définition parle de courbes, et jusqu'ici elle n'en montrait aucune. Les voici : le coût mesuré, et deux plafonds candidats.

12345678910-551015202530354045505560n (taille de l'entrée)opérationsrang n₀ = 3
Valeurs de f(x), g(x), h(x) sur l'intervalle [0 ; 10]
n (taille de l'entrée)fgh
0300
0,54,532
1664
1,57,596
29128
2,510,51510
3121812
3,513,52114
4152416
4,516,52718
5183020
5,519,53322
6213624
6,522,53926
7244228
7,525,54530
8274832
8,528,55134
9305436
9,531,55738
10336040
le coût, T(n) = 3n + 3 vaut 33le plafond de la preuve, k = 6 vaut 60un plafond plus serré, k = 4 vaut 40
Valeurs pour n = 10, le bord droit de la figure.
Le coût et deux plafonds. Avec k = 6, le plafond tient dès n = 1, où il touche le coût. Avec k = 4, plus serré, il faut attendre n = 3 : c'est le rang n₀, marqué par le trait vertical. Plus le facteur est petit, plus le rang est tardif, et la définition autorise les deux.
À manipuler
Le curseur sous la figure élargit l'axe horizontal. Le pousser jusqu'à quelques centaines : le croisement en n = 3 et le décalage de 3 au départ se tassent dans le coin, et le coût devient une droite de pente 3. Les trois droites, elles, gardent leurs pentes 3, 4 et 6 : un facteur constant ne disparaît jamais du dessin. Ce que veut dire « à un facteur constant près », c'est que la définition accepte n'importe lequel, pourvu qu'il soit fixe.
Revenir ensuite au début de la courbe, entre 0 et 3. Le plafond à 4n passe sous le coût : la majoration y est fausse, et c'est précisément ce que le rang n₀ permet d'ignorer. Une définition sans n₀ rejetterait un plafond parfaitement acceptable pour la seule raison qu'il échoue sur les premières valeurs.

Les classes usuelles

ComplexitéNomExemple typiquePour mille élémentsPour un million
O(1)O(1)constanteaccès à une case de tableau11
O(log⁡n)O(\log n)logarithmiquerecherche dichotomique1020
O(n)O(n)linéaireparcours d'une liste10³10⁶
O(nlog⁡n)O(n \log n)quasi-linéairetri par fusion10⁴2 × 10⁷
O(n2)O(n^2)quadratiquedouble boucle imbriquée10⁶10¹²
O(2n)O(2^n)exponentielleénumération de tous les sous-ensembles10³⁰¹inatteignable

L'ordre des lignes est celui des croissances comparées, vues dans le chapitre sur l'étude de fonction : toute puissance de nn l'emporte sur le logarithme, et l'exponentielle l'emporte sur toute puissance. Les nombres de la table prennent le logarithme en base 2, noté log⁡2\log_2, celui qui compte les divisions par deux : log⁡21 000≈10\log_2 1\,000 \approx 10. Dans un OO, la base ne s'écrit pas : changer de base multiplie le logarithme par une constante (log⁡2n=ln⁡n/ln⁡2\log_2 n = \ln n / \ln 2), et les constantes disparaissent.

Le logarithme mérite qu'on s'y arrête, parce qu'il est le seul dont l'origine ne se devine pas. Il vient du fait qu'on coupe en deux : chaque étape écarte la moitié de ce qui reste, et le nombre d'étapes est le nombre de fois qu'on peut diviser nn par 2.

Soit un tableau trié de quinze valeurs : 3 8 12 19 23 31 37 42 48 55 61 70 76 84 90. La dichotomie compare d'abord la cible à la valeur du milieu, 42. Selon la réponse, elle continue dans la moitié gauche (dont le milieu est 19) ou dans la moitié droite (dont le milieu est 70), et ainsi de suite. Toutes les suites de questions possibles forment un arbre. On cherche ici 61.

Graphe orienté15 sommets, 14 arcs
0 / 4
4219708315584312233748617690
Rien n'est encore visité.
L'arbre de toutes les recherches possibles dans un tableau trié de quinze valeurs. La recherche de 61 n'en parcourt qu'une branche : quatre comparaisons, une par niveau.
À manipuler
Dérouler les quatre étapes une à une, avec les flèches sous la figure : chaque comparaison colore un nœud et lui donne son rang, et la légende sous le dessin dit quelle moitié est écartée.
Compter, après chaque étape, les valeurs encore possibles : 15 au départ, puis 7, puis 3, puis 1. À la fin, quatre nœuds seulement sont colorés sur quinze : c'est ce qui est éliminé sans être visité qui explique le coût.

Le dessin montre pourquoi la dichotomie est si rapide : elle ne parcourt pas l'arbre, elle en suit une seule branche. Son coût est donc la profondeur de l'arbre, et non son nombre de nœuds. Au pire, il faut ⌊log⁡2n⌋+1\lfloor \log_2 n \rfloor + 1 comparaisons, où ⌊⋅⌋\lfloor \cdot \rfloor désigne la partie entière, l'arrondi à l'entier inférieur : 4 pour quinze valeurs, 10 pour mille, 20 pour un million, 33 pour les huit milliards d'habitants de la planète.

Un ordre de grandeur à retenir sans calcul

log⁡2\log_2 d'un million vaut environ 20, et log⁡2\log_2 d'un milliard environ 30. Multiplier la taille des données par mille n'ajoute que dix comparaisons. C'est ce comportement qui rend les index de bases de données praticables sur des tables que personne ne pourrait parcourir.

La table se lit mieux en la voyant. Le curseur sous la figure élargit l'axe horizontal, de trente jusqu'à cent mille : il suffit de le pousser pour voir les écarts se creuser.

24681012141618202224262830100200300400500600700800n (taille de l'entrée)opérations
Valeurs de f(x), g(x), h(x) sur l'intervalle [1 ; 30]
n (taille de l'entrée)fgh
1101
2,452,453,16736,0025
3,93,97,657515,21
5,355,3512,944528,6225
6,86,818,805646,24
8,258,2525,116368,0625
9,79,731,796594,09
11,1511,1538,7905124,3225
12,612,646,0574158,76
14,0514,0553,5656197,4025
15,515,561,29240,25
16,9516,9569,2105287,3025
18,418,477,3101338,56
19,8519,8585,5747394,0225
21,321,393,9922453,69
22,7522,75102,5523517,5625
24,224,2111,2458585,64
25,6525,65120,0647657,9225
27,127,1129,002734,41
28,5528,55138,0512815,1025
3030147,2067900
linéaire, n vaut 30quasi-linéaire, n log₂ n vaut 147,21quadratique, n² vaut 900
Valeurs pour n = 30, le bord droit de la figure.
Trois classes de complexité. À n = 30, le quadratique coûte déjà six fois le quasi-linéaire et trente fois le linéaire. En élargissant jusqu'à quelques milliers, ces rapports passent à plusieurs centaines et plusieurs milliers : le linéaire et le quasi-linéaire se couchent sur l'axe, seul le quadratique se voit encore.

Ce qu'il faut y regarder n'est pas la hauteur des courbes, mais ce que devient leur rapport quand le domaine s'élargit. À n=30n = 30, le quadratique vaut trente fois le linéaire ; à n=3 000n = 3\,000, il vaut trois mille fois le linéaire et deux cent soixante fois le quasi-linéaire. Le rapport entre deux classes n'est pas un nombre fixe : il grandit avec nn. Sur un banc d'essai réel, les constantes peuvent masquer ce rapport tant que nn reste petit, et c'est là que trente éléments trompent.

Ces classes se lisent dans le code, boucle par boucle.

Rappel de cours : lire une boucle

Le coût d'une boucle est son nombre de tours multiplié par le coût d'un tour. Quatre formes reviennent sans cesse.

  • Une boucle sur les nn éléments fait nn tours : Θ(n)\Theta(n).
  • Deux boucles imbriquées, la seconde parcourant tous les éléments qui suivent le premier : (n−1)+(n−2)+⋯+1+0=n(n−1)/2(n-1) + (n-2) + \dots + 1 + 0 = n(n-1)/2 tours, la somme d'une suite arithmétique, donc Θ(n2)\Theta(n^2).
  • Une boucle intérieure bornée : si elle fait au plus 10 tours quel que soit nn, le total ne dépasse pas 10n10n, et le coût reste Θ(n)\Theta(n), deux boucles ou pas.
  • Une boucle qui double son compteur, de 1 jusqu'à nn, fait environ log⁡2n\log_2 n tours : une vingtaine pour un million.

Le tri fusion combine les deux dernières idées. Il coupe le tableau en deux, puis chaque moitié en deux, jusqu'à des morceaux d'une seule valeur : cela fait log⁡2n\log_2 n niveaux de découpe. À chaque niveau, la fusion des morceaux recopie les nn valeurs une fois. D'où nlog⁡2nn \log_2 n recopies.

Exemple résolu. Sur les seize valeurs de la course ci-dessous, log⁡216=4\log_2 16 = 4 niveaux de seize recopies chacun : 4×16=644 \times 16 = 64 recopies. Sur mille valeurs, environ 10×1 000=10 00010 \times 1\,000 = 10\,000, quand une double boucle complète ferait 1 000×999/2=499 5001\,000 \times 999 / 2 = 499\,500 tours.

La courbe l'annonce, la course le fait subir. Deux tris travaillent ci-dessous sur les mêmes seize valeurs, un geste chacun par battement : l'un est quadratique, l'autre quasi-linéaire.

Tri fusion0 comparaison · 0 déplacement
Tri à bulles0 comparaison · 0 déplacement

comparéesdéplacéesordonnées entre ellesà leur place définitiveUn geste chacun par battement : la piste qui s'arrête la première en a fait moins.

Un tri fusion et un tri à bulles sur seize valeurs. Cent vingt-neuf gestes contre cent quatre-vingt-dix-huit : la fusion a fini quand les bulles en sont aux deux tiers, et l'écart s'ouvre à mesure que le tableau grandit.
À manipuler
Lancer, et regarder les deux compteurs de comparaisons s'écarter. Sur seize valeurs, la fusion en fait quarante-neuf, les bulles cent vingt : un facteur deux et demi, qui paraît modeste.
C'est justement le piège annoncé plus haut. Doubler la taille fait un peu plus que doubler le travail de la fusion, et quadruple celui des bulles ; à mille valeurs, la fusion fait moins de neuf mille comparaisons et les bulles près de cinq cent mille, près de soixante fois plus. Un banc d'essai sur seize valeurs aurait conclu à un écart sans importance, ce qui est exactement l'erreur que ce chapitre sert à ne pas commettre.
L'ordre de grandeur qui rend la table concrète

À un milliard d'opérations par seconde, un algorithme en O(n2)O(n^2) traite un million d'éléments en environ 17 minutes. Le même problème en O(nlog⁡n)O(n \log n) demande 20 millisecondes : cinquante mille fois moins. Et l'écart s'ouvre avec la taille, il ne se referme jamais : à cent millions d'éléments, le quadratique demande près de quatre mois là où le quasi-linéaire tient en trois secondes. Ce n'est plus une optimisation, c'est la différence entre faisable et infaisable.

L'exponentielle, elle, sort de l'échelle du temps. O(2n)O(2^n) avec n=100n = 100 demande environ 1,3×10301{,}3 \times 10^{30} opérations, soit quarante mille milliards d'années à la même cadence : trois mille fois l'âge de l'univers. Aucune machine, jamais, ne le fera.

Le réflexe du doublement

« Si on double la taille des données, que devient le temps ? » O(log⁡n)O(\log n) : il augmente d'une constante. O(n)O(n) : il double. O(nlog⁡n)O(n \log n) : il fait un peu plus que doubler. O(n2)O(n^2) : il quadruple. O(2n)O(2^n) : il est élevé au carré.

C'est aussi la façon la plus simple de deviner la complexité d'un code inconnu : mesurer sur nn, puis sur 2n2n, et lire le rapport.

Vérification rapideon peut se reprendre

1.Compter les opérations d'un algorithme donne T(n)=3n+3T(n) = 3n + 3. Que garde-t-on de ce résultat ?

2.Un algorithme en O(n²) est-il toujours plus lent qu'un O(n log n) ?

3.On double la taille des données. Un algorithme en O(n²) voit son temps…

4.Une recherche dichotomique dans un million de valeurs triées fait au pire combien de comparaisons ?

Meilleur cas, pire cas, cas moyen

Pour une même taille nn, le nombre d'opérations dépend souvent du contenu de l'entrée. La recherche linéaire trouve parfois au premier essai, parfois au dernier. On distingue donc trois fonctions de coût :

  • le meilleur cas, ici la cible en première case : Θ(1)\Theta(1). Rarement intéressant, il décrit un coup de chance ;
  • le pire cas, ici la cible absente : Θ(n)\Theta(n). C'est la garantie, donc ce qu'on annonce par défaut ;
  • le cas moyen, ici la cible présente à une position au hasard, chacune avec la probabilité 1/n1/n. La position pp coûte pp comparaisons, d'où une moyenne de (1+2+⋯+n)/n=(n+1)/2(1 + 2 + \dots + n)/n = (n + 1)/2 comparaisons, soit Θ(n)\Theta(n). C'est le plus réaliste, mais il exige une hypothèse sur les entrées, qu'il faut alors écrire.
Le cas et la notation sont deux questions différentes

On lit souvent « le meilleur cas est en Ω\Omega, le pire cas en OO ». C'est une confusion. Le cas dit de quelle entrée on parle ; la notation dit si on majore, minore ou encadre le coût de cette entrée. On peut donc encadrer le pire cas (Θ(n)\Theta(n) ci-dessus), ou seulement majorer le cas moyen si l'on ne sait pas mieux faire.

Le tri rapide, cas d'école du décalage

Son pire cas est Θ(n2)\Theta(n^2) : sur une liste déjà triée avec un pivot mal choisi. Son cas moyen est Θ(nlog⁡n)\Theta(n \log n), et c'est celui qu'on observe en pratique, surtout avec un pivot tiré au hasard. Les bibliothèques qui l'emploient lui ajoutent un garde-fou qui ramène le pire cas à nlog⁡nn \log n, et sorted, en Python, est un tri par fusion adapté, en Θ(nlog⁡n)\Theta(n \log n) au pire. Annoncer le seul pire cas du tri rapide nu donnerait une image fausse de son comportement réel.

Complexité spatiale

Définition

La complexité spatiale est la quantité de mémoire supplémentaire utilisée par l'algorithme, en fonction de nn, sans compter l'entrée elle-même.

Elle s'exprime avec les mêmes notations, et elle s'échange souvent contre du temps. La table de hachage, plus bas, en donne le cas type : elle consomme O(n)O(n) de mémoire pour ramener une recherche de O(n)O(n) à O(1)O(1) en moyenne.

Ne pas oublier la pile d'appels

Un algorithme récursif consomme de la mémoire à chaque appel imbriqué. Une récursion de profondeur nn occupe O(n)O(n) d'espace même si elle ne déclare aucune variable, et c'est ce qui provoque le RecursionError.

Les tables de hachage

Le contrôle des doublons pose, pour chaque paiement, une seule question : « cette référence a-t-elle déjà été vue ? ». Dans une liste, la réponse coûte un parcours de tout ce qui a déjà été vu. Une table de hachage la donne en temps constant, en moyenne. Reste à ouvrir la boîte : d'où vient ce O(1)O(1), et à quelle condition tient-il ?

Définition

Une table de hachage associe des clés à des valeurs. Une fonction de hachage transforme une clé en un indice de tableau ; on range la valeur à cet indice. Insertion, recherche et suppression coûtent alors O(1)O(1) en moyenne.

Le principe est celui du vestiaire : plutôt que de parcourir tous les manteaux pour trouver le sien, on calcule un numéro à partir du ticket et on va directement au bon casier.

L'analogie s'arrête là, et il vaut la peine de voir le calcul, parce que c'est lui qui explique à la fois la vitesse et les collisions. Le passage d'une clé à un indice se fait en deux temps.

Transformer la clé en entier. Pour une chaîne, on parcourt les caractères en combinant leurs codes, typiquement h←31 h+code(c)h \leftarrow 31\,h + \text{code}(c) pour chaque caractère cc, en partant de h=0h = 0. Le code d'un caractère est son numéro dans la table Unicode : 76 pour L, 121 pour y. Le résultat est un entier quelconque, souvent très grand, qui dépend de toute la chaîne et de l'ordre de ses lettres : changer une lettre change l'entier.

Ramener cet entier dans le tableau. On prend le reste de sa division par la taille mm du tableau : indice=h mod m\text{indice} = h \bmod m. Ce reste est nécessairement compris entre 0 et m−1m - 1, donc toujours un indice valide.

Le calcul complet sur Lyon, lettre par lettre et en partant de h=0h = 0 : 31×0+76=7631 \times 0 + 76 = 76 pour L, puis 31×76+121=2 47731 \times 76 + 121 = 2\,477 pour y, puis 31×2 477+111=76 89831 \times 2\,477 + 111 = 76\,898 pour o, enfin 31×76 898+110=2 383 94831 \times 76\,898 + 110 = 2\,383\,948 pour n. Sur une table de 8 cases, 2 383 948=8×297 993+42\,383\,948 = 8 \times 297\,993 + 4 : l'indice vaut 2 383 948 mod 8=42\,383\,948 \bmod 8 = 4. Sur une table de 7 cases, 2 383 948=7×340 5642\,383\,948 = 7 \times 340\,564 : l'indice vaut 0. Quatre lettres ont suffi à produire un entier à sept chiffres : c'est cette dispersion qui fait tout l'intérêt du procédé.

La figure fait le même calcul, sans borne, clé par clé. Chaque insertion affiche l'empreinte, le modulo et la case visée ; une clé qui tombe dans une case déjà occupée s'affiche en rouge.

Table de hachage à chaînageh ← h × 31 + code, puis h mod 8Cases :
Table vide. Insérer les clés une à une pour voir où chacune tombe.
  1. case 0
  2. case 1
  3. case 2
  4. case 3
  5. case 4
  6. case 5
  7. case 6
  8. case 7
0 clé dans 8 casesfacteur de charge α = 0,000 collisionplus longue liste : 0

Insérer les cinq villes sur 8 cases, puis passer à 7 et à 11 cases : la même suite de clés se répartit autrement. Ajouter ensuite quelques clés pour voir le facteur de charge monter et les listes s'allonger.

Deux conséquences se lisent directement sur ce calcul.

La première est la vitesse : quel que soit le nombre de clés déjà rangées, l'accès demande le même parcours de la chaîne et la même division. Rien ne dépend du nombre d'éléments, et c'est de là que vient le O(1)O(1).

La seconde est l'inévitabilité des collisions, c'est-à-dire de deux clés différentes envoyées dans la même case : le calcul projette un espace de clés immense sur mm cases seulement. Avec 7 cases, SiraP rejoint Lyon en case 0, et Bordeaux rejoint Nantes en case 2 : c'est le hasard ordinaire, et on ne peut pas l'empêcher.

Paris et SiraP sur 8 cases : une collision qui n'a rien d'un hasard

Sur 8 cases, Paris et son miroir SiraP tombent tous deux en case 3. Ce n'est pas de la malchance : avec cette fonction de hachage et une table de 8 cases, tout mot d'un nombre impair de lettres tombe dans la même case que son miroir, et une majuscule se confond avec sa minuscule. Avec 7 cases, la structure disparaît et les deux mots se séparent.

D'où la règle : avec une fonction de hachage aussi simple, on choisit une taille de table première. Les vraies implémentations gardent une puissance de deux, mais avec une fonction de hachage plus élaborée (Python) ou en brassant les bits de l'empreinte avant de la réduire (Java).

Pourquoi le miroir tombe dans la même case

L'explication emploie les congruences, présentées dans le chapitre sur l'arithmétique modulaire. On a 31=32−131 = 32 - 1, et 32 est un multiple de 8 : modulo 8, multiplier par 31 revient à changer de signe. Pour un mot de cinq lettres de codes c1,…,c5c_1, \dots, c_5, l'empreinte vaut donc, modulo 8, la somme alternée c1−c2+c3−c4+c5c_1 - c_2 + c_3 - c_4 + c_5, et cette somme est la même lue à l'envers. Une majuscule et sa minuscule ont des codes qui diffèrent de 32 : modulo 8, ils sont égaux.

Les collisions

Puisque les collisions sont inévitables, il faut savoir les ranger. Deux stratégies existent : le chaînage, où chaque case contient la liste des clés qui y tombent, et l'adressage ouvert, où l'on cherche la case libre suivante. La figure précédente pratique le chaînage : une collision y allonge simplement la liste de la case.

Facteur de charge

Le facteur de charge d'une table est α=nombre de cleˊsnombre de cases\alpha = \dfrac{\text{nombre de clés}}{\text{nombre de cases}}. Avec le chaînage et une bonne fonction de hachage, une liste contient en moyenne α\alpha clés, et une recherche coûte O(1+α)O(1 + \alpha).

Tant que α\alpha reste borné, la recherche reste en O(1)O(1) en moyenne. C'est pourquoi une table se redimensionne : quand α\alpha dépasse un seuil (deux tiers pour le dict de Python), elle alloue un tableau plus grand, en général deux fois plus grand, et y range à nouveau toutes les clés.

« En moyenne » et « amorti » ne disent pas la même chose

En moyenne porte sur les données : une recherche est rapide si les clés se répartissent bien, mais une recherche particulière peut tomber sur une case surchargée.

Amorti porte sur une suite d'opérations : un redimensionnement coûte O(n)O(n), mais comme la taille double à chaque fois, il n'arrive que rarement, et réparti sur toutes les insertions qui l'ont précédé, il ne coûte que O(1)O(1) par insertion.

Une insertion dans un dict est donc en O(1)O(1) amorti et en moyenne : les deux mots désignent deux raisons différentes.

Le O(1) est une moyenne, pas une garantie

Si toutes les clés atterrissent dans la même case, la table dégénère en liste chaînée et la recherche redevient O(n)O(n). C'est le pire cas, et il est réellement exploitable : une attaque par collisions de hachage consiste à envoyer à un serveur des clés choisies pour se percuter, jusqu'à l'effondrement. Les langages s'en protègent en mêlant à l'empreinte une valeur tirée au hasard au démarrage du programme : l'attaquant ne peut plus prévoir les cases, et l'empreinte d'une même chaîne change d'une exécution à l'autre.

En pratique, avec une bonne fonction de hachage et un facteur de charge maîtrisé, le coût reste O(1)O(1) en moyenne. Mais on écrit « O(1)O(1) en moyenne », jamais « O(1)O(1) » tout court.

Vérification rapideon peut se reprendre

1.Recherche linéaire dans une liste de n éléments. Quelle affirmation est juste ?

2.Une table de hachage donne un accès en O(1)…

3.Une table de 1 000 cases contient 3 000 clés, rangées par chaînage. Combien de clés contient une case, en moyenne ?

En Python, la table de hachage est le type dict, et l'ensemble, set, en est un cas particulier : une table qui ne range que des clés. Ce sont les dictionnaires et les ensembles du parcours Python, et leur rapidité vient de là. Le programme ci-dessous cherche la même référence dans une liste et dans un ensemble de deux cent mille références.

main.py
Sortie
>_ Prêt à exécuter…

Le rapport se compte en dizaines de milliers, et il grandit avec la liste : la recherche dans la liste la parcourt, en O(n)O(n), celle dans l'ensemble calcule une case, en O(1)O(1) en moyenne. Le programme répète chaque recherche avant de diviser, et c'est la règle de toute mesure : une opération brève ne se chronomètre jamais seule.

Les doublons, trois algorithmes

Le problème : étant donné une liste de références de paiement, deux d'entre elles sont-elles égales, et laquelle ? Chaque paiement y est réduit à sa référence, un numéro qui combine le fournisseur et la facture : deux saisies du même paiement portent la même référence. C'est la question posée en ouverture du chapitre. Trois algorithmes y répondent correctement, et ne s'y prennent pas du tout de la même façon ; l'écart se voit dès quelques milliers de paiements.

Double boucle : Θ(n2)\Theta(n^2) en temps, O(1)O(1) en espace

On compare chaque référence à toutes celles qui la suivent. Il y a (n−1)+(n−2)+⋯+1=n(n−1)/2(n-1) + (n-2) + \dots + 1 = n(n-1)/2 paires, la somme des termes d'une suite arithmétique (le chapitre sur les suites). Le pire cas, quand aucun paiement n'est en double, les compare toutes : le comportement est bien quadratique.

Tri puis voisins : Θ(nlog⁡n)\Theta(n \log n) en temps, O(n)O(n) en espace

On trie les références. Deux références égales finissent alors côte à côte, et il suffit de comparer chacune à sa voisine : n−1n - 1 comparaisons. Le tri coûte Θ(nlog⁡n)\Theta(n \log n) et domine le total. La mémoire vient de la copie triée que crée sorted, qui laisse la liste de départ intacte.

Ensemble : O(n)O(n) en moyenne en temps, O(n)O(n) en espace

Un seul parcours. Pour chaque référence, on teste si elle est déjà dans l'ensemble des références vues, puis on l'y range. L'ensemble rend ce test O(1)O(1) en moyenne, et c'est tout le gain : nn tests au lieu de n(n−1)/2n(n-1)/2 comparaisons.

Les trois coûts, de dix à un million de paiements. En échelle linéaire, l'ensemble se confondrait avec l'axe dès quelques centaines de paiements. On passe donc en échelle log-log : chaque graduation multiplie la précédente par dix, et une complexité en nan^a devient une droite de pente aa, parce que log⁡(c na)=log⁡c+alog⁡n\log(c\,n^a) = \log c + a \log n. L'écart entre les deux meilleurs reste modeste ; celui qui les sépare de la double boucle se compte en puissances de dix.

10100100010⁴10⁵10⁶10100010⁵10⁷10⁹10¹¹n (nombre de paiements)opérations
Valeurs de f(x), g(x), h(x) sur l'intervalle [10 ; 1000000]
n (nombre de paiements)fgh
101033,219350
50 009,550 009,5780 644,02221 250 475 045,125
100 009100 0091 661 126,5195 000 900 040,5
150 008,5150 008,52 579 348,863711 251 275 036,125
200 008200 0083 522 080,513820 001 600 032
250 007,5250 007,54 483 037,449531 251 875 028,125
300 007300 0075 458 518,353845 002 100 024,5
350 006,5350 006,56 446 077,476861 252 275 021,125
400 006400 0067 443 976,503980 002 400 018
450 005,5450 005,58 450 915,6866101 252 475 015,125
500 005500 0059 465 886,156125 002 500 012,5
550 004,5550 004,510 488 081,9542151 252 475 010,125
600 004600 00411 516 844,3343180 002 400 008
650 003,5650 003,512 551 624,7599211 252 275 006,125
700 003700 00313 591 959,3566245 002 100 004,5
750 002,5750 002,514 637 450,7006281 251 875 003,125
800 002800 00215 687 754,4842320 001 600 002
850 001,5850 001,516 742 569,528361 251 275 001,125
900 001900 00117 801 630,1506405 000 900 000,5
950 000,5950 000,518 864 700,2386451 250 475 000,125
1 000 0001 000 00019 931 568,5693500 000 000 000
ensemble, n vaut 1 000 000tri puis voisins, n log₂ n vaut 19 931 569double boucle, n²/2 vaut 5 × 10¹¹
Valeurs pour n = 1 000 000, le bord droit de la figure.
Les trois algorithmes des doublons, en échelle log-log. À un million de paiements, un million d'opérations pour l'ensemble, vingt millions pour le tri, cinq cents milliards pour la double boucle. L'ensemble et le tri sont deux droites presque parallèles de pente voisine de 1, la double boucle une droite de pente 2.

Ce qu'il faut y regarder, c'est la pente des droites, pas leur position. Deux d'entre elles montent presque au même rythme : le tri ne coûte qu'un facteur log⁡2n\log_2 n de plus que l'ensemble, une vingtaine à un million de paiements, soit un peu plus d'une graduation d'écart, qui ne grandit que très lentement. La troisième monte deux fois plus vite, et l'écart qui la sépare des deux autres s'ouvre de façon irrattrapable. Une pente, sur ce type de graphe, est un exposant.

Les trois algorithmes, écrits en Python et essayés sur quelques listes :

main.py
Sortie
>_ Prêt à exécuter…

Sur la quatrième liste, deux paiements sont en double, et les trois algorithmes ne désignent pas le même : 7781 pour la double boucle, 3302 pour les deux autres. Tous ont raison.

Pourquoi vérifier la propriété et non la réponse

Les trois algorithmes peuvent désigner des doublons différents quand il y en a plusieurs, et ils ont tous raison. Comparer leurs réponses à une réponse attendue ferait échouer un test sur un algorithme correct, et vérifier seulement qu'elles concordent laisserait passer trois algorithmes faux de la même façon. On vérifie donc la propriété demandée : une référence présente au moins deux fois, ou aucune réponse s'il n'y en a pas. C'est une habitude à prendre pour tout algorithme à solutions multiples.

Vérification rapideon peut se reprendre

1.Dans la version à ensemble, on range r dans l'ensemble après avoir testé sa présence. Pourquoi pas avant ?

2.Les doublons se trouvent en O(n) en moyenne avec un ensemble, et en Θ(n log n) par tri. Qu'apporte le tri ?

Complexité d'un problème, complexité d'un algorithme

C'est une confusion fréquente, et elle passe inaperçue tant qu'on n'a qu'un seul algorithme sous les yeux : on dit alors « ce problème est quadratique » quand seul l'algorithme l'est. Les doublons en donnent maintenant trois.

Deux notions distinctes

La complexité d'un algorithme est le coût de cet algorithme-là. Elle se calcule en le lisant.

La complexité d'un problème est le coût du meilleur algorithme possible pour le résoudre, y compris ceux que personne n'a encore trouvés. C'est une borne inférieure, et elle se démontre.

Sur les doublons

Trois algorithmes, trois complexités : Θ(n2)\Theta(n^2) au pire par la double boucle, Θ(nlog⁡n)\Theta(n \log n) par tri puis voisins, O(n)O(n) en moyenne avec un ensemble.

Pour le problème, une borne se voit tout de suite : il faut au moins lire chaque référence, donc Ω(n)\Omega(n). En moyenne, l'ensemble atteint cette borne, et on ne fera pas mieux. Mais la réponse change si l'on exige une garantie au pire cas. La table de hachage peut alors dégénérer en O(n2)O(n^2). Et l'on démontre, par une preuve qui dépasse ce chapitre, que tout algorithme qui se contente de comparer des références a besoin, dans le pire cas, d'au moins de l'ordre de nlog⁡nn \log n comparaisons. L'ensemble échappe à cette borne parce qu'il ne compare pas : il calcule une adresse. Le tri suivi des voisins l'atteint : dans ce cadre, il est optimal.

Même problème, deux réponses. La complexité d'un problème s'énonce toujours avec ses hypothèses : quel cas, et quelles opérations on s'autorise.

Pour le tri lui-même, la borne inférieure tient en un comptage.

Rappel de cours : la borne du tri, par un comptage

nn valeurs distinctes peuvent se présenter dans n!=1×2×⋯×nn! = 1 \times 2 \times \dots \times n ordres, et un algorithme de tri doit les distinguer tous, puisqu'il doit remettre chacun dans l'ordre. Une comparaison a deux issues ; kk comparaisons distinguent donc au plus 2k2^k situations. Il faut 2k≥n!2^k \geq n!, soit k≥log⁡2(n!)k \geq \log_2(n!), et log⁡2(n!)\log_2(n!) est de l'ordre de nlog⁡2nn \log_2 n. Aucun tri qui ne fait que comparer ne descend sous Ω(nlog⁡n)\Omega(n \log n) au pire.

Exemple résolu. Trois valeurs distinctes ont 3!=63! = 6 ordres possibles. Deux comparaisons ne distinguent que 22=42^2 = 4 situations, trois en distinguent 23=8≥62^3 = 8 \geq 6 : tout tri de trois valeurs fait au moins 3 comparaisons au pire.

Théorie contre mesure

La théorie prédit des rapports, pas des durées. Reste à la confronter à l'expérience : mesurer sur des tailles croissantes, dans le pire cas (aucun doublon), puis tracer en échelle logarithmique, où une complexité en nan^a devient une droite de pente aa.

Rappel de cours : lire un exposant, extrapoler une durée

Si la durée suit t=c nat = c\,n^a, deux mesures suffisent à trouver l'exposant aa. Le rapport des durées vaut t2/t1=(n2/n1)at_2 / t_1 = (n_2 / n_1)^a : la constante cc, qui dépend de la machine, disparaît. En prenant les logarithmes :

a=ln⁡(t2/t1)ln⁡(n2/n1)a = \frac{\ln(t_2 / t_1)}{\ln(n_2 / n_1)}

C'est la pente de la droite qui joint les deux mesures sur un graphe log-log. Dans l'autre sens, une complexité connue prédit une durée : t2=t1×T(n2)/T(n1)t_2 = t_1 \times T(n_2) / T(n_1).

Exemple résolu. Un programme met 0,2 s sur 3 000 éléments et 2,1 s sur 30 000 : a=ln⁡10,5/ln⁡10≈1,02a = \ln 10{,}5 / \ln 10 \approx 1{,}02, il est linéaire. Un programme quadratique met 1,5 s sur 2 000 éléments ; sur 6 000, la taille est multipliée par 3 et la durée par 32=93^2 = 9, soit 13,5 s. En 2n2^n, chaque valeur de plus double la durée : dix valeurs de plus la multiplient par 210=1 0242^{10} = 1\,024.

Le programme ci-dessous chronomètre les trois algorithmes des doublons, et calcule la pente par la droite des moindres carrés sur toutes les mesures plutôt qu'entre deux seulement : le bruit d'une mesure isolée y pèse moins.

main.py
Sortie
>_ Prêt à exécuter…

Lire la pente. En échelle log-log, un temps en n2n^2 donne une droite de pente 2 et un temps en nn une pente 1. Un temps en nlog⁡nn \log n donne une pente légèrement supérieure à 1 : environ 1,14 entre 250 et 16 000. Ce sont les trois droites de la figure des doublons. La pente mesurée est donc une estimation expérimentale de l'exposant : c'est la façon de vérifier une complexité annoncée sans lire le code.

Les mesures s'en approchent : une pente voisine de 2 pour la double boucle, voisine de 1 pour l'ensemble, et entre les deux pour le tri, souvent un peu au-dessus de 1,14, vers 1,2 ou 1,3. Des écarts sont à prévoir. Sur de petites tailles, les constantes pèsent plus que l'asymptotique. Surtout, la mémoire n'est pas uniforme : le processeur garde les données récentes dans un cache, une petite mémoire très rapide, et une structure qui n'y tient plus coûte plus cher à chaque accès. C'est ce qui fait monter les pentes du tri et de l'ensemble quand nn grandit. Pour le tri, la pente mélange en outre deux étapes de vitesses très différentes : sorted, écrit en C, est rapide, et la boucle des voisins, écrite en Python, est lente. Elle prend près de la moitié du temps aux petites tailles, et encore un quart à un tiers à 16 000 éléments. La pente lue dépend donc de la plage mesurée, et c'est exactement ce que veut dire « asymptotique » : la théorie dit ce qui l'emporte quand nn grandit, pas ce qui pèse à une taille donnée.

Vérification rapideon peut se reprendre

1.Complexité d'un problème et complexité d'un algorithme, quelle différence ?

2.Un programme met 0,3 s sur 10 000 éléments et 30 s sur 100 000. Quel exposant lit-on ?

Green IT

Tout ce qui précède se compte en opérations. Or une opération n'est pas gratuite : elle occupe un processeur, qui consomme de l'électricité, produite avec des émissions. Reste à savoir ce que ce raisonnement vaut vraiment, parce qu'il est souvent tenu trop vite.

Définition

Le Green IT désigne l'ensemble des démarches visant à réduire l'empreinte environnementale du numérique : fabrication des équipements, consommation d'énergie en exploitation, et fin de vie.

Quelques ordres de grandeur. Ils sont tirés de l'étude de l'ADEME et de l'Arcep sur l'empreinte environnementale du numérique en France, actualisée en janvier 2025 sur les données de 2022. Le numérique y représente 4,4 % de l'empreinte carbone de la France, soit 29,5 millions de tonnes équivalent CO₂. Cette empreinte se répartit entre les terminaux (50 %, fabrication et usage), les centres de données (46 %) et les réseaux (4 %). La part des centres de données a fortement augmenté par rapport à l'étude précédente, en partie parce que les centres situés à l'étranger et utilisés par des services français sont désormais comptés. Ces chiffres dépendent du périmètre retenu, et il faut les citer comme des estimations, avec leur source.

Le lien avec ce chapitre. Le temps de calcul se paie en énergie, et l'énergie en émissions. Passer un traitement quotidien de O(n2)O(n^2) à O(n)O(n) sur un million d'enregistrements réduit le nombre d'opérations d'un facteur de l'ordre du million : aucune action sur le refroidissement d'un centre de données n'approche ce rapport.

Mais le gain en énergie est plus petit que le gain en opérations, pour deux raisons. Les constantes d'abord : le rapport asymptotique n'est pas le rapport réel. Surtout, un serveur allumé en permanence consomme une part importante de sa puissance même à vide : finir un traitement plus vite n'économise que la part qui dépend du calcul. Le gain devient entier quand il permet d'éteindre une machine, de mutualiser, ou de ne pas en acheter une nouvelle. C'est par là que l'optimisation rejoint l'essentiel, puisque la fabrication pèse lourd dans l'empreinte.

Le rapport entre les deux coûts vaut n2/n=nn^2 / n = n : il grandit avec les données. Sur la figure log-log des doublons, c'est l'écart vertical entre la droite de la double boucle et celle de l'ensemble, qui gagne une graduation chaque fois que les données sont multipliées par dix. Un gain d'efficacité obtenu sur un algorithme ne s'use donc pas avec le temps ; il grandit à mesure que le parc de données grandit. C'est ce qui distingue une optimisation algorithmique d'un réglage de configuration.

Ce que ce rapport ne dit pas

Il compare des ordres de grandeur, pas des durées. Un algorithme en O(n)O(n) avec une constante énorme peut rester plus lent qu'un O(n2)O(n^2) sur de petites tailles, et c'est le cas courant en dessous de quelques dizaines d'éléments. La complexité décide du comportement quand nn grandit, elle ne décide pas de qui gagne aujourd'hui sur les données d'aujourd'hui. D'où la section précédente : théorie contre mesure.

Le PUE, ou ce que coûte un kilowattheure de calcul au compteur

Un centre de données ne consomme pas que pour calculer : il faut aussi refroidir les salles, convertir et secourir l'alimentation, éclairer. L'indicateur le plus répandu pour mesurer ce surcoût est le PUE (Power Usage Effectiveness), proposé en 2007 par le consortium The Green Grid et normalisé depuis (ISO/IEC 30134-2). C'est un rapport :

PUE=eˊnergie consommeˊe par tout le centreeˊnergie consommeˊe par les seuls eˊquipements informatiques\text{PUE} = \frac{\text{énergie consommée par tout le centre}}{\text{énergie consommée par les seuls équipements informatiques}}

Il vaut 1 dans le cas idéal, où tout ce qui entre sert à calculer. Les enquêtes annuelles de l'Uptime Institute situent la moyenne mondiale entre 1,5 et 1,6 depuis plusieurs années, et les plus grands opérateurs annoncent des valeurs proches de 1,1. Ces chiffres sont déclarés par les exploitants eux-mêmes : ce sont des ordres de grandeur, pas des mesures contrôlées.

La conséquence pour ce chapitre tient en une multiplication, et en une limite. Avec un PUE de 1,6, chaque kilowattheure de calcul évité par un meilleur algorithme en épargne jusqu'à 1,6 au compteur du centre : le refroidissement suit à peu près la chaleur dégagée, mais une partie des pertes du bâtiment (onduleurs, éclairage, ventilation de base) reste fixe, comme la consommation à vide d'un serveur. Le PUE est un rapport moyen : appliqué à une économie, il en donne le plafond, pas la valeur exacte. L'indicateur a une autre limite, qu'il faut connaître : il mesure l'efficacité du bâtiment, pas celle du logiciel. Un centre au PUE exemplaire qui fait tourner un algorithme quadratique là où un linéaire suffirait gaspille toujours, et seul le code peut supprimer ce gaspillage.

Ces notions se chiffrent avec quatre outils.

Rappel de cours : énergie, consommation à vide, émissions

L'énergie consommée est la puissance multipliée par la durée :

E=P×tE = P \times t

en wattheures (Wh) si PP est en watts et tt en heures. Un kilowattheure (kWh) vaut 1 0001\,000 Wh, soit 3,6×1063{,}6 \times 10^6 joules.

  • Un serveur allumé consomme sa puissance à vide en permanence. Un calcul n'ajoute que (Pcalcul−Pvide)×t(P_{\text{calcul}} - P_{\text{vide}}) \times t : c'est la seule part qu'un meilleur algorithme fait disparaître, tant que le serveur reste allumé.
  • Au compteur d'un centre de données, l'énergie d'un calcul évité est multipliée au plus par le PUE.
  • Les émissions sont l'énergie multipliée par un facteur d'émission, en kilogrammes de CO₂ équivalent par kilowattheure (kg CO₂e/kWh) : de l'ordre de 0,05 pour l'électricité française, et de 0,45 en moyenne mondiale.
  • L'empreinte annuelle d'un équipement répartit sa fabrication sur ses années de service : (fabrication+usage annuel×anneˊes)/anneˊes(\text{fabrication} + \text{usage annuel} \times \text{années}) / \text{années}.

Exemple résolu. Un traitement nocturne fait passer un serveur de 100 W à vide à 250 W pendant 30 minutes. Il ajoute (250−100)×0,5=75(250 - 100) \times 0{,}5 = 75 Wh par nuit, soit 75×365≈2775 \times 365 \approx 27 kWh par an, et au plus 27×1,5≈4127 \times 1{,}5 \approx 41 kWh au compteur d'un centre de PUE 1,5. Avec l'électricité française, cela fait au plus 41×0,05≈241 \times 0{,}05 \approx 2 kg CO₂e par an. À vide, le serveur consomme 100×8 760=876100 \times 8\,760 = 876 kWh par an, puisqu'une année compte 8 7608\,760 heures. Le calcul en représente 3 % : le rendre mille fois plus rapide ne fait gagner qu'environ 3 % de la consommation du serveur, tant qu'il reste allumé.

Pour l'empreinte annuelle, un ordinateur portable dont la fabrication a émis 200 kg CO₂e et dont l'usage en émet 5 par an pèse (200+4×5)/4=55(200 + 4 \times 5) / 4 = 55 kg CO₂e par an s'il sert quatre ans, et (200+6×5)/6≈38(200 + 6 \times 5) / 6 \approx 38 s'il en sert six.

L'effet rebond

Un traitement dix fois plus rapide ne divise pas forcément la consommation par dix. Puisqu'il ne coûte presque plus rien, on le lance toutes les heures au lieu d'une fois par jour, sur plus de données et pour plus d'utilisateurs. La consommation totale peut alors augmenter. C'est l'effet rebond, observé depuis le XIXe siècle sur le charbon, et fréquent en informatique. Il se chiffre : un traitement de 60 minutes ramené à 6, mais lancé douze fois par jour au lieu d'une, consomme 72 minutes de calcul par jour au lieu de 60. Une optimisation n'est un gain environnemental que si l'usage reste maîtrisé.

Les leviers, à combiner :

  1. Choisir le bon algorithme et la bonne structure de données. C'est, du côté du code, le levier au meilleur rapport effort/gain, et de loin.
  2. Ne pas calculer deux fois la même chose : mise en cache, mémoïsation, calculs incrémentaux.
  3. Déplacer les traitements lourds vers les heures où l'électricité est la moins carbonée. Le contenu en CO₂ d'un kilowattheure varie d'une heure à l'autre selon les centrales en service, et RTE le publie en temps réel pour la France sur son service éCO2mix. Un traitement qui n'a pas à répondre dans l'instant, comme le contrôle nocturne des doublons, peut attendre le moment le plus favorable ; une requête d'utilisateur, non.
  4. Dimensionner au besoin réel : un serveur surdimensionné consomme même à vide.
  5. Allonger la durée de vie du matériel, ce que rend possible un logiciel qui n'exige pas toujours plus. À l'échelle d'un système d'information, ce levier pèse souvent davantage que tous les autres, puisque la fabrication domine l'empreinte des terminaux.
Vérification rapideon peut se reprendre

1.Réduire la complexité d'un traitement a un effet Green IT parce que…

2.Un traitement nocturne passe de 2 heures à 2 minutes sur un serveur qui reste allumé jour et nuit. La consommation du serveur est-elle divisée par 60 ?

3.Après optimisation, un rapport qui coûtait une heure de calcul n'en coûte plus qu'une minute. L'équipe décide de le produire toutes les cinq minutes au lieu d'une fois par jour. Comment s'appelle ce phénomène ?

Retour aux paiements

La question d'ouverture se tranche maintenant en quelques lignes de calcul. L'historique compte n=107n = 10^7 paiements, et la machine fait un milliard d'opérations par seconde.

  • La double boucle compare 107×(107−1)/2≈5×101310^7 \times (10^7 - 1) / 2 \approx 5 \times 10^{13} paires, soit environ 50 000 secondes : près de 14 heures. Le contrôle ne tient pas dans les huit heures de la nuit, et il tiendra moins encore quand l'historique doublera, puisque sa durée quadruplera, à plus de deux jours.
  • Le tri puis les voisins demande de l'ordre de nlog⁡2n≈2,3×108n \log_2 n \approx 2{,}3 \times 10^8 opérations : un quart de seconde.
  • L'ensemble fait 10710^7 tests : un centième de seconde. Le rapport avec la double boucle, (n−1)/2(n - 1)/2, environ cinq millions, ne doit rien à la machine.

En énergie, la réponse est plus modeste. Sur le serveur du rappel de cours, 100 W à vide et 250 W en calcul, la double boucle ajoute 150×13,9≈2 100150 \times 13{,}9 \approx 2\,100 Wh par contrôle, soit environ 25 kWh par an pour douze clôtures, et au plus 37,5 kWh au compteur d'un centre de PUE 1,5. Le serveur en consomme 876 par an à vide : s'il reste allumé pour d'autres services, la bonne version n'économise qu'environ 3 % de sa consommation. Le gain devient entier s'il peut être éteint, servir à autre chose, ou ne jamais être acheté. Le bon algorithme rend le contrôle possible ; il ne devient une économie d'énergie importante que si le matériel suit.

La méthode

  1. Nommer nn en toute première ligne. « nn = nombre de paiements à contrôler » : sans cela, la complexité ne veut rien dire.
  2. Compter les opérations élémentaires, boucle par boucle : nombre de tours multiplié par le coût d'un tour, en identifiant la boucle qui domine.
  3. Préciser le cas : pire, moyen ou meilleur. Par défaut, le pire.
  4. Écrire OO pour une majoration, Θ\Theta quand on a l'encadrement, quel que soit le cas étudié.
  5. Prouver une majoration en exhibant un n0n_0 et un kk : c'est tout ce qu'exige la définition.
  6. Donner aussi la complexité spatiale. Un gain de temps payé en mémoire est un compromis, pas un progrès automatique.
  7. Confronter à la mesure : répéter les mesures brèves, tracer en log-log, lire la pente a=ln⁡(t2/t1)/ln⁡(n2/n1)a = \ln(t_2/t_1) / \ln(n_2/n_1), comparer à l'exposant théorique.
  8. Conclure en énergie quand le sujet est le Green IT : E=P×tE = P \times t sur la seule part au-dessus de la consommation à vide, au plus multipliée par le PUE au compteur, puis par le facteur d'émission, sans oublier l'effet rebond.

Mettre en pratique

Une complexité s'annonce en notation asymptotique et se vérifie en nombres. Le bloc ci-dessous reprend, dans un autre contexte, les rappels du chapitre : lire une boucle, la dichotomie, lire un exposant et extrapoler une durée, l'énergie et les émissions. Chaque question a son indice.

Un contrôle des numéros de série, des comparaisons aux kilowattheures

  • 1.

    Un atelier grave un numéro de série sur chaque pièce, et un contrôle compare chaque numéro d'un lot à tous les suivants pour repérer ceux gravés deux fois. Combien de comparaisons fait-il au pire sur 4 000 pièces ?

  • 2.

    Il met 0,5 seconde sur ces 4 000 pièces. Combien de secondes sur 20 000 ?

  • 3.

    Une version qui range les numéros vus dans un ensemble fait un test par pièce. Combien de tests sur 20 000 pièces ?

  • 4.

    Dans le registre trié des numéros, une recherche dichotomique fait au pire combien de comparaisons sur 20 000 pièces ?

  • 5.

    Un programme inconnu met 0,4 s sur 5 000 pièces et 6,6 s sur 20 000. Quel exposant lit-on ?

  • 6.

    Chaque nuit, le contrôle fait passer le serveur de 80 W à vide à 230 W pendant 20 minutes. Combien de kilowattheures le calcul ajoute-t-il par an ?

  • 7.

    Au plus, combien de kilogrammes de CO₂e par an, au compteur d'un centre de PUE 1,6, avec 0,05 kg CO₂e par kilowattheure ?

Les deux dernières réponses se lisent ensemble : 18 kilowattheures par an, moins d'un kilogramme et demi de CO₂e. Le serveur en consomme environ 700 par an à vide : rendre ce contrôle cent fois plus rapide ne changerait presque rien à sa consommation. Ce qui compte ici, c'est de pouvoir éteindre ou partager le serveur.

Chaque exercice type emploie un rappel de cours déjà donné : extrapoler une durée et lire un exposant (théorie contre mesure), la définition de OO (notation de Landau), l'énergie et la consommation à vide (Green IT). Un seul outil est nouveau, la règle de l'enchaînement.

Rappel de cours : enchaîner des étapes

Quand un algorithme exécute ses étapes l'une après l'autre, leurs coûts s'additionnent, et la somme a la classe de son terme qui croît le plus vite : O(nlog⁡n)+O(n)=O(nlog⁡n)O(n \log n) + O(n) = O(n \log n). La complexité totale est celle de l'étape la plus coûteuse.

Exemple résolu. Une lecture des nn paiements, en Θ(n)\Theta(n), suivie d'une double boucle, en Θ(n2)\Theta(n^2), coûte Θ(n2)\Theta(n^2) : sur mille paiements, mille lectures et un demi-million de comparaisons.

Un algorithme traite 1 000 éléments en 1 seconde. Combien pour 10 000, en O(n)O(n), O(nlog⁡n)O(n \log n) et O(n2)O(n^2) ?

O(n)O(n) : facteur 10 → 10 secondes.

O(nlog⁡n)O(n \log n) : facteur 10×(log⁡10000/log⁡1000)=10×4/3≈1310 \times (\log 10000 / \log 1000) = 10 \times 4/3 \approx 13 → 13 secondes.

O(n2)O(n^2) : facteur 100 → 100 secondes.

Refaire le calcul pour 1 000 000 d'éléments : O(n)O(n) donne 1 000 s (17 minutes), O(n2)O(n^2) donne 10610^6 s, soit 11 jours et demi. L'écart ne cesse de croître, et c'est précisément ce que dit l'asymptotique.

Un contrôle de doublons met 0,8 s sur 20 000 paiements et 12,6 s sur 80 000. Quel est son exposant, et combien mettra-t-il sur 320 000 paiements ?

La taille est multipliée par 4 et la durée par 12,6/0,8=15,7512{,}6 / 0{,}8 = 15{,}75. L'exposant vaut

a=ln⁡15,75ln⁡4≈2,7571,386≈1,99a = \frac{\ln 15{,}75}{\ln 4} \approx \frac{2{,}757}{1{,}386} \approx 1{,}99

soit 2, à la précision de la mesure : le contrôle est quadratique, sans doute une double boucle.

De 80 000 à 320 000, la taille est encore multipliée par 4, donc la durée par 42=164^2 = 16 : 12,6×16≈12{,}6 \times 16 \approx 202 secondes, plus de trois minutes. Extrapoler en ligne droite, en multipliant par 4, aurait annoncé 50 secondes : quatre fois trop peu. Lire l'exposant sert précisément à ne pas prolonger par une droite ce qui monte au carré.

Prouver que T(n)=5n2+3n+7=O(n2)T(n) = 5n^2 + 3n + 7 = O(n^2)

Pour tout n≥1n \geq 1 on a n≤n2n \leq n^2 et 1≤n21 \leq n^2, donc :

5n2+3n+7≤5n2+3n2+7n2=15n25n^2 + 3n + 7 \leq 5n^2 + 3n^2 + 7n^2 = 15n^2

La définition est vérifiée avec n0=1n_0 = 1 et k=15k = 15. ∎

On peut même dire mieux : T(n)=Θ(n2)T(n) = \Theta(n^2), puisque T(n)≥5n2T(n) \geq 5n^2 pour tout n≥1n \geq 1, ce qui fournit la minoration avec k=5k = 5.

Le tri prend O(nlog⁡n)O(n \log n), la comparaison des voisins O(n)O(n). Quelle est la complexité totale ?

O(nlog⁡n)O(n \log n).

On additionne les étapes : T(n)=O(nlog⁡n)+O(n)T(n) = O(n \log n) + O(n). Le terme dominant l'emporte, puisque nlog⁡nn \log n croît plus vite que nn.

C'est la règle générale d'un enchaînement séquentiel : la complexité totale est celle de l'étape la plus coûteuse. Accélérer la comparaison des voisins ne changerait donc pas la classe tant que le tri est là. Cela peut pourtant payer à une taille donnée : dans la mesure de la section « Théorie contre mesure », la boucle des voisins, écrite en Python, prend encore un quart à un tiers du temps à 16 000 éléments. Pour changer de classe, il faut éviter de trier, et c'est ce que fait l'ensemble.

Un serveur consomme 90 W à vide et 210 W en calcul. Un traitement nocturne y tourne 3 heures ; réécrit, il ne dure plus que 6 minutes. Que gagne-t-on en énergie sur un an, et quelle part de la consommation du serveur ?

Avant. Le calcul ajoute (210−90)×3=360(210 - 90) \times 3 = 360 Wh par nuit, soit 360×365=131 400360 \times 365 = 131\,400 Wh, 131,4 kWh par an.

Après. 120×0,1=12120 \times 0{,}1 = 12 Wh par nuit, soit 4,4 kWh par an. Le gain vaut 131,4−4,4=127131{,}4 - 4{,}4 = 127 kWh par an, et au plus 127×1,5≈190127 \times 1{,}5 \approx 190 kWh au compteur d'un centre de PUE 1,5.

La part. À vide, le serveur consomme 90×8 760=788,490 \times 8\,760 = 788{,}4 kWh par an ; avec le calcul d'avant, 788,4+131,4=919,8788{,}4 + 131{,}4 = 919{,}8 kWh. La réécriture en économise 127/919,8≈127 / 919{,}8 \approx 14 %, pour un calcul trente fois plus court.

Le reste, 86 %, est la consommation à vide : il ne disparaît que si le serveur peut être éteint entre deux traitements, ou partagé avec d'autres services.

Chapitre révisé en octobre 2026 · Une erreur dans ce chapitre ?

Dans la bibliothèque d'exercices