Adloun

Complexité algorithmique

Cours · Python (école d'ingénieur), chapitre 6 · école d'ingénieur, informatique · le début du chapitre, la suite sur Adloun

6.1 Introduction et Motivation

Déterminer la complexité d'un algorithme (aussi appelée son coût) consiste à évaluer les ressources nécessaires à son exécution : essentiellement le temps de calcul et, dans une moindre mesure, la quantité de mémoire requise. Ces besoins dépendent de nombreux paramètres matériels (puissance du processeur, vitesse d'accès à la mémoire) qui sortent du domaine théorique de l'informatique.

Il est donc impossible d'attribuer une valeur absolue (en secondes ou en octets) à un algorithme. En revanche, nous pouvons évaluer son ordre de grandeur de croissance en fonction de la taille de l'entrée. Cela permet d'identifier l'algorithme le plus efficace parmi plusieurs résolvant le même problème.

Considérons un exemple concret : la recherche du nombre de diviseurs d'un entier naturel .


def diviseurs1(n):
    d = 0
    k = 1
    while k <= n:
        if n % k == 0:
            d += 1
        k += 1
    return d

def diviseurs2(n):
    d = 0
    k = 1
    while k * k < n:
        if n % k == 0:
            d += 2
        k += 1
    if k * k == n:
        d += 1
    return d

Dans diviseurs1, la boucle s'exécute fois. Le temps d'exécution est proportionnel à . Si on multiplie par 100, le temps de calcul est multiplié par 100 (complexité linéaire).

Dans diviseurs2, on exploite le fait que si divise , alors divise aussi . L'un de ces deux diviseurs est inférieur ou égal à . Il suffit donc de chercher les diviseurs inférieurs ou égaux à , la boucle s'exécutant fois. Le temps d'exécution est proportionnel à . Si on multiplie par 100, le temps n'est multiplié que par 10 (complexité en racine carrée).

6.2 Modèle de Calcul et Instructions Élémentaires

Pour évaluer la complexité d'un algorithme de manière uniforme, on définit un modèle de calcul (généralement la machine RAM à processeur unique) où certaines opérations de base, dites instructions élémentaires, ont un coût constant (noté ) indépendant de leurs opérandes. Parmi elles :

iRemarqueAttention : Coûts cachés en Python

En Python, certaines opérations syntaxiquement simples cachent des coûts élevés :


L = list(range(1000))
x = L[500]           # O(1) : accès indexé (tableau dynamique)
x = L.index(500)     # O(n) : parcourt la liste jusqu'à trouver 500
print(500 in L)      # O(n) : parcourt toute la liste dans le pire cas
copie = L[100:200]   # O(k) : crée une nouvelle liste de taille k (copie!)
L.sort()             # O(n log n) : tri en place

De même, la comparaison de deux chaînes de caractères de longueur a une complexité dans le pire des cas.

6.3 Notations de Landau (, , , )

Soit le nombre d'instructions élémentaires exécutées par un algorithme pour une entrée de taille . Pour exprimer son comportement asymptotique (quand devient grand), on utilise les notations suivantes :

Définition 6.1Notations Asymptotiques
  • Grand O () : Borne supérieure asymptotique. Le coût de l'algorithme ne dépasse pas un certain multiple de pour les grandes valeurs de :

  • Grand Oméga () : Borne inférieure asymptotique (meilleur des cas) :

  • Grand Thêta () : Encadrement asymptotique serré. L'ordre de grandeur est exactement proportionnel à :

  • Petit o () : Négligeabilité. Le rapport tend vers 0 quand :

Exemple 6.2Illustration des notations

Pour :

  • (car pour )
  • (car )
  • (car encadrement serré)
  • (car )
  • (car le rapport tend vers 3, pas vers 0)

6.3.1 Échelles de Complexité

Le tableau suivant illustre la différence d'échelle colossale du temps de calcul théorique requis pour différents types de complexité, sur une base matérielle de opérations par seconde (1 GHz) :

Taille
7 ns100 ns0.7 s10 s1 ms ans
10 ns1 s10 s1 ms1 s ans
13 ns10 s133 s100 ms17 sIndéfini
17 ns100 s2 ms10 s11.6 joursIndéfini
20 ns1 ms20 ms17 min32 ansIndéfini
Figure : Temps d'exécution théorique selon la complexité ( opérations/seconde).

On qualifie classiquement les complexités de la façon suivante :

ComplexitéQualificationExemple d'algorithme
ConstantAccès à un élément de liste par index
LogarithmiqueRecherche dichotomique
RacineRecherche des diviseurs optimisée
LinéaireParcours de liste, recherche linéaire
Quasi-linéaireTri fusion, tri rapide (moyenne)
QuadratiqueTri à bulles, double boucle pleine
CubiqueProduit matriciel naïf
ExponentielleFibonacci récursif, sous-ensembles
FactorielleProblème du voyageur de commerce (force brute)
Figure : Classification des complexités courantes.

6.3.2 Complexités des Structures de Données Python

Le tableau suivant récapitule les complexités temporelles des opérations courantes sur les structures de données natives de Python :

Opérationlistdictset
Accès par index/clé---
Affectation par index/clé---
Recherche (x in S)
Insertion à la fin (append) amorti------
Insertion au début / milieu------
Ajout d'une clé/valeur---
Suppression par valeur/clé
Parcours
Tri (sort())------
Figure : Complexités des opérations sur les structures de données Python (cas moyen).
iRemarqueComplexité amortie

La complexité amortie est le coût moyen d'une opération sur une séquence d'opérations. Par exemple, l'ajout append() sur une liste Python a un coût amorti car la sur-allocation périodique du tableau sous-jacent est rare (le coût de réallocation est réparti sur toutes les insertions).

La suite du cours est sur Adloun

Le chapitre en entier, formules et figures comprises, avec le tuteur, les fiches, les QCM et les exercices.

Lire la suite du cours

En accès libre, sans inscription.

Exercices corrigés de ce chapitre

Lire la suite du cours →