Vous êtes sur la page 1sur 6

Chapitre 1

Notions de complexité

Dans ce chapitre, nous présentons la notion de complexité, qui décrit le temps et la mémoire
nécessaires pour exécuter un algorithme et caractérise donc son efficacité. Nous étudions ensuite la
complexité de tous les algorithmes présentés.

1.1 Introduction
Un même problème peut être, le plus souvent, résolu par plusieurs algorithmes, c’est pourquoi
il est très important de pouvoir comparer l’efficacité des différents algorithmes pouvant être mis en
œuvre pour résoudre ce problème. Deux critères principaux sont généralement considérés quand on
étudie l’efficacité d’un algorithme ; le premier est le temps de calcul qu’on appelle aussi parfois coût
de l’algorithme, le second est l’espace mémoire nécessaire à l’exécution de l’algorithme. Si l’espace
mémoire reste un critère important de l’évaluation de l’efficacité d’un algorithme, en pratique, dans
la majorité des problèmes à résoudre, le temps de calcul devient un paramètre principal mesurant
l’efficacité d’un algorithme. Par exemple, pour un logiciel interactif, un temps de réponse court est
un élément essentiel du confort de l’utilisateur. De même, certains programmes industriels doivent
être utilisés un grand nombre de fois dans un délai très court.// Dans ce cours, nous nous inté-
resserons à la complexité en temps d’un algorithme que, par abus de langage, nous appellerons
simplement « complexité d’un algorithme ».
Exemple :

1 def diviseurs ( n ) :
2 for i in range (1 , n +1) :
3 if n % i == 0 :
4 print ( i )

Cet algorithme réalise exactement n calculs de restes de divisions euclidiennes, n comparaisons



et au plus n affichages. Cependant, on peut√ aussi utiliser le fait que si n = p × q avec p ≥ n, alors
q est un diviseur
√ de n inférieur ou égal à n. Il suffit donc de chercher chaque diviseur q inférieur
ou égal à n et de calculer p = n/q pour obtenir tous les diviseurs. Dans le cas où n est un carré
parfait, on prend soin de ne pas afficher sa racine carrée deux fois :
1 import math
2 def diviseurs ( n ) :
3 for i in range (1 , int ( math . sqrt ( n ))+1):
4 if n % i == 0 :
5 print ( i )

1
CPGE :Mohammedia CHAPITRE 1. NOTIONS DE COMPLEXITÉ

