Il y a beaucoup de questions sur la façon d'analyser le temps d' exécution des algorithmes (voir, par exemple, l' exécution d'analyse et d'analyse de l' algorithme ). Beaucoup sont similaires, par exemple ceux qui demandent une analyse des coûts des boucles imbriquées ou des algorithmes de division et de conquête, mais la plupart des réponses semblent être faites sur mesure.
D'autre part, les réponses à une autre question générale expliquent la situation dans son ensemble (en particulier en ce qui concerne l'analyse asymptotique) avec quelques exemples, mais pas comment se salir les mains.
Existe-t-il une méthode générale structurée pour analyser le coût des algorithmes? Le coût peut être la durée d'exécution (complexité temporelle) ou une autre mesure de coût, telle que le nombre de comparaisons exécutées, la complexité d'espace ou autre chose.
Ceci est supposé devenir une question de référence pouvant être utilisée pour orienter les débutants; d'où sa portée plus large que d'habitude. Veillez à donner des réponses générales, présentées de manière didactique, illustrées par au moins un exemple, mais couvrant néanmoins de nombreuses situations. Merci!
la source
Réponses:
Traduire du code en mathématiques
Avec une sémantique opérationnelle (plus ou moins) formelle , vous pouvez traduire littéralement le code (pseudo-) d'un algorithme en une expression mathématique qui vous donne le résultat, à condition que vous puissiez manipuler l'expression sous une forme utile. Cela fonctionne bien pour les mesures de coût additives telles que le nombre de comparaisons, les échanges, les instructions, les accès à la mémoire, les cycles de certaines machines abstraites, etc.
Exemple: Comparaisons dans Bubblesort
Considérons cet algorithme qui trie un tableau donné
A
:Supposons que nous voulions effectuer l'analyse de l'algorithme de tri habituel, à savoir compter le nombre de comparaisons d'éléments (ligne 5). Nous notons immédiatement que cette quantité ne dépend pas du contenu du tableaun
A
, mais de sa longueur . Nous pouvons donc traduire littéralement les boucles (imbriquées) en sommes (imbriquées); la variable de boucle devient la variable de somme et la plage est reportée. On a:for
où est le coût pour chaque exécution de la ligne 5 (que nous comptons).1
Exemple: échanges dans Bubblesort
Je noterai par le sous-programme qui consiste en lignes vers et par les coûts d'exécution de ce sous-programme (une fois).Pi,j Ci,j
i
j
Maintenant, disons que nous voulons compter les échanges , c’est la fréquence à laquelle est exécutée. Il s’agit d’un "bloc de base", c’est un sous-programme qui est toujours exécuté de manière atomique et a un coût constant (ici, ). La sous-traitance de ces blocs est une simplification utile que nous appliquons souvent sans réfléchir ni en parler. 1P6,8 1
Avec une traduction similaire à celle ci-dessus, nous arrivons à la formule suivante:
( i , j ) P 5 , 9A(i,j) indique l'état du tableau avant la -ième itération de .(i,j) P5,9
Notez que j'utilise au lieu de comme paramètre; on verra bientôt pourquoi. Je n’ajoute pas et tant que paramètres de car les coûts ne dépendent pas d’eux ici (dans le modèle de coûts uniformes , c’est-à-dire); en général, ils pourraient juste.n i j C 5 , 9A n i j C5,9
Il est clair que les coûts de dépendent du contenu de (les valeurs et , plus précisément), nous devons donc en tenir compte. Nous sommes maintenant confrontés à un défi: comment "décompresser" ? Eh bien, nous pouvons rendre la dépendance sur le contenu de explicite: A C 5 , 9 AP5,9 A C5,9 A
A[j]
A[j+1]
Pour tout tableau d'entrée donné, ces coûts sont bien définis, mais nous voulons un énoncé plus général. nous devons faire des hypothèses plus fortes. Laissez-nous enquêter sur trois cas typiques.
Le pire des cas
En regardant la somme et en notant que , nous pouvons trouver une limite supérieure triviale pour le coût:C5,9(A(i,j))∈{0,1}
Mais cela peut-il arriver , c’est-à-dire qu’un est atteint pour cette limite supérieure? Il se trouve que oui: si nous entrons un tableau d’éléments distincts par paires inversement triés, chaque itération doit effectuer un swap¹. Par conséquent, nous avons calculé le nombre exact de swaps de Bubblesort dans le pire des cas .A
Le meilleur des cas
Inversement, il existe une limite inférieure triviale:
Cela peut aussi arriver: sur un tableau déjà trié, Bubblesort n'exécute pas un échange.
Le cas moyen
Le pire et le meilleur des cas ouvrent un fossé considérable. Mais quel est le nombre typique de swaps? Afin de répondre à cette question, nous devons définir ce que signifie "typique". En théorie, nous n'avons aucune raison de préférer une entrée à une autre et nous supposons donc généralement une distribution uniforme de toutes les entrées possibles, c'est-à-dire que chaque entrée est également probable. Nous nous limitons aux tableaux avec des éléments distincts par paires et supposons ainsi le modèle de permutation aléatoire .
Ensuite, nous pouvons réécrire nos coûts comme ceci²:
Nous devons maintenant aller au-delà de la simple manipulation des sommes. En examinant l'algorithme, nous constatons que chaque échange supprime exactement une inversion de (nous échangeons seulement les voisins³). Autrement dit, le nombre de swaps effectuées sur est exactement le nombre de retournements de . Ainsi, nous pouvons remplacer les deux sommes intérieures et obtenirA A inv(A) A
Heureusement pour nous, il a été déterminé que le nombre moyen d’inversions est
qui est notre résultat final. Notez que cela représente exactement la moitié du coût le plus défavorable.
i = n-1
la boucle externe qui ne fait jamais rien ne soit pas exécutée.La méthode générale
Nous avons vu dans l'exemple que nous devons traduire la structure de contrôle en mathématiques; Je présenterai un ensemble typique de règles de traduction. Nous avons également vu que le coût d'un sous-programme donné peut dépendre de l' état actuel , c'est-à-dire (approximativement) des valeurs actuelles des variables. Comme l'algorithme modifie (généralement) l'état, la méthode générale est un peu lourde à noter. Si vous commencez à vous sentir confus, je vous suggère de revenir à l'exemple ou de créer le vôtre.
Nous notons avec l’état actuel (imaginez-le comme un ensemble d’assignations de variables). Lorsque nous exécutons un programme qui commence dans l’état , nous nous retrouvons dans l’état (à condition que se termine).ψ ψ ψ/P
P
P
Déclarations individuelles
S;
Si vous ne qu'une seule déclaration , vous lui affectez des coûts . Ce sera généralement une fonction constante.Expressions
Si vous avez une expression
E
de la formeE1 ∘ E2
(par exemple, une expression arithmétique∘
pouvant être addition ou multiplication, vous additionnez les coûts de manière récursive:Notez que
vous devrez donc peut-être faire preuve de souplesse avec cette règle.
Séquence
Étant donné un programme
P
sous forme de séquence de programmesQ;R
, vous ajoutez les coûts àLes conditions
Étant donné un programme
P
de la formeif A then Q else R end
, les coûts dépendent de l'état:En général, l’évaluation
A
peut très bien changer l’état, d’où la mise à jour des coûts des différentes branches.Boucles For
Étant donné un programme
P
du formulairefor x = [x1, ..., xk] do Q end
, attribuer des coûtsoù est l'état avant le traitement de la valeur , c'est-à-dire après l'itération avec étant défini sur , ..., .ψi
Q
xi
x
x1
xi-1
Notez les constantes supplémentaires pour la maintenance de la boucle; la variable de boucle doit être créée ( ) et attribuer ses valeurs ( ). Ceci est pertinent puisquecinit_for cstep_for
xi
peut être coûteux etfor
boucle avec un corps vide (par exemple, après avoir simplifié dans le meilleur des cas avec un coût spécifique) n'a pas de coût nul si elle effectue des itérations.Alors que les boucles
Étant donné un programme
P
du formulairewhile A do Q end
, attribuer des coûtsEn inspectant l'algorithme, cette récurrence peut souvent être bien représentée sous la forme d'une somme similaire à celle de for-loops.
Exemple: considérons cet algorithme court:
En appliquant la règle, on obtient
avec des coûts constants, pour les instructions individuelles. Nous supposons implicitement que ceux-ci ne dépendent pas de l'état (les valeurs de et ); Cela peut être vrai ou non dans la "réalité": pensez aux débordements!c…
i
x
Nous devons maintenant résoudre cette récurrence pour . Nous notons que ni le nombre d'itérations ni le coût du corps de la boucle ne dépendent de la valeur de , nous pouvons donc le supprimer. Il nous reste cette récurrence:C1,4
i
Cela résout avec des moyens élémentaires de
réintroduire symboliquement l'état complet; si , alors .ψ={…,x:=5,…} ψ(x)=5
Appels de procédure
Étant donné un programme
P
de la formeM(x)
pour un paramètre (s)x
oùM
est une procédure avec paramètre (nommé)p
, attribuer des coûtsNotez à nouveau la constante supplémentaire (qui peut en fait dépendre de !). Les appels de procédure sont coûteux en raison de la manière dont ils sont mis en œuvre sur de vraies machines, et dominent parfois même le temps d'exécution (par exemple, évaluer naïvement la récurrence des numéros de Fibonacci).ccall ψ
Je passe sous silence certaines questions sémantiques que vous pourriez avoir avec l'état ici. Vous voudrez faire la distinction entre l'état global et les appels locaux à la procédure. Supposons que nous ne transmettons état global ici et
M
obtient un nouvel état local, initialisé en définissant la valeur dep
lax
. De plus,x
peut être une expression que nous supposons (généralement) être évaluée avant de la transmettre.Exemple: considérons la procédure
Selon les règles, on obtient:
Notez que nous ne tenons pas compte de l'état global, car il est
fac
clair qu'aucun accès à aucun. Cette récurrence particulière est facile à résoudre pourNous avons présenté les fonctionnalités linguistiques que vous rencontrerez dans un pseudo-code typique. Méfiez-vous des coûts cachés lorsque vous analysez un pseudo-code de haut niveau. en cas de doute, dépliez-vous. La notation peut sembler lourde et est certainement une question de goût; les concepts énumérés ne peuvent cependant pas être ignorés. Cependant, avec une certaine expérience, vous pourrez voir immédiatement quelles parties de l'état sont pertinentes pour quelle mesure de coût, par exemple la "taille du problème" ou le "nombre de sommets". Le reste peut être supprimé - cela simplifie considérablement les choses!
Si vous pensez maintenant que cela est beaucoup trop compliqué, être conseillé: il est ! Déduire les coûts exacts des algorithmes dans tout modèle si proche des machines réelles pour permettre des prédictions d'exécution (même relatives) est une tâche ardue. Et cela ne prend même pas en compte la mise en cache et autres effets pervers sur de vraies machines.
Par conséquent, l'analyse algorithmique est souvent simplifiée au point d'être mathématiquement traitable. Par exemple, si vous n'avez pas besoin de coûts exacts, vous pouvez surestimer ou sous-estimer à tout moment (pour les limites supérieures et inférieures): réduire l'ensemble des constantes, supprimer les conditions, simplifier les sommes, etc.
Une note sur le coût asymptotique
Ce que vous trouverez habituellement dans la littérature et sur le Web, c'est l'analyse "Big-Oh". Le terme approprié est l' analyse asymptotique , ce qui signifie qu'au lieu de calculer les coûts exacts comme nous l'avons fait dans les exemples, vous ne donnez les coûts que jusqu'à un facteur constant et dans la limite (grosso modo, "pour Big ").n
Ceci est (souvent) juste puisque les déclarations abstraites ont des coûts (généralement inconnus) en réalité, selon la machine, le système d'exploitation et d' autres facteurs, et à court runtimes peut être dominé par le système d'exploitation mise en place du processus en premier lieu et ainsi de suite. Donc, vous obtenez une perturbation, de toute façon.
Voici comment l'analyse asymptotique est liée à cette approche.
Identifiez les opérations dominantes (qui induisent des coûts), c’est-à-dire les opérations qui se produisent le plus souvent (jusqu’à facteurs constants). Dans l'exemple Bubblesort, un choix possible est la comparaison de la ligne 5.
Vous pouvez également lier toutes les constantes des opérations élémentaires à leur maximum (en haut), respectivement. leur minimum (par le bas) et effectuer l’analyse habituelle.
Assurez-vous de bien comprendre la signification des symboles Landau . Rappelez-vous que de telles limites existent pour les trois cas ; utiliser n'implique pas une analyse dans le pire des cas.O
Lectures complémentaires
L'analyse des algorithmes présente de nombreux défis et astuces. Voici quelques lectures recommandées.
Comment décrire, valider et analyser des algorithmes?
Comment pouvons-nous supposer que les opérations de base sur les nombres prennent un temps constant?
Qu'est-ce qui constitue une unité de temps dans l'analyse d'exécution?
Il existe de nombreuses questions liées à l'analyse d'algorithme qui utilisent des techniques similaires à celle-ci.
la source
Nombre de déclarations d'exécution
Il y a une autre méthode, défendue par Donald E. Knuth dans sa série The Art of Computer Programming . Contrairement à la traduction de l'ensemble de l'algorithme en une seule formule , il fonctionne indépendamment de la sémantique du code du côté "assembler les objets" et permet de passer à un niveau inférieur uniquement lorsque cela est nécessaire, en partant d'une vue "d'aigle". Chaque affirmation peut être analysée indépendamment des autres, ce qui conduit à des calculs plus clairs. Cependant, la technique se prête bien à un code assez détaillé, pas à un pseudo-code de niveau supérieur.
La méthode
C'est assez simple en principe:
Calculer les coûts totaux
Vous pouvez insérer des estimations et / ou des quantités symboliques à tout moment, ce qui affaiblit resp. généraliser le résultat en conséquence.
Sachez que l’étape 3 peut être arbitrairement complexe. C'est généralement là que vous devez travailler avec des estimations (asymptotiques) telles que " " pour obtenir des résultats.e77∈O(nlogn)
Exemple: recherche en profondeur d'abord
Considérez l'algorithme de traversée de graphique suivant:
Nous supposons que le graphe (non dirigé) est donné par des listes de contiguïté sur les nœuds . Soit le nombre d'arêtes.{0,…,n−1} m
En regardant simplement l'algorithme, nous voyons que certaines instructions sont exécutées aussi souvent que d'autres. Nous introduisons quelques espaces réservés , et pour les compteurs d'exécution :A B C ei
En particulier, car chaque appel récursif de la ligne 8 provoque un appel de la ligne 3 (le premier étant causé par l'appel d'origine ). De plus, car la condition doit être vérifiée une fois par itération, puis une fois de plus afin de la quitter.e8=e3−1 e6=e5+e7
foo
dfs
while
Il est clair que . Maintenant, lors d’une preuve de correction, nous montrerons qu’il est exécuté exactement une fois par noeud; c'est-à-dire que . Mais ensuite, nous parcourons chaque liste d'adjacence exactement une fois et chaque bord implique deux entrées au total (une pour chaque nœud incident); nous obtenons itérations au total. En utilisant cela, nous dérivons le tableau suivant:A=1 B=n C=2m
foo
Cela nous conduit à des coûts totaux de exactement
En instanciant des valeurs appropriées pour le nous pouvons obtenir des coûts plus concrets. Par exemple, si nous voulons compter les accès à la mémoire (par mot), nous utiliserionsCi
et obtenir
Lectures complémentaires
Voir au bas de mon autre réponse .
la source
L’analyse algorithmique, comme la démonstration de théorèmes, est en grande partie un art (par exemple, il existe des programmes simples (comme le problème de Collatz ) que nous ne savons pas analyser). Nous pouvons convertir un problème de complexité d’algorithme en problème mathématique, comme l’a expliqué de manière exhaustive Raphaël , mais pour pouvoir exprimer une limite sur le coût d’un algorithme en termes de fonctions connues, il nous reste à:
la source