So, guten Morgen. Und haben Sie einen Ton dahinten
? Alles klar. Sehr schön. Gut, so,
wir sind in der Mitte oder im,
sagen wir mal, im letzten Viertel unseres ersten Kapitels,
wo es um erweiterte Analysetechniken von Algorithmen ergeht.
Wir haben ganz am Anfang uns nochmal so die Grundlagen
angeschaut mit dem Okerkül und mit den Rekurrenzgleichungen.
Und in der letzten Vorlesung haben wir die probabilistische Analyse gesehen und
mit der können wir zum einen den mittleren Fall analysieren.
Das war das eine Szenario, also die Average-Case-Analyse und das andere Szenario,
was wir in Zukunft vielleicht auch noch haben werden,
ist, dass wir randomisierte Algorithmen haben,
die einen Zufallszahlengenerator verwenden und wo wir dann abhängig von dem Verhalten des Zufallszahlengenerators die Laufzeit analysieren wollen.
Dafür brauchen wir die provabilistische Analyse und das Entscheidende hierbei war,
dass wir den Algorithmus beschreiben durchs sogenannte Indikator-Funktion.
Wir suchen uns also Ereignisse raus. Die Schlüsselereignisse sind für
die Laufzeit des Verfahrens und dann betrachten wir das als eine Zufallsvariable,
die dann entsprechend, ja, je nachdem,
ob es um die Eingabe geht oder um Zufallszahlengenerator, eins oder null sein kann.
Und dann berechnen wir den Erwartungswert darüber und kriegen dann eine
mittlere oder eine erwartete Laufzeit. So, das haben wir
in der letzten Vorlesung ja abschließend behandelt mit ein paar Beispielen.
Das werde ich jetzt da nicht nochmal wiederholen,
sodass wir jetzt einsteigen können in das letzte Thema,
der dieses Analysekapitels.
Und zwar geht es heute um die amortisierte Analyse
Die amortisierte Analyse betreibt man immer dann, wenn man Datenstrukturen hat
mit entsprechenden Zugriffsoperationen, die in einem Algorithmus so verstreut vorkommen.
Also wir haben zwei, drei, vier verschiedene Funktionen,
die eine Datenstruktur verändern und unser Grundalgorithmus verwendet diese Datenstruktur entsprechend,
um dann sein Ergebnis zu berechnen.
Häufig hängt die Laufzeit des Algorithmus, dann nicht unerheblich von
der Laufzeit der Operation der Datenstruktur ab. Also Beispiel,
wer zum Beispiel kürzeste Wegeberechnung,
da brauchen wir so eine Prioritätswarteschlange und wenn man dann die Gesamtlaufzeit des Verfahrens anguckt,
dann ist das Das ist ganz entscheidend,
wie effizient jetzt diese Prioritätswarteschlange implementiert ist und wie viel Zeit wir für die Operation dieser Prioritätswarteschlange benötigen.
Ja, das heißt, dieses Szenario kommt häufig vor und die Art und Weise,
wie wir solche Algorithmen typischerweise analysieren, ist wie folgt.
Wir sehen jetzt also in dem Algorithmus diese Datenstruktur, Operationen und wir überlegen uns,
was ist denn die Worst Case Laufzeit dieser Datenstrukturoperation bei einer gewissen Eingabegröße N.
Das wäre dann hier T-OP von N. Und dann betrachten wir den Gesamtalgorithmus und zählen dann,
wie häufig wird denn diese Funktion aufgerufen Und dann sehen wir,
das hängt wahrscheinlich auch irgendwie von N ab und sehen dann,
wir haben K von N viele Aufrufe dieser Operation.
Und dann wissen wir schon, naja,
im Worst Case haben wir dann K mal T,
also K von N mal T von N. Field
Zeit, die wir benötigen, asymptotisch,
für die Durchführung dieser Datenstrukturoperation während der Gesamtausführungszeit des Algorithmus.
Das ist die Technik, so wie wir es normalerweise machen.
Und auf diese Art und Weise haben wir dann zum Beispiel
die Laufzeit bestimmt für den Deixtra-Algorithmus, wo dann entsprechend so ein M-Log-N rauskommt.
Okay, Wir bearbeiten ja in diesem Kapitel gerade wieder diese Analysetechniken,
weil wir wollen effiziente Algorithmen entwickeln,
aber wir müssen ja auch herausfinden, dass sie wirklich effizient sind.
Und wir möchten natürlich in diesen ganzen Laufzeitabschätzungen,
wie wir sie hier betreiben, nicht so viel Platz verschwenden in der Schranke.
Ja, wir möchten also, dass die Schranke ja möglichst dicht ist.
An der realen Laufzeit. So, wenn man das hier betrachtet,
dann fällt relativ schnell auf,
dass genau an dieser Stelle eine sehr grobe Abschätzung passiert,
indem wir einfach sagen, okay,
wir nehmen den Worst Case für die Operation TVN
und multiplizieren das mit der Anzahl der Ausführung
Wir werden sicherlich in vielen Fällen in die Situation haben,
dass zwar diese Operation hier K mal ausgeführt wird,
dass aber viele dieser K-Operationen, diese Worst-Case-Laufzeit gar nicht benötigen.
Die brauchen viel weniger Laufzeit. Und wenn wir einfach K mal den Worst-Case annehmen,
haben wir die Laufzeit in diesem Augenblick schon deutlich überschätzt.
Das wollen wir im folgenden Heilen beheben und
unsere Analyse an dieser Stelle
besser machen
Die Technik, die in der Informatik dazu entwickelt wurde,
ist die sogenannte amortisierte Analyse. Und diesen Begriff des Amortisierens,
den kennen wir ja auch aus unserem Sprachgebrauch.
Also wir kompensieren Dinge gegeneinander. Und das macht man hier auch in der Laufzeitanalyse. Was wir machen werden,
ist, dass wir nicht mehr die Analyse der Datenstrukturoperation von der Anzahl,
wie häufig sie auftritt, komplett entkoppeln und sagen,
Worst-Case-Laufzeit für die Operation und dann, wie häufig tritt sie auf,
sondern dass wir die Sequenz der Operationsausführung als Ganzes betrachten versuchen,
die Laufzeit für diese komplette Sequenz zu bestimmen.
Und dann, wenn wir das gemacht haben, dann sagen wir,
dass die Laufzeit für die Operation Tee von N durch K wäre.
Also Tee von N ist die Gesamtlaufzeit zur Abarbeitung der kompletten
Sequenz von Operationen. Und wir wissen, das waren K-Operationen.
Dann hätten wir amortisiert, die Kosten Tee von N durch K.
So, das sieht schon so ein bisschen wieder aus,
als würden wir eine mittlere Laufzeit bestimmen.
Also wir berechnen die Gesamtlaufzeit, dividieren das durch K.
Die amortisierte Analyse hat aber nicht viel mit der mittleren,
der Average Case Analyse
zu tun
Das sind zwei relativ getrennte Dinge. Die amortisierte Analyse berechnet
diesen WRT von End durch K. nur,
um ihnen ein Richtwert zu geben, wie lange benötigt diese Funktion.
Sie machen aber am Ende eine Worst-Case-Abschätzung,
denn sie garantieren das nach Ausführungen von K.-Operationen,
sie eine Laufzeitschranke in jedem Fall einhalten werden.
Dann wissen sie, dass sie wieder auch für den Worst-Case
die Laufzeit korrekt abgeschätzt geschätzt haben. Okay, das heißt,
die amortizierte Laufzeit liefert für die Gesamtheit der Ausführung,
also wenn wir dann K-Operationen ausführen, eine obere Schranke für den Worst Case.
Es ist nach wie vor eine Worst Case Analyse,
aber nur, wenn wir wirklich eine ganze Sequenz von Operationen betrachten,
für jede einzelne Operation geben wir jetzt die Laufzeit an,
als TVN durch K.
Und das ist nicht notwendigerweise eine obere Schranke für die Ausführungszeit einer Operation.
Eine Operation kann durchaus länger dauern als diese Zeit,
aber eingebettet in der Sequenz von den anderen Operationen haben wir dann die gewünschte Laufzeitschranke.
Doch, das hört sich jetzt alles etwas esoterisch an.
Deswegen brauchen wir ein schönes Beispiel, wo man das hervorragend dran sieht.
Und wir haben dafür, oder ich habe dafür ein ganz,
ganz einfaches Datenstrukturbeispiel, was sie sicherlich auch kennen.
Es ist einfach nur ein Stack. Ja,
und der Stack wird realisiert durch ein Array mit einer gewissen Anzahl von Elementen,
also die maximale Größe, die dieser Stack haben kann.
Und dann hat der Stack einen zusätzlichen,
ein zusätzliches Attribut, das heißt Top.
Und damit geben wir einfach die Position des obersten Elements
im Stack
Das ist also die klassische Implementierung eines Stacks durch ein Array. Gut,
die Operation hier oben, die kennen sie wahrscheinlich,
wir haben eine Operation, die nur True oder Falls zurückliefert,
je nachdem, ob der Stack leer ist oder Elemente enthält.
Das heißt, wir gucken, wenn es.top gleich null ist,
dann ist kein Element auf dem Stack,
dann ist der Stack leer und wir geben True zurück.
Anderenfalls sind Elemente auf dem Stack und wir geben Falls zurück
. Das ist also unsere Funktion, die nur sagt,
ob der Stack Elemente enthält oder nicht.
Dann haben wir die Head-Funktion, die ist eigentlich im folgenden
für uns
relativ unwichtig
Ist aber eben die typische, eine typische Operation auf dem Stack,
die einfach schaut, ob der Stack leer ist,
wenn der leer ist, gibt es eine Fehlermeldung,
ja, das hier kommt als Fehlermeldung Unterlauf,
weil wir auch ein Element zugreifen wollen,
was nicht auf dem Stack existiert.
Und anderenfalls geben wir das oberste Element des Stacks zurück und
das oberste Element ist durch das Attribut top markiert.
Und dann greifen wir auf das Array zu und dann gibt es
das oberste Element dann entsprechend an die aufrufende Funktion zurück.
Für unsere Laufzeitbetrachtung entscheidend ist, wie wir Elemente auf
den Stack drauflegen und herunternehmen
Dann ein Element drauflegen, machen wir mit einer Push-Operation,
die wieder eine Fehlermeldung ausgibt, wenn wir die maximale Element,
Anzahl Elemente im Stack erreicht haben.
Und anderenfalls erhöhen wir dieses Attribut Top um eins und speichern
dann in dem Array an dieser Position top den Wert,
den wir auf dem Stack ablegen wollen, also dieses X hier.
Die Pop-Operation nimmt ein Element vom Stack wieder herunter.
Auch die prüft am Anfang, ob wir einen Fehler produzieren,
weil wir auf einen leeren Stack zugreifen,
dann können wir natürlich
kein Element herunternehmen
Anderenfalls dekrementieren wir hier den Zähler um eins und geben dann das Element,
was auf der obersten Position des Stacks lag,
zurück als Teil dieser Pop-Operation.
Die macht also dann gleich auch noch einen Hate mit.
Aber das Entscheidende ist, sie nimmt halt ein Element runter.
So, das ist ein ganz normaler Stack. So,
wir gehen jetzt mal davon aus,
dass wir irgendeine Anwendung haben, wo wir,
wo wir weg.
Ich gebe jetzt mal selber weg
Ah. Aha, okay. Wo wir eine Operation benötigen,
die mehrere Elemente gleichzeitig vom Stack herunternimmt. Und diese Operation nennen wir
Multipop und die kriegt den Stack und als Parameter zusätzlich noch ein K.
Und was diese Operation macht, ist, dass sie K-Elemente
vom Stack herunternimmt. Das macht sie mit einer Wildschleife,
solange also der Stack nicht leer ist und das K
noch größer ist als null,
rufen wir unsere Pop-Funktion auf,
nehmen also ein Element vom Stack
herunter und dekrementieren hier das K um eins
So, jetzt können Sie argumentieren, ja, das ist ja eine ganz blöde Implementierung,
weil ich könnte ja einfach nur hier diesen Top-Zähler gleich um K einfach verringern,
ja, einfach da die K abziehen.
Da haben Sie natürlich recht. Es ist jetzt nicht eine besonders effiziente Implementierung,
aber es geht hier darum,
ich möchte Ihnen ja diese Analysetechnik zeigen und dafür hätte ich halt ganz gerne hier die Wahlschleife,
die brauchen wir jetzt,
um die Analyse zu betreiben und sicherlich fallen Ihnen noch andere Beispiele ein,
wo wir eine ähnliche Situation haben wie die hier.
Okay, gut, so, das ist also die Das erste Beispiel,
ich werde also das immer wieder verwenden heute im Laufe der Vorlesung.
Und ich habe auch noch ein zweites Beispiel für Sie.
Und das haben wir deswegen gewählt, weil da nicht so ganz offensichtlich
eine Datenstruktur vorkommt und wir trotzdem diese amortisierte Analyse verwenden können.
Es geht um eine Funktion, die quasi einen Binärzähler darstellt
und diesen mit einem Aufruf jeweils um eins erhöht.
Der Binärzähler wird als dargestellt durch Log and Feede Bits,
also das ist ja
zweier Logorithmus
Und endest damit also die größte Zahl, die wir entsprechend darstellen können.
Und wir zählen jetzt quasi diesen Binärzähler hoch,
durch den ganzen Zahlenraum, wenn sie so wollen.
Und die Binary Increment-Funktion erhöht diesen Binärzähler um genau einen,
um genau einen Wert. Hier ist so ein Beispiel,
ja, das ist der während der Erzähler wert.
Hier sehen Sie die Binärzahl der entsprechenden des entsprechenden Zählerwerts.
Und dann kann man hier auch sehen, was passiert.
Wir haben also hier die verschiedenen Positionen des Pit-Arrays und dann
müssen wir höre entsprechend der Beneacodierung immer zunächst größeren Zahl gehen.
Wie machen wir das? Wir machen das so,
dass wir von der hintersten Position, also der niederwärtigsten Stelle,
nach oben zur höchstwertigsten Stelle laufen.
Also quasi von rechts nach links. Und wir haben hier so
ein Zähler-I und solange I noch kleiner ist als die Länge
des Arrays und A von I gleich eins ist, machen wir Folgendes.
Wir setzen das A von I an dieser Position auf Null
und erhöhen das I um eins.
Wenn diese Wahlschleife dann abbricht und wir Hier noch mit dem
I kleiner sind als die Gesamtlänge des RASA,
dann setzen wir das A von I auf 1.
Hier ist übrigens noch ein Fehler, ist noch alte Nomenklatur,
hier müsste eigentlich A. längs stehen, damit das ganz korrekt ist, so wie hier unten.
Das ist nichts anderes, das ist die alte Common Nomenklatur,
die ist hier, es wird wahrscheinlich noch ein paar Mal passieren,
dass die irgendwie noch stehen geblieben ist beim Umwandeln der Folien.
Okay, gut, so, also wie kann man sich das vorstellen? Ja,
am besten stellt man sich das so vor,
man weiß, wenn hinten, eigentlich müsste man,
wenn hinten nur eine Null steht, ist es ganz einfach,
dann ist die nächstgrößere Binärzahl, die,
wo wir an diese hintere Stelle dann eine Eins schreiben,
Also der einfache Teil, wenn hinten schon einer einsteht,
dann haben wir eigentlich einen Überlauf in die nächste Position und dieser,
oder einen Übertrag in die nächste Position, in dieser Übertragbasiert so lange, bis irgendwann mal das eine Null steht.
Das heißt, wenn wir zum Beispiel die Sieben haben,
als 0-1-1-1 und wollen dann eins addieren,
dann müssen wir von hinten die erste Position suchen,
wo eine Null steht,
da schreiben wir eine Eins rein und alle anderen wechseln die Wertigkeit von 1 nach Null.
Okay, das ist also diese Binary Increment-Funktion und die lassen
wir jetzt einfach durchlaufen, um den Zahlenraum zu beschreiben.
Also wir wollen irgendwie den komplett durchlaufen lassen für alle möglichen Werte,
von N. Ja, und dann können wir auch direkt sagen,
welche Laufzeit das kosten würde.
Wir wissen also, wenn unsere Zahlenraum N ist,
dann brauchen wir Login viele Bits, um die Zahl darzustellen.
Und weil wir hier wieder so eine Wild-Schleife haben,
wissen wir, naja, im Worst Case,
also im Worst Case müssen wir quasi hier durch alle Bits durchlaufen,
also dass die kompletten Login-Bits anschauen.
Und ja, dass die drinnen ist konstant. Das heißt,
diese Inkrementfunktion hat ein paar konstante Laufzeitanteile und diese Wildschleife.
Und die hat im Worst Case Login-Durchläufe.
Und das bedeutet, die Laufzeit hierfür ist O von Login.
Und wenn wir jetzt N mal diese Inkrement-Operation ausführen wollen,
dann hätten wir eine Gesamtlaufzeit für diese Operation von NO von N-Login.
Bei diesem Zähler, da sieht man schon sehr gut,
dass diese Abschätzung eigentlich nicht gut ist, dass die eine sehr grobe Schranke darstellt.
Weil hier in der Abbildung sehen sie so grau unterlegt,
die Bits, die wir tatsächlich anfassen müssen.
Also die, die tatsächlich ihren Wert wechseln,
wenn wir von der einen zur nächsten Zeile entsprechend wechseln.
Und die Laufzeit, die wir haben, die können wir eigentlich,
die ist proportional zu der Anzahl dieser grauen Bits,
weil nur die betrachten wir, die Wildschleife terminiert,
sobald wir hier eine Null sehen.
Endlock-in auf der anderen Seite wären alle Bits, die wir hätten.
Wir gehen im Worst Case davon aus, wir müssen alle Bits betrachten.
Ja, und wir zählen hier N-Zahlenwerte auf,
also dann hätten wir entsprechend Endlock-in. Das heißt,
das, was hier alle Bits, die weiß markiert sind,
Das ist das, was wir überschätzen.
Also unsere Laufzeitschranke geht davon aus, dass wir alle Bits
in jeder Zeile anfassen. Und das ist natürlich viel zu viel.
Ja. Und hier stellt sich nun die Frage,
also offensichtlich gibt es natürlich Fälle, wo wir so viele Bits anfassen.
Also hier, das geht jetzt natürlich nicht bis hier 256,
sondern du bist 16. Aber wenn man mal guckt zum Beispiel hier,
der Wechsel von 15 nach 16,
da schauen wir uns schon Login, halbe viele Bits an.
Ja, das ist offensichtlich schon proportional zu ennen.
Die können sich gerne noch
einen Stuhl nehmen
Sind noch ein paar oder den? Für mich ist das okay.
Es sieht ein bisschen unbequem aus. Gut, okay. So,
und die Frage stellt sich jetzt,
wie können wir diese Abschätzung ein bisschen geschickter machen,
sodass wir eine genauere, präzisere,
obere Schranke für dieses einmalige Ausführen der Binary-Increment-Operation bekommen.
Gut, zwei Beispiele, an denen wir sehen,
dass eine Worst-Case-Analyse ungünstig ist. Achso, vielleicht nochmal hier,
habe ich jetzt noch nicht über die Laufzeiten gesprochen,
bei der Stack bei der Stack-Datenstruktur. Also offensichtlich haben alle Operationen,
die wir hier oben sehen, eine konstante Laufzeit,
also O von 1 und nur bei der Multipop-Operation ist es eben unklar,
wie häufig diese Wild-Schleife durchlaufen wird. Das heißt,
wir wissen, sie wird wahrscheinlich ka mal durchlaufen,
es sei denn,
dass der Stack vorher leer wird und wenn wir mal den Fall mal ausschließen,
wäre das sowas wie O von K. Aber es wäre zumindest nicht mehr konstant,
weil im ungünstigsten Fall würden sie hier N-Elemente vom Stack
wieder herunternehmen wollen und dann müsste diese Wild-Schleife hier N-Mahl
durchlaufen werden
Okay, gut. Also zwei Beispiele,
wo wir eine genauere präzisere Abschätzung gerne hätten.
Und nun schauen wir uns einfach mal ein paar Mechanismen an,
wie wir das Ganze machen können. Also es gibt drei verschiedene Ansätze,
die alle natürlich auf dem gleichen Prinzip basieren,
nämlich darauf, dass wir irgendwie versuchen,
ja,
die Laufzeit für die einzelne Operation genauer abzuschätzen und gegebenenfalls einfach
Kosten zwischen Operationen hin und her zu schieben,
um diese präzisere Abschätzung zu erhalten
Die erste Methode heißt Aggregatanalyse und diese Aggregatanalyse ist quasi die direkte Umsetzung von dem,
was ich gerade so umgangssprachlich schon beschrieben habe, nämlich,
dass wir uns einfach überlegen, Kar-Operationen hintereinander,
wie lange dauert das insgesamt?
Und dann dividieren wir das durch K und kriegen dann so eine amortisierte Aufzeit.
Die Aggregatanalyse ist wenig formal und mehr so ein argumentativer Ansatz
und wir werden gleich in den nächsten beiden Ansätzen sehen,
wie man das weiter formalisieren kann
Okay, so, wir gucken uns den Stack an,
wir haben also die Push-Operation, die Polytic Constante Zeit,
die Pop-Operation, konstante Zeit, die Multipop-Operation,
O von K-Zeit, beziehungsweise was immer auf dem Stack drauf liegt,
würde im Worst Case O von N bedeuten,
wenn wir N-Operationen ausführen,
wäre das im Worst Case O von N-Quadrat und auch das ist natürlich eine deutliche Überschätzung der tatsächlichen Laufzeit.
Mit der Aggregatmethode würden wir jetzt überlegen,
können wir, also was passiert,
wenn wir eine ganze Sequenz von Operationen betrachten
Die Erkenntnis, die man da hat, ist die,
dass wir von einem Steak ein Element nur herunternehmen können,
wenn wir es vorher auf sein Steak hinaufgelegt haben.
Damit ist die Anzahl der Pop-Operationen, die wir durchführen können,
auf alle Fälle niedriger als die Anzahl der Push-Operationen.
Wir gehen mal davon aus, dass der Stack am Anfang leer war.
Okay, das heißt, wir machen ab und zu mal
einen Push, ab und zu mal eine Pop-Operation.
Und offensichtlich ist die Anzahl der Push-Operationen größer als die Anzahl
der Pop-Operationen. Gut, es kann natürlich noch sein,
dass wir Fehlermeldungen kriegen,
weil wir häufig Pop aufrufen auf dem leeren Stack
Den Fall brauchen wir, glaube ich, nicht weiter betrachten.
Das wäre dann eh nur ein konstanter Laufzeitanteil, wenn so eine Fehlermeldung auftritt.
Okay, auch so eine Multipop-Operation können wir dabei berücksichtigen,
indem wir sagen, da diese Multipop-Operation sind ja nur K, viele Pop-Operationen hintereinander.
Und für jede dieser Pop-Operationen, den Multipop,
hatten wir vorher auch eine Push-Operation. Okay, so,
dann können wir argumentieren und sagen, wenn wir N-Operationen haben,
dann haben wir maximal N-Push-Operationen und weil die Anzahl der Push-Operationen
größer ist als die Anzahl der Pop-Operationen,
haben wir auch maximal N-Pop-Operationen,
die wir durchführen können
Das wäre also das, was wir maximal tun können,
streng genommen könnten wir,
wenn das natürlich irgendwie eine kleinere Zahl als N,
also in der Summe haben wir in der Operation,
ja, aber naja, also abgeschätzt,
N-Push-Operationen und dann natürlich höchstens in Pop-Operationen und inklusive dem,
was in dem Multipop passiert und dann wissen wir die Gesamtlaufzeit à la Push- und Pop-Operation,
wäre dann O von N, ja,
weil die Push- und die Pop-Operation, jede ja nur konstante Zeit benötigt,
wäre es also N plus N,
also O von N und dann könnten wir sagen,
die amortisierte Laufzeit aller Operationen wäre O von N wo ich N,
OVNN hier wieder als Repräsentant gemeint, irgendeine Konstante mal enden,
dann können wir das enden,
da raus dividieren und kürzen und dann bleibt der O von 1 raus.
Das bedeutet, die Gesamtlaufzeit, die wir haben für die Operation,
wenn wir Endoperation ausführen, ist OVN und amortisiert,
würden unsere Operationen konstante Laufzeit benötigen.
Und jetzt sehen Sie hoffentlich, also es ist nach wie vor
eine Worst-Case-Abschätzung für die Laufzeit, daran hat sich nichts geändert.
Wenn wir N-Operationen betrachten, wenn wir aber sagen,
die amortisierte Laufzeit beträgt von 1,
dann ist das nicht notwendigerweise eine obere Schranke,
weil in der Sequenz von Operationen könnte durchaus eine Multipop-Operation sein,
die länger braucht, wo die Laufzeit von N abhängt.
Aber wir wissen, dass die nur auftreten kann,
wenn wir vorher entsprechend viele Push-Operationen hatten und dadurch ist sichergestellt,
diese Operation kann nicht häufig auftreten und damit ist sichergestellt,
dass bei N-Operationen wir eben diese Schranke einhalten können von O
von N insgesamt und O von 1 pro Operation
Okay, das war Aggregat-Methode und ich hoffe,
sie haben gemerkt, das war eigentlich eine rein argumentative Geschichte,
die ich hier gemacht habe.
Ich habe irgendwie ihnen nahegelegt, dass das jetzt alles so stimmt.
Und bei so einer ganz einfachen Datenstruktur, wie mit diesem
Push und Pop, geht das natürlich auch recht einfach.
Aber wenn wir komplexere Datenstrukturen analysieren wollen,
dann brauchen wir vielleicht auch schon ein etwas formaleren Mechanismus.
Wir gucken uns mal nochmal das Binary Increment an.
Hier ist jetzt schon die Frage, wie können wir das jetzt machen?
Also hier sehen wir jetzt nicht so klare Operationen wie dieses Push und Pop,
sondern wir haben ja nur diese eine Operation.
Und diese eine Operation, die ist,
manchmal braucht die lange und manchmal ist die ganz schnell fertig.
Manchmal geht es in konstanter Zeit und ein paar Aufrufen braucht die
auch entsprechend proportional zu Login-Zeit, um ausgeführt zu werden.
Das heißt, hier können wir jetzt nicht die Laufzeit so
zwischen verschiedenen Operationen hin und her schieben.
Zumindest nicht, wenn wir auf der Ebene dieses Binary Increments schauen.
In so einer Situation muss man vielleicht etwas fein granularer hinschauen
und man schaut dann hier rein und versucht dann,
diesen Algorithmus zu zu zerlegen.
In einzelne Teile, die wir dann wieder einzeln in der
Laufzeit bewerten können. Und wenn man mal schaut,
hier, dann gibt es zwei Dinge, die passieren.
Und zwar hier in dieser Wildschleife, da werden Bits immer auf Null gesetzt.
Also solange diese Wildschleife läuft, wird immer ein Bit auf Null gesetzt.
Und hier nach der Wildschleife wird ein Bit auf 1 gesetzt.
Wir könnten also sagen, es gibt zwei Arten von Operationen.
Es gibt eine Reset-Operation und diese Reset-Operation hat die Aufgabe,
ein Bit von 1 auf Null
zu setzen
Und dann gibt es noch eine Set-Operation und die Set-Operation
hat die Aufgabe, ein Bit von null.
Also die Waldschleife, wenn die hier endet,
ist das Bit ja null, von null auf eins zu setzen.
Und wenn wir diesen Algorithmus betrachten,
mit diesen zwei Operationen mit einem Reset und einer Set-Operation,
dann können wir wieder überlegen, welche Laufzeitanteile fallen denn auf Reset und welche fallen denn auf Set.
Und wenn wir ein Binary Increment angucken,
also ein Aufruf hier von, dann sehen wir,
also von den Set-Operationen gibt es nur eine.
Egal was passiert, wir setzen nur ein Bit auf eins
Letzte sozusagen, nachdem diese Wildschleife durchgekommen gelaufen ist.
Bei der Reset-Operation ist die Sache unklar,
da haben wir diese Wildschleife und bei der,
also es könnten durchaus nur ein, zwei.
Es könnten aber im schlimmsten Fall auch lock and minus
eins viele Bits sein, die wir umwandeln müssen.
Das heißt, die Anzahl der Reset-Operationen, die kennen wir nicht genau.
Die variiert ganz stark von Aufruf zu Aufruf dieser Funktion.
Und jetzt machen wir das Gleiche wie mit dem Pop und
dem Multipop. Beim Multipop wussten wir auch nicht,
wie häufig das Pop ausgeführt wird und hatten so eine Variabilität drin.
Aber aber wir wussten, dass wir höchstens so viele Multi-Pop,
also Pop-Operationen im Multi-Pop ausführen können, wie wir vorher Push-Operationen hatten.
Hier ist die Situation natürlich ähnlich. Unser Bit ist ja
entweder eins oder null und offensichtlich können wir einen Witten nur von eins auf null setzen,
wenn wir es vorher von null auf eins gesetzt haben.
Das heißt, jede Set-Operation, jede Reset-Operation,
die wir hier in der Wild-Schleife haben,
setzt dir eine Set-Operation irgendwann vorher voraus, weil nur sonst,
nur dann ist ja das Bit entsprechend auch eins.
Und genau auf diese Art und Weise können wir jetzt auch
argumentieren und die Laufzeit für das Binary Increment bestimmen
Okay, hier nochmal, Worst-Case-Analyse, die Waldschleife hätte Lock-in-Durchläufe,
Aufrufe von Bininary Increment macht auch von N-Log-in.
Aggregat-Methode würde sagen, also wir haben zwei Möglichkeiten,
wie diese Bits wechseln können und die Laufzeit ist abhängig von
der Anzahl der Zustandswechsel der Bits, also der Anzahl der Fälle,
wo das Bit von 1 auf 0 oder 0 auf 1 wechselt.
Ja, und dann könnten wir sagen, A von 0,
das ändert jetzt während jedes Aufrufes seinen Wert.
Und das A von 1, aber nur bei jedem zweiten Mal,
Und wenn wir jetzt genauer hinschauen, dann sehen wir,
dass A von I bei jedem zwei Hochiten-Durchlauf seinen Wert ändert.
Wir müssen mal aufs Beispiel gucken. Also A 0 ändert
je immer seinen Wert, in jedem Aufruf. A1,
nur bei jedem zweiten Mal, A2 bei jedem vierten Aufruf,
A3 bei jedem achten Aufruf und so weiter.
Das heißt, wir aggregieren jetzt über diese verschiedenen Zustände.
Wir gucken uns diese Set- und Reset-Operationen an,
die wir hier noch nicht explizit so benennen müssen und zählen die einfach.
Wie häufig die auftreten
Okay, das können wir jetzt ganz einfach aufaddieren,
was wir machen ist,
wir gehen jetzt die einzelnen Spalten durch und überlegen uns,
wie häufig kommt da so ein Bittwechsel vor,
da haben wir also die Summe von I gleich null bis Log-in.
Das ist also die Summe über alle Spalten, über alle Bits,
die wir quasi haben. Und dann wissen wir,
dass für die Position null hätten wir also N-Bittwechsel,
also das niederwärtigste wird, ändert in jedem, in jedem Zähler,
in jeder Zählererhöhung, jeder, der Endzählererhöhung sein wird.
Und das nächste jeder zweiten, also hätten wir en halbe,
viele Bittwechsel, dann ein Viertel, ein Achtel und so weiter.
Das heißt, was wir haben, ist Summe von I
gleich 0 bis Login, N durch 2 hoch I,
ist hier nach unten abgerundet, falls das N keine Zweierpotent sein sollte.
Okay, so, können wir abschätzen, indem wir sagen,
okay, das N können wir vor die Summe ziehen,
dann hätten wir N mal die Summe von I gleich 0 bis Login,
1 durch 2 hoch I.
Das ist also ein halb Buch,
I ist eine geometrische oder exponentielle Reihe und wir können hier sogar,
weil diese Reihe konvergent ist, das Log-in abschätzen durch unendlich,
ohne dass wir groß was kaputt machen.
Und dann hätten wir halt die Summe von i gleich 0
bis unendlich einen halb hoch i oder eins durch zwei Uri.
Und das ist eins durch eins minus eineinhalb. Das heißt,
diese Summe hier, die wird eigentlich nie größer als zwei.
Und damit wäre das gleich zwei N. Und damit wäre
die Gesamtlaufzeit in O von N. Das heißt,
diese erste Abschätzung, die wir gesehen haben,
die Worst Case Aufzeit O von N-Log-in,
die ist nicht falsch,
denn das Okalkül beschreibt ja immer nur eine Oberschranke,
die ist nur sehr grob
Ja, die Oberschranke ist wesentlich höher als notwendig.
Und wenn wir etwas genauer hinschauen, dann sehen wir,
dass wir eine Laufzeit von O von N haben.
Für N-Aufrufe dieser Funktion. Dann können wir sagen,
ja, was ist jetzt die amortisierte Laufzeit?
Die amortisierte Laufzeit wäre, also die Laufzeit pro Binary Reincrement-Funktion.
Und da würden wir sagen, naja,
für N brauchen wir O von N. Das heißt,
für jede Funktion benötigen wir O von N durch N.
Hier wieder O von N als Repräsentant, also konstante mal N.
Können wir das N rauskürzen,
bleibt also O von konstanter,
also O von
Zeit übrig. Das wäre also die amortisierte Laufzeit für Binary Increment. Beachten Sie wieder,
die Laufzeit ist eine Worst-Case-Abschätzung,
wenn wir eine komplette Sequenz von End-Operationen betrachten,
dann wissen wir,
die Laufzeit wird nicht stärker wachsen als O von N,
aber wenn wir dann auf eine Operation schauen,
dann kommen wir auf eine amortisierte Laufzeit von O von 1
und das ist für jede Einzeloperation nicht unbedingt eine Worst-Case-Abschätzung,
weil einzelne Operationen können durchaus
Log-End-Site benötigen
Okay, das ist die Aggregatmethode und jetzt wollen wir versuchen,
diesen Prozess etwas zu formalisieren, weil nicht alle Beispiele,
die wir in Zukunft sehen werden, sind so einfach wie diese beiden hier.
Und wir werden kompliziertere Datenstrukturen haben und da brauchen wir Möglichkeiten, dass etwas formeller aufzuschreiben.
Und die nächste Technik, die man sich dann so überlegt hat,
die nennt sich Accounting oder Account-Methode und die hat eine ganz einfache Analogie.
Wir überlegen uns einfach, dass unsere Objekte,
die wir haben, das sind die Objekte,
die wir zum Beispiel auf dem Stack legen oder unsere Wits,
je nachdem, was wir gerade verwalten,
dass die quasi so eine Art Zeitbankkonto haben.
Die haben also ein Zeitkonto und auf dieses Zeitkonto können sie
was einzahlen und von dem Zeitkonto können sie sich auch bedienen und Zeit wieder herunternehmen.
Das heißt, was wir jetzt machen werden, ist,
wir werden den Operationen Kosten zuweisen und werden sagen,
du brauchst unserer Meinung nach so lang, ja,
also so viele Zeiteinheiten und diese Zuweisung von Kosten wird die tatsächlichen Kosten teilweise überschätzen,
teilweise vielleicht auch unterschätzen
Und wir werden dann zeigen, dass in der Sequenz von Operationen
wir die Zeit immer nur überschätzen und damit wieder eine Worst-Case-Laufzeit erhalten.
Gut, okay. Was machen wir jetzt? Also wir legen Kosten
fest und sagen, die Operation benötigt so und so viel.
Zeiteinheiten. Wenn die tatsächliche Ausführung jetzt weniger Zeit benötigt,
dann haben wir die Kosten überschätzt. Und dieses Überschätzen,
das merken wir uns,
indem wir diesem Objekt eine Art Bankkonto geben und da ein Guthaben einzahlen und sagen,
okay, also hier,
da ist jetzt noch eine Zeiteinheit oder fünf oder was auch immer auf unserem auf unserem Konto, ja.
Was passiert bei einer Unterschätzung? Also wenn die Operation tatsächlich
länger dauert als das, was wir vorher angegeben haben,
dann gucken wir auf unser Konto und kompensieren quasi die Kosten der Operation durch unser Guthaben,
was wir bei dem Objekt gespeichert haben. Okay,
das ist der Ansatz und das ist genau wie das Girokonto im täglichen Leben,
wo sie dann mal mehr und mal weniger Geld für irgendetwas ausgeben
und sie haben einfach so eine Art Puffer und das,
worauf sie achten, ist,
dass der Puffer möglichst nie unter Null läuft,
ist jedenfalls gut darauf zu achten
So, wir verwenden jetzt also C-I-Strich für die amortisierten Kosten,
sagen also, das ist,
das wird unsere Variable sein für die amortisierten Kosten.
Und dann wird die Ausführung der Operation natürlich auch reale Kosten haben.
Dafür verwenden wir C-I. Dann schauen wir uns jetzt die Operationsfolge an
von N-Operationen und die haben jetzt also Nummern, also eins bis irgendwie N.
Und das, was wir sicherstellen müssen, ist,
dass immer gilt,
dass die Summe der amortisierten Kosten größer oder gleich die Summe
der realen Kosten ist
Okay, die amortisierten Kosten können dann je nach Operation auch variieren.
Also das heißt, wir überlegen uns für jede Datenstrukturoperation,
was diese amortifizierten Kosten sein sollen. Also bis eben war
das ja konstant. Also wir hatten nur einen Wert,
der nachher rausgekommen ist. Hier können die unterschiedlich sein.
Und die Differenz zwischen den hier der Summe,
der amortisierten Kosten und hier der Summe der realen Kosten ist genau das,
um wie viel wir die Laufzeit nach wie vor überschätzen werden.
Und die sollte natürlich möglichst klein sein,
um eine präzise Abschätzung zu kriegen
Okay, so, wir gucken uns wieder unseren Stack an
und überlegen uns, wie das Ganze funktionieren könnte.
Bei der Accounting-Methode brauchen wir jetzt so einen kreativen Prozess.
Wir müssen nämlich einmal sagen, wie wir die Kosten, die amortisierten Kosten festlegen wollen.
Und darunter verbirgt sich dann auch schon das ganze Geheimnis,
weil wenn man das einfach gemacht hat,
muss man dann nur noch überprüfen, dass diese Regeln hier gelten,
also dass insbesondere die Summe der amortisierten Kosten immer größer gleich die Summe der realen Kosten ist.
Okay, also was nehmen wir mal an? Also wir
können erst mal angucken, was sind die tatsächlichen Kosten.
Wir wissen, die Push-Operationen braucht konstante Zeit.
Wir gehen mal davon aus, es wäre eine Zeiteinheit,
die das dann kostet. Die Pop-Operation auch. Und Multipop,
ja, Es ist wahrscheinlich in den meisten Fällen K,
also die Anzahl der Elemente,
die wir vom Stack runternehmen oder Betrag von S für den Fall,
dass nicht keine K-Elemente mehr auf dem Stack draufliegen,
sondern weniger und dann hört ihr die Wahlschleife vorher auf.
Also das sind die tatsächlichen Kosten. Jetzt wollen wir amortisierte Kosten angeben und wir wenden jetzt quasi den gleichen Gedankengang an,
wie eben auch bei der Aggregatmethode, weil wir haben ja gesagt,
naja, wir haben ja, wir wissen die Push-Operation,
die haben ja eigentlich eine feste Laufzeit und wir wissen,
dass wir ein Element nur vom Stack herunternehmen können,
wenn wir es vorher draufgelegt haben.
Also machen wir einfach Folgendes
Wir geben einer Push-Operation mal mehr Kosten.
Wir weisen der Mehrkosten zu, als sie tatsächlich hat.
Nämlich zwei Zeiteinheiten. Und dann wissen wir,
wenn wir die Push-Operation ausgeführt haben,
dann haben wir von den Zweizeiteinheiten eine verbraucht,
weil die Push-Operation, die kostet ja etwas.
Und eine Zeiteinheit haben wir quasi auf das Guthaben-Konto von dem
auf den Stack gelegten Objekt draufgelegt.
Damit haben alle Objekte, die auf dem Stack liegen,
quasi ein Konto mit einer Zeiteinheit
als Guthaben
Und dann kommen jetzt die Pop-Operationen. Und bei den Pop-Operationen
nehmen wir an, die kosten nichts. Die brauchen keine
Zeit. Wir wissen natürlich, dass es eine Unterschätzung ist,
aber wir haben ja auch noch das Guthaben der Objekte.
Und wir sehen, dass das, oder werden gleich sehen,
dass das natürlich genau hinkommt. Das heißt, wenn wir
ein Element vom Stack herunternehmen, dann kostet das eine Zeiteinheit.
Amortisiert haben wir null angenommen, aber wir haben ja noch eine Zeiteinheit,
auf dem Guthaben konnte dieses Objekts, was wir herunternehmen.
Und die verwenden wir. Und damit sind die Kosten wieder
ausgeglichen. Beim Multipop funktioniert das dann genauso,
weil wir K-Elemente vom Stack nehmen Jedes Objekt,
was auf dem Stack liegt, hat ein Guthabenkonto mit einer Zeiteinheit,
also haben wir insgesamt K-Zeiteinheiten gespart und können die alle vom Stack herunternehmen und dann hätten wir hier entsprechend die Null.
Ja, können dann also mit der Null rechnen und hätten
dann eine genaue Kompensation der tatsächlichen Kosten durch die Guthaben.
Okay, das ist der kreative Anteil, wenn man die Accounting-Methode anwendet,
dass man sich überlegt, wie man die Kosten aufteilt.
Und danach muss man nur noch nachrechnen,
dass das Ganze auch stimmt. Was man zeigen muss,
ist, dass für jede mögliche Sequenz von Operation,
die heißen hier O1 bis ON, Ja,
die können ja jetzt in unterschiedlichen Arten und Weisen auftreten.
Jede dieser Sequenzen hat Kosten, CI,
also C1 bis CN und für jede mögliche Sequenz
muss die Summe der amortisierten Kosten dieser Operationssequenz immer größer,
gleich die Summe der realen Kosten sein.
Okay, wie machen wir das? Wir verwenden einen Induktionsbeweis
. Wir gehen also jetzt von N-1-Operation zu N-Operation.
Und was wir zeigen werden, ist,
dass die Höhe der Überschätzung,
der Kosten genau der Höhe des Stacks entspricht
Ja, das ist auch nachvollziehbar, wenn wir oben nochmal gucken,
mit jeder Push-Operation, erhöhen wir die Überschätzung um eins.
Wir gehen ja von zwei Zeiteinheiten aus, brauchen aber nur eine Zeiteinheit.
Bleibt also eine Zeiteinheit über und die legen wir entsprechend auf unser Konto.
Das ist also das, was wir überschätzt haben.
Und dementsprechend hat jedes Element auf dem Stack eine Zeiteinheit.
Und wenn wir das Element vom Stack runternehmen,
verbrauchen wir diese Zeiteinheit entsprechend wieder. Das heißt,
die Differenz zwischen der Summe,
der amortisierten Kosten und der Summe der realen Kosten
entspricht gerade der Anzahl Elemente auf dem Stack nach den J-Operationen
Okay, wie zeigt man das jetzt, also für J
gleich null? Wissen wir, der Stack ist leer.
Es gilt also S von null, ist auf alle Fälle
null. Und damit sind noch gar keine Kosten aufgetreten,
aber auf alle Fälle wäre dann diese Differenz gleich null und
die Summe der amortisierten Kosten erhöhte der summe der realen Kosten entsprechend.
Das war natürlich alles noch null, weil noch gar nichts passiert ist.
So, wenn das J größer als null ist,
haben wir jetzt also eine Sequenz von J-Operationen und per Induktionsannahme,
wissen wir, dass bis zur J-1. Operation unsere Annahme korrekt war.
Das heißt, die Summe der amortisierten Kosten bis J minus 1,
minus die Summe der realen Kosten bis J minus 1,
entspricht gerade der Stackhöhe.
Der Anzahl der Elemente auf den Stack. Okay,
so, jetzt wollen wir den Fall lösen für die JT-Operation.
Wir betrachten also die Differenz der amortisierten und der realen Kosten für die JT-Operation,
beziehen die letzte Operation jetzt aus der Summe raus.
Dann haben wir hier die Summe jeweils von I bis 1
bis J minus 1 von CI-Strich,
also von den amortisierten Kosten
Minus die Summe der ersten J minus 1 Operationen von den realen Kosten.
Da steht hier und dann die amortisierten Kosten der letzten Operationen
minus die realen Kosten der letzten Operationen. Okay,
nach Induktionsannahme wissen wir,
dass diese beiden Summen hier vorne gerade der Stackhöhe entsprechend zum Zeitpunkt J minus 1,
also können wir hier dafür schreiben,
Betrag von sj minus 1 und dann haben wir diese letzte Operation hier.
Gut, und jetzt müssen wir die möglichen Operationen der Reihe
nach betrachten und schauen, dass unsere Annahme korrekt ist,
also dass wir hier am Ende wieder SJ stehen haben und
da haben wir diese drei die wir betrachten müssen.
Wir haben also zunächst die Push-Operation. Bei der Push-Operation
wissen wir, die amortisierten Kosten sind zwei.
Die realen Kosten sind eins. Das setzen wir also hier
entsprechend ein. Haben wir hier also die Höhe des Stacks
zum Zeitpunkt J minus eins plus zwei minus eins.
Das ist also die Höhe des Stacks zum Zeitpunkt J minus eins plus eins.
Und da wir ja ein Element auf den Stack raufgelegt haben,
entspricht das gerade der Höhe des Stacks zum Zeitpunkt J.
Bei einer Pop-Operation machen wir jetzt das Gleiche. Wir wissen
die amortisierten Kosten sind null. Die realen Kosten sind eins,
macht also die Höhe des Stacks zum Zeitpunkt J
minus eins
Minus 1 und das entspricht, da wir ein Element vom Stack heruntergenommen haben.
Genau, der neuen Stackhöhe SJ. Und bei einer Multi-Pop-Operation
haben wir wieder die Stackhöhe zum Zeitpunkt J minus 1.
Die amortisierten Kosten sind null und wir subtrahieren jetzt die realen Kosten.
Das ist das Minimum von der Stackhöhe zu dem Zeitpunkt.
SJ minus 1 und K. Und das entspricht natürlich
genau der Anzahl der Elemente, die vom Stack verschwinden.
Und damit ist das die neue Stackhöhe SJ.
Und damit haben wir gezeigt, dass die Differenz zwischen amortisierten
Kosten und realen Kosten immer gerade die Stackhöhe ist.
Die Stackhöhe ist immer größer, gleich null. Und weil
die Stackhöhe immer größer, gleich null ist, folgt,
dass die amortisierten Kosten immer größer gleich den realen Kosten sind.
Okay, damit haben wir eine Laufzeitabschätzung für den amortisierten Fall. Wir wissen also,
wenn wir amortisiert annehmen, dass eine Push-Operation konstante Zeit benötigt,
die Pop- und die Multipop-Operation auch,
dann gilt für jede mögliche Sequenz von Push- und Pop-Operationen,
dass die der Länge N,
dass sie insgesamt in O von N-Zeit bearbeitet werden kann
Selbst wenn eine Einzeloperation gegebenenfalls länger als konstante Zeit benötigt.
Okay, hier nochmal zusammengefasst die Art und Weise,
wie man das Ganze betrachtet.
Wir haben also die eine Datenstrukturoperation, das ist die Push-Operation.
Da bezahlen wir zwei Zeiteinheiten. Wir haben die Pop-Operation,
dann nehmen wir was vom Stack runter. Da bezahlen wir
nichts, sondern bedienen uns bei dem Gutachten des Objekts.
Und werden entsprechend sowohl beim Pop als auch beim Multi-Pop-Operation,
bei der Multipop-Operation, damit die Kosten decken.
Und die Operationen bezahlen. Ja, okay, so,
diese Accounting-Methode können wir jetzt auch für das Binary-Increment anwenden
Und ja, hier ist wieder die Worst-Case-Analyse,
die haben wir schon gesehen. Was wir jetzt machen ist,
wir schauen uns jetzt tatsächlich genau diese beiden Operationen an,
wenn wir das Bit setzen oder zurücksetzen.
Hier ist nochmal unser Pseudo-Code für die Binary-Increment-Funktion und wir sehen
hier in dieser Zeile in der Wild-Schleife,
das hier ist die Reset-Operation,
das Bit wird also auf Null gesetzt und hier am Ende
der Wild-Schleife ist die Set-Operation,
hier wird das Bit jetzt entsprechend auf eins gesetzt
Wir wissen, die Laufzeit ist proportional zur Anzahl der Set- und Reset-Operationen.
Das sehen wir diesem Code schon an, ja,
weil die Laufzeit hier ist proportional zur Anzahl der Wild-Schleifen-Durchläufe.
Das sind die Reset-Operationen und dann kommt immer noch die Set-Operation dazu.
Okay, wie verteilen wir unsere Kosten?
Wir verteilen die Kosten natürlich wieder so, dass uns
die Analyse gleich leicht fallen wird. Wir wissen schon,
in dem Algorithmus ist die Anzahl der Set-Operationen immer eins,
ja, also in jedem Beine-Reincrement-Aufruf, während die Anzahl der Reset-Operationen variabel ist.
Naja, wenn wir dann einfach sagen,
die Kosten für die Reset-Operation setzen wir mal auf null,
Das hätte den Vorteil, dass diese Wildschleife jetzt keine Rolle mehr spielt,
weil die Gesamtkosten hier null wären.
Und dann müssen wir natürlich irgendwie der Set-Operation Kosten zuordnen,
die es erlauben, auch die Reset-Operation wieder zu kompensieren.
Und dann sagen wir, na gut,
dann geben wir halt für die Save-Operation zwei Zeiteinheiten,
die erste, um das Bit zu setzen.
Oh, die zweite wandert wieder auf unser Guthabenkonto für
das Bit jetzt in diesem Fall, für das Objekt.
Und die werden wir dann verwenden in dem Augenblick, wo wir das Reset machen.
Und bevor wir dann das nächste Reset machen,
wissen wir schon, dass wir eine Set-Operation gemacht haben und
das dann wieder ein neues Guthaben da ist
Okay, warum funktioniert das? Zu Beginn des Zählprozesses sind
alle Bits erstmal null. Davon gehen wir aus, dass
wir da anfangen. Die Reset-Operation auf BTI wird nur ausgeführt,
falls A von I gleich eins ist und damit A von I gleich eins gilt,
wo es vorher eine Set-Operation ausgeführt werden auf diesem entsprechenden Bit.
Und das hat sozusagen dort für ein Guthaben gesorgt.
Zu jedem Zeitpunkt im Zählprozess ist die Anzahl der Set-Operation,
größere rechte Anzahl der Reset-Operationen und damit ist dann die Summe von Igles 1 bis NC,
strich größer, gleich die Summe von Igles 1 bis NCI,
also die Summe der amortisierten Kosten
Größer als die der reellen Kosten. Auch hier könnte man jetzt so,
wie wir es eben gesehen haben, so einen Korrektheitsbeweis durchführen,
indem man quasi eine Induktion macht über die einzelnen Zählerschritte.
Aber ja, das habe ich jetzt hier mal nicht gemacht.
Ich glaube, es ist ziemlich klar, wie das aussehen würde.
So, die gesamten amortisierten Laufzeitkosten für Binary Increment wäre dann
also, wir haben N mal die Z-Operation und wir haben
Im ungünstigsten Fall vielleicht N-Log-N mal die Reset-Operation,
aber das interessiert uns eigentlich gar nicht mehr,
weil wie amortisierte Zeit der Reset-Operation haben wir auf null gesetzt.
Das heißt, der Teil fällt weg. Ja,
und wir haben entsprechend die Laufzeit abgeschätzt über die Set-Operation.
Und dann wissen wir, das TZ, das war konstant,
zwei Zeiteinheiten, macht also O von N.
Und die Betrachtungsweise hier ist, beim Sätzen eines Bits
werden zwei Einheiten bezahlt, Zeiteinheiten.
Und das eine wird verbraucht fürs Sätzen,
die andere Zeiteinheit verbleibt bei dem Objekt.
Und dann wird das beim Rücksetzen,
also bei der Reset-Operation,
verbraucht das Bit
Die Zeiteinheit. Gut, das war die Accounting-Methode. Eigentlich nicht schlecht.
Damit haben wir schon einen gewissen Formalismus. Problem ist,
wir müssen jetzt also diese amortisierten Kosten vorher irgendwie festlegen.
Wir müssen uns überlegen, was wollen wir denn da angeben?
Es gibt noch ein weiteres Problem oder sagen wir mal kein Problem.
Also es gibt eine weitere Eigenschaft, die nicht so optimal ist.
Und zwar haben bei uns die Objekte quasi dieses Guthaben angesammelt.
Und die verbrauchen das auch wieder. Das ist in
den Beispielen, die wir gesehen haben,
völlig in Ordnung
Komplexeren Operation könnte es durchaus sein, dass wir nicht explizit
sagen wollen, dass diese Zeiteinheiten bei speziellen Objekten liegen.
Eigentlich ist es ja völlig egal. Wir sammeln irgendwie
Geld oder Zeiteinheiten und die wollen wir irgendwie wieder verbrauchen.
Und warum soll jetzt jedes Objekt so eine Art Bankkonto haben?
Warum machen wir das nicht so, dass wir sagen,
wir nehmen nur ein großes Konto.
Wir nehmen nur ein Konto für die gesamte Ausführung und da
reservieren wir einfach oder da speichern wir quasi alle Kosteneinheiten,
die wir nicht verbraucht haben.
Und das ist tatsächlich die Technik, die man am Ende
am häufigsten einsetzt, insbesondere wenn die Datenstrukturen komplexer werden.
Und diese Methode heißt Potenzialmethode. Die Analogie ist jetzt,
dass wir nicht mehr von einem Bankkonto reden, sondern von einer Potenzialfunktion.
Die Potenzialfunktion beschreibt also eine Differenz zwischen zwei Zuständen und die speichert quasi unser Guthaben global.
Also für alle Objekte gemeinsam in einer Funktion oder in einem Konto,
wenn sie sie wollen. Okay, also was machen wir?
Wir haben wieder amortisierte Kosten für jede Operation. Und alle,
wir haben dann die realen Kosten, die ziehen wir von den amortisierten ab.
Und wenn dann was übrig bleibt, dann legen wir das
auf das Potenzial drauf. Und wenn es zu wenig war,
dann nehmen wir etwas vom Potenzial herunter, um die Laufzeit wieder zu kompensieren.
Diese Potenzialfunktion mit dem folgenden Immerphie heißen und die Art und Weise,
wie wir jetzt strategisch vorgehen, ist,
dass wir nicht mehr uns für jede Funktion überlegen, ja,
wie viel amortisierte Kosten sollen wir denn da jetzt mal ansetzen,
sondern wir überlegen uns die Potenzialfunktion.
Wir denken uns eine Potenzialfunktion aus, ja, von der wir glauben,
dass sie das Laufzeitverhalten korrekt beschreibt. Wenn wir das machen,
dann können wir die amortisierten Kosten berechnen,
wir müssen sie dann nicht mehr explizit angeben,
sondern was wir dann machen, ist, wir sagen,
okay, wir haben die Datenstruktur und wir haben eine spezielle Operation,
die führen wir aus und die amortisierten Kosten,
der für diese Ausführung ist, Die sind die realen Kosten,
CI, plus die Potenzialdifferenz.
Das heißt, das Potenzial zum Zeitpunkt I,
also nach Ausführung der Operation minus, das Potenzial vor Ausführung der Operation.
Das heißt, die amortisierten Kosten können wir dann berechnen,
aus der Potenzialfunktion heraus. Wir müssen also nicht mehr
eine Zeit, einen Kostenwert festlegen für jede Operation,
sondern wir denken uns diese Potenzialfunktion aus.
Natürlich haben wir auch hier wieder dieses kreative Element,
wo wir uns ausdenken müssen, wie sieht denn jetzt diese Potenzialfunktion aus?
Also das wird dadurch nicht wirklich einfacher,
wenn man ganz ehrlich ist
Die Methode ist einfach flexibler und sie erlaubt einfach Zeit zwischen
den verschiedenen Funktionen und auch zu verschiedenen Objekten in der Datenstruktur,
beliebig hin und her zu schieben, weil wir nur eine Funktion haben,
die dieses Aufgesparten, diese Aufgesparten Kosteneinheiten sammelt.
Okay, wenn wir davon ausgehen, dass die amortisierten Kosten so berechnet werden,
dann können wir natürlich eine beliebige Folge von Operationen betrachten,
von Jigerch 1 bis NCI-Strich und können dann überlegen,
gut, was gilt jetzt für diese beliebige Folge,
da können wir dann einsetzen,
dass die amortisierten Kosten reale Kosten plus Potenzialdifferenz ist.
Das setzen wir hier entsprechend ein. Und dann haben wir
hier etwas, was wir als Teleskopsumme kennen.
Das heißt, wir haben ja hier die Summe,
die läuft von Igarch 1 bis N. Und dann sehen wir,
dass wir den Potenzialwert I addieren und dem Potenzialwert I minus 1 subtrahieren.
Im nächsten Sumanten würden wir jetzt diesen Wert wieder addieren und
den nächsten subtrahieren, sodass sich immer zwei Werte aufheben gegeneinander.
Das ist also so eine Teleskopsumme. Und was dann eben übrig bleibt,
ist einfach nur die Summe über die realen Kosten plus,
Potenzial an Nachausführung aller N-Operationen im Minus, das Potenzial vor Ausführung der Operationen.
Und hieraus leitet sich jetzt sofort die Bedingung ab,
die gelten muss, damit diese Analyse korrekt ist.
Und die Bedingung lautet, dass Vieh von DN
immer größer gleich Vieh von den Null sein muss.
Also das heißt, die amortisierten Kosten, das Potenzial zum Zeitpunkt
N muss immer größer oder gleich das Potenzial zum Zeitpunkt Null
sein. Damit wäre dieser Themen hier größer als Null und
damit wären die amortisierten Kosten in der Summe größer als die
realen Kosten
Ja. Ja, die Art und Weise,
wie man es in der Praxis macht, ist,
dass man davon ausgeht,
dass das Vieh am Anfang den Wert null hat und meistens zeigt man einfach,
dass diese Potenzialwerte immer größer gleich null bleiben.
Ja, und wenn man das sichergestellt hat,
dann hat man automatisch das auch hier gezeigt.
Hier sollte man aber sagen, hier steht extra so Praxis davor.
Das muss nicht unbedingt so sein, ja.
Also das ist keine notwendige Voraussetzung dafür, dass die Laufzeit
korrekt abgeschätzt werden.
Theoretisch könnte das Potenzial sogar
negativ werden
Das Entscheidende, das Entscheidende ist nur,
dass nach Ablauf der N-Operation das Potenzial wieder positiv ist.
Also irgendwie, wenn man diesen Potenzialfall Lauf sehen,
immer nach jeder Ausführung könnte das, wie gesagt, durchaus negativ werden.
Aber am Ende, wenn diese N-Operation abgeschlossen ist,
dann muss das Potenzial positiv sein. Oder man könnte auch sagen,
das Potenzial muss einfach höher sein als das Potenzial,
Vorausführung dieser Operationssequenz.
Ja, okay, das ist die Potenzialmethode und auch die
guckt man sich am besten an einem Beispiel an
Wir nehmen wieder unseren Stack. Und beim Stack können sie
sich wahrscheinlich jetzt schon denken, was unsere Potenzialfunktion ist.
Da ist es nämlich relativ offensichtlich. Da nehmen wir einfach
die Stackhöhe. Wir sagen, unsere Potenzialfunktion ist die Anzahl der Elemente
auf den Stack. So, okay. Und dann,
das ist der kreative Anteil gewesen. Wir mussten überlegen,
wie wir dieses Potenzial festlegen, sodass es nachher alles so hinkommt.
Und der Gedankenweg dahin ist einfach der,
dass wir überlegt, dass wir uns überlegen, naja,
wir wissen, es kommen noch die teuren Pop-Operationen,
die was vom Stack herunternehmen
Wenn wir einfach für diesen Fall Guthaben angesammelt haben, dann wissen wir,
dass diese Operation quasi mit amortisierten Kosten null laufen kann.
Und dann überlegt man sich,
wie viel Guthaben muss ich denn haben für so eine teure Operation,
die vielleicht dann irgendwann spät in der Sequenz kommt.
Und dann sagen sie, naja,
also sie müssen ja jedes Element vom Stack runternehmen.
Also scheint als Guthaben wohl die Snackhöhe irgendwie sinnvoll zu sein.
Das ist so der Gedankenweg, wie man zu dieser Funktion kommt.
Okay, so, stimmt das jetzt?
Das müssen wir jetzt kontrollieren. Wir schauen und sehen,
dass das Potenzial zu Anfang gleich null ist,
weil der Stack am Anfang
leer ist
Und wir sehen auch, dass die Potenzialfunktion zu jedem Zeitpunkt größer gleich null ist. Das heißt,
die Anzahl der Elemente auf dem Zweck kann nicht negativ werden und deswegen,
und weil die Potenzialfunktion ja die Anzahl der Elemente auf dem Slack ist,
ist die in Marco, ich sage gleich null.
Was sind jetzt die amortisierten Kosten,
die können wir jetzt berechnen, dass der angenehme Teil,
ja, wir müssen nämlich jetzt nicht mehr überlegen,
was wird es bei einer Sequenz von Operationen passiert,
sondern wir können einfach sagen, okay,
wenn das eine korrekte Potenzialfunktion ist,
dann hat die Push-Operation die amortizierten Kosten CI-Strich gleich reale Kosten plus Potenzial,
Nachausführung der Operation, Minuspotenzial vor Ausführung der Operation.
Das bedeutet, reale Kosten 1 plus das Vieh ist die Stackhöhe,
also Stackhöhe zum Zeitpunkt i-stackhöhe zum Zeitpunkt i minus 1,
weil wir eine Push-Operation gemacht haben,
ist der Zweck nach Ausführung der Operation,
eins höher als vorher.
Das heißt, s-i minus-s-i minus 1 ist 1 und das gibt zusammen dann Kosten 2.
Die Pop-Operation, ja, genau das Gleiche. Wir haben
reale Kosten plus Potenzial zum Zeitpunkt Minuspotenzial zum Zeitpunkt minus eins,
also Vorausführung. Dann haben wir reale Kosten eins plus Stackhöhe
zum Zeitpunkt i minus Stackhöhe zum Zeitpunkt i minus eins.
Wir wissen, wir haben ein Element vom Stack heruntergenommen.
Der Stack wird also um eins kleiner sein als vorher.
Das heißt, die Differenz von den beiden ist minus eins.
Insgesamt kommt dann also null raus. Und die Multipop-Operation,
ja, bei der Multipop-Operation haben wir jetzt die realen Kosten.
Das ist K. Wir müssen keine Elemente vom Stack herunternehmen.
Plus Die beiden Potenzial wäre, also Potenzial wäre zum Zeitpunkt
i-potenzial, wäre zum Zeitpunkt i minus eins.
Ja, also K plus Stackhöhe zum Zeitpunkt i minus
Stackhöhe zum Zeitpunkt i minus eins. Wir haben K-Elemente vom Stack heruntergenommen.
Das heißt, die Differenz zwischen diesen beiden hier ist genau
K. Und dann kommt da null raus. Das ist eigentlich hier
nicht ganz exakt aufgeschrieben, weil wir jetzt den Fall hier ignorieren,
dass auf dem Stack keine K-Elemente mehr draufliegen.
Da müsste man hier eigentlich eine Fallunterscheidung machen.
Und den zweiten Fall noch dazuschreiben. Das können Sie sich
noch da drunter schreiben. Und wenn das natürlich eintritt,
dann sind die realen Kosten nicht K,
sondern die realen Kosten werden ja dann die Stackhöhe,
weil alles vom Stack abgebaut wird.
Und die Stackhöhe Nachausführung der Operationen wäre null. Und hier
würde die Stack-Operation vorher stehen und die sind ja alle weg.
Das entspricht dann dem und dann kommt auch null raus.
Den Fall müsste man streng genommen eigentlich hier noch da drunter schreiben.
Dieser Sonderfall, dass die Multipop-Operation alle Elemente herunternimmt. Okay,
das heißt, amortisierte Kosten 2.00 und wir haben gesehen,
dass die Potenzialfunktion immer positiv ist.
Das heißt, diese amortisierten Kosten stellen für jede Folge von Operationen
eine obere Schranke da in der Summe natürlich nur.
Und damit haben wir für N-Operation eine Worst-Case-Laufzeit von OVN.
Frage. Ich habe gerade bei Multipop gesagt,
dass man Freiunterscheidungen machen müsste. Man müsste bei Pop doch
auch eine Folgeunterscheidung machen, denn nichts auf dem Steady,
weil dann ja die Kosten ein sind.
Ja, das ist nicht verringert. Und sie meinten gerade,
dass es für eine beliebige Folge Worst-Case-Abschätzung ist.
Ich gebe die Folge Pop, Push, Pop.
Da habe ich nach der amortisierten hier null plus 2.0.
Aber tatsächlich habe ich noch für Pop-Operationen ein
Ja, ja, das stimmt, das stimmt. Ja,
ja, da haben sie recht, da haben sie recht.
Also das heißt, diese Analyse berücksichtigt nicht den Fall,
dass sie beliebig viele Pop-Operationen ausführen können,
die immer auf dem leeren Stack operieren,
weil sie dann immer eine Kosteneinheit verbrauchen.
Sie kommen gleich dran, da Ihnen das schon aufgefallen ist,
können Sie das denn heilen? Ja, man könnte jetzt so sagen,
ist im Worst Case 1 bei Pop und dann bleibt man
bei Konstanten raus, macht nichts auf jeden Fall stimmt.
Genau, genau, das wäre die Art und Weise,
wenn man das jetzt genau, das wirklich richtig behandeln würde,
könnte man hier eine Eins einsetzen und nee,
hier darf man nichts einsetzen,
wir müssen natürlich hier über die Potenzialfunktion argumentieren.
Dann ist es schon ein bisschen schwieriger.
Dann müsste man da die Anzahl der Stack-Unterläufe,
also die Anzahl der Fehler irgendwie, in die Potenzialfunktion einbauen.
Das wäre ein bisschen unangenehm. Aber das könnte man
zur Not natürlich auch noch irgendwie machen. Ich denke,
da wir ja nicht gesagt haben, wie wir Fehler behandeln,
kann das ja noch einmal auftreten.
Dann bricht das Problem bei mir ab. Wir haben ja
keinerlei Möglichkeit, diese Fehler abzufangen. Ja, das ist...
Der Eindruck, mehr gehen wir also gut. Genau,
das ist immer die besonders benutzerfreundliche Art und Weise der Programmierung.
Ja, sowohl irgendein interner Fehlerauftritt gibt es den Segmentation Falt
und die Möglichkeit, den Fehlerbericht zu senden.
Ich sage nicht wohin.
Wo sie das irgendwie ankosten für die Fehlerbehandlung dabei
Ja, ich glaube, wie das Soziale war,
ist dann nicht ganz falsch,
weil dann kam natürlich einfach, sagen wir,
wir meditieren nie in die Sequenzen, uns angucken können.
Wir können ja diese Sequenz auch irgendwie einschränken.
Wir können ja sagen, dass wir auch nur ausdrücken,
ja. Irgendwann hat er die Fenster. Genau, das
könnte man auch machen. Ja, dass man sagt,
dass... Ich muss ihn wieder damit intlementiert haben,
wahrscheinlich eh nicht richtig.
Genau, genau, das stimmt, da ist ein Fehler auch drin.
Gut, okay, schauen wir uns uns unser Binary Inkrement
an und überlegen uns hier
eine Potenzialfunktion
Was wir machen, ist, ich werde das mal,
damit das ein bisschen kompakter aufzuschreiben ist,
bezeichne ich mal mit BI die Anzahl der Bits,
die gerade auf eins gesetzt sind zu dem Zeitpunkt I.
Also es ist ja schon klar, dass das irgendwie mit
der Anzahl der auf eins gesetzten Bits zu tun haben muss.
Das bezeichnen wir als WI und TI. Es ist jetzt
die Anzahl der Bits, die während der Iton-Inkrement-Inkrement-Operation von eins
auf null gesetzt werden.
Also das BI beschreibt also die Anzahl der Einsen
und hat irgendwas damit zu tun
Ja, hat was mit dem Potenzial zu tun,
wie wir gleich sehen werden. Und das T-I beschreibt irgendwie
die Kosten, weil das T-I sagt uns,
wie viele Bits wir von eins auf null setzen müssen.
Das ist also das, was in der Wildschleife quasi passiert.
Das bedeutet also, die realen Kosten sind immer proportional zu T-I plus eins.
Ja, also T-I-Bits werden von eins auf null gesetzt.
Und dann haben wir noch das letzte Bit von der Set-Operation.
Das wird von null auf eins gesetzt. Das heißt,
reale Kosten sind proportional zu T-I plus eins.
Und dann haben wir noch dieses B-I und können überlegen,
wie es mit dem B-I passiert.
Das ist die Anzahl der Bits, die auf eins
gesetzt sind
Und dann sehen wir, wenn wir so eine Operation ausgeführt haben,
dann verringert sich ja die Anzahl der auf eins gesetzten Bits.
Und zwar, dass wir zu B-1, also die Anzahl der Bits,
die wir vorher haben, minus T-I plus 1,
also T-I, viele Bits wechseln die Position von 1 auf 0.
Die müssen wir abziehen. Und ein Bit wechseln aber von 0
auf 1 und das müssen wir wieder addieren. So,
und eigentlich wäre das sogar gleich,
wir schreiben jetzt hier aber kleiner gleich für den Fall,
dass wir einen Zähler Overflow haben, also für den Fall,
dass wir von der Einserzeit sind es eins auf die komplette Null rüberzählen.
Das ist eigentlich auch ein Fall, den wir gar nicht betrachten,
aber formal schreiben wir dann lieber hier kleiner gleich hin.
Für alle anderen Fälle ist das auch identisch, also gleich.
So, als Potenzialfunktion verwenden wir die Anzahl der auf eins
gesetzten Witz. Das haben wir eben schon gesehen,
dass das immer ein Maß dafür ist oder es ist
ein ganz gutes Maß dafür ist für den Aufwand,
den wir noch betreiben müssen für diese Reset-Operation,
wo wir ja nicht genau wissen, wie teuer die ist.
Und dann können wir wieder unsere Analyse machen.
Das geht hier auch ganz einfach. Wir wissen also
diese Anzahl der auf eins gesetzten Bits ist,
Am Anfang null, der Zähler ist null am Anfang.
Und zu jedem Zeitpunkt ist diese Anzahl natürlich größer, gleich null.
Wir können da keine negativen Zahlen haben. Und was sind
jetzt die amortisierten Kosten für die Ausführung einer Binary Increment-Funktion?
Da hätten wir reale Kosten plus Potenzialdifferenz. Die realen Kosten
hier ist TI plus 1, das steht hier.
Die Potenzialdifferenz, also Potenzialfunktion, ist die Anzahl der
auf eins gesetzten Bits,
So, und dann wissen wir,
dass wir das BI abschätzen können durch BI minus 1,
minus TI plus 1,
also die Anzahl der dann noch gesetzten Bits,
das können wir hier einsetzen an dieser Stelle.
Und dann steht hier TI plus 1 von den realen Kosten plus BI minus 1,
minus TI plus 1 für die Anzahl der auf 1 gesetzten Bits nach Ausführung der Operation,
minus BI minus 1 für die Anzahl der auf 1 gesetzten Bits vor der Operation.
Und das ist genau zwei. Ja,
also amortisierte Kosten 2 und damit wäre das konstant
Okay, und damit sind wir durch die Tür.
Das heißt, eine beliebige Folge von Binary Increment-Funktionen auf Funktionsaufrufen
von N-Binary Increment-Funktionsaufrufen hätte dann Kosten von N.
Ja, und so haben wir wieder gezeigt,
dass wir amortisiert für jeden Aufruf konstante Zeit benötigen.
Was machen wir spaßeshalber noch machen kann, ist,
dass man sich überlegt, okay, was passiert denn,
wenn der Zähler von irgendeinem Zustand EU zu irgendeinem Zustand V zählt?
Also wir wollen jetzt eine, wir sagen jetzt nicht,
der fängt immer bei Null an der Zähler,
sondern der hat schon
irgendeinen Wert
Und dann zählen wir jetzt ein bisschen, bis zu der
nächsten Zahl V und haben jetzt so einen Intervall von USV.
Und wie viel Zeit benötigen wir dann? Auch das kann
die Methode quasi leisten. Wir setzen einfach für diesen Fall,
wir haben ja die Summe von U bis V,
wird in den realen Kosten.
Das ist, da haben wir Zweit-Zeit-Einheiten für die amortisierten Kosten,
minus die Potenzialdifferenz zum Zeitpunkt V, ist vielleicht ein bisschen klein.
B steht BV plus BU, also Potenzialdifferenz zum Zeitpunkt
Nachausführung der Operation,
des BV und hier Potenzial
Der SBU. Und wenn wir das einsetzen, kriegen wir dann hier
genau zweimal vminus uplus 1, minus BV plus BU. Das heißt,
wir können von den beiden Zählerständen die Anzahl der Einsen bestimmen und die Differenz davon und wissen,
dass das genau die Zeit ist, die wir zusätzlich benötigen,
aber da das eben auch maximal nur Lock-in viele Bits sein können,
ist das natürlich immer noch beschränkt durch O von N.
Also auch jede Teilsequenz ist immer durch O von N beschränkt. Das heißt jetzt aber nicht,
dass dann die amortisierten Kosten konstant ist,
weil wenn ich eine Teilsequenz wähle, der Länge 1,
wo ich aber gerade den Zähler von 011711 1111 auf 11000000 Umschalte,
habe ich nur gesagt, dass die Laufzeit beschränkt ist durch N, ja, nicht konstant ist.
Ja, also ich weiß, wenn die Initial sein soll.
Ja. Und wie dann Login maximal am Bitzkippen können.
Warum ist es da nicht durch einfach Login geschränkt?
Also, weil wir die Anzahl der,
also es ist durch die Differenz von U&V bestimmt.
Und diese Summe hier kann maximal bis N laufen,
indem wir den Zähler komplett durchlaufen lassen.
Also, das ist eine Abschätzung hier.
Das kommt zustande. Ach, Witz ist dann enger
Nein, nein, nein, nein.
Bei 8 Bits ist N256. Der Erzähler kann 256 Zustände durchlaufen.
Das passiert hier in dieser Summe. Ja? Okay,
ja, ja. Also diese, das hier,
das hier ist beschränkt durch Login und der Teil durch N.
Ja, ja? Ja, klar. Also es sind
jetzt irgendwie auch N. Informieren oder was?
Genau. Im Worst Case, ja. Okay. So
, okay, so, super. Jetzt wollen wir mal
was Richtiges analysieren
Und nicht so Spielfunktionen. Jetzt analysieren wir etwas,
was wir in der Praxis in der Programmierung ganz häufig nutzen.
Und zwar sind das dynamische Arrays. Und die kann man
eben genau mit dieser amortisierten Analyse hervorragend analysieren.
Was sind diese dynamischen Arrays? Die dynamischen Arrays sind,
wir benutzen Datenstrukturen, wo wir Arrays brauchen.
Zum Beispiel Stacks oder Warteschlangen etc. So wie wir das eben
auch gesehen haben beim Stack. Das Problem dabei ist allerdings,
dass unsere Programmiersprachen eigentlich immer voraussetzen, dass wir eine gewisse Menge Speicher vorher reservieren.
Dazu müssen wir sagen, wie groß der Stack maximal werden
kann oder die Warteschlange oder die Hash-Tabelle, wo immer wir unser Array benötigen.
Und das ist natürlich ein Problem, weil wir das vorher nicht wissen.
Entweder machen wir eine ganz grobe Abschätzung und wir brauchen unheimlich viel Speicher.
Oder wir machen eine ganz knappe Abschätzung, aber wenn es dann schief geht,
dann müssen wir jedes Mal sagen, Arror geht leider nicht.
Deswegen brauchen wir einen Mechanismus, um diese Array-Größen anzupassen. Wir wollen also,
wenn wir jetzt zum Beispiel ein Array haben, Stapel,
einem Stack oder in einer Warteschlange,
dann wollen wir das erstmal klein machen und dann benutzen wir das Area,
wenn wir dann sehen, oh, wir brauchen mehr Platz,
dann holen wir uns mehr Speicherplatz und dann können wir das ja kopieren und weitermachen damit, ja.
Das wäre also die Strategie. Wir müssen kopieren,
weil das Speichermanagement im Computer wird nicht dafür sorgen können,
dass an der Stelle,
wo sie jetzt schon das Array platziert haben im Speicher,
danach so viel Platz ist, dass sie das dann entsprechend nochmal verdoppeln können.
Das wissen sie ja nicht. Okay,
also das heißt, unser Speichermanagement funktioniert wie folgt
Müssten eigentlich Vorverwendungen die Größe festlegen.
Und wenn das dann zu klein oder zu groß ist,
haben wir jedes Mal ein Problem,
bei zu klein kann das Programm nicht ausgeführt werden,
weil zu groß haben wir eine schlechte Auslastung des Speichers.
Das, was man macht, sind dynamische Arrays,
die passen ihre Größe bei dem Bedarf an.
Und sie sagen jetzt vielleicht schon alle, da kann ich
mich ja gar nicht dran erinnern, dass ich sowas brauche.
Ich benutze es auch immer, den Vector oder die Setklasse oder sonst was.
Das mag alles sein. Das liegt aber auch nur daran,
weil genau diese Datenstrukturen, die sie dann in ihrer Library benutzen,
diese Technik verwenden werden,
die ich ihnen jetzt zeigen werde,
nämlich diese dynamischen Arrays realisieren
So, diese dynamischen Race haben jetzt natürlich ein Problem,
ja. Also wenn ich jetzt sage, ich mache das
mal ein bisschen kleineres Array, dann arbeite ich damit.
Und jetzt kommt der Zeitpunkt, wo sie sagen, es hat nicht gepasst.
Ja, jetzt habe ich noch ein Element, das müsste da rein.
Array ist zu klein. Dann sagen sie, oh,
kein Problem, ich mache das Area einfach größer.
Aber der Speicherbereich liegt jetzt woanders. Das heißt,
alles, was sie da stehen haben, müssen sie kopieren.
Ja, und dann haben sie das Problem,
dass die Einfügeoperation nicht mehr konstante Zeit benötigt, weil alles,
was sie im Array stehen haben, müssen sie kopieren.
Das ist teuer. Und hier stellt sich natürlich jetzt die Frage,
können wir dieses vergrößern des Arrays und wir werden es auch verkleidern,
um die Speicherauslastung zu optimieren.
Wie können wir das so gestalten, dass wir amortisiert für
das Speichern in dem Array konstante Zeit benötigen?
Ja, das ist unser Ziel. Wir wollen nach wie vor,
dass wenn wir ein Objekt irgendwo in den Speicher legen,
also in das Array reinkopieren, konstante Zeit notwendig ist.
Und zwar so, dass der Speicher bis zu einem gewissen Grade optimal ausgenutzt wird. So,
im Folgenden werden wir also mit T immer unser Array bezeichnen,
T für Table, so wie Array und T.G. ist also das Attribut,
was die Größe Böse, das Arrays beschreibt und T von NT.N ist die Anzahl der belegten Felder.
Also die Anzahl der Felder, wo sie was reingeschrieben haben.
Das können sie je nach Anwendung, kann das
was anderes sein. Also die Stackhöhe bei einer Hash-Tabelle,
wer ist die Anzahl der Einträge, die sie schon benutzt haben etc.
Und aus den beiden berechnen wir zusammen einen Belegungsfaktor und der
Belegungsfaktor ist das Verhältnis von den Benutzen oder belegten Feldern,
dividiert durch die Tabellengröße oder Array-Größe.
Das ist unser Belegungsfaktor und der Belegungsfaktor sagt uns,
wie effizient eigentlich unsere Datenstruktur ist
Okay, und dann haben wir, ja,
dann haben wir noch den Sonderfall,
dass unser Array ganz am Anfang vielleicht leer ist,
also Größe Null hat, dann sagen wir,
das hat den Belegungsfaktor eins.
Es ist nur, damit wir die Division durch eins verweilen.
In der Praxis wird man das natürlich nicht so machen.
Also in der Praxis wird man diese ganze Geschichte mit einer
sinnvollen Array-Größe starten und unter die auch nicht zurückfallen.
Am Anfang ist das natürlich relativ ineffektiv. Und man macht
das dann nur, wenn die Arrays größer werden.
So, was ist jetzt unsere Strategie, die Strategie,
die sich hinter diesen Datenstrukturen verbirgt, ist die Expansionskontraktionsstrategie.
Das heißt, wir fügen etwas in das Array ein
Der Belegungsfaktor 1 ist, dann ist das Array voll,
also die Anzahl der gespeicherten Elemente ist genauso groß
wie die Anzahl der, wie Tabellengröße.
Und wir werden in diesem Fall die Array-Größe verdoppeln.
Wenn wir sie verdoppelt haben, ist danach der Belegungsfaktor eineinhalb.
So, jetzt fügen wir dann wieder was ein,
dann löschen wir mal wieder was. Und wenn wir löschen,
dann kann es sein, dass der Belegungsfaktor unter ein Viertel fällt.
Und wenn der unter ein Viertel fällt,
werden wir die Array-Größe halbieren.
Danach ist der Belegungsfaktor wieder
ein Halb
Damit erreichen wir, dass wir bei so einem dynamischen Wachsen und Verkleinern
des Airrays im Mittel irgendwie so immer so bei einem Halb liegen.
Und das ist schon eine ganz sinnvolle Auslastungszahl.
Man sollte natürlich auch nicht ignorieren, dass dieses Verdoppeln im Zweifelsfall
auch irgendwann an die Speichergrenzen stoßen kann.
Also die Strategie ist dann auch nicht perfekt,
aber dann liegt es in meiner Regel daran,
dass der Speicherdienst zur Verfügung haben,
vielleicht dann nicht groß genug ist.
So, also diese Strategie, die stellt sicher,
dass wir zu jedem Zeitpunkt einen Belegungsfaktor haben,
der größer gleich ein Viertel ist
Weil wenn es kleiner wird als ein Viertel,
halbieren wir ja, dass Array. Und ja,
und dass wir auch immer korrekt arbeiten können,
selbst wenn das Array mal sozusagen überläuft, weil wir dann,
dann ist der Belegungsfaktor 1, weil es dann entsprechend vergrößern.
Okay, die, das Betriebssystem muss jetzt zwei Operationen machen.
Das Betriebssystem macht eine, also auf der Betriebssystemseite müssen wir
ein Elocate machen. Die C-Programmierer kennen das noch. Die
Jahre war Programmierer, haben es im Leben nicht gesehen.
Ja, das Elocate reserviert im Speicher,
also irgendwo im Speicher,
eine gewisse Größe
Von K Speicherzellen, für K-Objekte. Und die,
es ist für uns also so ein K-Elementiges Array.
Und das Betriebssystem verwaltet auch das Größenattribut AG,
also wie viele Elemente, dass Array groß ist.
Es verwaltet nicht AN, also wie viele Objekte tatsächlich in
dem Array gespeichert werden, dass es dem Betriebssystem egal ist.
Aber dann gibt es noch die Free-Funktion und die
gibt den Speicher für das RAR wieder frei.
Okay, das sind die Basisoperationen. Jetzt brauchen wir noch
ein bisschen mehr. Wir brauchen natürlich noch
eine Copy-Funktion
Weil wir müssen, wenn wir das Array jetzt vergrößern,
den Inhalt rüber kopieren. Auch das gibt es auf Betriebssysteme-Ebenen,
verschiedene Art und Weise. Wir brauchen also eine Copy-Operation mit
zwei Parametern B und A. Das sind beides mal Arrays.
Und diese Copy-Operation kopiert jetzt den Inhalt des ARSA nach B.
Dabei müssen wir natürlich sicherstellen, dass B eine größere Tabelle ist als A-Elemente hat.
Also wenn in A in viele Elemente sind, dann muss B
mindestens so viel Platz haben, um die alle aufzunehmen.
Anderenfalls gibt es eine Fehlermeldung und die werden wir
in der Analyse jetzt nicht berücksichtigen
Diese Copy-Funktion ist nicht ganz ohne. Ja,
im ersten Augenblick denkt man, ja klar, Mem-Copy,
da wird einfach so ein Speicherbereich hin und her kopiert.
Das ist es aber häufig nicht, weil wir ja mit Eric-Positionen Dinge verbinden.
Ja, also eine Eric-Position hat ja für uns in der Regel eine Semantik.
Da verbirgt sich ja etwas dahinter. Zum Beispiel eine Hash-Funktion,
ja, die dann sagt, wo wir ein Objekt wiederfinden.
Und dann können wir nicht einfach das alles kopieren.
Das heißt, diese Kopierfunktion ist anwendungsspezifisch häufig. Und auch
die Laufzeit ist natürlich so ein bisschen so eine Frage.
Also wir werden in unserer Analyse jetzt von einem sehr günstigen Fall ausgehen,
nämlich, dass wir diese Kopieroperation in O von AN erledigen können.
Das heißt, AN ist die Anzahl der Elemente, die im RAR belegt sind.
Nicht die Größe der Tabelle. Und das ist überhaupt nicht trivial,
das zu machen, weil, wenn sie sich mal überlegen,
sie hatten jetzt so einen Array,
normalerweise würden sie das wahrscheinlich so kopieren,
dass sie einmal über das Array drüberlaufen und gucken, ah,
da steht was, das muss ich jetzt rüber kopieren.
Das geht dann natürlich nicht, weil dann hängt die Laufzeit
hier nicht von der Anzahl der gespeicherten Elemente,
sondern von der Größe der Tabelle A ab.
Und dann wäre es potenziell etwas anderes.
Wenn sie zum Beispiel einen Stack haben,
ist das
kein Problem
Aber hier muss man halt aufpassen. Jetzt könnten sie sagen,
ja, aber wenn wir das Array doch vergrößern,
dann wissen wir doch in dem Augenblick, wo wir es vergrößern,
ist das Array A. ja komplett voll.
Ist ja voll rein, ist ja alles reingeschrieben sozusagen, ja.
Also ist ja A. von N gleich A. von G. A. Punkt
N ist gleich A.G., weil es Array ja voll ist,
dann wäre es egal. Das stimmt. Also das heißt,
beim Expansionsschritt spielt es noch nicht so eine Rolle.
Aber beim Kontraktionsschritt, da wird es übel, ja,
weil da ist nämlich das AN nur ein Viertel von AG
Also da ist nur jede vierte Speicherzelle im Mittel verbraucht.
Und da haben wir ein Problem. Das ist auch nur ein Faktor,
aber trotzdem spielt das dann hier dann im Zweifelsfall eine Rolle.
Okay, so. Ja, dann brauchen wir noch
zwei Operationen, die irgendwie was in das Array einfügen.
Das ist unsere Insight und die Lead-Operation.
Und die kümmern sich jetzt nicht mehr um die Array-Größe.
Und das ist genau das, was sie jetzt zum Beispiel,
wenn sie einen Vektor benutzen in der Programmiersprache, der sich dynamisch anpasst.
Das ist die Operation, die sie nach außen im Winterface sehen.
Da sehen sie den Instagram und die Lead können das Objekt da einfügen.
Und ob dann irgendwie der Speicher nicht passt oder so,
Nee, das war jetzt falsch rum. Das sind die Operationen,
um da die Tabelle einzufügen und nach außen sehen sie die Operationen,
die quasi die Tabellengröße entsprechend automatisch anpasst.
Die geht jetzt davon aus, dass an der Stelle X Platz ist.
Okay, so, so sieht jetzt das Table Insight und das Table Delead aus.
Das sind quasi die Funktionen, die sie nach außen hin sehen.
Ja, wenn sie ein Objekt einfügen. Sie wollen jetzt
also ein Objekt X in die Tabelle Tee einfügen.
Der Algorithmus geht wie folgt. Wir gucken,
ob die Größe des Aris 0 ist.
Wenn ja, dann holen wir uns erstmal Speicher für
ein Datenelement
Dann wissen wir, wir können dieses eine Element hier speichern.
Anderenfalls schauen wir noch, ob TN gleich TG ist.
Wenn die gleichgroß sind, also wenn in der Tabelle T
genauso viele Elemente gespeichert sind wie die Tabelle,
groß ist, dann haben wir ein Problem.
Wir können nämlich das neue Element X nicht mehr
in der Tabelle speichern. Also vergrößern wir unser Ray,
indem wir ein neues Array aluziieren, T-Strich.
Das ist doppelt so groß wie das alte Array.
Und dann müssen wir unsere Copy-Funktion verwenden,
die kopiert dann
das RAT nach T-Strich
Und dann machen wir noch ein Free von dem alten RAT,
das brauchen wir jetzt nicht mehr.
Und dann lassen wir Tee auf den Strich verweisen. Ja,
das ist jetzt hier keine Zuweisung in dem Sinne,
dass hier was kopiert wird, sondern dass es nur eine,
ja, das Tee ist wie ein Pointer, ja,
also wie eine Adresse.
Wir merken uns jetzt einfach in Tee, das neue RAT-Strich.
Ja. Okay, dieser ganze Bereich von dem Anfang des
IF bis hier bis zum Ende des L-Zweiges dient dazu,
die Tabelle anzupassen.
Und danach können wir jetzt ein Element einfügen.
Da kommt die Inside-Operation und fügen das Objekt X ein und
das T von Envirt um 1 erhöht.
Ja,
wir haben ein Element mehr in der Tabelle gespeichert
Okay. So, und was machen wir bei einem De-Lead
bei einem De-Lead? Was mir jetzt gerade noch auffällt,
also rein formell, wenn man ganz exakt sein will,
müsste man natürlich hier noch das T-Strich.n kopieren.
Also das, weil ich ja gesagt habe,
es wird nicht vom Betriebssystem verwaltet,
also müsst ihr sowas stehen wie T-Strich von N,
Leichtee von N, damit man hier jetzt keinen Fehler macht.
Aber ich glaube, das ist klar, ist das.
Teleportee, sonst könntet ihr auch fürs Insert
das nicht verwalten
Ja, also sie wissen, was ich meine.
Also die Anzahl N, das Attribut N muss irgendwie auch mitverwaltet werden.
Egal. Gut, okay, beim Delete geht es eigentlich genauso.
Wir haben natürlich nicht den Fall Fehlermeldungen, fassen wir hier nicht an.
Also wir löschen das Objekt, wir gehen davon aus,
es ist tatsächlich da. Damit ist auch,
dass Array auf alle Fälle da und nicht die große Null.
Das DVN Enved U1 verkleinert. Wenn wir jetzt sehen,
dass die Auslastung kleiner ist als ein Viertel, das heißt,
dass viermal TVN in kleiner ist als TVN G,
dann kommt der Kontraktionsschritt.
Das heißt, wir besorgen uns ein neues Array,
was nur noch halb so groß ist
Und jetzt kopieren wir wieder von Tee nach T-Strich,
löschen das alte RAT und das Tee wird zu T-Strich.
Auch hier kann man jetzt wieder argumentieren, ja,
also in der Praxis würde das Betriebssystem wahrscheinlich eine Verkleinerung des ARAs dadurch realisieren,
dass einfach der hintere Teil des ARAs freigegeben wird und der vordere bestehen bleibt,
dann müsste man vielleicht auch nicht kopieren.
Das stimmt, dann hat man hier natürlich auch einen geringeren Aufwand.
Aber wir gehen mal davon aus,
dass das hier tatsächlich so funktioniert
Okay, jetzt können wir die Laufzeit betrachten von diesen Funktionen.
Dann sehen wir, okay, also Table Insert,
da haben wir also das elementare Insert,
also hier brauchen wir eine Zeit für diese Insert-Operation.
Und wir haben dann die Zeit für das dynamische Speichermanagement.
Das ist der obere Teil dieses Algorithmus. Die Laufzeit für Elocate,
SO von K, weil wir mal davon ausgehen,
dass wir diesen Speicher initialisieren müssen.
Da müssen wir alle Speicher zählen, wo einmal ankacken.
Die Laufzeit für die Free-Operation ist allerdings O von 1.
Das entspricht tatsächlich auch das Verhalten des Betriebssystems.
Da müssen sie nur die Look-Up-Table des Betriebssystems verwalten und
den Speicher entsprechend in der Look-Up-Table als frei wiederum markieren.
Die Laufzeit für die Inside-Operation, ist jetzt T-Insert.
Wir haben natürlich auch noch eine Deleted-Operation, die beiden könnten asymmetrisch sein.
Also Insert könnte zum Beispiel teurer oder billiger sein als ein Delete.
Den Fall werden wir hier auch nicht weiter betrachten.
Wir werden mal davon ausgehen, dass die beiden asymptotisch in
etwa die gleiche Zeit benötigen. Meistens ist es konstant,
wenn man es in Array einfügt,
aber wenn sie sich zum Beispiel vorstellen, ihre Datenstruktur wäre eine Prioritätswarteschlange,
dann wäre Insert und die Lead in der Größenordnung von Login.
Okay, so ist das Verhalten des dynamischen Arrays.
Das sind die Algorithmen, die wir haben. Und jetzt
brauchen wir eine Potenzialfunktion, mit der wir herausfinden können,
was das asymptotische Laufzeitverhalten dieser Funktion ist. Denn eins ist klar,
wenn es uns hier erwischt und wir müssen die Expansion oder die Kontraktion machen,
dann benötigen wir im Worst Case O von Endzeit,
wenn hier N-Objekte gespeichert sind.
Während wir ansonsten eher konstante Zeit benötigen. Und es ist aber auch klar,
dass diese Expansion und Kontraktion nur entsprechend selten auftritt.
Gut, okay, da machen wir jetzt einen Break und
dann am Mittwoch gucken wir uns an,
wie das amortisiert dann aussieht.
Das war es dann für heute