Es gibt viele Fragen zur Laufzeitanalyse von Algorithmen (siehe zB Laufzeitanalyse und Algorithmusanalyse ). Viele sind ähnlich, zum Beispiel diejenigen, die nach einer Kostenanalyse von verschachtelten Schleifen oder Divide & Conquer-Algorithmen fragen, aber die meisten Antworten scheinen maßgeschneidert zu sein.
Andererseits erklären die Antworten auf eine andere allgemeine Frage das größere Bild (insbesondere in Bezug auf die asymptotische Analyse) mit einigen Beispielen, aber nicht, wie man sich die Hände schmutzig macht.
Gibt es eine strukturierte, allgemeine Methode zur Analyse der Kosten von Algorithmen? Die Kosten können die Laufzeit (Zeitkomplexität) oder ein anderes Maß für die Kosten sein, z. B. die Anzahl der ausgeführten Vergleiche, die Platzkomplexität oder etwas anderes.
Dies soll eine Referenzfrage werden, auf die Anfänger hinweisen können; daher sein weiter gefasster Anwendungsbereich. Bitte achten Sie darauf, allgemeine, didaktisch aufbereitete Antworten zu geben, die an mindestens einem Beispiel illustriert sind, aber dennoch viele Situationen abdecken. Vielen Dank!
Antworten:
Code in Mathematik übersetzen
Bei einer (mehr oder weniger) formalen operativen Semantik können Sie den (Pseudo-) Code eines Algorithmus buchstäblich in einen mathematischen Ausdruck umwandeln, der das Ergebnis liefert, vorausgesetzt, Sie können den Ausdruck in eine nützliche Form umwandeln. Dies funktioniert gut für additive Kostenmaße wie die Anzahl der Vergleiche, Auslagerungen, Anweisungen, Speicherzugriffe, Zyklen, die einige abstrakte Maschinenanforderungen usw. erfordern.
Beispiel: Vergleiche in Bubblesort
Betrachten Sie diesen Algorithmus, der ein bestimmtes Array sortiert
A
:A
for
Dabei ist der Preis für jede Ausführung von Zeile 5 (die wir zählen).1
Beispiel: Swaps in Bubblesort
Ich werde bezeichne das Unterprogramm , das aus Linien besteht zu und durch die Kosten für die Ausführung dieses Unterprogrammes (einmal). C i , jPi,j Ci,j
i
j
Nehmen wir nun an, wir wollen Swaps zählen , also wie oft ausgeführt wird. Dies ist ein "Basisblock", dh ein Unterprogramm, das immer atomar ausgeführt wird und konstante Kosten verursacht (hier ). Das Zusammenziehen solcher Blöcke ist eine nützliche Vereinfachung, die wir oft anwenden, ohne darüber nachzudenken oder darüber zu sprechen. 1P6,8 1
Mit einer ähnlichen Übersetzung wie oben kommen wir zu folgender Formel:
Beachten Sie, dass ich anstelle von als Parameter verwende. wir werden gleich sehen warum. Ich addiere und als Parameter von da die Kosten hier nicht davon abhängen (also im einheitlichen Kostenmodell ); im Allgemeinen könnten sie nur.A n i j C5,9
Es ist klar, dass die Kosten von vom Inhalt von (den Werten und insbesondere) abhängen , daher müssen wir dies berücksichtigen. Jetzt stehen wir vor einer Herausforderung: Wie "packen" wir ? Nun, wir können die Abhängigkeit von explizit machen:P5,9 A C5,9 A
A[j]
A[j+1]
Für ein bestimmtes Eingabearray sind diese Kosten genau definiert, wir möchten jedoch eine allgemeinere Aussage treffen. wir müssen stärkere Annahmen treffen. Untersuchen wir drei typische Fälle.
Der schlimmste Fall
wir nur die Summe betrachten und feststellen, dass , können wir eine unbedeutende Obergrenze für die Kosten finden:C5,9(A(i,j))∈{0,1}
Aber kann das passieren , dh wird ein für diese Obergrenze erreicht? Wie sich herausstellt, ja: Wenn wir ein umgekehrt sortiertes Array von paarweise unterschiedlichen Elementen eingeben, muss bei jeder Iteration ein Swap durchgeführt werden¹. Daher haben wir die exakte Worst-Case- Anzahl von Swaps von Bubblesort abgeleitet.A
Der beste Fall
Umgekehrt gibt es eine triviale Untergrenze:
Dies kann auch passieren: Auf einem bereits sortierten Array führt Bubblesort keinen einzelnen Swap aus.
Der durchschnittliche Fall
Am schlimmsten und bestenfalls eine ziemliche Lücke öffnen. Aber was ist die typische Anzahl von Swaps? Um diese Frage zu beantworten, müssen wir definieren, was "typisch" bedeutet. Theoretisch haben wir keinen Grund, einen Eingang einem anderen vorzuziehen, und gehen daher normalerweise von einer gleichmäßigen Verteilung über alle möglichen Eingänge aus, dh jeder Eingang ist gleich wahrscheinlich. Wir beschränken uns auf Arrays mit paarweise unterschiedlichen Elementen und nehmen daher das zufällige Permutationsmodell an .
Dann können wir unsere Kosten so umschreiben²:
Jetzt müssen wir über die einfache Manipulation von Summen hinausgehen. Beim Betrachten des Algorithmus stellen wir fest, dass jeder Swap genau eine Inversion in (wir tauschen immer nur die Nachbarn³). Das heißt, die Anzahl von Swaps auf ausgeführt ist genau die Anzahl von Inversionen von . Somit können wir die inneren zwei Summen ersetzen und bekommenA A inv(A) A
Glücklicherweise wurde die durchschnittliche Anzahl der Inversionen ermittelt
Welches ist unser Endergebnis. Beachten Sie, dass dies genau die Hälfte der Worst-Case-Kosten ist.
i = n-1
der äußeren Schleife, die niemals etwas tut, nicht ausgeführt wird.Die allgemeine Methode
Wir haben im Beispiel gesehen, dass wir Kontrollstruktur in Mathematik übersetzen müssen; Ich werde ein typisches Ensemble von Übersetzungsregeln vorstellen. Wir haben auch gesehen, dass die Kosten für ein bestimmtes Unterprogramm vom aktuellen Status abhängen können, dh (ungefähr) von den aktuellen Werten von Variablen. Da der Algorithmus (normalerweise) den Status ändert, ist es etwas umständlich, die allgemeine Methode zu notieren. Wenn Sie sich verwirrt fühlen, schlage ich vor, dass Sie zum Beispiel zurückkehren oder sich selbst ein Bild machen.
Wir bezeichnen den aktuellen Status mit (stellen Sie sich dies als eine Reihe von Variablenzuweisungen vor). Wenn wir ein Programm auszuführen , beginnend im Zustand beenden wir im Zustand up (vorausgesetzt , endet).ψ ψ ψ/P
P
P
Einzelne Aussagen
Bei nur einer AnweisungCS(ψ)
S;
weisen Sie die Kosten . Dies ist normalerweise eine konstante Funktion.Ausdrücke
Wenn Sie einen Ausdruck
E
der Form habenE1 ∘ E2
(z. B. einen arithmetischen Ausdruck, bei dem∘
es sich um Addition oder Multiplikation handeln kann), addieren Sie die Kosten rekursiv:Beachten Sie, dass
Daher müssen Sie möglicherweise mit dieser Regel flexibel sein.
Reihenfolge
Bei einem Programm
P
als ProgrammfolgeQ;R
addieren Sie die Kosten zuBedingungen
Bei einem Programm
P
der Formif A then Q else R end
hängen die Kosten vom Staat ab:In der Regel kann die Auswertung
A
sehr wohl den Zustand ändern, daher die Aktualisierung der Kosten für die einzelnen Filialen.For-Loops
Weisen Sie bei einem vorgegebenen Programm
P
des Formularsfor x = [x1, ..., xk] do Q end
die Kosten zuDabei ist der Zustand vor der Verarbeitung für den Wert , dh nach der Iteration mit dem Setzen auf , ..., .ψi
Q
xi
x
x1
xi-1
Beachten Sie die zusätzlichen Konstanten für die Schleifenwartung. Die Schleifenvariable muss erstellt ( ) und ihre Werte zugewiesen werden ( ). Dies ist relevant seitcinit_for cstep_for
xi
kann teuer sein undfor
Schleife mit leerem Hauptteil (z. B. nach Vereinfachung in einer Best-Case-Einstellung mit bestimmten Kosten) hat keine Nullkosten, wenn sie Iterationen ausführt.While-Schleifen
Weisen Sie bei einem vorgegebenen Programm
P
des Formularswhile A do Q end
die Kosten zuDurch die Überprüfung des Algorithmus kann diese Wiederholung häufig als eine ähnliche Summe wie für for-Schleifen dargestellt werden.
Beispiel: Betrachten Sie diesen kurzen Algorithmus:
Durch die Anwendung der Regel erhalten wir
mit einigen konstanten Kosten für die einzelnen Anweisungen. Wir gehen implizit davon aus, dass diese nicht vom Zustand abhängen (die Werte von und ); Dies mag in der "Realität" zutreffen oder auch nicht: Denken Sie an Überläufe!c…
i
x
Jetzt müssen wir diese Wiederholung für lösen . Wir stellen fest, dass weder die Anzahl der Iterationen noch die Kosten des Schleifenkörpers vom Wert von abhängen , sodass wir ihn fallen lassen können. Wir bleiben mit dieser Wiederholung:C1,4
i
Dies löst mit elementaren Mitteln zu
symbolisch den vollen Zustand wieder einführen; Wenn , dann ist .ψ={…,x:=5,…} ψ(x)=5
Prozeduraufrufe
Weisen Sie bei einem Programm
P
der FormM(x)
für einige Parameter, beix
denenM
es sich um eine Prozedur mit einem (benannten) Parameterp
handelt, Kosten zuBeachten Sie noch einmal die zusätzliche Konstante (die tatsächlich von abhängen kann !). Prozeduraufrufe sind teuer, da sie auf realen Maschinen implementiert sind und manchmal sogar die Laufzeit dominieren (z. B. naives Auswerten der Fibonacci-Nummernwiederholung).ccall ψ
Ich beschreibe einige semantische Probleme, die Sie möglicherweise mit dem Staat haben. Sie möchten den globalen Status und solche lokalen Prozeduraufrufe unterscheiden. Nehmen wir an, wir übergeben hier nur den globalen Status und erhalten
M
einen neuen lokalen Status, der durch Setzen des Werts auf "p
to" initialisiert wirdx
. Darüber hinausx
kann es sich um einen Ausdruck handeln, von dem wir (normalerweise) annehmen, dass er vor dem Bestehen ausgewertet wird.Beispiel: Betrachten Sie die Vorgehensweise
Gemäß den Regeln erhalten wir:
Beachten Sie, dass wir den globalen Status außer Acht lassen, da auf
fac
keinen eindeutig zugegriffen wird. Diese besondere Wiederholung ist leicht zu lösenWir haben die Sprachfunktionen, die Ihnen begegnen werden, in einem typischen Pseudocode behandelt. Passen Sie bei der Analyse von Pseudocodes auf hoher Ebene auf versteckte Kosten auf. im Zweifelsfall aufklappen. Die Notation mag umständlich erscheinen und ist sicherlich Geschmackssache; Die aufgeführten Konzepte können jedoch nicht ignoriert werden. Mit etwas Erfahrung können Sie jedoch sofort erkennen, welche Teile des Staates für welches Kostenmaß relevant sind, zum Beispiel "Problemgröße" oder "Anzahl der Eckpunkte". Der Rest kann fallengelassen werden - das vereinfacht die Sache erheblich!
Wenn Sie jetzt denken, dass dies viel zu kompliziert ist, seien Sie gewarnt: es ist ! Es ist eine schwierige Aufgabe, die genauen Kosten von Algorithmen in jedem Modell abzuleiten, das sich so nah an realen Maschinen befindet, dass Laufzeitvorhersagen (auch relativ) möglich sind. Dabei werden Caching und andere unangenehme Effekte auf realen Maschinen nicht berücksichtigt.
Daher wird die Algorithmusanalyse häufig so vereinfacht, dass sie mathematisch nachvollziehbar ist. Wenn Sie zum Beispiel keine genauen Kosten benötigen, können Sie zu jedem Zeitpunkt (für obere bzw. untere Grenzen) über- oder unterschätzen: Reduzieren Sie die Menge der Konstanten, beseitigen Sie die Bedingungen, vereinfachen Sie die Summen und so weiter.
Ein Hinweis zu den asymptotischen Kosten
Was Sie normalerweise in der Literatur und im Internet finden, ist die "Big-Oh-Analyse". Der eigentliche Begriff ist asymptotische Analyse. Das bedeutet, dass Sie anstelle der exakten Ableitung der Kosten wie in den Beispielen nur Kosten bis zu einem konstanten Faktor und im Grenzbereich angeben (grob gesagt "für großes ").n
Dies ist (oft) gerecht, da abstrakte Aussagen in der Realität einige (im Allgemeinen unbekannte) Kosten verursachen, abhängig von Maschine, Betriebssystem und anderen Faktoren, und kurze Laufzeiten möglicherweise von dem Betriebssystem dominiert werden, das den Prozess überhaupt erst einrichtet, und so weiter. Sie stören also sowieso.
Hier ist, wie die asymptotische Analyse mit diesem Ansatz zusammenhängt.
Identifizieren Sie dominante Vorgänge (die Kosten verursachen), dh Vorgänge, die am häufigsten auftreten (bis zu konstanten Faktoren). Im Bubblesort-Beispiel ist eine mögliche Auswahl der Vergleich in Zeile 5.
Alternativ binden Sie alle Konstanten für Elementaroperationen an ihr Maximum (von oben) resp. ihr Minimum (von unten) und führen Sie die übliche Analyse.
Stellen Sie sicher, dass Sie die Bedeutung der Landau-Symbole verstehen . Denken Sie daran, dass solche Grenzen für alle drei Fälle existieren . Die Verwendung von impliziert keine Worst-Case-Analyse.O
Weitere Lektüre
In der Algorithmusanalyse gibt es noch viele weitere Herausforderungen und Tricks. Hier finden Sie einige Leseempfehlungen.
Wie kann man Algorithmen beschreiben, beweisen und analysieren?
Wie können wir annehmen, dass grundlegende Operationen mit Zahlen eine konstante Zeit benötigen?
Was ist eine Zeiteinheit in der Laufzeitanalyse?
Es gibt viele Fragen zur Algorithmus-Analyse , die ähnliche Techniken verwenden.
quelle
Anzahl der Anweisungen zur Ausführung
Es gibt eine andere Methode, die Donald E. Knuth in seiner Reihe The Art of Computer Programming vertreten hat . Im Gegensatz zur Übersetzung des gesamten Algorithmus in eine einzige Formel funktioniert er unabhängig von der Semantik des Codes auf der Seite "Zusammensetzen" und ermöglicht es, aus der Sicht eines Adlers nur dann auf eine niedrigere Ebene zu gelangen, wenn dies erforderlich ist. Jede Aussage kann unabhängig vom Rest analysiert werden, was zu klareren Berechnungen führt. Die Technik eignet sich jedoch gut für ziemlich detaillierten Code, nicht so sehr für Pseudocode höherer Ebenen.
Die Methode
Im Prinzip ist es ganz einfach:
Gesamtkosten berechnen
Sie können jederzeit Schätzungen und / oder symbolische Größen einfügen, um Verallgemeinern Sie das Ergebnis entsprechend.
Beachten Sie, dass Schritt 3 beliebig komplex sein kann. In der Regel müssen Sie dort mit (asymptotischen) Schätzungen wie " " arbeiten, um Ergebnisse zu erzielen.e77∈O(nlogn)
Beispiel: Tiefensuche
Betrachten Sie den folgenden Graph-Traversal-Algorithmus:
Wir nehmen an, dass der (ungerichtete) Graph durch Adjazenzlisten auf den Knoten . Sei die Anzahl der Kanten.{0,…,n−1} m
Schon beim Betrachten des Algorithmus stellen wir fest, dass einige Anweisungen genauso oft ausgeführt werden wie andere. Wir führen einige Platzhalter , und für die Ausführungszähler :A B C ei
Insbesondere ist da jeder rekursive Aufruf in Zeile 8 einen Aufruf in Zeile 3 verursacht (und einer durch den ursprünglichen Aufruf von ). Außerdem ist da die Bedingung einmal pro Iteration, dann aber noch einmal überprüft werden muss, um sie zu verlassen.e8=e3−1 e6=e5+e7
foo
dfs
while
Es ist klar, dass . Nun würden wir bei einem Korrektheitsnachweis zeigen, dass genau einmal pro Knoten ausgeführt wird; das heißt, . Aber dann durchlaufen wir jede Adjazenzliste genau einmal und jede Kante impliziert insgesamt zwei Einträge (einen für jeden Incident-Knoten); Insgesamt erhalten wir Iterationen. Daraus leiten wir folgende Tabelle ab:A=1 B=n C=2m
foo
Dies führt uns zu Gesamtkosten von genau
Indem wir geeignete Werte für instanziieren, können wir konkretere Kosten ableiten. Wenn wir beispielsweise die Speicherzugriffe (pro Wort) zählen möchten, würden wir verwendenCi
und bekomme
Weitere Lektüre
Siehe am Ende meiner anderen Antwort .
quelle
Die Analyse von Algorithmen ist, wie das Beweisen von Theoremen, größtenteils eine Kunst (z. B. gibt es einfache Programme (wie das Collatz-Problem ), die wir nicht analysieren können). Wir können ein Algorithmuskomplexitätsproblem in ein mathematisches Problem umwandeln, wie es Raphael ausführlich beantwortet hat. Um jedoch die Kosten eines Algorithmus in Form von bekannten Funktionen auszudrücken, müssen wir Folgendes tun:
quelle