Algorithmiques élémentaires & pensée computationnelle · L1 · Section 4/6
Évaluer la complexité
Progression
#Évaluer la complexité
Ordres de grandeur, notation grand-O, coûts en temps et mémoire : l'intuition d'abord, le formalisme ensuite.
#Prérequis
Les algorithmes de la page algorithmes servent d'exemples ; savoir compter les itérations d'une boucle suffit pour suivre.
#Objectifs d'apprentissage
- Estimer le nombre d'opérations d'un algorithme en fonction de la taille n de l'entrée.
- Classer les ordres de grandeur courants et sentir leurs écarts concrets.
- Écrire la relation de récurrence du coût d'un algorithme récursif et la résoudre.
- Distinguer coût en temps et coût en espace.
- Reconnaître le meilleur cas, le cas moyen et le pire cas, et savoir lequel communiquer.
#Pourquoi la complexité ?
La question pratique : mon programme tiendra-t-il le coup avec dix fois plus de données ? Mesurer des secondes sur une machine donnée ne répond pas : la machine change, le compilateur change, la charge change. La complexité algorithmique compte les opérations élémentaires en fonction de la taille de l'entrée, indépendamment de la machine. C'est un modèle, imparfait mais comparable d'un algorithme à l'autre et prédictif quand n grandit.
#La notation grand-O
Le grand-O décrit le comportement asymptotique : le taux de croissance du coût quand n tend vers l'infini. On ignore les constantes multiplicatives et les termes dominés : s'écrit , car pour n grand, le terme écrase tout le reste.
#Ordres de complexité courants
| Notation | Nom | n = 1 000 : ordre de grandeur des opérations |
|---|---|---|
| O(1) | constant | 1 |
| O(log n) | logarithmique | 10 |
| O(n) | linéaire | 1 000 |
| O(n log n) | linéarithmique | 10 000 |
| O(n²) | quadratique | 1 000 000 |
| O(2ⁿ) | exponentiel | un nombre à 302 chiffres |
| O(n!) | factoriel | un nombre à 2 568 chiffres |
L'écart entre les lignes est la leçon : passer de O(n²) à O(n log n) sur n = 1 000 divise le travail par cent, quand accélérer la machine de 10 % ne gagne que 10 %.
#Ce que cela coûte vraiment
Compter les opérations ne dit rien tant qu'on ne les convertit pas en temps. Supposons qu'une machine traite une opération en une microseconde et qu'un programme doive énumérer toutes les parties d'un ensemble à éléments — il y en a . Le temps total vaut alors microsecondes :
| Parties | Temps d'exécution | |
|---|---|---|
| 0 | 1 | 1 µs |
| 10 | 1 024 | 1,024 ms |
| 20 | 1 048 576 | 1,048 s |
| 30 | 1 073 741 824 | 17,9 min |
| 40 | 12,7 jours | |
| 50 | 35,7 ans | |
| 60 | 366 siècles |
Le tableau mérite d'être lu deux fois. Entre et , on n'a ajouté que 40 éléments — et le temps est passé d'une seconde à plus de trois millénaires. Une microseconde par opération est une hypothèse généreuse ; la conclusion ne dépend pas d'elle. Aucune amélioration matérielle ne compense une croissance exponentielle : gagner un facteur 1 000 sur la machine revient à ajouter 10 à avant que le temps redevienne le même.
#Quand la force brute ne suffit plus : le voyageur de commerce
Le cas d'école est le problème du voyageur de commerce. Toutes les villes d'une région sont reliées deux à deux ; on cherche le chemin le plus court permettant de toutes les parcourir sans jamais repasser deux fois par la même, en revenant au point de départ. L'algorithme naïf est évident : énumérer tous les parcours possibles, puis sélectionner le plus court.
Le seul problème est le nombre de parcours. Fixer la ville de départ, puis parcourir les autres dans un ordre quelconque, c'est choisir une permutation des villes restantes. Il y a donc
Pour 12 villes, parcours : à une microseconde chacun, environ 40 secondes. Pour 20 villes, : près de quatre mille ans. La factorielle croît plus vite que toute exponentielle — l'ordre de grandeur de est , que la formule de Stirling précise :
#Exemples
- O(1) : accéder à un élément d'un tableau par son index ; tester l'appartenance dans un
set. - O(log n) : recherche dichotomique dans un tableau trié.
- O(n) : parcourir tous les éléments ; recherche linéaire ; trouver le maximum.
- O(n log n) : tri par comparaison optimal (tri fusion, tri rapide en moyenne,
sortedde Python). - O(n²) : boucles imbriquées parcourant toutes les paires ; tri à bulles ; tri par sélection.
- O(2ⁿ) : énumérer tous les sous-ensembles d'un ensemble ; Fibonacci récursif naïf.
- O(n!) : énumérer toutes les permutations d'un ensemble ; voyageur de commerce par force brute.
#Compter les opérations : deux exemples
#Recherche linéaire
1def recherche_lineaire(tableau, element):2 for i in range(len(tableau)):3 if tableau[i] == element:4 return i5 return -1Au pire cas, l'élément est absent : la boucle fait n comparaisons. Le coût en temps est O(n). L'espace utilisé est celui du tableau lui-même, plus un index : O(1) en espace supplémentaire.
#Recherche dichotomique
1def recherche_dichotomique(tableau, element):2 gauche, droite = 0, len(tableau) - 13 while gauche <= droite:4 milieu = (gauche + droite) // 25 if tableau[milieu] == element:6 return milieu7 elif tableau[milieu] < element:8 gauche = milieu + 19 else:10 droite = milieu - 111 return -1Chaque itération fait une comparaison et divise l'espace de recherche par deux. Après k itérations, il reste candidats ; la boucle s'arrête quand , soit . Coût en temps O(log n), espace supplémentaire O(1).
#L'effet en pratique
1print([n * (n - 1) // 2 for n in (100, 1_000, 10_000)])2print([int(__import__('math').log2(n)) for n in (100, 1_000, 10_000)])Sorties :
1[4950, 499500, 49995000]2[6, 9, 13]Un algorithme quadratique fait environ 50 millions de paires à parcourir pour n = 10 000 ; le logarithmique s'en sort avec 13 étapes. À un million d'opérations par seconde, le premier demande 50 secondes, le second 13 microsecondes.
#Résoudre la récurrence d'un algorithme récursif
Compter les itérations d'une boucle suffit pour un algorithme itératif. Pour un algorithme récursif, le coût s'exprime par une relation de récurrence : le coût est fonction des coûts pour . Résoudre cette relation, c'est en tirer le terme général — donc l'ordre de grandeur. Les équations de récurrence sont aux mathématiques discrètes ce que les équations différentielles sont aux mathématiques continues, et elles proviennent de deux sources : les définitions inductives, et les stratégies « diviser pour régner » qui utilisent la récursivité.
#Le vocabulaire
- Ordre : si ne dépend que de , la relation est d'ordre 1 ; si elle dépend de et , elle est d'ordre 2. Plus généralement, si fait intervenir les termes d'indices entre et , la relation est d'ordre .
- Complète : tous les termes pour interviennent.
- Linéaire : est une combinaison linéaire des .
- De partition : ne fait intervenir que des termes . Les algorithmes dichotomiques donnent des équations de partition avec .
#Exemple 1 : la recherche dichotomique (partition, )
Soit le nombre de comparaisons de la recherche dichotomique dans un tableau trié de taille , avec . Chaque appel fait une comparaison et relance la recherche sur la moitié du tableau :
On opère le changement de variable , qui transforme la partition en suite récurrente ordinaire. Posons :
C'est une suite arithmétique de raison 1, donc . Le changement de variable inverse () donne
Lecture. La récurrence de partition s'est ramenée à une récurrence linéaire par changement de variable : c'est la méthode générale annoncée par le cours. Le résultat est l'ordre — mais il est maintenant démontré, et non seulement constaté.
#Exemple 2 : des droites dans le plan (arithmétique)
On trace droites dans le plan, deux à deux sécantes en des points distincts. Combien de régions délimitent-elles ? Notons ce nombre. On observe , , , , et l'on comprend que la -ième droite, coupée par les précédentes, ajoute régions :
C'est le cas avec . La méthode de sommation consiste à écrire la relation aux rangs successifs puis à additionner membre à membre :
Les termes intermédiaires s'annulent et il reste
Lecture. Croissance quadratique, . La somme des premiers entiers est le coût caché de tous les algorithmes « une boucle qui fait de plus en plus de travail ».
#Exemple 3 : les tours de Hanoï (géométrique)
Déplacer disques exige de déplacer disques sur un pilier intermédiaire, de déplacer le grand disque, puis de ramener les disques :
On multiplie la relation au rang par 1, celle au rang par 2, celle au rang par 4, et ainsi de suite, puis on additionne :
En sommant, les termes intermédiaires s'annulent et il reste
Lecture. Croissance exponentielle : 64 disques demanderaient déplacements, soit près de six siècles à un déplacement par nanoseconde. La récurrence dit ici quelque chose de définitif, qu'aucune optimisation d'implémentation ne changera : le problème est intrinsèquement exponentiel.
#Exemple 4 : le tri rapide au pire cas (ordre 1 avec terme polynomial)
Le tri rapide, lorsque le pivot est systématiquement extrémal, ne retire qu'un élément par partition. Sa complexité au pire cas vérifie
La somme des relations donne , d'où
soit . Lecture. C'est exactement l'écart entre le cas moyen et le pire cas annoncé plus haut : la récurrence montre pourquoi le pire cas est quadratique — la profondeur de récursion devient au lieu de .
#Tableau récapitulatif
| Algorithme | Récurrence | Solution | Ordre |
|---|---|---|---|
| Recherche dichotomique | , | ||
| droites dans le plan | , | ||
| Tours de Hanoï | , | ||
| Tri rapide, pire cas | , |
Ces quatre résolutions, et plusieurs autres, sont détaillées dans les annales corrigées.
#Temps contre espace
La complexité en temps compte les opérations ; la complexité en espace compte la mémoire supplémentaire. Les deux s'échangent souvent :
- La dichotomique est O(1) en espace supplémentaire : elle ne garde que trois indices.
- Le tri fusion est O(n log n) en temps mais O(n) en espace : il recopie la moitié gauche avant de fusionner.
- La mémoïsation de Fibonacci naïf passe de O(2ⁿ) en temps à O(n), en payant O(n) en mémoire pour le cache.
Il n'y a pas de bon choix universel : sur mobile ou dans un navigateur, la mémoire est parfois la ressource la plus rare ; en calcul intensif, c'est le temps. L'important est de connaître les deux nombres avant de décider.
#Meilleur cas, cas moyen, pire cas
Le même algorithme n'a pas un seul coût :
- Pire cas : l'entrée la plus défavorable. C'est la garantie qu'on promet : « jamais plus de O(n log n) ».
- Meilleur cas : l'entrée la plus favorable. Rarement utile, sauf à documenter un raccourci (recherche linéaire sur le premier élément : O(1)).
- Cas moyen : l'espérance sur des entrées typiques, souvent ce qu'on observe en pratique.
Exemples : le tri rapide est O(n log n) en moyenne mais O(n²) au pire cas (pivot systématiquement extrémal). La recherche linéaire est O(1) au mieux (élément en première position) et O(n) au pire. Quand on annonce une complexité sans préciser, c'est le pire cas qu'on doit avoir en tête.
#Règles de calcul rapides
- Boucles successives : les coûts s'additionnent, le maximum domine.
forpuisforsur n éléments : O(n) + O(n) = O(n). - Boucles imbriquées : les coûts se multiplient.
fordansfor: O(n) × O(n) = O(n²). - Diviser le problème par deux à chaque étape et faire un travail constant : O(log n).
- Diviser en deux et faire un travail linéaire par étape : O(n log n).
- Une fonction à k appels récursifs imbriqués sur une entrée réduite de 1 : souvent O(2ⁿ) ou pire (Fibonacci naïf).
- Récurrence de la forme : sommer sur les rangs successifs. Si est constant, la solution est linéaire ; si , elle est quadratique.
- Récurrence de la forme avec : suite géométrique, croissance exponentielle en .
- Récurrence de partition : changement de variable , qui la ramène à une suite arithmétique en ; solution .
#Exercices
- Classez par ordre de croissance : , , , , , , .
- Donnez la complexité en temps et en espace supplémentaire de
recherche_dichotomique, puis du tri par sélection (deux boucles imbriquées, échanges en place). - Un traitement prend 2 secondes sur n = 1 000 avec un algorithme O(n²). Combien de temps attendre pour n = 3 000 ? Et si l'algorithme était O(n) ?
- Énumérez tous les sous-ensembles d'un ensemble de 20 éléments : pourquoi O(2ⁿ) rend-il la chose hors de portée ?
- Un algorithme récursif divise son entrée en deux et fait une comparaison constante. Écrivez sa relation de récurrence, résolvez-la par changement de variable, et donnez son ordre.
- Le coût d'un algorithme vérifie et . Résolvez la récurrence et dites, en une phrase, ce que le résultat implique pour .
- Un programme énumère les permutations de éléments pour trouver la meilleure. Combien de cas examine-t-il pour ? Pourquoi est-ce pire que d'énumérer les sous-ensembles ?
Éléments de correction
- Du plus rapide au plus lent quand n grandit : .
- Dichotomique : O(log n) en temps (espace divisé par deux à chaque tour), O(1) en espace supplémentaire (trois indices). Tri par sélection : O(n²) en temps (n − 1 + n − 2 + … + 1 comparaisons, soit n(n−1)/2), O(1) en espace (tri en place).
- O(n²) : le travail est multiplié par (3000/1000)² = 9, donc 18 secondes. O(n) : multiplié par 3, donc 6 secondes. La leçon : le facteur d'échelle suit l'exposant.
- sous-ensembles : encore faisable. Mais chaque élément ajouté double le travail : 30 éléments donnent plus d'un milliard de sous-ensembles, 50 en donnent , hors de portée de toute machine pour un traitement non trivial. La croissance exponentielle interdit l'énumération exhaustive au-delà de quelques dizaines d'éléments.
- et . On pose et , d'où et , soit . En revenant à : , donc l'ordre est .
- On multiplie la relation au rang par 1, celle du rang par 2, etc., puis on additionne : . Pour , cela fait opérations : à une opération par nanoseconde, près de six siècles. La récurrence démontre que le problème est intrinsèquement exponentiel, aucune optimisation d'implémentation n'y changera rien.
- permutations à examiner. C'est pire que l'énumération des sous-ensembles parce que croît plus vite que : à , contre , soit onze ordres de grandeur d'écart.