• Aucun résultat trouvé

1.4 Rappels sur la complexité

1.4.1 Compter exactement?

Calculer la complexité d’un algorithme est une tâche souvent jugée difficile. Effec-tivement, que la complexité detchisla2()soit polynomiale enn, par exemple, n’a rien d’évidement. (Attention ! icinn’est pas la taille de l’entrée.) Cela ne vient pas de la dé-finition de la complexité, mais tout simplement de la nature des objets mesurés : les algorithmes et les programmes.

Calculer le nombre d’opérations élémentaires exécutées est évidemment très diffi-cile puisqu’il n’y a déjà pas de méthode systématique pour savoir si ce nombre est fini ou pas : c’est le problème de laHaltequi est indécidable. En fait, un théorème (celui de

29. Notez bien qu’il s’agit du « maximum » pas du « nombre total » de mots mémoires consomés.

30. Les opérations arithmétiques sur des entiers plus grands, comme [0, n2[, ne sont pas vraiment un problème. Elles prennent aussi un temps constant en simulant l’opération avec des couples d’entiers de [0, n[.

24 CHAPITRE 1. INTRODUCTION Henry Gordon Rice) établit que pour toute propriété non triviale31définie sur un pro-gramme n’est pas décidable. Des exemples de propriété sont : « Est-ce que le propro-gramme termine par une erreur ? » ou « Peut-on libérer un pointeur précédemment allouée ? » ou encore « Le programme contient-il un virus ? ».

En fait, il n’est même pas la peine d’aller voir des algorithmes très compliqués pour percevoir le problème. Considérons le programme suivant32renvoyant le nombre d’étapes nécessaires pour atteindre la valeur 1 par la suite définie par

n 7→

(n/2 sinest pair 3n+ 1 sinon

int Syracuse(int n){

int k=0;

while(n>1){

n = (n&1)? 3*n+1 : n/2;

k++;

}

return k;

}

Trouver la complexité en temps de Syracuse(n) fait l’objet de nombreuses re-cherches33, voir aussi l’ouvrage figure 1.7 et l’article récent de Terence Tao [Tao20]

montrant que « c’est presque vrai pour presque tous les entiers34». Il y a aussi d’ex-cellentes vidéos35 sur ce problème. En fait, on ne sait pas si la boucle while s’arrête toujours au bout d’un moment, c’est-à-dire si sa complexité est finie ou pas, ou dit en-core autrement, si la suite des valeurs denfinit toujours par atteindre1.

En passant, si dans Syracuse(n) on remplace 3*n+1 par a*n+b, alors il est indéci-dable de savoir si, étant donnés les paramètres(a,b), la fonction ainsi généralisée ter-mine toujours. Encore une fois, pour certaines valeurs comme(a=2,b=0),(a=3,b=-1)ou

31. Une propriété triviale d’un programme serait une propriété qui donnerait toujours la même ré-ponse, quelque soit le programme et/ou quelque soit l’entrée. Par exemple, « Quelle est la taille d’un programme ? » ou « Le programme contient-il plus de trois fonctions ? » sont des propriétés triviales car elle ne dépend pas de l’entrée.

32. On peut aussi faire récursif en une seule ligne :

int Syracuse(int n){ return (n>1)? 1+Syracuse( (n&1)? 3*n+1 : n/2 ) : 0; } 33. https://fr.wikipedia.org/wiki/Conjecture_de_Syracuse

34. Plus précisément, pour presque tous les entiersn >0 et toute fonctionf croissante positive aussi petite que l’on veut, comme log lognpar exemple, la suite partant denfinit toujours par atteindre une valeur< f(n).

35. CommeThe Simplest Math Problem No One Can Solve.

1.4. RAPPELS SUR LA COMPLEXITÉ 25

Figure 1.7 – Ouvrage de  consacré au problème « 3x+ 1 » et lettre de Goldbach à Euler en.

(a=1,b=-1)on sait répondre36. Mais aucun algorithme n’arrivera jamais à donner la ré-ponse pour tout (a,b). On est en quelque sorte condamné à produire une solution ou preuvead hocpour chaque paire(a,b)ou famille de paires.

Le mathématicien Paul Erdős, ci-contre à gauche, a dit à propos de ce problème (qu’on appelle aussi conjecture de Collatz, conjecture d’Ulam ou encore problème « 3x+ 1 » et qui a été vérifiée37 pour tout entier<5×1018) :

« Les mathématiques ne sont pas encore prêtes pour de tels problèmes ».

36. Cela boucle trivialement pour(a=2,b=0) et n=2, et plus généralement pourn=2et tout(a=i+ 1,b=2j)pour touti, jN. Pour(a=3,b=-1)etn=7la suite devient 7,20,10,5,14,7, ...ce qui boucle donc aussi. Pour(a=1,b=-1)la suite ne fait que décroître, donc la fonction s’arrête toujours.[Exercice. Peut-on conclure pour tout(a,b)tels quea+b>1 est impair ?]

37. En juinune « preuve » non confirmée a été encore annoncée [Sch18].

26 CHAPITRE 1. INTRODUCTION Beaucoup de problèmes très difficiles peuvent se formuler en simple problème de complexité et d’analyse d’algorithme, comme la conjecture de Goldbach (cf. figure1.7) qui dit :

« Tout nombre entier pair supérieur à trois est la somme de deux nombres pre-miers. ».

[Exercice. Transformez cette conjecture en une instance du problème de laHalte.]

Face à ceci, il y a deux attitudes :

• Pessimiste : la complexité c’est compliquée ! c’est sans espoir.

• Optimiste : on peut espérer produire des algorithmes qui défis les mathématiques ! qui finalement marchent sans qu’on puisse dire et comprendre pourquoi.

Dans la pratique, on n’aura pas à traiter de cas si complexes, en tout cas cette an-née. Cependant, compter exactement le nombre d’opérations élémentaires ou de mots mémoires est souvent difficile en pratique même pour des algorithmes ou programmes relativement simples, commetchisla2()ou la fonctionf()définie page35.

La première difficulté qui s’oppose au comptage exact du nombre d’opérations élé-mentaires d’un programme est qu’on ne sait pas toujours quelles sont les opérations élémentaires qui se cachent derrière les instructions du langage, qu’il soit compilé (comme le C) ou interprété (comme le Python). Le compilateur peut cacher certaines opérations/optimisations, et l’interpréteur peut réaliser des tas d’opérations sans le dire (gestion de la mémoire38 par exemple) ! Certains langages proposent de nombreuses instructions qui sont non élémentaires, comme certaines opérations de listes enPython. Ensuite, le nombre d’opérations peut varier non seulement avec la taille de l’entrée, mais aussi avec l’entrée elle-même. Si l’on cherche un élément pair dans un tableau de taille n, le nombre d’opérations sera très probablement dépendant des valeurs du tableau.

Parenthèse.Il faut bien connaître le langage pour identifier ce qui est élémentaire et ce qui ne l’est pas (voir aussiicipour la complexité des listes enPython).

Le codePythonsuivant est linéaire ou quadratique enn?

>>> n=4

>>> V=[0]*n ; V [0, 0, 0, 0]

Et celui-ci ? (hors affichage de la matrice bien sûr)

>>> M=[[0]*n]*n ; M

[[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]

38. On peut considérer quemalloc()prend un temps constant, mais quecalloc()etrealloc() prennent un temps proportionnel à la taille mémoire demandée.

1.4. RAPPELS SUR LA COMPLEXITÉ 27 Ce n’est peut-être pas quadratique, car :

>>> M[0][0]=1 ; M

[[1, 0, 0, 0], [1, 0, 0, 0], [1, 0, 0, 0], [1, 0, 0, 0]]

>>> id(M[0]), id(M[1]), id(M[2]), id(M[3])

140230182489912, 140230182489912, 140230182489912, 140230182489912

EnPythontout est pointeur d’objet.

>>> M[0]=0 ; M

[0, [1, 0, 0, 0], [1, 0, 0, 0], [1, 0, 0, 0]]

>>> id(0), id(M[0]), id(M[1][1]), id(M[1][0])

140230179984688, 140230179984688, 140230179984688, 140230179984664

Essayons de calculer la complexité en temps dans l’exemple suivant : int T[n];

for(i=0;i<n;i++) T[i++] = 2*i-1;

Parenthèse. Il n’est pas conseillé d’utiliser une instruction comme T[i++] = 2*i-1; La raison est qu’il n’est pas clair si la seconde occurrence dei(à droite du=) à la même valeur que la première. Sur certains compilateurs, commegccon auraT[0]=-1car l’incrémenta-tion deià lieu après la fin de l’instruction (définie par le;final). Pour d’autres39, on aura T[0]=1. Ce qui est sûr est que l’option de compilation-Walldegccproduit un warning40. Mais est-ce bien sûr qu’on n’écrit pas en dehors des indices[0, n[?

Compter exactement le nombre d’opérations élémentaires n’est pas facile. Que se passe-t-il vraiment avecint T[n]? Combien y-a-t’il d’opérations dans la seule instruc-tion T[i++] = 2*i-1? Une incrémentation, une multiplication, une soustraction, une écriture, donc 4 ?41 On fait aussi à chaque boucle une incrémentation, un saut et une comparaison (dans cet ordre d’ailleurs). Soit un total de 7 instructions par boucle. Et combien de fois boucle-t-on ? n/2 ou plutôt bn/2c? Donc cela fait 7· bn/2c opérations plus le nombre d’instructions élémentaires pour int T[n] et i=0 (en espérant que le nombre d’instructions pourint T[n]ne dépende pas den). Bref, même sur un exemple très simple, cela devient vite assez laborieux d’avoir un calcul exact du nombre d’opé-rations élémentaires.

En fait, peu importe le nombre exact d’opérations élémentaires. Avec un processeur 1 GHz, une opération de plus ou de moins ne fera jamais qu’une différence se mesurant en milliardième de secondes, soit le temps que met la lumière pour parcourir 30 cm.

39. Comme celui en lignehttps://www.tutorialspoint.com/compile_c_online.php 40. Message qui est :warning: unsequenced modification and access to ’i’

41. On calcule peut-être aussi l’adresse deT+isauf si le compilateur s’aperçoit queTest une adresse constante. Dans ce cas il saura gérer un pointeurp = Tqu’il incrémentera au fur et à mesure avecp++.

28 CHAPITRE 1. INTRODUCTION Dans cet exemple, on aimerait surtout dire que la complexité de l’algorithme est linéaire en n. Car ce qui est important c’est que sin double, alors le temps doublera.

Cela reste vrai que la complexité soit 7bn/2c ou 4n−1. Et cela restera vrai, très cer-tainement, quelque soit le compilateur ou le langage utilisé. Si la complexité était en n4, peu importe le coefficient devant n4, lorsquen double, le temps est multiplié par 24 = 16. En plus, que peut-on dire vraiment du temps d’exécution puisque qu’un pro-cesseur cadencé à 2 GHz exécutera les opérations élémentaires deux fois plus vite qu’un processeur à 1 GHz.

Enfin, ce qui importe c’est la complexitéasymptotique, c’est-à-dire lorsque la taille nde l’entrée est grande, tend vers l’infini.

En effet, quandnest petit, de toutes façons, peu importe l’algorithme, cela ira vite.

Ce qui compte c’est lorsque les données sont de taille importante. C’est surtout là qu’il faut réfléchir à l’algorithme, car tous ne se valent pas. Différents algorithmes résolvant le même problème sont alors comparés selon les valeurs asymptotiques de leur com-plexité. Ce n’est évidemment qu’un critère. Un autre critère, plus pratique, est celui de la facilité de l’implémenter correctement. Mais c’est une autre histoire.