Pourquoi la complexité algorithmique est-elle si importante ?
Imagine que tu dois chercher un mot dans un dictionnaire. Tu pourrais le faire page par page, ce serait très long. Ou tu pourrais utiliser l'ordre alphabétique et diviser le problème par deux à chaque étape. C'est exactement l'enjeu de la complexité algorithmique : évaluer l'efficacité d'un algorithme, non pas en secondes, mais en fonction de la taille des données qu'il doit traiter. En NSI, et plus tard en études d'informatique, c'est un concept fondamental pour concevoir des programmes rapides et optimisés, surtout quand on travaille avec de gros volumes de données.
La notation Big O : le langage universel de l'efficacité
La notation Big O (ou Grand O) est le moyen standard de décrire les performances d'un algorithme. Elle permet de classer les algorithmes en fonction de la façon dont leur temps d'exécution ou leur besoin en mémoire (espace) augmente lorsque la taille n des données d'entrée grandit. On se concentre sur l'ordre de grandeur, pas sur les constantes. Par exemple, un algorithme en O(5n) est considéré comme étant en O(n).
Les complexités les plus courantes en NSI
Voici les complexités que tu rencontreras le plus souvent, classées de la plus efficace à la moins efficace :
- O(1) - Complexité constante : Le temps est indépendant de la taille des données. Exemple : accéder à un élément d'un tableau par son index.
- O(log n) - Complexité logarithmique : Le temps augmente très lentement. C'est le cas de la recherche dichotomique.
- O(n) - Complexité linéaire : Le temps est proportionnel à la taille des données. Exemple : parcourir une liste une fois.
- O(n log n) - Complexité quasi-linéaire : Très courant pour les algorithmes de tri efficaces comme le Tri Fusion.
- O(n²) - Complexité quadratique : Le temps augmente avec le carré de n. Souvent dû à des boucles imbriquées. Exemple : le Tri à Bulles.
- O(2ⁿ) - Complexité exponentielle : Le temps double à chaque ajout de donnée. Très inefficace pour des n grands.
Exemples concrets en Python
Passons à la pratique avec du code Python que tu peux exécuter et modifier.
O(n) : La recherche linéaire
Parcourir une liste élément par élément est un classique de la complexité linéaire.
def recherche_lineaire(liste, valeur): for element in liste: # Cette boucle s'exécute n fois if element == valeur: return True return False # Si liste a 1000 éléments, dans le pire cas, on fait 1000 comparaisons.
O(n²) : Vérifier les doublons dans une liste
Une double boucle imbriquée est souvent le signe d'une complexité quadratique.
def contient_doublon(liste): n = len(liste) for i in range(n): # n itérations for j in range(i+1, n): # Environ n/2 itérations en moyenne if liste[i] == liste[j]: return True return False # Pour n=1000, on peut avoir jusqu'à ~500 000 comparaisons !
O(log n) : La puissance de la recherche dichotomique
Cet algorithme ne fonctionne que sur une liste triée. À chaque étape, il divise l'espace de recherche par deux.
def recherche_dichotomique(liste_triee, valeur): gauche = 0 droite = len(liste_triee) - 1 while gauche <= droite: milieu = (gauche + droite) // 2 if liste_triee[milieu] == valeur: return True elif liste_triee[milieu] < valeur: gauche = milieu + 1 # On cherche dans la moitié droite else: droite = milieu - 1 # On cherche dans la moitié gauche return False # Pour n=1 000 000, il faut au maximum ~20 étapes ! (log2(1 000 000) ≈ 20)
Comment analyser la complexité d'un algorithme ?
Pour déterminer la complexité d'un algorithme, suis ces étapes :
- Identifier la taille de l'entrée (n) : C'est souvent la longueur d'une liste, le nombre de nœuds dans un graphe, etc.
- Compter les opérations élémentaires en fonction de n. Concentre-toi sur la boucle la plus imbriquée.
- Garder le terme qui croît le plus vite et ignorer les constantes multiplicatives. 3n² + 10n + 100 devient O(n²).
- Considérer le pire des cas (worst-case), qui est le plus utilisé en NSI pour garantir les performances.
N'oublie pas qu'un algorithme peut avoir une complexité en temps différente de sa complexité en espace (mémoire utilisée).
Exercice d'application : Analyser ce code
Essaie de trouver la complexité de la fonction suivante avant de lire la réponse.
def fonction_mystere(n): total = 0 for i in range(n): for j in range(10): total += i * j return total
Ici, la boucle externe fait n itérations. La boucle interne en fait toujours 10, quel que soit n. Le nombre total d'opérations est donc 10 * n. On ignore la constante 10, donc la complexité est O(n), et non O(n²) ! C'est une erreur fréquente.
