No history yet

Complexité et Optimisation Algorithmique

Mesurer l'efficacité

Un programme qui fonctionne, c'est bien. Un programme qui fonctionne efficacement, c'est mieux. Quand on manipule de grandes quantités de données, la différence entre un algorithme rapide et un algorithme lent n'est pas une question de secondes, mais parfois de minutes, d'heures, voire de jours.

Mais comment mesurer l'« efficacité » ? On ne peut pas se fier au temps d'exécution sur une machine spécifique, car cela dépend du processeur, de la mémoire et d'autres facteurs. On a besoin d'une méthode universelle pour décrire comment la performance d'un algorithme évolue lorsque la taille des données d'entrée augmente. C'est là qu'intervient la notation Grand O (Big O notation).

La notation Grand O nous donne une approximation de la complexité d'un algorithme en se concentrant sur son comportement à grande échelle. On ignore les constantes et les termes de plus bas degré pour ne garder que ce qui domine lorsque la taille de l'entrée, notée n, devient très grande.

On distingue deux types principaux de complexité :

  • Complexité temporelle : Le nombre d'opérations que l'algorithme exécute en fonction de n.
  • Complexité spatiale : La quantité de mémoire supplémentaire que l'algorithme utilise en fonction de n.

Par exemple, un algorithme qui parcourt une liste de n éléments une seule fois pour trouver une valeur a une complexité temporelle de O(n)O(n). Si l'algorithme accède directement à un élément via son index, l'opération est en temps constant, soit O(1)O(1), car le temps ne dépend pas de la taille de la liste.

Le meilleur, le pire et la moyenne

La performance d'un algorithme n'est pas toujours constante. Elle peut varier considérablement en fonction de la structure des données d'entrée. Pour avoir une image complète, on analyse trois scénarios clés :

Le meilleur cas : La configuration d'entrée qui permet à l'algorithme de s'exécuter le plus rapidement possible.

Le cas moyen : La performance attendue pour une entrée typique ou aléatoire.

Le pire cas : La configuration d'entrée qui force l'algorithme à effectuer le maximum d'opérations.

Prenons l'exemple du tri par insertion. Cet algorithme parcourt une liste et insère chaque élément à sa place correcte dans la partie déjà triée. Si la liste est déjà triée (meilleur cas), l'algorithme n'a qu'à la parcourir une fois, ce qui donne une complexité de O(n)O(n).

En revanche, si la liste est triée en ordre inverse (pire cas), chaque élément doit être déplacé jusqu'au début de la liste. Cela nécessite un nombre d'opérations proportionnel au carré de la taille de la liste, soit O(n2)O(n^2). En pratique, c'est souvent la complexité du pire cas qui nous intéresse le plus, car elle garantit une borne supérieure sur la performance de l'algorithme, quelles que soient les données.

AlgorithmeMeilleur casCas moyenPire cas
Tri à bullesO(n)O(n)O(n2)O(n^2)O(n2)O(n^2)
Tri par insertionO(n)O(n)O(n2)O(n^2)O(n2)O(n^2)
Tri par sélectionO(n2)O(n^2)O(n2)O(n^2)O(n2)O(n^2)

Diviser pour mieux régner

Pour dépasser les limites des algorithmes en O(n2)O(n^2), les informaticiens ont développé des stratégies plus astucieuses. L'une des plus puissantes est le paradigme « Diviser pour régner ». L'idée est simple et élégante :

  1. Diviser : Scinder le problème principal en plusieurs sous-problèmes plus petits du même type.
  2. Régner : Résoudre les sous-problèmes de manière récursive. Si les sous-problèmes sont assez petits, on les résout directement.
  3. Combiner : Fusionner les solutions des sous-problèmes pour obtenir la solution du problème initial.

Le tri fusion (MergeSort) est l'exemple parfait de cette approche. Il divise une liste en deux moitiés, trie récursivement chaque moitié, puis fusionne les deux moitiés triées en une seule liste. Ce processus de fusion est l'étape clé où le travail s'effectue.

La magie de cette approche est que la complexité temporelle du tri fusion est de O(nlogn)O(n \log n) dans tous les cas. Pour de grandes listes, la différence avec O(n2)O(n^2) est astronomique. Un autre algorithme célèbre utilisant ce paradigme est le (QuickSort), qui est souvent plus rapide en pratique mais dont le pire cas est en O(n2)O(n^2).

La puissance de la récursivité

Les algorithmes de type « Diviser pour régner » sont presque toujours implémentés à l'aide de la récursivité. Une fonction récursive est une fonction qui s'appelle elle-même pour résoudre une version plus petite du même problème.

Chaque fonction récursive doit avoir deux composantes essentielles :

Un cas de base : Une condition qui arrête la récursion. Sans cela, la fonction s'appellerait indéfiniment.

Un pas récursif : L'appel de la fonction à elle-même, mais avec une entrée qui la rapproche du cas de base.

L'exemple classique est le calcul de la factorielle d'un nombre n, notée n!n!. La factorielle de 5 est 5×4×3×2×15 \times 4 \times 3 \times 2 \times 1. On peut la définir de manière récursive : la factorielle de n est nn multiplié par la factorielle de (n1)(n-1). Le cas de base est que la factorielle de 0 est 1.

fonction factorielle(n):
  // Cas de base
  si n == 0:
    retourner 1
  
  // Pas récursif
  sinon:
    retourner n * factorielle(n - 1)

L'analyse de la complexité des algorithmes récursifs se fait à l'aide de (recurrence relations). Ce sont des équations qui décrivent le coût d'un algorithme en fonction de son coût sur des entrées plus petites. Pour le tri fusion, la relation est T(n)=2T(n/2)+O(n)T(n) = 2T(n/2) + O(n), ce qui signifie que le temps pour trier n éléments est égal à deux fois le temps pour trier n/2 éléments (les deux appels récursifs), plus le temps pour fusionner les résultats (O(n)O(n)). La résolution de cette relation donne bien O(nlogn)O(n \log n).

Comprendre ces concepts de complexité, de paradigmes et de récursivité est essentiel. Cela permet de ne pas seulement écrire du code qui fonctionne, mais d'écrire du code intelligent, optimisé et capable de gérer les défis du monde réel.

Quiz Questions 1/6

À quoi sert principalement la notation Grand O (Big O) en informatique ?

Quiz Questions 2/6

Parmi les complexités temporelles suivantes, laquelle représente l'algorithme le plus efficace pour une très grande taille d'entrée n ?