<< Page principale
La complexité en informatique permet de définir la difficulté de résolution d'un problème.
Elle se définit en fonction du nombres d'opérations élémentaires à réaliser sur un traitement (complexité en temps).
Il existe également la complexité en espace qui définit la taille occupée par les données pour un problème donné.
Pour introduire une complexité, on utilise la notation $O(x)$ ou $x$ est une expression en fonction des données à traiter. Cette notation a été introduite par Paul Bachmann (1837-1920), mathématicien allemand, puis reprise par Edmund Landau (1877-1938), également mathématicien allemand. $O(x)$ signifie proportionnel à $x$ :
$∃α ∈ ℕ$, alors $O(αx)$ se notera $O(x)$.
Par exemple, calculer la moyenne de $n$ nombres est un traitement qui possède une compexité en $O(n)$ car il faut réaliser $n-1$ additions et une division, soit $n$ opérations arithmétiques.
Voici quelques ordres de grandeur :
Expression | Exemple |
$O(1)$ | lire une valeur dans un tableau en fonction de son indice |
$O(n/2)$ | rechercher une valeur dans une liste de valeurs |
$O(n × log(n))$ | trier un tableau de données avec une méthode efficace (quick sort) |
$O(n^2)$ | trier un tableau de données avec une méthode pas très efficace (bubble sort) |
$O(n^3)$ | faire le produit de deux matrices carrées de dimension $n$ |
$O(n!)$ | maximum de parcimonie en bioinformatique |
$O(e^n)$ | les échecs |
Il est parfois difficile de donner une valeur exacte de la complexité. On procède alors en donnant :
Soit une liste ou un tableau de valeurs entières. Quelle est la complexité de la recherche d'une valeur dans cette liste ?
Le code Python est le suivant :
Voici également le code C de la recherche dans une liste :
La complexité en moyenne est donc la moyenne de ces deux complexités, soit $O({1 + n}/{2})$, simplifié en $O({n}/{2})$ mais comme $1/2$ est une constante entière, on doit normalement garder $O(n)$.
Exercice 1.1
Ecrire un programme de recherche dichotomique d'une valeur entière $v$ dans un tableau trié $t$. Donner la complexité de la recherche.
L'algorithme consiste à choisir deux bornes : $inf$ et $sup$ qui initialement sont affectées à $0$ et $taille(tableau)-1$.
A chaque étape on calcule $m = (inf + sup) /2$, puis :
On continue tant que $inf ≤ sup$, sinon on retourne -1 pour indiquer que la valeur n'a pas été trouvée.
La complexité ne prend pas en compte les données qui peuvent avoir une influence sur le temps d'exécution d'un algorithme.
L'exemple le plus simple pour comprendre ce concept est le tri d'un tableau de données. Prenons l'algorithme du tri à bulles :
Voici l'évolution du tri sur un tableau trié en ordre inverse :
6 5 4 3 2 1
5 4 3 2 1 6
4 3 2 1 5 6
3 2 1 4 5 6
2 1 3 4 5 6
1 2 3 4 5 6
Ci dessous, on trouve les temps d'exécution d'un tri à bulles pour un tableau de 65_536 entiers en Java et en C++ avec des données :
Langage / Données | croissant | décroissant | aléatoire |
Java | 0,394 | 2,017 | 4,876 |
C++ | 0,769 | 5,156 | 4,457 |
Echanges | 0 | 2_147_450_880 | $≈$ 1_073_418_792 |
On observe qu'en fonction des données on aura un temps d'exécution rapide ou long. Cela est dû au nombre d'échanges de données :
Cependant, on observe, en Java, que le temps d'exécution si les données sont en ordre décroissant est plus faible que celui des données aléatoires bien que le nombre d'échanges soit supérieur pour les données en ordre décroissant. Cela peut sembler contradictoire, mais en fait cela est dû à la prédiction de branchement.
Nous venons de voir que bien que le nombre d'échanges soit plus important il peut engendrer un temps d'exécution plus court, ce qui est paradoxal mais normal du point de vue du microprocesseur.
Le cas du produit de matrices est emblématique de l'inadéquation qui peut résulter entre complexité et l'implantation d'un algorithme. On rappelle que le produit de deux matrices carrées de dimension $n$ est de la forme :
On doit pour chaque $c_i^j$ calculer : $$ c_{ i}^{j} = ∑↙{i=1}↖n a_i^k × b_k^j $$
On a donc un algorithme en $O(n^3)$. Il est possible de trouver des algorithmes de complexité plus faible comme celui de Strassen en $O(n^{2,807})$ mais qui n'est efficace que sur des matrices de grande taille.
Si on exécute l'algorithme du produit de matrices carrées de complexité $O(n^3)$ sur un Pentium E5300, on observe le comportement suivant :
Pour certaines dimensions de la matrice on observe des temps d'exécution anormalement élevés :
n | temps n |
temps n-1 |
temps n+1 |
1024 | 25.39 | 7.26 | 7.24 |
1280 | 28.46 | 14.56 | 14.80 |
1536 | 84.37 | 25.59 | 25.77 |
1792 | 107.88 | 41.30 | 41.45 |
1920 | 72.07 | 51.17 | 51.32 |
2048 | 227.36 | 62.71 | 63.07 |
Ainsi, pour $n=2048$ on met 227 secondes pour effectuer le produit alors que pour $n = 2047$, on ne mettra que 62 secondes et 63 secondes pour $n = 2049$. L'explication est liée à l'ordre dans lequel on accède les données qui est très pénalisant pour certaines tailles de matrices et qui cause de nombreux défauts dans la mémoire cache.
Un défaut de cache signifie que la donnée n'est pas en mémoire cache et donc il faut la charger depuis la mémoire centrale.
La théorie de la complexité cherche à classer les problèmes de décision selon la complexité des algorithmes capables de les résoudre
Il existe plusieurs classes de problèmes, dont les problèmes :
Voici plusieurs liens qui traitent du sujet :
Pour résumer :
La classe P contient l'ensemble des problèmes polynomiaux, c'est à dire facile à résoudre et pour lesquels on peut exhiber un algorithme (en temps) polynomial, de la forme $n^k$.
la classe NP contient l'ensemble des problèmes polynomiaux non déterministes qui peuvent être résolus par un algorithme de complexité polynomiale pour une machine de Turing non déterministe : en d'autres termes on est capable d'examiner en parallèle un nombre important (éventuellement exponentiel de cas) mais un en temps polynomial.
Si un problème est NP-Complet, cela signifie qu'il n'existe pas d'autre choix (tant qu'on a pas démontré $P=NP$) que de passer par une recherche exhaustive afin d'en trouver une solution ou de montrer qu'il ne possède pas de solution.
Il est parfois possible, pour certaines instances, de trouver une propriété qui permette de résoudre simplement et efficacement le problème.
Rappels concernant les complexités :
$n$ | $O(n)$ | $O(n × log(n))$ | $O(n × log_2(n))$ | $O(n^2)$ | $O(2^n)$ | $O(n!)$ |
10 | 10 | 10 | 33,21 | 100 | 1024 | 3,62 $10^6$ |
20 | 20 | 26,02 | 86,43 | 400 | 1,04 $10^{6}$ | 2,43 $10^{18}$ |
30 | 30 | 44,31 | 147,20 | 900 | 1,07 $10^{9}$ | 2,65 $10^{32}$ |
40 | 40 | 64,08 | 212,87 | 1600 | 1,09 $10^{12}$ | 8,15 $10^{47}$ |
Notation | Définition |
𝑂(1) | constante |
𝑂(𝑙𝑜𝑔(𝑛)) | logarithmique |
𝑂(𝑛) | linéaire |
𝑂(𝑛×𝑙𝑜𝑔(𝑛)) | quasi-linéaire |
𝑂(𝑛2) | quadratique |
𝑂(𝑛3) | cubique |
𝑂(2𝑛) | exponentielle |
𝑂(𝑛!) | factorielle |
Exercice 1.2
Quelle est la complexité de la fonction est_premier qui indique si un nombre est premier ou non ? Donner une implantation de cette fonction et en déduire la complexité.
Exercice 1.3
Soit le graphe suivant modélisé par la matrice donnée ci-après. Ecrire une fonction qui vérifie s'il existe un chemin entre deux sommets du graphe.
On représentera le graphe sous forme matricielle comme ceci :
A | B | C | D | E | F | G | |
A | 1 | 1 | |||||
B | 1 | 1 | 1 | 1 | 1 | ||
C | 1 | 1 | |||||
D | 1 | 1 | 1 | ||||
E | 1 | 1 | 1 | ||||
F | 1 | 1 | |||||
G | 1 |
Un 1 en ligne 'A' colonne 'B' signifie qu'il existe un arc entre les sommets 'A' et 'B'.
Pour déterminer s'il existe un chemin entre un sommet $X$ et un sommet $Y$, on peut procéder de manière non récursive en utilisant deux listes :
L'algorithme de recherche est alors le suivant :