Was ist mit "konstanter amortisierter Zeit" gemeint, wenn über die zeitliche Komplexität eines Algorithmus gesprochen wird?
Was ist mit "konstanter amortisierter Zeit" gemeint, wenn über die zeitliche Komplexität eines Algorithmus gesprochen wird?
Antworten:
Amortisierte Zeit in einfachen Worten erklärt:
Wenn Sie eine Operation millionenfach ausführen, ist Ihnen der schlimmste oder der beste Fall dieser Operation nicht wirklich wichtig. Sie kümmern sich darum, wie viel Zeit insgesamt benötigt wird, wenn Sie die Operation millionenfach wiederholen .
Es spielt also keine Rolle, ob die Operation von Zeit zu Zeit sehr langsam ist, solange "ab und zu" selten genug ist, um die Langsamkeit zu verwässern. Im Wesentlichen amortisierte Zeit bedeutet "durchschnittliche Zeit pro Operation, wenn Sie viele Operationen ausführen". Die amortisierte Zeit muss nicht konstant sein. Sie können eine lineare und logarithmische Amortisationszeit haben oder was auch immer.
Nehmen wir das Beispiel von mats für ein dynamisches Array, zu dem Sie wiederholt neue Elemente hinzufügen. Normalerweise dauert das Hinzufügen eines Elements konstant (d. H. O(1)
). Jedes Mal, wenn das Array voll ist, weisen Sie doppelt so viel Speicherplatz zu, kopieren Ihre Daten in die neue Region und geben den alten Speicherplatz frei. Unter der Annahme, dass Zuweisungen und Freigaben in konstanter Zeit ausgeführt werden, dauert dieser Vergrößerungsprozess einige O(n)
Zeit, wobei n die aktuelle Größe des Arrays ist.
Jedes Mal, wenn Sie vergrößern, benötigen Sie ungefähr doppelt so viel Zeit wie beim letzten Vergrößern. Aber Sie haben auch doppelt so lange gewartet, bevor Sie es getan haben! Die Kosten für jede Erweiterung können somit auf die Einfügungen "verteilt" werden. Dies bedeutet, dass auf lange Sicht die Gesamtzeit für das Hinzufügen von m Elementen zum Array O(m)
und damit die amortisierte Zeit (dh die Zeit pro Einfügung) beträgt O(1)
.
Dies bedeutet, dass im schlimmsten Fall im schlimmsten Fall standardmäßig O (1) oder eine konstante Zeit verwendet wird. Ein häufiges Beispiel ist das dynamische Array. Wenn wir bereits Speicher für einen neuen Eintrag zugewiesen haben, ist das Hinzufügen O (1). Wenn wir es nicht zugewiesen haben, werden wir dies tun, indem wir beispielsweise das Doppelte des aktuellen Betrags zuweisen. Diese bestimmte Einfügung ist nicht O (1), sondern etwas anderes.
Wichtig ist, dass der Algorithmus garantiert, dass nach einer Folge von Operationen die teuren Operationen amortisiert werden und dadurch die gesamte Operation O (1) gerendert wird.
Oder strenger ausgedrückt:
Es gibt eine Konstante c, so dass für jede Folge von Operationen (auch eine, die mit einer kostspieligen Operation endet) der Länge L die Zeit nicht größer als c * L ist (Danke Rafał Dowgird ).
Um eine intuitive Denkweise zu entwickeln, sollten Sie Elemente in ein dynamisches Array einfügen (z. B. std::vector
in C ++). Zeichnen wir ein Diagramm, das die Abhängigkeit der Anzahl der Operationen (Y) zeigt, die zum Einfügen von N Elementen in ein Array erforderlich sind:
Vertikale Teile des schwarzen Graphen entsprechen einer Neuzuweisung des Speichers, um ein Array zu erweitern. Hier können wir sehen, dass diese Abhängigkeit grob als Linie dargestellt werden kann. Und diese Liniengleichung ist Y=C*N + b
( C
ist konstant, b
in unserem Fall = 0). Daher können wir sagen, dass wir C*N
Operationen im Durchschnitt ausgeben müssen, um N Elemente zum Array C*1
hinzuzufügen , oder Operationen, um ein Element hinzuzufügen (amortisierte konstante Zeit).
Ich fand die folgende Wikipedia-Erklärung nützlich, nachdem ich sie dreimal wiederholt gelesen hatte:
Quelle: https://en.wikipedia.org/wiki/Amortized_analysis#Dynamic_Array
"Dynamisches Array
Amortisierte Analyse der Push-Operation für ein dynamisches Array
Stellen Sie sich ein dynamisches Array vor, dessen Größe zunimmt, wenn weitere Elemente hinzugefügt werden, z. B. eine ArrayList in Java. Wenn wir mit einem dynamischen Array der Größe 4 beginnen würden, würde es eine konstante Zeit dauern, vier Elemente darauf zu schieben. Das Verschieben eines fünften Elements auf dieses Array würde jedoch länger dauern, da das Array ein neues Array mit der doppelten aktuellen Größe (8) erstellen, die alten Elemente auf das neue Array kopieren und dann das neue Element hinzufügen müsste. Die nächsten drei Push-Operationen würden in ähnlicher Weise eine konstante Zeit in Anspruch nehmen, und dann würde die nachfolgende Addition eine weitere langsame Verdoppelung der Array-Größe erfordern.
Wenn wir eine beliebige Anzahl von Pushs n auf ein Array der Größe n betrachten, stellen wir im Allgemeinen fest, dass Push-Operationen eine konstante Zeit benötigen, mit Ausnahme der letzten, die O (n) Zeit benötigt, um die Größenverdopplungsoperation auszuführen. Da es insgesamt n Operationen gab, können wir den Durchschnitt davon nehmen und feststellen, dass zum Verschieben von Elementen auf das dynamische Array Folgendes erforderlich ist: O (n / n) = O (1), konstante Zeit. "
Nach meinem Verständnis als einfache Geschichte:
Angenommen, Sie haben viel Geld. Und Sie möchten sie in einem Raum stapeln. Und Sie haben lange Hände und Beine, so lange Sie jetzt oder in Zukunft brauchen. Und Sie müssen alles in einem Raum ausfüllen, damit es einfach abgeschlossen werden kann.
Sie gehen also direkt zum Ende / zur Ecke des Raums und beginnen, sie zu stapeln. Während Sie sie stapeln, wird dem Raum langsam der Platz ausgehen. Beim Füllen war es jedoch einfach, sie zu stapeln. Habe das Geld, lege das Geld. Einfach. Es ist O (1). Wir müssen kein vorheriges Geld bewegen.
Sobald der Raum keinen Platz mehr hat. Wir brauchen einen anderen Raum, der größer ist. Hier gibt es ein Problem, da wir nur 1 Raum haben können, so dass wir nur 1 Schloss haben können, müssen wir das gesamte vorhandene Geld in diesem Raum in den neuen größeren Raum verschieben. Bewegen Sie also alles Geld von einem kleinen Raum in einen größeren Raum. Das heißt, stapeln Sie alle erneut. Wir müssen also das gesamte vorherige Geld bewegen. Es ist also O (N). (unter der Annahme, dass N die Gesamtzahl des Geldes des vorherigen Geldes ist)
Mit anderen Worten, es war einfach bis N, nur 1 Operation, aber wenn wir in einen größeren Raum ziehen müssen, haben wir N Operationen durchgeführt. Mit anderen Worten, wenn wir den Durchschnitt ermitteln, ist es am Anfang 1 Einfügung und 1 weitere Bewegung, während Sie in einen anderen Raum ziehen. Insgesamt 2 Operationen, eine Einfügung, eine Bewegung.
Angenommen, N ist groß wie 1 Million, selbst in dem kleinen Raum, sind die 2 Operationen im Vergleich zu N (1 Million) nicht wirklich eine vergleichbare Zahl, daher wird sie als konstant oder O (1) betrachtet.
Angenommen, wir erledigen all das in einem anderen größeren Raum und müssen uns wieder bewegen. Es ist immer noch das gleiche. Sagen wir, N2 (sagen wir 1 Milliarde) ist ein neuer Geldbetrag im größeren Raum
Wir haben also N2 (einschließlich N des vorherigen, da wir alle von einem kleinen zu einem größeren Raum wechseln).
Wir brauchen immer noch nur zwei Operationen, eine wird in einen größeren Raum eingefügt, dann eine weitere Bewegungsoperation, um in einen noch größeren Raum zu gelangen.
Selbst für N2 (1 Milliarde) sind es also jeweils 2 Operationen. das ist wieder nichts. Es ist also konstant oder O (1)
Wenn also das N von N auf N2 oder ein anderes ansteigt, spielt es keine große Rolle. Es ist immer noch konstant oder O (1) -Operationen sind für jedes der N erforderlich.
Nehmen wir nun an, Sie haben N als 1, sehr klein, die Anzahl der Geldbeträge ist gering und Sie haben einen sehr kleinen Raum, der nur für eine Anzahl von Geldmengen geeignet ist.
Sobald Sie das Geld im Raum füllen, ist der Raum gefüllt.
Wenn Sie in den größeren Raum gehen, nehmen Sie an, dass nur noch ein Geld darin passt, insgesamt 2 Geldzählungen. Das heißt, das vorherige bewegte Geld und 1 mehr. Und wieder ist es gefüllt.
Auf diese Weise wächst das N langsam und es ist kein konstantes O (1) mehr, da wir das gesamte Geld aus dem vorherigen Raum verschieben, aber nur noch 1 Geld mehr aufnehmen können.
Nach 100 Mal passt das neue Zimmer auf 100 Geldbeträge aus dem vorherigen und 1 weiteres Geld, das es aufnehmen kann. Dies ist O (N), da O (N + 1) O (N) ist, dh der Grad von 100 oder 101 ist gleich, beide sind Hunderte, im Gegensatz zur vorherigen Geschichte von Eins zu Millionen und Eins zu Milliarden .
Dies ist also eine ineffiziente Methode, um Räume (oder Speicher / RAM) für unser Geld (Variablen) zuzuweisen.
Ein guter Weg ist es also, mehr Platz mit Potenzen von 2 zuzuweisen.
1. Raumgröße = passt 1 Zählung des Geldes
2. Raumgröße = passt 4 Zählung des Geldes
3. Raumgröße = passt 8 Zählung des Geldes
4. Raumgröße = passt 16 Zählung des Geldes
5. Raumgröße = passt 32 Zählung des Geldes
6. Raumgröße = passt 64 Anzahl Geld
7. Raumgröße = passt 128 Anzahl Geld
8. Raumgröße = passt 256 Anzahl Geld
9. Raumgröße = passt 512 Anzahl Geld
10. Raumgröße = passt 1024 Anzahl Geld
11. Raumgröße = passt 2.048 Anzahl Geld
. ..
16. Zimmergröße = passt 65.536 Anzahl Geld
...
32. Zimmergröße = passt 4.294.967.296 Anzahl Geld
...
64. Zimmergröße = passt 18.446.744.073.709.551.616 Anzahl Geld
Warum ist das besser? Weil es am Anfang langsam und später schneller zu wachsen scheint, verglichen mit der Speichermenge in unserem RAM.
Dies ist hilfreich, da im ersten Fall, obwohl es gut ist, der Gesamtbetrag der pro Geld zu erledigenden Arbeit fest ist (2) und nicht mit der Raumgröße (N) vergleichbar ist, könnte dies auch der Raum sein, den wir in der Anfangsphase eingenommen haben groß (1 Million), die wir möglicherweise nicht vollständig nutzen, je nachdem, ob wir im ersten Fall überhaupt so viel Geld zum Sparen bekommen.
Im letzten Fall, Potenzen von 2, wächst es jedoch an den Grenzen unseres RAM. Mit zunehmender Potenz von 2 bleibt sowohl die Armotized-Analyse konstant als auch freundlich zu dem begrenzten RAM, über das wir heute verfügen.
Die obigen Erläuterungen gelten für die Aggregatanalyse, die Idee, "einen Durchschnitt" über mehrere Operationen zu ziehen. Ich bin nicht sicher, wie sie auf die Banker-Methode oder die Physiker-Methoden der amortisierten Analyse angewendet werden.
Jetzt. Ich bin mir der richtigen Antwort nicht ganz sicher. Aber es hätte mit der Hauptbedingung der beiden Methoden von Physikern und Bankern zu tun:
(Summe der fortgeführten Anschaffungskosten)> = (Summe der tatsächlichen Betriebskosten).
Die Hauptschwierigkeit, mit der ich konfrontiert bin, besteht darin, dass ich nicht sicher bin, wie ich die Bedeutung der amortisierten Kosten bewerten soll, da sich die amortisierten asymptotischen Betriebskosten von den normalen asymptotischen Kosten unterscheiden.
Wenn dann jemand meine amortisierten Kosten angibt, weiß ich, dass dies nicht dasselbe ist wie die normalen asymptotischen Kosten. Welche Schlussfolgerungen muss ich dann aus den amortisierten Kosten ziehen?
Da einige Vorgänge überlastet sind, während andere Vorgänge unterlastet sind, könnte eine Hypothese lauten, dass die Angabe der fortgeführten Anschaffungskosten einzelner Vorgänge bedeutungslos wäre.
Zum Beispiel: Für einen Fibonacci-Haufen ist es bedeutungslos, die fortgeführten Anschaffungskosten von nur abnehmendem Schlüssel als O (1) anzugeben, da die Kosten durch "Arbeit früherer Operationen zur Erhöhung des Potentials des Haufens" reduziert werden.
ODER
Wir könnten eine andere Hypothese haben, die folgende Gründe für die fortgeführten Anschaffungskosten hat:
Ich weiß, dass der teuren Operation mehrere kostengünstige Operationen vorausgehen werden.
Aus Gründen der Analyse werde ich einige kostengünstige Vorgänge überladen, so dass sich ihre asymptotischen Kosten nicht ändern.
Mit diesen erhöhten kostengünstigen Vorgängen kann ich beweisen, dass der teure Betrieb eine geringere asymtotische Kosten hat.
Somit habe ich die ASYMPTOTISCHE GEBUNDENE der Kosten von n Operationen verbessert / verringert.
Daher sind die Analyse der amortisierten Kosten + die Grenzen der amortisierten Kosten nur noch für die teuren Vorgänge anwendbar. Die billigen Operationen haben die gleichen asymptotisch amortisierten Kosten wie ihre normalen asymptotischen Kosten.
Die Leistung einer Funktion kann gemittelt werden, indem die "Gesamtzahl der Funktionsaufrufe" in die "Gesamtzeit für alle getätigten Aufrufe" geteilt wird. Selbst Funktionen, die für jeden Anruf länger dauern, können auf diese Weise gemittelt werden.
Das Wesentliche an einer Funktion, die ausgeführt Constant Amortized Time
wird, ist, dass diese "durchschnittliche Zeit" eine Obergrenze erreicht, die nicht überschritten wird, wenn die Anzahl der Anrufe weiter erhöht wird. Ein bestimmter Anruf kann in der Leistung variieren, aber auf lange Sicht wird diese durchschnittliche Zeit nicht immer größer.
Dies ist das wesentliche Verdienst von etwas, bei dem Leistung erbracht wird Constant Amortized Time
.