Quelqu'un peut-il expliquer la complexité amortie en termes simples? J'ai eu du mal à trouver une définition précise en ligne et je ne sais pas comment elle se rapporte entièrement à l'analyse des algorithmes. Tout ce qui est utile, même s'il est référencé de l'extérieur, serait très apprécié.
La complexité amortie est la dépense totale par opération, évaluée sur une séquence d'opérations.
L'idée est de garantir le coût total de l'ensemble de la séquence, tout en permettant aux opérations individuelles d'être beaucoup plus chères que le coût amorti.
Exemple:
Le comportement de C++ std::vector<>
. Lorsque Push_back()
augmente la taille du vecteur au-dessus de sa valeur pré-allouée, il double la longueur allouée.
Un seul Push_back()
peut donc prendre O(N)
à exécuter (car le contenu du tableau est copié dans la nouvelle allocation de mémoire).
Cependant, comme la taille de l'allocation a été doublée, les prochains appels N-1
À Push_back()
prendront chacun O(1)
pour s'exécuter. Ainsi, le total des opérations N
prendra encore O(N)
; ce qui donne à Push_back()
un coût amorti de O(1)
par opération.
Sauf indication contraire, la complexité amortie est une garantie asymptotique dans le pire des cas pour toute séquence d'opérations. Ça signifie:
Tout comme pour la complexité non amortie, la notation big-O utilisée pour la complexité amortie ignore à la fois les frais généraux initiaux fixes et les facteurs de performance constants. Ainsi, dans le but d'évaluer les performances amorties de big-O, vous pouvez généralement supposer que toute séquence d'opérations amorties sera "suffisamment longue" pour amortir une dépense de démarrage fixe. Plus précisément, pour l'exemple std::vector<>
, C'est pourquoi vous n'avez pas à vous soucier de savoir si vous rencontrerez réellement N
des opérations supplémentaires: la nature asymptotique de l'analyse suppose déjà que vous le ferez.
Outre la longueur arbitraire, l'analyse amortie ne fait pas d'hypothèses sur la séquence des opérations dont vous mesurez le coût - c'est une garantie du pire des cas sur toute séquence possible des opérations. Peu importe à quel point les opérations sont mal choisies (par exemple, par un adversaire malveillant!), Une analyse amortie doit garantir qu'une séquence d'opérations suffisamment longue peut ne pas coûter systématiquement plus que la somme de leurs coûts amortis. C'est pourquoi (sauf mention spécifique en tant que qualificatif), la "probabilité" et le "cas moyen" ne sont pas pertinents pour l'analyse amortie - pas plus qu'ils ne le sont pour une analyse big-O ordinaire dans le pire des cas!
Dans une analyse amortie, le temps nécessaire pour effectuer une séquence d'opérations de structure de données est moyenné sur toutes les opérations effectuées ... L'analyse amortie diffère de l'analyse de cas moyen dans la mesure où la probabilité n'est pas impliquée; une analyse amortie garantit la performance moyenne de chaque opération dans le pire des cas.
(d'après Cormen et al., "Introduction to Algorithms")
Cela peut être un peu déroutant, car il dit à la fois que le temps est moyen et que ce n'est pas une analyse de cas moyen. Permettez-moi donc d'essayer d'expliquer cela par une analogie financière (en effet, "amorti" est un mot le plus souvent associé à la banque et à la comptabilité.)
Supposons que vous gérez une loterie. (Ne pas acheter un billet de loterie, ce que nous verrons dans un instant, mais gérer la loterie elle-même.) Vous imprimez 100 000 billets, que vous vendrez pour 1 unité monétaire chacun. Un de ces billets donnera droit à l'acheteur à 40 000 unités monétaires.
Maintenant, en supposant que vous pouvez vendre tous les billets, vous gagnez 60 000 unités monétaires: 100 000 unités monétaires en ventes, moins le prix de 40 000 unités monétaires. Pour vous, la valeur de chaque ticket est de 0,60 unité monétaire, amortie sur l'ensemble des tickets. Il s'agit d'une valeur fiable; vous pouvez miser dessus. Si vous en avez assez de vendre les billets vous-même et que quelqu'un arrive et propose de les vendre pour 0,30 unité monétaire chacun, vous savez exactement où vous en êtes.
Pour l'acheteur de loterie, la situation est différente. L'acheteur a une perte attendue de 0,60 unité monétaire lors de l'achat d'un billet de loterie. Mais c'est probabiliste: l'acheteur pourrait acheter dix billets de loterie par jour pendant 30 ans (un peu plus de 100 000 billets) sans jamais gagner. Ou ils pourraient acheter spontanément un seul billet un jour et gagner 39 999 unités monétaires.
Appliqué à l'analyse des infrastructures de données, nous parlons du premier cas, où nous amortissons le coût de certaines opérations de structures de données (disons, insérer) sur toutes les opérations de ce type. L'analyse de cas moyen traite de la valeur attendue d'une opération stochastique (disons, recherche), où nous ne pouvons pas calculer le coût total de toutes les opérations, mais nous pouvons fournir une analyse probabiliste du coût attendu d'une seule opération.
On dit souvent que l'analyse amortie s'applique à la situation où une opération à coût élevé est rare, et c'est souvent le cas. Mais pas toujours. Prenons, par exemple, la soi-disant "file d'attente du banquier", qui est une file d'attente premier entré, premier sorti (FIFO), constituée de deux piles. (Il s'agit d'une structure de données fonctionnelle classique; vous pouvez créer des piles LIFO bon marché à partir de nœuds à liaison unique immuables, mais les FIFO bon marché ne sont pas si évidents). Les opérations sont mises en œuvre comme suit:
put(x): Push x on the right-hand stack.
y=get(): If the left-hand stack is empty:
Pop each element off the right-hand stack and
Push it onto the left-hand stack. This effectively
reverses the right-hand stack onto the left-hand stack.
Pop and return the top element of the left-hand stack.
Maintenant, je prétends que le coût amorti de put
et get
est O(1)
, en supposant que je commence et finis avec une file d'attente vide. L'analyse est simple: j'ai toujours put
sur la pile de droite et get
de la pile de gauche. Donc, à part la clause If
, chaque put
est un Push
, et chaque get
est un pop
, les deux étant O(1)
. Je ne sais pas combien de fois j'exécuterai la clause If
- cela dépend du modèle de put
s et get
s - mais je sais que chaque élément se déplace exactement une fois de la pile de droite à la pile de gauche. Ainsi, le coût total sur toute la séquence de n put
s et n get
s est: n Push
es, n pop
s et n move
s , où un move
est un pop
suivi d'un Push
: en d'autres termes, 2n put
s et get
s donnent 2n Push
es et 2n pop
s. Ainsi, le coût amorti d'un seul put
(ou get
) est un Push
et un pop
.
A noter que les queues de banquier sont appelées ainsi précisément en raison de l'analyse de complexité amortie (et de l'association du mot "amorti" avec la finance). Les files d'attente des banquiers sont la réponse à ce qui était autrefois une question d'entrevue courante, bien que je pense qu'elle soit maintenant considérée comme trop connue: créez une file d'attente qui implémente les trois opérations suivantes en temps amorti O(1) :
1) Obtenez et supprimez l'élément le plus ancien de la file d'attente,
2) Mettez un nouvel élément dans la file d'attente,
3) Trouvez la valeur de l'élément maximum actuel.
Le principe de la "complexité amortie" est que, bien que quelque chose puisse être assez complexe lorsque vous le faites, puisque ce n'est pas fait très souvent, il est considéré comme "pas complexe". Par exemple, si vous créez un arbre binaire qui doit être équilibré de temps en temps - dites une fois tous les 2^n
insertions - car bien que l'équilibrage de l'arbre soit assez complexe, il ne se produit qu'une fois sur n insertions (par exemple une fois au numéro d'insertion 256, puis à nouveau au 512e, 1024e, etc.). Sur toutes les autres insertions, la complexité est O(1) - oui, cela prend O(n) une fois toutes les n insertions, mais ce n'est que 1/n
probabilité - nous multiplions donc O(n) par 1/n et obtenons O (1). Donc, on dit que "la complexité amortie d'O (1)" - parce que comme vous ajouter plus d'éléments, le temps consacré au rééquilibrage de l'arbre est minime.
Moyens amortis répartis sur des cycles répétés. Le comportement le plus défavorable est garanti de ne pas se produire avec beaucoup de fréquence. Par exemple, si le cas le plus lent est O (N), mais que les chances que cela se produise ne sont que O (1/N), et sinon le processus est O (1), alors l'algorithme aurait quand même amorti la constante O(1) fois. Il suffit de considérer le travail de chaque O(N) exécution à répartir en N autres exécutions.
Le concept dépend du fait d'avoir suffisamment de pistes pour diviser le temps total. Si l'algorithme n'est exécuté qu'une seule fois, ou s'il doit respecter un délai à chaque exécution, la complexité du pire des cas est plus pertinente.
Supposons que vous essayez de trouver le kème plus petit élément d'un tableau non trié. Le tri du tableau serait O (n logn). Alors, trouver le kème plus petit nombre est simplement localiser l'indice, donc O (1).
Étant donné que le tableau est déjà trié, nous n'avons plus besoin de trier à nouveau. Nous n'atteindrons jamais le pire des cas plus d'une fois.
Si nous effectuons n requêtes pour essayer de localiser kth le plus petit, ce sera toujours O (n logn) car il domine O (1). Si nous faisons la moyenne du temps de chaque opération ce sera:
(n logn)/n ou O (logn). Donc, complexité temporelle/nombre d'opérations.
C'est la complexité amortie.
Je pense que c'est comme ça que ça se passe, je l'apprends juste aussi ..
C'est un peu similaire à multiplier la complexité du pire des cas de différentes branches dans un algorithme avec la probabilité d'exécuter cette branche et d'ajouter les résultats. Donc, si une branche est très peu susceptible d'être prise, elle contribue moins à la complexité.