Complexité des algorithmes
Une version article du chapitre pour comprendre l'essentiel rapidement, vérifier si le niveau correspond, puis basculer vers Wilo pour la pratique guidée et le suivi.
Lecture
5 chapitres
Un parcours éditorialisé et navigable.
Pratique
12 questions
Quiz et cartes mémoire à ouvrir après la lecture.
Objectif
Terminale générale
Format rapide pour vérifier si le chapitre correspond.
Chapitre 1
Introduction à la complexité algorithmique
Pourquoi étudier la complexité ?
L'étude de la complexité algorithmique est fondamentale en informatique. Elle nous permet de comprendre et de prédire la performance des programmes que nous écrivons. Imaginez que vous avez plusieurs recettes pour faire un gâteau : certaines sont rapides, d'autres prennent beaucoup de temps, et d'autres encore demandent beaucoup d'ingrédients. En informatique, c'est pareil pour les algorithmes.
L'objectif principal est de pouvoir réaliser une comparaison d'algorithmes de manière objective. Si deux algorithmes résolvent le même problème, comment choisir le meilleur ? Intuitivement, on pourrait penser à tester les deux et voir lequel est le plus rapide. Cependant, cette approche a des limites, comme nous le verrons plus tard. L'analyse de la complexité offre une méthode théorique pour évaluer cette efficacité.
L'importance en informatique de cette discipline est cruciale. Elle permet aux développeurs de :
- Choisir l'algorithme le plus adapté à un problème donné, surtout quand la taille des données augmente.
- Anticiper le comportement d'un programme face à des volumes de données importants.
- Identifier les goulots d'étranglement (les parties lentes) d'un programme pour les optimiser.
- Concevoir des algorithmes plus efficaces dès le départ.
En résumé, étudier la complexité, c'est apprendre à évaluer la "qualité" d'un algorithme indépendamment de la machine qui l'exécute ou du langage de programmation utilisé.
Définition de la complexité
La complexité d'un algorithme est une mesure des ressources consommées par cet algorithme pour s'exécuter. Ces ressources sont principalement de deux types :
- Complexité temporelle : C'est la quantité de temps qu'un algorithme prend pour s'exécuter en fonction de la taille de ses données d'entrée. Il ne s'agit pas du temps réel mesuré en secondes (qui dépend de la machine), mais du nombre d'opérations élémentaires effectuées.
- Complexité spatiale : C'est la quantité de mémoire (espace de stockage) qu'un algorithme requiert pour s'exécuter, toujours en fonction de la taille de ses données d'entrée. Cela inclut la mémoire pour stocker les variables, les structures de données, etc.
Le but est de trouver une fonction mathématique qui décrit comment le temps ou l'espace nécessaire augmente à mesure que la taille des données d'entrée (souvent notée ) augmente. Par exemple, si un algorithme prend étapes pour un problème de taille , sa complexité temporelle sera "linéaire". Si un autre prend étapes, elle sera "quadratique".
Mesure de la complexité : approche empirique vs. théorique
Il existe deux grandes manières d'évaluer la performance d'un algorithme :
-
Mesure du temps d'exécution (approche empirique) : Cette méthode consiste à implémenter l'algorithme et à le faire tourner sur un ordinateur, en mesurant le temps qu'il prend pour différentes tailles de données d'entrée. On utilise des fonctions de chronométrage fournies par les langages de programmation.
- Avantages : Donne une mesure concrète et réelle.
- Inconvénients (limites de l'approche empirique) :
- Dépend de la machine utilisée (processeur, RAM, etc.).
- Dépend du langage de programmation et du compilateur/interpréteur.
- Dépend des autres programmes exécutés en même temps sur la machine.
- Nécessite d'implémenter l'algorithme avant de pouvoir le tester.
- Ne permet pas de prédire le comportement sur des entrées beaucoup plus grandes que celles testées.
-
Nécessité d'une approche théorique : Pour pallier les limites de l'approche empirique, on utilise l'analyse théorique de la complexité. Cette approche est indépendante de la machine et du langage. Elle se concentre sur le nombre d'opérations élémentaires que l'algorithme doit effectuer.
- Elle permet de prédire la croissance du temps d'exécution lorsque la taille des données d'entrée tend vers l'infini.
- Elle facilite la comparaison objective d'algorithmes.
- Elle permet d'évaluer un algorithme avant même de l'avoir codé.
C'est cette approche théorique que nous allons explorer en détail, en nous concentrant principalement sur la complexité temporelle.
Chapitre 2
Complexité temporelle : le compte des opérations
Opérations élémentaires
Une opération élémentaire est une opération qui prend un temps constant et borné, quelle que soit la taille des données. On considère généralement que les opérations suivantes sont élémentaires :
- Affectation :
x = 5,y = a. - Comparaison :
a < b,x == y. - Opération arithmétique :
a + b,x * 2,y / 3. - Accès mémoire : Lecture ou écriture d'une valeur dans une variable ou à une adresse spécifique dans un tableau (ex:
tab[i]). - Appel de fonction sans boucle ou récursion (si son corps est constant).
Chacune de ces opérations est considérée comme prenant "une unité de temps". Le but est de compter combien de ces unités de temps sont nécessaires en fonction de la taille de l'entrée .
Analyse de boucles simples
Les boucles sont souvent les parties les plus coûteuses d'un algorithme car elles répètent un bloc d'opérations plusieurs fois.
Boucle for
Considérons une boucle for simple :
somme = 0
for i in range(n): # n représente la taille de l'entrée
somme = somme + i
- L'initialisation
somme = 0est 1 opération. - La boucle
for i in range(n)va s'exécuternfois (pouriallant de 0 àn-1). - À chaque itération :
somme = somme + i: 1 addition, 1 affectation (2 opérations).- L'incrémentation de
iet la comparaisoni < nsont également des opérations (environ 2 par itération).
- Total :
- Soit environ opérations.
- Lorsque devient très grand, le terme domine. On dit que la complexité est linéaire en .
Boucle while
Le principe est similaire pour une boucle while :
i = 0
while i < n: # n représente la taille de l'entrée
print(i)
i = i + 1
- L'initialisation
i = 0est 1 opération. - La condition
i < nest évaluéen+1fois (la dernière fois pour sortir de la boucle). - Le corps de la boucle s'exécute
nfois :print(i): 1 opération (affichage).i = i + 1: 1 addition, 1 affectation (2 opérations).
- Total :
- Soit environ opérations.
- Là encore, la complexité est linéaire en .
Le nombre d'itérations est la clé de l'analyse des boucles.
Analyse de structures conditionnelles
Les structures conditionnelles (if/else) introduisent une notion de choix dans l'exécution de l'algorithme.
if condition:
# Bloc A
else:
# Bloc B
Pour analyser la complexité d'une structure conditionnelle, on doit considérer différents scénarios :
- Cas le pire (worst-case) : C'est le scénario qui entraîne le plus grand nombre d'opérations. C'est généralement celui que l'on privilégie pour l'analyse de la complexité car il garantit que l'algorithme ne fera jamais "pire" que cette estimation.
- Cas le meilleur (best-case) : C'est le scénario qui entraîne le plus petit nombre d'opérations. C'est rarement utilisé car il ne donne aucune garantie sur la performance générale.
- Cas moyen (average-case) : C'est le nombre moyen d'opérations effectuées sur toutes les entrées possibles. C'est souvent difficile à calculer car cela nécessite des hypothèses sur la distribution des entrées.
Exemple : Recherche d'un élément dans un tableau non trié.
- Cas le pire : L'élément recherché est le dernier du tableau, ou il n'est pas dans le tableau. L'algorithme doit parcourir tout le tableau (complexité linéaire).
- Cas le meilleur : L'élément recherché est le premier du tableau. L'algorithme le trouve immédiatement (complexité constante).
Exemples d'algorithmes simples
Reprenons quelques algorithmes pour illustrer le comptage des opérations. Pour simplifier, nous allons nous concentrer sur le nombre de fois qu'une opération "clé" est exécutée.
Recherche séquentielle (ou linéaire)
On cherche un élément x dans un tableau tab de taille n.
def recherche_seq(tab, x):
for i in range(len(tab)): # Boucle s'exécute len(tab) = n fois max
if tab[i] <mark> x: # 1 comparaison par itération
return True
return False
- Cas le pire :
xn'est pas dans le tableau, ouxest le dernier élément. La boucle s'exécutenfois. Chaque itération fait une comparaison. La complexité est linéaire, proportionnelle à . On écrira . - Cas le meilleur :
xest le premier élément. La boucle s'exécute 1 fois. La complexité est constante==. On écrira . - Cas moyen : Si l'élément est présent, il est en moyenne au milieu du tableau, soit comparaisons. La complexité reste .
Calcul de somme
Calculer la somme de tous les éléments d'un tableau de taille n.
def calculer_somme(tab):
somme = 0
for element in tab: # Boucle s'exécute n fois
somme = somme + element # 1 addition, 1 affectation
return somme
- La boucle s'exécute
nfois. À chaque itération, il y a un nombre constant d'opérations (une addition, une affectation). - La complexité est toujours linéaire, proportionnelle à . On écrira .
Recherche du maximum
Trouver le plus grand élément dans un tableau de taille n.
def trouver_max(tab):
if not tab:
return None # Gérer le cas du tableau vide
maximum = tab[0] # 1 initialisation
for i in range(1, len(tab)): # Boucle s'exécute n-1 fois
if tab[i] > maximum: # 1 comparaison par itération
maximum = tab[i] # 1 affectation (pas toujours exécutée)
return maximum
- La boucle s'exécute fois. À chaque itération, il y a au moins une comparaison.
- Le nombre d'affectations de
maximumvarie, mais la comparaison est toujours là. - La complexité est linéaire, proportionnelle à . On écrira .
Chapitre 3
Notations asymptotiques
Introduction à la notation Grand O (O)
La notation Grand O (prononcé "grand O" ou "O de") est la plus couramment utilisée. Elle décrit la borne supérieure du temps d'exécution d'un algorithme. En d'autres termes, elle nous donne un ordre de grandeur du temps d'exécution dans le cas le pire.
Formellement, on dit qu'une fonction est s'il existe des constantes positives et telles que pour tout , . En clair, cela signifie que pour des valeurs de suffisamment grandes, ne croîtra jamais plus vite que multiplié par une constante.
Exemple : Si un algorithme prend opérations, on dit qu'il est .
- On ignore les constantes multiplicatives (ici 4) car pour de très grands , elles sont moins importantes que la puissance de .
- On ignore les termes d'ordre inférieur (ici +1) car pour de très grands , ils deviennent négligeables par rapport au terme dominant ().
L'objectif est de trouver la fonction la plus simple possible qui représente le taux de croissance de .
Classes de complexité courantes
Voici les classes de complexité les plus courantes, classées de la plus efficace à la moins efficace pour de grandes valeurs de :
- : constante
- Le temps d'exécution ne dépend pas de la taille de l'entrée. C'est le plus rapide !
- Exemple : Accéder à un élément dans un tableau par son index (
tab[5]).
- : logarithmique
- Le temps d'exécution augmente très lentement avec la taille de l'entrée. Souvent rencontré avec les algorithmes qui divisent le problème en deux à chaque étape.
- Exemple : Recherche dichotomique dans un tableau trié.
- : linéaire
- Le temps d'exécution est directement proportionnel à la taille de l'entrée.
- Exemple : Recherche séquentielle, parcours d'un tableau, calcul de somme.
- : quasi-linéaire
- Plus lent que linéaire, mais beaucoup plus rapide que quadratique. C'est la complexité de certains des algorithmes de tri les plus efficaces.
- Exemple : Tri fusion, Tri rapide (en moyenne).
- : quadratique
- Le temps d'exécution est proportionnel au carré de la taille de l'entrée. Souvent rencontré avec des boucles imbriquées.
- Exemple : Tri par sélection, Tri par insertion, Tri à bulles.
- : exponentielle
- Le temps d'exécution double à chaque fois que la taille de l'entrée augmente d'une unité. Devient très vite impraticable même pour de petites valeurs de .
- Exemple : Calcul de la suite de Fibonacci par récursion naïve, résolution du problème du voyageur de commerce par force brute.
Tableau comparatif des croissances (pour ) :
| Notation | |||
|---|---|---|---|
| (constant) | 1 | 1 | 1 |
| (linéaire) | 10 | 100 | 1000 |
| (quadratique) | 100 | 10000 | 1000000 |
| (exponentielle) | 1024 |
On voit clairement l'impact de l'ordre de grandeur sur la performance. Un algorithme est acceptable pour , mais catastrophique pour .
Application des notations aux algorithmes
Pour déterminer la complexité d'un algorithme :
- Identifier la variable d'entrée : C'est la taille des données (nombre d'éléments dans un tableau, longueur d'une chaîne, etc.).
- Compter les opérations élémentaires : Estimer combien d'opérations sont réalisées en fonction de . Concentrez-vous sur les boucles et les appels récursifs.
- Identifier le terme dominant : Si vous avez une expression comme , le terme dominant est .
- Simplifier l'expression avec la notation Grand O : On supprime les constantes multiplicatives et les termes d'ordre inférieur. devient .
Exemple : Boucles imbriquées
for i in range(n):
for j in range(n):
print(i, j)
- La boucle extérieure s'exécute
nfois. - Pour chaque exécution de la boucle extérieure, la boucle intérieure s'exécute
nfois. - Le corps de la boucle intérieure (
print(i, j)) s'exécute fois. - La complexité est donc .
La comparaison d'ordres de grandeur est essentielle. Un algorithme sera toujours préférable à un pour de grandes valeurs de , même si pour de très petits , l'algorithme pourrait être plus rapide à cause des constantes cachées par la notation Grand O.
Chapitre 4
Analyse de la complexité d'algorithmes classiques
Algorithmes de tri
Les algorithmes de tri réorganisent les éléments d'une liste dans un ordre spécifique (croissant ou décroissant). La complexité est généralement mesurée par le nombre de comparaisons et d'échanges.
Tri par sélection ()
Le tri par sélection fonctionne en trouvant le plus petit élément du tableau non trié et en le plaçant au début. Il répète ce processus pour le reste du tableau.
- Pour trouver le minimum dans un tableau de éléments, il faut comparaisons.
- Ensuite, on échange cet élément.
- Pour le deuxième plus petit, on cherche dans les éléments restants, soit comparaisons.
- ...
- Le nombre total de comparaisons est .
- La complexité est donc (quadratique).
Tri par insertion ()
Le tri par insertion construit la liste triée un élément à la fois. À chaque étape, il prend un élément du tableau non trié et l'insère à sa position correcte dans la partie déjà triée.
- Pour insérer le 2ème élément, il faut au maximum 1 comparaison et 1 déplacement.
- Pour insérer le 3ème élément, il faut au maximum 2 comparaisons et 2 déplacements.
- ...
- Pour insérer le -ième élément, il faut au maximum comparaisons et déplacements.
- Le nombre total d'opérations est .
- La complexité est donc (quadratique) dans le cas le pire (tableau trié en ordre inverse).
- Dans le cas le meilleur (tableau déjà trié), la complexité est car chaque élément est comparé une seule fois.
Tri fusion ()
Le tri fusion est un algorithme de type "diviser pour régner". Il divise le tableau en deux moitiés, trie récursivement chaque moitié, puis fusionne les deux moitiés triées.
- La division du tableau en moitiés prend à chaque niveau de récursion.
- Il y a niveaux de récursion (car on divise par 2 à chaque fois).
- À chaque niveau, l'opération de fusion de deux sous-tableaux triés prend un temps linéaire par rapport au nombre total d'éléments à fusionner (soit ).
- Donc, la complexité est (quasi-linéaire). C'est beaucoup plus efficace que pour de grandes listes.
Algorithmes de recherche
Recherche séquentielle ()
Comme vu précédemment, pour trouver un élément dans un tableau non trié, il faut potentiellement parcourir tout le tableau.
- Complexité : dans le cas le pire.
Recherche dichotomique ()
La recherche dichotomique (ou binaire) est beaucoup plus rapide, mais elle a un prérequis : le tableau doit être trié.
L'algorithme fonctionne en comparant l'élément recherché avec l'élément du milieu du tableau.
- Si c'est l'élément recherché, on a trouvé.
- S'il est plus petit, on réduit la recherche à la moitié gauche du tableau.
- S'il est plus grand, on réduit la recherche à la moitié droite du tableau.
- À chaque étape, la taille de l'espace de recherche est divisée par deux.
- Le nombre d'étapes nécessaires pour réduire un tableau de taille à un seul élément est .
- Complexité : .
Exemple : Pour un tableau de 1 million d'éléments (), . Il ne faut que 20 comparaisons dans le pire des cas, contre 1 million pour la recherche séquentielle !
Impact du choix de l'algorithme
Le choix de l'algorithme a un impact majeur sur l'évolutivité des solutions. Un algorithme sera parfaitement utilisable pour , mais deviendra impraticable pour . Un algorithme ou aura une performance bien meilleure sur de grands jeux de données.
Il est important de comprendre qu'il y a souvent un compromis temps/espace. Certains algorithmes plus rapides (meilleure complexité temporelle) peuvent nécessiter plus de mémoire (pire complexité spatiale), et vice-versa. Le choix dépendra des contraintes spécifiques du problème et des ressources disponibles. Par exemple, le tri fusion est en temps, mais nécessite d'espace supplémentaire pour les fusions, alors que le tri par insertion est en temps, mais en espace (il trie "en place").
Chapitre 5
Complexité spatiale et limites
Définition de la complexité spatiale
La complexité spatiale d'un algorithme mesure la quantité de mémoire utilisée par l'algorithme pour s'exécuter, en fonction de la taille de ses données d'entrée . Cette mémoire est généralement mesurée en unités de stockage (octets) ou en nombre de variables/cellules mémoire.
Elle inclut :
- La mémoire pour stocker les variables d'entrée.
- La mémoire pour stocker les variables locales et temporaires de l'algorithme.
- La mémoire pour les structures de données auxiliaires créées par l'algorithme.
- La pile d'appels pour les fonctions récursives.
Comme pour la complexité temporelle, on s'intéresse à l'ordre de grandeur de cette consommation d'espace en fonction de .
Exemples de complexité spatiale
- Algorithmes in-place : Un algorithme est dit "in-place" s'il ne nécessite qu'une quantité de mémoire supplémentaire constante (ou très faible) par rapport à la taille de l'entrée. Sa complexité spatiale est .
- Exemple : Le tri par insertion est un algorithme in-place. Il ne crée pas de nouveau tableau significatif.
- Utilisation de structures auxiliaires : Certains algorithmes créent de nouvelles structures de données dont la taille dépend de .
- Exemple : Le tri fusion nécessite un tableau temporaire de taille pour la phase de fusion. Sa complexité spatiale est .
- Le stockage d'une copie du tableau d'entrée entraînera également une complexité spatiale de .
- Impact sur les ressources : Dans les environnements avec des ressources mémoire limitées (systèmes embarqués, serveurs avec de très grandes données), la complexité spatiale peut être un facteur limitant aussi important que la complexité temporelle. Un algorithme en espace peut être acceptable, mais un en espace pourrait rapidement saturer la mémoire pour des élevés.
Limites de calcul et problèmes insolubles
Même avec les algorithmes les plus efficaces, il existe des limites fondamentales à ce que les ordinateurs peuvent résoudre :
- Problèmes indécidables : Ce sont des problèmes pour lesquels il a été prouvé qu'aucun algorithme ne peut exister pour les résoudre dans tous les cas. Le plus célèbre est le problème de l'arrêt (déterminer si un programme donné va s'arrêter ou boucler indéfiniment). Il n'y a pas d'algorithme universel pour ça.
- Problèmes NP-complets : Ce sont des problèmes pour lesquels aucun algorithme "efficace" (c'est-à-dire avec une complexité polynomiale comme , , etc.) n'a été trouvé à ce jour. Les meilleurs algorithmes connus ont une complexité exponentielle ( ou pire).
- Exemples : Problème du voyageur de commerce, problème du sac à dos, problème de satisfiabilité booléenne.
- L'implication pratique est que pour ces problèmes, si est grand, il est impossible de trouver la solution exacte dans un temps raisonnable. On doit souvent se contenter de solutions approchées ou d'heuristiques qui donnent de "bonnes" solutions, mais pas nécessairement la meilleure.
L'étude de la complexité nous aide donc à comprendre non seulement comment rendre nos programmes plus rapides, mais aussi ce qui est fondamentalement faisable ou infaisable en informatique.
Après la lecture
Passe à la pratique avec deux blocs bien visibles
Une fois le cours lu, ouvre soit le quiz pour vérifier la compréhension, soit les flashcards pour mémoriser les idées importantes. Les deux s'ouvrent dans une fenêtre dédiée.
Suite naturelle
Tu veux aller plus loin que l'article ?
Retrouve le même chapitre dans Wilo avec la suite des questions, la répétition espacée, les corrigés complets et une progression suivie dans le temps.