6 if n // i != i :
7 print ( n // i )
√ √ √
Cet algorithme réalise un calcul √ de racine carrée, b nc calculs de reste, entre b nc et 2b nc
comparaisons et entre 1 et 2b nc affichages.
En général, on cherche à déterminer comment le temps d’exécution d’un algorithme varie en fonction
d’un paramètre qu’on appelle la taille du problème. Le temps de recherche des diviseurs d’un entier n
dépend den, qu’on pourra donc naturellement prendre comme√taille du problème. Comme on l’a vu,
selon l’algorithme, ce temps peut être proportionnel à n ou à n. De même, quand on s’interrogera
sur l’efficacité d’un algorithme manipulant des tableaux, on cherchera à comprendre comment le
temps d’exécution de cet algorithme varie en fonction du nombre d’éléments de ce tableau. Une
autre possibilité pour la taille du problème est de considérer la taille de la représentation des données
passées à ce programme. Par exemple, pour un programme traitant un texte, on prend pour taille
du problème le nombre de caractères de ce texte.
L’évaluation du temps mis par un algorithme pour s’exécuter est un domaine de recherche à part
entière, car elle se révèle parfois très difficile. Néanmoins, dans de nombreux cas, cette évaluation
peut se faire en appliquant quelques règles simples.

1.2 II. Déterminer le coût d’un algorithme


Pour déterminer le coût d’un algorithme, on se fonde en général sur le modèle de complexité
suivant :
— Une affectation, une comparaison ou l’évaluation d’une opération arithmétique ayant en
général un faible temps d’exécution, celui-ci sera considéré comme l’unité de mesure du coût
d’un algorithme.
— Le coût des instructions p et q en séquence est la somme des coûts de l’instruction p et de
l’instruction q.
— Le coût d’un test if b : p else : q est inférieur ou égal au maximum des coûts des instructions
p et q, plus le temps d’évaluation de l’expression b.
— Le coût d’une boucle for i in iterable : p est égal au nombre d’éléments de l’itérable multiplié
par le coût de l’instruction p si ce dernier ne dépend pas de la valeur de i. Quand le coût du
corps de la boucle dépend de la valeur de i, le coût total de la boucle est la somme des coûts
du corps de la boucle pour chaque valeur de i.
— Le cas des boucles while est plus complexe à traiter puisque le nombre de répétitions n’est
en général pas connu a priori. On peut majorer le nombre de répétitions de la boucle de
la même façon qu’on démontre sa terminaison et ainsi majorer le coût de l’exécution de la
boucle.

1.3 Complexité et notation O


En réalité, lorsqu’on cherche à évaluer l’efficacité d’un algorithme, il est souvent inutile d’aller
jusqu’à ce niveau de détail : on se contentera de√dire que le nombre d’opérations élémentaires ef-
fectuées est par exemple proportionnel à n ou à n. Il y a plusieurs raisons à cela.

les différentes opérations élémentaires considérées ne demandent pas toutes exactement le même
temps de calcul et cachent donc un facteur multiplicatif, borné mais très compliqué à déterminer
précisément. D’autre part, le même programme peut être exécuté sur deux machines différentes,
l’une étant par exemple deux fois plus rapide que l’autre. Cela ne change évidemment rien à l’ef-
ficacité intrinsèque de l’algorithme et ce qui nous intéresse réellement n’est pas le temps précis
d’exécution d’un programme, mais l’ordre de grandeur de ce temps en fonction de la taille des

2TSI 2 A.HAOUDIGUI
CPGE :Mohammedia CHAPITRE 1. NOTIONS DE COMPLEXITÉ

données.

Une dernière notion à considérer est celle du terme dominant dans le temps d’exécution d’un
algorithme. Par exemple, si on a déterminé que ce temps était proportionnel àn2 + 3n, dès que la
taille n des données devient un peu importante, il est connu que le terme 3n augmente beaucoup
moins vite que n2 : on dit qu’il est négligeable devant ce dernier. Pour décrire l’efficacité d’un
algorithme, seul le terme qui croît le plus vite a donc un intérêt. Par exemple, ici pour n ≥ 3, on
a n2 + 3n ≤ 2n2 ; la quantité n2 + 3n est donc bornée, à partir d’un certain rang, par le produit
de n2 et d’une constante. On dit alors que la quantité de n2 + 3n est « un grand O de n2 » et on
écrira n2 + 3n = O(n2 ).

1.4 La relation O
La notation f (x) = O(g(x)) signifie qu’il existe c > 0 tel que f (x) ≤ c.g(x) dès que x est
suffisamment grand. Noter qu’elle ne s’applique qu’à des fonctions positives à partir d’une certaine
valeur n0 . On a :
— Réflexivité : f (x) = O(f (x))
— transitivité : si f (x) = O(g(x)) et g(x) = O(h(x)), alors f (x) = O(h(x))
— linéarité : si f 1(x) = O(f 2(x)) et g1(x) = O(g2(x)), alors f1 (x) + a.g1(x) = O(f 2(x) +
a.g2(x)). f (x) = O(a.f (x)) pour tout réel a > 0 ;
De manière générale, on dira qu’un algorithme a une complexité en O(f(n)) si son coût est, à
partir d’un certain rang, inférieur au produit de f(n) par une constante .

1.5 V. Les complexités les plus utilisées sont :


— Constante : O(1) accéder au premier élément d’un ensemble de données.
— Logarithmique : O(log(n)) couper un ensemble de données en deux parties égales, puis
ces moitié en deux parties égales, etc.
— Linéaire : O(n) parcourir un ensemble de données.
— Quasi-linéaire : O(nlog(n)) couper répétitivement un ensemble de données en deux et
combiner les solutions partielles pour calculer la solution générale.
— Quadratique : O(n2) parcourir un ensemble de données en utilisant deux boucles imbri-
quées.
— Polynomial : O(np ) parcourir un ensemble de données en utilisant p boucles imbriquées.
— Exponentielle : O(2n ) générer tous les sous ensembles possibles d’un ensemble de données.
Cependant, on peut donner les ordres de grandeur des temps d’exécution que l’on rencontre en
pratique pour un problème de taille n = 106 sur un ordinateur personnel effectuant un milliard
d’opérations par seconde.

2TSI 3 A.HAOUDIGUI
CPGE:Mohammedia CHAPITRE 1. NOTIONS DE COMPLEXITÉ

Classe Nom courant Temps estimé Remarques


Le temps d’exécution ne dépend pas des
O(1) Temps constant 1ns données traitées, ce qui est assez rare !
En pratique, cela correspond à une exécution
quasi instantanée. Bien souvent, à cause du
codage binaire de l’information, c’est en fait
la fonction log2(n) qu’on voit apparaître ,
mais comme la complexité est définie à un
facteur près, la base du logarithme n’a pas
O(logn) Logarithmique 10ns d’importance.
Le temps d’exécution d’un tel algorithme ne
devient supérieur à une minute que pour des
données de taille comparable à celle des
mémoires vives disponibles actuellement. Le
problème de la gestion de la mémoire se
posera donc avant celui de l’efficacité en
O(n) Linéaire 1ms temps.
Cette complexité reste acceptable pour des
O(n2 ) Quadratique 1/4h données de taille moyenne mais pas au-delà.
Ici, nk est le terme de plus haut degré d’un
polynôme en n ; il n’est pas rare de voir des
O(nk ) Polynomiale 30 ans si k=3 complexités en O(n3 ) ou O(n4 )
milliards d’années Un algorithme d’une telle
complexité est impraticable sauf pour de très
petites données(n < 50). Comme pour la
complexité logarithmique, la l’exponentielle
ne change fondamentalement rien à
O(2n ) Exponentielle Plus de 10300 000 l’inefficacité de l’algorithme.

1.6 Complexité en espace


On appelle complexité en espace d’un algorithme la place nécessaire en mémoire pour le faire
fonctionner. Elle s’exprime également sous la forme d’un O(f (n)) où n est la taille du problème.
Évaluer la complexité en espace d’un algorithme ne pose pas de difficulté ; il suffit de faire le total
des tailles en mémoire des différentes variables utilisées. Une première exception à la règle est le
cas où on alloue dynamiquement de l’espace mémoire au cours du programme. L’autre cas est celui
des fonctions récursives, qui cachent souvent une complexité en espace élevée.

1.7 Complexité des fonctions récursives


On explique ici comment calculer le coût d’une fonction récursive, à savoir le nombre d’opérations
élémentaires qu’elle effectue ou son occupation mémoire totale.
Prenons l’exemple de la fonction u :
1 def u ( n ):
2 if n == 0:
3 return 2.
4 else :
5 x = u (n -1)

2TSI 4 A.HAOUDIGUI
CPGE :Mohammedia CHAPITRE 1. NOTIONS DE COMPLEXITÉ

6 return 0.5 * ( x + 3. / x )

et évaluons le nombre d’opérations arithmétiques (addition, multiplication et division) qu’elle


effectue. Si n désigne la valeur de son argument, notons C(n) ce nombre d’opérations. En suivant
la définition de la fonction u, on obtient les deux équations suivantes :

C(0) = 0

C(n) = C(n − 1) + 3
En effet dans le cas n = 0, on ne fait aucune opération arithmétique. Et dans le cas n > 0, on fait
d’une part un appel récursif sur la valeur n − 1, d’où C(n − 1) opérations, puis trois opérations
arithmétiques (une multiplication, une addition et une division). Il s’agit d’une suite arithmétique
de raison 3, dont le terme général est :
C(n) = 3n
Le nombre d’opérations arithmétiques effectuées par la fonction u est donc proportionnel à n. Si en
revanche on avait écrit la fonction u plus naïvement, avec deux appels récursifs u(n−1), c’est-à-dire :
1 def u ( n ):
2 if n == 0:
3 return 2.
4 else :
5 return 0.5 * ( u (n -1) + 3. / u (n -1))

alors les équations définissant C(n) seraient les suivantes :

C(0) = 0

C(n) = C(n − 1) + C(n − 1) + 3


En effet, il convient de prendre en compte le coût C(n − 1) des deux appels à u(n − 1). Il s’agit
maintenant d’une suite arithmético-géométrique, dont le terme général est :

C(n) = 3(2n − 1)

Une autre manière d’évaluer le coût d’une fonction récursive est de calculer le nombre d’appels,
puis d’évaluer le coût de chaque appel. Si on note A(n) le nombre d’appels récursifs dans les deux
exemples précédents, on a :
A(0) = 0
A(n) = A(n − 1) + 1
dans le premier cas, et :
A(0) = 0
A(n) = A(n − 1) + A(n − 1) + 1
dans le second cas.
Le terme général est donc A(n) = n dans le premier cas et A(n) = 2n − 1 dans le second. Puisqu’on
n’a ici aucune opération arithmétique dans le cas de base n = 0 et exactement trois opérations
arithmétiques dans le cas récursif, on retrouve immédiatement la valeur de C(n) calculée précé-
demment. D’une manière générale, la valeur de C(n) ne se déduit pas toujours aussi facilement de
la valeur de A(n). En effet, il peut y avoir des opérations dans le cas de base et/ou un nombre
d’opérations arithmétiques variant selon la valeur de n dans le cas récursif.

2TSI 5 A.HAOUDIGUI
CPGE :Mohammedia CHAPITRE 1. NOTIONS DE COMPLEXITÉ

Chaque appel récursif alloue de la mémoire pour les paramètres effectifs et les variables locales
de cet appel. L’occupation mémoire d’un calcul récursif admet donc pour majorant le produit du
nombre d’appels récursifs par la quantité de mémoire allouée par chaque appel. Dans les deux
exemples précédents, on a calculé explicitement le nombre d’appels A(n). L’occupation mémoire
est donc 2n dans le premier cas (il y a deux cases mémoire, une pour n et une autre pour x) et
2n −1 dans le second cas (il y a une case mémoire, pour n). Cependant, dans le second cas, les 2n −1
cases mémoire ne seront pas utilisées simultanément. En effet, celles allouées pour le premier appel
à u(n − 1) peuvent être réutilisées pour le second (et en pratique elles le sont). Pour une analyse
plus fine de l’occupation mémoire, il convient donc de calculer le nombre d’appels imbriqués.

2TSI 6 A.HAOUDIGUI

Vous aimerez peut-être aussi