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
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 est le nombre d'opérations élémentaires effectuées sur une entrée de taille .
Compter, sur un exemple. Rechercher une valeur dans une liste non triée de é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'à quand elle est absente. C'est ce second nombre, celui de la recherche infructueuse, qui donne le coût à retenir.
programme principal
Dans le pire des cas (la valeur est absente), la boucle fait 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 « a-t-il dépassé ? » et l'incrémentation de . Trois opérations restent hors des tours : l'initialisation de , le dernier test, celui qui fait sortir de la boucle, et le retour final. On obtient opérations.
Le coefficient dépend de ce qu'on décide d'appeler « une opération », et le terme constant devient négligeable dès que dépasse quelques dizaines. Ce qui survit à tous ces choix arbitraires, c'est que croît proportionnellement à . C'est cette information-là qu'on garde, et rien d'autre.
La notation de Landau
Il reste à écrire « croît proportionnellement à » 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 . C'est exactement ce que fournit la notation ci-dessous.
On dit que est dominée par , et on note , s'il existe un rang et un réel tels que :
En clair : à partir d'un certain rang , la courbe de reste sous celle de agrandie par un facteur fixe . Les deux paramètres portent chacun une permission : permet d'ignorer les constantes, permet d'ignorer le début de la courbe.
Ce que la définition écarte, du même coup : elle ne dit rien de , 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 :
- , la majoration : « ne croît pas plus vite que ». C'est la plus utilisée, parce qu'on veut garantir un plafond.
- , la minoration : « croît au moins aussi vite que ». Formellement, s'il existe et tels que pour tout : la même définition que , avec l'inégalité retournée. C'est la notation des bornes inférieures, celles qui disent qu'aucun algorithme ne peut faire mieux.
- , l'encadrement : et à la fois, c'est-à-dire à partir d'un certain rang. C'est la plus précise, et souvent celle qu'on veut vraiment dire.
Un algorithme en est aussi, formellement, en , et en . La majoration reste vraie, elle est simplement mauvaise.
Quand on affirme une complexité, dire si c'est une majoration ou un encadrement. Écrire quand on le sait est plus fort, plus précis, et signale qu'on a compris la différence.
Il s'agit de montrer que , c'est-à-dire de trouver un rang et un facteur qui conviennent.
Le couple et convient. Pour tout on a , donc .
Donc : la définition est vérifiée avec et . ∎
Le couple n'est pas unique : n'importe quel conviendrait, avec un adapté. Avec , par exemple, équivaut à , et le rang devient . 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.
| n (taille de l'entrée) | f | g | h |
|---|---|---|---|
| 0 | 3 | 0 | 0 |
| 0,5 | 4,5 | 3 | 2 |
| 1 | 6 | 6 | 4 |
| 1,5 | 7,5 | 9 | 6 |
| 2 | 9 | 12 | 8 |
| 2,5 | 10,5 | 15 | 10 |
| 3 | 12 | 18 | 12 |
| 3,5 | 13,5 | 21 | 14 |
| 4 | 15 | 24 | 16 |
| 4,5 | 16,5 | 27 | 18 |
| 5 | 18 | 30 | 20 |
| 5,5 | 19,5 | 33 | 22 |
| 6 | 21 | 36 | 24 |
| 6,5 | 22,5 | 39 | 26 |
| 7 | 24 | 42 | 28 |
| 7,5 | 25,5 | 45 | 30 |
| 8 | 27 | 48 | 32 |
| 8,5 | 28,5 | 51 | 34 |
| 9 | 30 | 54 | 36 |
| 9,5 | 31,5 | 57 | 38 |
| 10 | 33 | 60 | 40 |
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é | Nom | Exemple typique | Pour mille éléments | Pour un million |
|---|---|---|---|---|
| constante | accès à une case de tableau | 1 | 1 | |
| logarithmique | recherche dichotomique | 10 | 20 | |
| linéaire | parcours d'une liste | 10³ | 10⁶ | |
| quasi-linéaire | tri par fusion | 10⁴ | 2 × 10⁷ | |
| quadratique | double boucle imbriquée | 10⁶ | 10¹² | |
| exponentielle | énumération de tous les sous-ensembles | 10³⁰¹ | inatteignable |
L'ordre des lignes est celui des croissances comparées, vues dans le chapitre sur l'étude de fonction : toute puissance de 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é , celui qui compte les divisions par deux : . Dans un , la base ne s'écrit pas : changer de base multiplie le logarithme par une constante (), 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 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.
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 comparaisons, où 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.
d'un million vaut environ 20, et 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.
| n (taille de l'entrée) | f | g | h |
|---|---|---|---|
| 1 | 1 | 0 | 1 |
| 2,45 | 2,45 | 3,1673 | 6,0025 |
| 3,9 | 3,9 | 7,6575 | 15,21 |
| 5,35 | 5,35 | 12,9445 | 28,6225 |
| 6,8 | 6,8 | 18,8056 | 46,24 |
| 8,25 | 8,25 | 25,1163 | 68,0625 |
| 9,7 | 9,7 | 31,7965 | 94,09 |
| 11,15 | 11,15 | 38,7905 | 124,3225 |
| 12,6 | 12,6 | 46,0574 | 158,76 |
| 14,05 | 14,05 | 53,5656 | 197,4025 |
| 15,5 | 15,5 | 61,29 | 240,25 |
| 16,95 | 16,95 | 69,2105 | 287,3025 |
| 18,4 | 18,4 | 77,3101 | 338,56 |
| 19,85 | 19,85 | 85,5747 | 394,0225 |
| 21,3 | 21,3 | 93,9922 | 453,69 |
| 22,75 | 22,75 | 102,5523 | 517,5625 |
| 24,2 | 24,2 | 111,2458 | 585,64 |
| 25,65 | 25,65 | 120,0647 | 657,9225 |
| 27,1 | 27,1 | 129,002 | 734,41 |
| 28,55 | 28,55 | 138,0512 | 815,1025 |
| 30 | 30 | 147,2067 | 900 |
Ce qu'il faut y regarder n'est pas la hauteur des courbes, mais ce que devient leur rapport quand le domaine s'élargit. À , le quadratique vaut trente fois le linéaire ; à , 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 . Sur un banc d'essai réel, les constantes peuvent masquer ce rapport tant que reste petit, et c'est là que trente éléments trompent.
Ces classes se lisent dans le code, boucle par 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 éléments fait tours : .
- Deux boucles imbriquées, la seconde parcourant tous les éléments qui suivent le premier : tours, la somme d'une suite arithmétique, donc .
- Une boucle intérieure bornée : si elle fait au plus 10 tours quel que soit , le total ne dépasse pas , et le coût reste , deux boucles ou pas.
- Une boucle qui double son compteur, de 1 jusqu'à , fait environ 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 niveaux de découpe. À chaque niveau, la fusion des morceaux recopie les valeurs une fois. D'où recopies.
Exemple résolu. Sur les seize valeurs de la course ci-dessous, niveaux de seize recopies chacun : recopies. Sur mille valeurs, environ , quand une double boucle complète ferait 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.
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.
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.
À un milliard d'opérations par seconde, un algorithme en traite un million d'éléments en environ 17 minutes. Le même problème en 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. avec demande environ 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.
« Si on double la taille des données, que devient le temps ? » : il augmente d'une constante. : il double. : il fait un peu plus que doubler. : il quadruple. : il est élevé au carré.
C'est aussi la façon la plus simple de deviner la complexité d'un code inconnu : mesurer sur , puis sur , et lire le rapport.
1.Compter les opérations d'un algorithme donne . 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 , 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 : . Rarement intéressant, il décrit un coup de chance ;
- le pire cas, ici la cible absente : . 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é . La position coûte comparaisons, d'où une moyenne de comparaisons, soit . C'est le plus réaliste, mais il exige une hypothèse sur les entrées, qu'il faut alors écrire.
On lit souvent « le meilleur cas est en , le pire cas en ». 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 ( ci-dessus), ou seulement majorer le cas moyen si l'on ne sait pas mieux faire.
Son pire cas est : sur une liste déjà triée avec un pivot mal choisi. Son cas moyen est , 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 à , et sorted, en Python, est un tri par fusion adapté, en au pire. Annoncer le seul pire cas du tri rapide nu donnerait une image fausse de son comportement réel.
Complexité spatiale
La complexité spatiale est la quantité de mémoire supplémentaire utilisée par l'algorithme, en fonction de , 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 de mémoire pour ramener une recherche de à en moyenne.
Un algorithme récursif consomme de la mémoire à chaque appel imbriqué. Une récursion de profondeur occupe 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 , et à quelle condition tient-il ?
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 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 pour chaque caractère , en partant de . 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 du tableau : . Ce reste est nécessairement compris entre 0 et , donc toujours un indice valide.
Le calcul complet sur Lyon, lettre par lettre et en partant de : pour L, puis pour y, puis pour o, enfin pour n. Sur une table de 8 cases, : l'indice vaut . Sur une table de 7 cases, : 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.
- case 0
- case 1
- case 2
- case 3
- case 4
- case 5
- case 6
- case 7
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 .
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 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.
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 , et 32 est un multiple de 8 : modulo 8, multiplier par 31 revient à changer de signe. Pour un mot de cinq lettres de codes , l'empreinte vaut donc, modulo 8, la somme alternée , 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.
Le facteur de charge d'une table est . Avec le chaînage et une bonne fonction de hachage, une liste contient en moyenne clés, et une recherche coûte .
Tant que reste borné, la recherche reste en en moyenne. C'est pourquoi une table se redimensionne : quand 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 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 , 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 par insertion.
Une insertion dans un dict est donc en amorti et en moyenne : les deux mots désignent deux raisons différentes.
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 . 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 en moyenne. Mais on écrit « en moyenne », jamais « » tout court.
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.
Le rapport se compte en dizaines de milliers, et il grandit avec la liste : la recherche dans la liste la parcourt, en , celle dans l'ensemble calcule une case, en 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 : en temps, en espace
On compare chaque référence à toutes celles qui la suivent. Il y a 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 : en temps, 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 : comparaisons. Le tri coûte 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 : en moyenne en temps, 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 en moyenne, et c'est tout le gain : tests au lieu de 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 devient une droite de pente , parce que . L'écart entre les deux meilleurs reste modeste ; celui qui les sépare de la double boucle se compte en puissances de dix.
| n (nombre de paiements) | f | g | h |
|---|---|---|---|
| 10 | 10 | 33,2193 | 50 |
| 50 009,5 | 50 009,5 | 780 644,0222 | 1 250 475 045,125 |
| 100 009 | 100 009 | 1 661 126,519 | 5 000 900 040,5 |
| 150 008,5 | 150 008,5 | 2 579 348,8637 | 11 251 275 036,125 |
| 200 008 | 200 008 | 3 522 080,5138 | 20 001 600 032 |
| 250 007,5 | 250 007,5 | 4 483 037,4495 | 31 251 875 028,125 |
| 300 007 | 300 007 | 5 458 518,3538 | 45 002 100 024,5 |
| 350 006,5 | 350 006,5 | 6 446 077,4768 | 61 252 275 021,125 |
| 400 006 | 400 006 | 7 443 976,5039 | 80 002 400 018 |
| 450 005,5 | 450 005,5 | 8 450 915,6866 | 101 252 475 015,125 |
| 500 005 | 500 005 | 9 465 886,156 | 125 002 500 012,5 |
| 550 004,5 | 550 004,5 | 10 488 081,9542 | 151 252 475 010,125 |
| 600 004 | 600 004 | 11 516 844,3343 | 180 002 400 008 |
| 650 003,5 | 650 003,5 | 12 551 624,7599 | 211 252 275 006,125 |
| 700 003 | 700 003 | 13 591 959,3566 | 245 002 100 004,5 |
| 750 002,5 | 750 002,5 | 14 637 450,7006 | 281 251 875 003,125 |
| 800 002 | 800 002 | 15 687 754,4842 | 320 001 600 002 |
| 850 001,5 | 850 001,5 | 16 742 569,528 | 361 251 275 001,125 |
| 900 001 | 900 001 | 17 801 630,1506 | 405 000 900 000,5 |
| 950 000,5 | 950 000,5 | 18 864 700,2386 | 451 250 475 000,125 |
| 1 000 000 | 1 000 000 | 19 931 568,5693 | 500 000 000 000 |
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 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 :
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.
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.
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.
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.
Trois algorithmes, trois complexités : au pire par la double boucle, par tri puis voisins, 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 . 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 . 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 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.
valeurs distinctes peuvent se présenter dans ordres, et un algorithme de tri doit les distinguer tous, puisqu'il doit remettre chacun dans l'ordre. Une comparaison a deux issues ; comparaisons distinguent donc au plus situations. Il faut , soit , et est de l'ordre de . Aucun tri qui ne fait que comparer ne descend sous au pire.
Exemple résolu. Trois valeurs distinctes ont ordres possibles. Deux comparaisons ne distinguent que situations, trois en distinguent : 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 devient une droite de pente .
Si la durée suit , deux mesures suffisent à trouver l'exposant . Le rapport des durées vaut : la constante , qui dépend de la machine, disparaît. En prenant les logarithmes :
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 : .
Exemple résolu. Un programme met 0,2 s sur 3 000 éléments et 2,1 s sur 30 000 : , 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 , soit 13,5 s. En , chaque valeur de plus double la durée : dix valeurs de plus la multiplient par .
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.
Lire la pente. En échelle log-log, un temps en donne une droite de pente 2 et un temps en une pente 1. Un temps en 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 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 grandit, pas ce qui pèse à une taille donnée.
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.
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 à 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 : 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.
Il compare des ordres de grandeur, pas des durées. Un algorithme en avec une constante énorme peut rester plus lent qu'un 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 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.
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 :
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.
L'énergie consommée est la puissance multipliée par la durée :
en wattheures (Wh) si est en watts et en heures. Un kilowattheure (kWh) vaut Wh, soit joules.
- Un serveur allumé consomme sa puissance à vide en permanence. Un calcul n'ajoute que : 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 : .
Exemple résolu. Un traitement nocturne fait passer un serveur de 100 W à vide à 250 W pendant 30 minutes. Il ajoute Wh par nuit, soit kWh par an, et au plus kWh au compteur d'un centre de PUE 1,5. Avec l'électricité française, cela fait au plus kg CO₂e par an. À vide, le serveur consomme kWh par an, puisqu'une année compte 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 kg CO₂e par an s'il sert quatre ans, et s'il en sert six.
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 :
- 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.
- Ne pas calculer deux fois la même chose : mise en cache, mémoïsation, calculs incrémentaux.
- 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.
- Dimensionner au besoin réel : un serveur surdimensionné consomme même à vide.
- 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.
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 paiements, et la machine fait un milliard d'opérations par seconde.
- La double boucle compare 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 opérations : un quart de seconde.
- L'ensemble fait tests : un centième de seconde. Le rapport avec la double boucle, , 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 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
- Nommer en toute première ligne. « = nombre de paiements à contrôler » : sans cela, la complexité ne veut rien dire.
- 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.
- Préciser le cas : pire, moyen ou meilleur. Par défaut, le pire.
- Écrire pour une majoration, quand on a l'encadrement, quel que soit le cas étudié.
- Prouver une majoration en exhibant un et un : c'est tout ce qu'exige la définition.
- Donner aussi la complexité spatiale. Un gain de temps payé en mémoire est un compromis, pas un progrès automatique.
- Confronter à la mesure : répéter les mesures brèves, tracer en log-log, lire la pente , comparer à l'exposant théorique.
- Conclure en énergie quand le sujet est le Green IT : 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 (notation de Landau), l'énergie et la consommation à vide (Green IT). Un seul outil est nouveau, la règle de l'enchaînement.
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 : . La complexité totale est celle de l'étape la plus coûteuse.
Exemple résolu. Une lecture des paiements, en , suivie d'une double boucle, en , coûte : 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 , et ?
: facteur 10 → 10 secondes.
: facteur → 13 secondes.
: facteur 100 → 100 secondes.
Refaire le calcul pour 1 000 000 d'éléments : donne 1 000 s (17 minutes), donne 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 . L'exposant vaut
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 : 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
Pour tout on a et , donc :
La définition est vérifiée avec et . ∎
On peut même dire mieux : , puisque pour tout , ce qui fournit la minoration avec .
Le tri prend , la comparaison des voisins . Quelle est la complexité totale ?
.
On additionne les étapes : . Le terme dominant l'emporte, puisque croît plus vite que .
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 Wh par nuit, soit Wh, 131,4 kWh par an.
Après. Wh par nuit, soit 4,4 kWh par an. Le gain vaut kWh par an, et au plus kWh au compteur d'un centre de PUE 1,5.
La part. À vide, le serveur consomme kWh par an ; avec le calcul d'avant, kWh. La réécriture en économise 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
Dénombrement, preuves en O, croisements de coûts, pile d'appels, borne d'un problème, hachage, doublons, mesure, coût énergétique, PUE et effet rebond.
- Prédire l'effet d'un doublementNiveau 1
- Un badge en double dans l'exportNiveau 2
- Exhiber n₀ et kNiveau 2
- La recherche dichotomiqueNiveau 2
- Débogage : chaque clé dans toutes les casesNiveau 2
- Compter les paires en un seul passageNiveau 2
- Une minute de calcul, et une pente à lireNiveau 2
- Ce que le bon algorithme économise vraimentNiveau 3