Chapitre 2 : Analyse des algorithmes
Transcription
Chapitre 2 : Analyse des algorithmes
Chapitre 2 Analyse des algorithmes Analyser un algorithme consiste à calculer les ressources qui seront nécessaires à son exécution. Mais avant cela, il faut d’abord s’assurer que l’algorithme est correct, c’est-à-dire qu’il calcule bien ce pourquoi il a été écrit. 2.1 Preuves de la correction d’un algorithme Comment s’assurer qu’un algorithme calcule bien ce qu’il est censé calculer ? Comment s’assurer qu’un algorithme termine quelle que soit les données qui lui seront fournies en entrée ? Les algorithmes sont suffisamment formalisés pour qu’on puisse prouver qu’ils possèdent les propriétés attendues de correction ou de terminaison. Les preuves sont facilitées lorsque les algorithmes sont bien écrits, et en particulier s’ils sont décomposés en de nombreux sous algorithmes courts et bien spécifiés. Ce sont bien évidemment les structures itératives et les appels récursifs qui nécessitent le plus de travail. 2.1.1 Preuve de la correction d’une structure itérative Considérons le schéma d’algorithme suivant : 7 8 CHAPITRE 2. ANALYSE DES ALGORITHMES Algorithme 4: Structure itérative générique résultat : R début Initialisation # Point A tant que Condition faire # Point B1 Instructions # Point B2 fintq # Point C fin Un invariant de boucle est une propriété ou une formule logique, — qui est vérifiée après la phase d’initialisation, — qui reste vraie après l’exécution d’une itération, — et qui, conjointement à la condition d’arrêt, permet de montrer que le résultat attendu est bien celui qui est calculé. Exemple 1 Pour l’algorithme 2 du chapitre précédent, qui insère un élément x dans un tableau trié T [1, n], considérons la propriété I suivante : la juxtaposition des tableaux T [1, i] et T [i + 2, n + 1] 1 est égale au tableau initial et x est plus petit que tous les éléments du deuxième tableau 2 . ⌧ 1. La propriété est vérifiée après l’initialisation (point A) puisque T [1, i] est égal au tableau initial (i = n), et que le second tableau T [i+2, n+ 1] est vide. 2. Si la propriété I est vraie au début d’une itération (point B1), elle est vraie à la fin de cette itération (point B2) puisque le dernier élément T [i] du premier tableau passe en tête du second (l’ordre n’est pas modifié), et puisque l’on a x < T [i] et que l’indice est modifié en conséquence. Si la condition reste vraie, alors la propriété est donc vérifiée au début de l’itération suivante. 3. Si la condition est fausse (point C), on peut montrer que x est à tous les éléments du premier tableau. En e↵et, 1. si m > n, T [m, n] désigne un tableau vide. 2. propriété vraie si le deuxième tableau est vide. 2.1. PREUVES DE LA CORRECTION D’UN ALGORITHME 9 — c’est vrai si i = 0, car le premier tableau est vide, — c’est vrai si i > 0 et x T [i], car le tableau T [1, i] est trié. Comme l’invariant est vrai, x est à la fois à tous les éléments de T [1, i] et < à tous les éléments de T [i + 2, n + 1]. Il suffit donc d’écrire T [i + 1] = x pour obtenir le résultat attendu. Remarque : Pour prouver la correction d’une boucle Pour, on peut remarquer que Pour i:=1 à n faire Instructions équivaut à i:=1 Tant que i<= n faire Instructions i:=i+1 et utiliser la technique précédente. Exemple 2 Pour prouver la correction de l’algorithme 1, on considère l’invariant de boucle suivant : ⌧ T [1, j 1] est trié . 1. Après l’initialisation, la propriété est vraie puisque j = 2 et que le tableau T [1, j 1] ne contient qu’un seul élément. 2. Supposons que T [1, j 1] soit trié au début d’une itération. Après appel de l’algorithme 2, le tableau T [1, j] est trié. Après l’incrémentation de j, le tableau T [1, j 1] est trié. 3. Si la condition devient fausse, c’est que j = n + 1. Le tableau T [1, n] est donc trié. 2.1.2 Preuve de correction d’un algorithme récursif On prouve la correction d’un algorithme récursif au moyen d’un raisonnement par récurrence. Exemple 3 Pour prouver la correction de l’algorithme 3, on e↵ectue une récurrence sur le nombre N = n m + 1 d’éléments du tableau. — si N = 1, alors m = n et on vérifie que l’algorithme est correct dans ce cas ; 10 CHAPITRE 2. ANALYSE DES ALGORITHMES — soit N 1. Supposons que le programme soit correct pour tout tableau de longueur N et considérons un tableau T [m, n] de N + 1 éléments : n m + 1 = N + 1. En particulier, m 6= n puisque N 1. Soit k = b m+n 2 c. On a m k < n. En e↵et, m<n)m< m+n m+n <n)mb c = k < n. 2 2 Donc, les deux tableaux passés en argument des appels récursifs ont au plus N éléments. En e↵et, le premier a k m + 1 éléments et k m + 1 n m N ; le second a n k éléments et n k n m N . Par hypothèse de récurrence, quelque soit le résultat du test T [k] < x, l’algorithme donnera une réponse correcte. 2.1.3 Exercices 1. Recherche du plus grand élément d’un tableau (version itérative) : Algorithme 5: rechercheMaxIter entrée : T [1, n] est un tableau d’entiers. sortie : le plus grand élément de T [1, n]. début M ax := T [1] pour i := 2 à n faire si T [i] > M ax alors M ax := T [i] retourner M ax (a) Remplacez la structure itérative Pour par un Tant que. (b) Trouvez l’invariant de boucle. (c) Prouvez la correction de l’algorithme. 2. Recherche du plus grand élément d’un tableau (version récursive) : Algorithme 6: rechercheMaxRec entrée : T [1, n] est un tableau d’entiers. sortie : le plus grand élément de T [1, n]. début si n = 1 alors retourner T [1] sinon retourner max(T [n], M axRec(T [1, n 1])) 2.2. COMPLEXITÉ DES ALGORITHMES 11 (a) Prouvez la correction de l’algorithme par récurrence sur le nombre d’éléments du tableau. 2.2 Complexité des algorithmes En plus d’être correct, c’est-à-dire d’e↵ectuer le calcul attendu, on demande à un algorithme d’être efficace, l’efficacité étant mesurée par le temps que prend l’exécution de l’algorithme ou plus généralement, par les quantités de ressources nécessaires à son exécution (temps d’exécution, espace mémoire, bande passante, . . . ). Le temps d’exécution d’un algorithme dépend évidemment de la taille des données fournies en entrée, ne serait-ce que parce qu’il faut les lire avant de les traiter. La taille d’une donnée est mesurée par le nombre de bits nécessaires à sa représentation en machine. Cette mesure ne dépend pas du matériel sur lequel l’algorithme sera programmé. Mais comment mesurer le temps d’exécution d’un algorithme puisque les temps de calcul de deux implémentations du même algorithme sur deux machines di↵érentes peuvent être très di↵érents ? En fait, quelle que soit la machine sur laquelle un algorithme est implémenté, le temps de calcul du programme correspondant est égal au nombre d’opérations élémentaires effectuées par l’algorithme, à un facteur multiplicatif constant près, et ce, quelle que soit la taille des données d’entrée de l’algorithme. Par opérations élémentaires on entend les évaluations d’expressions, les a↵ectations, et plus généralement tous les traitements en temps constant ou indépendant de l’algorithme (entrées-sorties, . . . ). Le détail de l’exécution de ces opérations au niveau du processeur par l’intermédiaire d’instructions machine donnerait des résultats identiques à un facteur constant près. Autrement dit, pour toute machine M, il existe des constantes m et M telles que pour tout algorithme A et pour toute donnée d fournie en entrée de l’algorithme, si l’on note T (d) le nombre d’opérations élémentaires e↵ectuées par l’algorithme avec la donnée d en entrée et TM (d) le temps de calcul du programme implémentant A avec la donnée d en entrée, mT (d) TM (d) M T (d). Soit, en utilisant les notations de Landau (voir section 8.2), T = ⇥(TM ). On peut donc définir la complexité en temps d’un algorithme comme le nombre d’opérations élémentaires T (n) e↵ectuées lors de son exécution sur des données de taille n. Il s’agit donc d’une fonction. On distingue 12 CHAPITRE 2. ANALYSE DES ALGORITHMES — la complexité dans le pire des cas (worst case complexity) : Tpire (n) est le nombre d’opérations maximal calculé sur toutes les données de taille n ; — la complexité dans le meilleur des cas (best case complexity) : Tmin (n) est le nombre d’opérations minimal calculé sur toutes les données de taille n ; — la complexité en moyenne (average case complexity) : Tmoy (n) le nombre d’opérations moyen calculé sur toutes les données de taille n, en supposant qu’elles sont toutes équiprobables. Analyser un algorithme consiste à évaluer la ou les fonctions de complexité qui lui sont associées. En pratique, on cherche à évaluer le comportement asymptotique de ces fonctions relativement à la taille des entrées (voir section 8.2). En considérant le temps d’exécution, dans le pire des cas ou en moyenne, on parle ainsi d’algorithmes — en temps constant : T (n) = ⇥(1), — logarithmiques : T (n) = ⇥(log n), — polylogarithmiques : T (n) = ⇥((log n)c ) pour c > 1, — linéaires : T (n) = ⇥(n), — quasilinéaires : T (n) = ⇥(n log n), — quadratiques : T (n) = ⇥(n2 ), — polynomiaux : T (n) = ⇥(nk ) pour k 2 N, — exponentiels : T (n) = ⇥(k n ) pour k > 1. Si un traitement élémentaire prends un millionième de seconde, le tableau suivant décrit les temps d’exécutions approximatifs de quelques classes de problèmes en fonction de leurs tailles. Taille n/ T (n) 10 100 1000 104 105 106 log2 n 0.003 ms 0.006 ms 0.01 ms 0.013 ms 0.016 ms 0.02 ms n 0.01 ms 0.1 ms 1 ms 10 ms 100 ms 1s n log2 n 0.03 ms 0.6 ms 10 ms 0.1 s 1.6 s 20 s n2 0.1 ms 10 ms 1s 100 s 3 heures 10 jours 2n 1 ms 1014 siecles D’un autre point de vue, le tableau ci-dessous donne la taille des problèmes de complexité T que l’on peut traiter en une seconde en fonction de la rapidité de la machine utilisée (F est le nombre d’opérations élémentaires exécutées par secondes). 2.2. COMPLEXITÉ DES ALGORITHMES F /T (n) 106 107 109 1012 2n 20 23 30 40 n2 1.000 3.162 31.000 106 13 n log2 n 63.000 600.000 4.107 3.1010 n 106 107 109 log2 n 10300.000 103.000.000 En pratique, un algorithme est constitué d’instructions agencées à l’aide de structures de contrôle que sont les séquences, les embranchements et les boucles (nous verrons plus tard comment traiter le cas particulier des fonctions récursives, mais de toutes façons toute fonction récursive s’écrit aussi itérativement). — Le coût d’une séquence de traitements est la somme des coûts de chaque traitement. — Le coût d’une structure itérative est la somme des coûts des traitements e↵ectués lors des passages successifs dans la boucle. Par exemple il arrive fréquemment que l’on e↵ectue n fois une boucle avec des coûts dégressifs (Cn, C(n 1),. . . , C). Dans ce cas T (n) = C(n+(n 1)+(n 2)+. . .+1) = C n X i=1 i = C n(n + 1) = ⇥(n2 ). 2 — Le coût d’un embranchement (si test alors traitement1 sinon traitement2) est le coût du plus coûteux des deux traitements. 2.2.1 Exemples d’analyse d’algorithmes Taille d’un tableau Un tableau de n constantes de taille k (entiers, réels, . . .) a une taille de kn. Mais on peut facilement démontrer que f (n) = ⇥(g(n)) ssi f (kn) = ⇥(g(kn)), lorsque g est constante, linéaire, quasilinéaire ou polynomiale. Par ailleurs, si f (n) est exponentielle, c’est a fortiori aussi le cas de f (kn). On pourra donc supposer, dans les calculs de complexité, que la taille d’un tableau de n éléments constants est n. L’algorithme d’insertion d’un élément dans un tableau trié Dans le pire des cas, celui où l’entier à insérer est plus petit que tous les éléments du tableau, l’algorithme 2 requiert — 2n + 2 a↵ectations, 14 CHAPITRE 2. ANALYSE DES ALGORITHMES — 2n + 1 comparaisons d’entiers, — n soustractions et n + 1 addition, — n + 1 évaluations d’une expression booléenne pour un tableau de n éléments. En supposant de plus que chaque instruction élémentaire s’exécute en un temps constant, on en déduit que Tpire (n) = ⇥(n). L’algorithme d’insertion ordonnée est linéaire dans le pire des cas : à des constantes additives et multiplicatives près, le temps d’exécution est égal au nombre d’éléments du tableau. On voit facilement que Tmin (n) = ⇥(1) : en e↵et, dans le meilleur des cas, l’élement à insérer est plus grand que tous les éléments du tableau et la boucle n’est pas exécutée. Si l’on suppose que l’élément à insérer et un élément pris au hasard dans le tableau, son insertion nécessitera en moyenne n/2 comparaisons. On aura donc, Tmoy (n) = ⇥(n), soit une complexité moyenne du même ordre que la complexité du pire des cas. L’algorithme de tri par insertion Dans le pire des cas, celui où le tableau en entrée est déjà trié, mais par ordre décroissant, l’algorithme nécessite — n 1 comparaisons et a↵ectations pour la condition de la boucle Pour — n 1 appels à l’algorithme d’insertion ordonnée, pour des tailles de tableaux variant de 1 à n 1. À des constantes additives et multiplicatives près, le temps d’exécution dans le pire des cas de l’algorithme de tri par insertion est donc égal à (n 1) + 1 + 2 + . . . + (n 1) = n 1+ n(n 1) 2 = ⇥(n2 ) soit en appliquant la remarque sur l’égalité à des constantes additives et multiplicatives près de la taille des données et du nombre d’éléments du tableau, Tpire (n) = ⇥(n2 ). On voit facilement que Tmin (n) = ⇥(n), cas où le tableau est déjà ordonné par ordre croissant. En e↵et, chaque appel à l’algorithme d’insertion ordonnée est exécuté en temps constant. Si tous les éléments du tableau en entrée sont tirés selon la même loi de probabilités, on peut montrer que Tmoy (n) = ⇥(n2 ). Autrement dit, s’il peut arriver que le temps d’exécution soit linéaire, il y a beaucoup plus de chance qu’il soit quadratique en la taille des données en entrée. 2.2. COMPLEXITÉ DES ALGORITHMES 15 L’algorithme de recherche dichotomique Si le tableau contient un seul élément, l’algorithme exécutera 2 comparaisons (coût total : c1 ) Si le tableau contient n > 1 éléments, l’algorithme exécutera — 3 opérations arithmétiques, une a↵ectation et une comparaison entre 2 entiers (coût total : c2 ), et — un appel récursif sur un tableau possédant dn/2e ou bn/2c éléments, soit dn/2e dans le pire des cas. On en déduit que si n > 1, Tpire (n) = Tpire (dn/2e) + c2 . Cette équation est simple à résoudre lorsque n est égal à une puissance de 2 : Tpire (2h ) = Tpire (2h 1 ) + c2 = Tpire (2h 2 ) + 2c2 = . . . = c1 + hc2 . Dans le cas général, soit n = a h 2h + . . . + a 1 21 + a 0 l’écriture de n en base 2 : ah = 1 et 0 ai 1 pour 0 i < h. On a 2h n < 2h+1 et donc 2h 1 dn/2e 2h . On en déduit que c1 + hc2 Tpire (n) c1 + (h + 1)c2 . En remarquant que h = ⇥(log2 n), on en déduit que Tpire (n) = ⇥(log2 n). La recherche dichotomique d’un élément dans un tableau trié se fait donc en temps logarithmique. L’analyse d’algorithmes conduit souvent à des équations récursives de la forme T (n) = aT (dn/be) + f (n) ou aT (bn/bc) + f (n) (2.1) où b > 1, a > 0 et f est une fonction. Le théorème 2 de la section 8.4 donne la solution de ces équations dans le cas général. Dans l’exemple précédent, on se trouve dans le cas (2) du théorème, avec a = 1 et b = 2 : on retrouve que Tpire (n) = ⇥(log2 n). Il est essentiel de savoir utiliser ce théorème mais il est aussi utile de pouvoir retrouver rapidement le résultat cherché. Nous décrivons ci-dessous quelques calculs approximatifs, à partir de l’équation simplifiée T (n) = aT (n/b) + f (n), qui recouvrent un grand nombre de cas courants. (2.2) 16 CHAPITRE 2. ANALYSE DES ALGORITHMES Calculs “à la main” de la complexité d’une fonction définie par une relation de récurrence — Nombre d’itérations : si l’on itère k fois l’équation 2.2, l’argument de T devient n/bk . On a n/bk = 1 ssi k = logb n. Il faut donc environ logb n itérations pour trouver la constante de référence T (1). — On en déduit le développement suivant : T (n) = f (n) + af (n/b) + · · · + ak 1 f (n/bk 1 T (n) = f (n) + f (n/b) + · · · + f (n/bk 1 ) + ⇥(alogb n ). Si a = 1, on a ). — Si f (n) = c, T (n) = kc = ⇥(logb n). — Si f (n) = n, T (n) = n(1 + 1 bk b 1 1 + ··· + k 1) = n b b b 1 = ⇥(n). Si a = b, on a T (n) = f (n) + bf (n/b) + · · · + bk 1 f (n/bk 1 ) + ⇥(n). — Si f (n) = c, T (n) = c(1 + b + · · · + bk 1 ) + ⇥(n) = n b 1 + ⇥(n) = ⇥(n). 1 — Si f (n) = n, T (n) = n(1 + 1 + · · · + 1) + ⇥(n) = nk + ⇥(n) = ⇥(n logb n). Si a > b, on peut écrire a = bh avec h > 1. On a T (n) = f (n) + af (n/b) + · · · + ak 1 f (n/bk 1 ) + ⇥(nh ). — Si f (n) = c, T (n) = 1 + a + · · · + ak 1 + ⇥(nh ) = nh 1 + ⇥(nh ) = ⇥(nh ). a 1 2.2. COMPLEXITÉ DES ALGORITHMES 17 — Si f (n) = n, T (n) = n(1+a/b+· · ·+(a/b)k 1 )+⇥(nh ) = n (a/b)k 1 +⇥(nh ) = ⇥(nh ). a/b 1 Ce que l’on peut résumer dans le tableau suivant, T (n) f (n) = c f (n) = n a=1 logb n n a=b n n logb n a = bh nh nh Pour les autres cas, utilisez le théorème. Le tri par fusion Le tri par fusion est un algorithme qui applique la stratégie “diviser pour régner ”. Le tableau à trier est divisé en deux sous-tableau de même taille (à un élément près), l’algorithme est appelé récursivement sur chacun de ces sous-tableaux, et les tableaux résultants sont alors fusionnés. Algorithme 7: TriFusion entrée : T [1, n] est un tableau d’entiers, n 1. résultat : les éléments de T sont ordonnés par ordre croissant. début si n > 1 alors T1 := triF usion(T [1, bn/2c]) T2 := triF usion(T [bn/2c + 1, n]) T := F usion(T1 , T2 ) On peut montrer que l’algorithme de fusion est linéaire en fonction du nombre n d’éléments. Soit T (n) la complexité de l’algorithme en fonction de n. L’équation de récurrence correspondante est T (n) = T (dn/2e) + T (bn/2c) + n + c. Méthode générale (voir le théorème 2 de la section 8.4). On est dans le cas 2 puisque : f (n) = n + c, a = b = 2, logb a = 1 et f (n) = O(n). Donc T (n) = ⇥(n log2 n). Solution en développant directement la récurrence. 18 CHAPITRE 2. ANALYSE DES ALGORITHMES On peut considérer l’équation plus simple suivante : T (n) = 2T (n/2)+n. On a : T (n) = 2T (n/2) + n = 4T (n/4) + 2n = · · · = 2k T (n/2k ) + kn. Les appels récursifs se terminent lorsque n ' 2k , soit k ' log2 (n) : il y a au plus log2 (n) appels récursifs imbriqués. On en déduit que T (n) ' nc + n log2 (n). Le terme dominant étant n log2 (n), on en déduit que T (n) = ⇥(n log2 (n)). n 2x n n/2 hauteur : dlog2 ne 2x n n/2 log n n/4 n/8 1 n/4 n/8 1 1 n/8 n/4 n/8 n/8 2x n n/4 n/8 n/8 n/8 .... Figure 2.1 – Arbre des appels récursifs pour l’algorithme de tri par fusion. Solution intuitive. Si l’on pressent que la complexité de la fonction T vérifiant T (n) = 2T (n/2) + n est ⇥(n log2 n), on peut essayer de vérifier que cette intuition est juste, c’est-à-dire, s’il existe une fonction T vérifiant à la fois l’équation de récurrence et T (n) = ⇥(n log2 n). Posons T (n) = a · n log2 n + bn + c et essayons ensuite de fixer les constantes au moyen de l’équation de récurrence. On a : T (n) = a · n log2 n + bn + c = 2T (n/2) + n = 2[a · n/2 log2 n/2 + bn/2 + c] + n = a · n log2 n an + bn + 2c + n. On en déduit que a = 1 et c = 0, b n’étant pas contraint. La fonction T (n) = n log2 n vérifie donc l’équation T (n) = 2T (n/2) + n. 2.2. COMPLEXITÉ DES ALGORITHMES 2.2.2 19 Exercices Exercice 1 Montrez que 1. n log n = O(n2 ), 2. n log n = ⌦(n), 3. n + sin(n) ⇠ n. 4. A t-on 2n = ⇥(en ) ? 5. A t-on elog2 (n+1) = !(n) ? Exercice 2 Quelles sont les classes de complexité des fonctions suivantes : 1. f (n) = 2n3 + 4n2 + 23 , 2. f (n) = log (n2 + 3n 1), 3. f (n) = 2 log2 (n2 +1) 2 . Exercice 3 Quelle est la complexité de l’algorithme suivant : Algorithme 8: début pour i := 1 à n ⇤ n faire u := i tant que u > 1 faire u := u/2 Exercice 4 Calculez la complexité de la fonction qui calcule le produit de deux matrices carrées A et B de n lignes et n colonnes. Exercice 5 Prouvez la correction de l’algorithme récursif suivant : Algorithme 9: expo entrée : x est un réel et n est un entier sortie : xn . début si x = 0 alors retourner 0 si n = 0 alors retourner 1 si n est pair alors retourner expo(x ⇤ x, n/2) sinon retourner x ⇤ expo(x ⇤ x, (n 1)/2) 20 CHAPITRE 2. ANALYSE DES ALGORITHMES Combien de multiplications faut-il e↵ectuer pour calculer x20 ? pour calp p culer x2 ? pour calculer x2 1 ? Quelle est la complexité de l’algorithme - en terme de nombre de multiplications ? Exercice 6 Écrivez un algorithme qui prend en entrée une matrice carrée n ⇥ n et retourne la liste des coordonnées des cases dont la valeur est à la fois maximale sur leur ligne et minimale sur leur colonne. Quelle est la complexité de cet algorithme ? Exercice 7 : Calcul de l’élément majoritaire d’un tableau. Etant donné un ensemble E de n éléments, on dit qu’un élément e est majoritaire dans E si le nombre d’occurrences de e est strictement supérieur à n/2. 1. Quel est la complexité d’un algorithme qui teste si un élément e est majoritaire ou non dans E ? On cherche maintenant à savoir s’il existe un élément majoritaire, et si oui, à le déterminer. 2. Proposez un algorithme de complexité quadratique. 3. On considère maintenant l’approche suivante, basée sur le principe diviser pour régner : pour rechercher l’élément majoritaire éventuel dans l’ensemble E, on répartit les n éléments de E dans deux ensembles (approximativement) de même taille E1 et E2 . Montrez que si un élément est majoritaire dans E, alors il est majoritaire dans E1 ou dans E2 . Ecrivez une procédure de calcul correspondant à cette idée. Quelle est sa complexité ? Une troisième méthode sera proposée au prochain TD.