Die Big-O-Notation (auch "asymptotische Wachstumsnotation" genannt) ist wie Funktionen "aussehen", wenn man konstante Faktoren und Dinge in der Nähe des Ursprungs außer Acht lässt . Wir verwenden es, um über Folgendes zu sprechen wie die Dinge skalieren .
Grundlagen
für "ausreichend" große Eingaben...
f(x) O(upperbound)
bedeutet f
"wächst nicht schneller als" upperbound
f(x) (justlikethis)
mittlere f
"wächst genau wie" justlikethis
f(x) (lowerbound)
bedeutet f
"wächst nicht langsamer als" lowerbound
Die Big-O-Notation kümmert sich nicht um konstante Faktoren: Die Funktion 9x²
soll "genau wie" wachsen 10x²
. Ebenso wenig wie Big-O asymptotisch die Notation zu beachten nicht asymptotisch stuff ("stuff near the origin" oder "what happens when the problem size is small"): die Funktion 10x²
soll "genau wie" wachsen 10x² - x + 2
.
Warum wollen Sie die kleineren Teile der Gleichung ignorieren? Weil sie von den großen Teilen der Gleichung völlig in den Hintergrund gedrängt werden, wenn man immer größere Maßstäbe anlegt; ihr Beitrag wird in den Hintergrund gedrängt und irrelevant. (Siehe Beispielabschnitt.)
Mit anderen Worten: Es geht um die Verhältnis wenn man ins Unendliche geht. Wenn Sie die tatsächlich benötigte Zeit durch die O(...)
erhalten Sie einen konstanten Faktor im Grenzfall großer Eingaben. Intuitiv macht das Sinn: Funktionen "skalieren" miteinander, wenn man die eine multiplizieren kann, um die andere zu erhalten. Das ist, wenn wir sagen...
actualAlgorithmTime(N) O(bound(N))
e.g. "time to mergesort N elements
is O(N log(N))"
... das bedeutet, dass für "ausreichend große" Problemgrößen N (wenn wir die Dinge in der Nähe des Ursprungs ignorieren), gibt es eine Konstante (z.B. 2,5, frei erfunden), so dass:
actualAlgorithmTime(N) e.g. "mergesort_duration(N) "
< constant < 2.5
bound(N) N log(N)
Es gibt viele Möglichkeiten, die Konstante zu wählen; oft ist die "beste" Wahl als "konstanter Faktor" des Algorithmus bekannt... aber wir ignorieren ihn oft, so wie wir nicht-größte Terme ignorieren (siehe Abschnitt "Konstante Faktoren", warum sie normalerweise keine Rolle spielen). Man kann sich die obige Gleichung auch als Schranke vorstellen und sagen: " Im schlimmsten Fall wird die Zeit, die dafür benötigt wird, nie schlechter sein als etwa N*log(N)
innerhalb eines Faktors von 2,5 (ein konstanter Faktor, der uns nicht viel bedeutet) ".
Generell, O(...)
ist am nützlichsten, weil wir uns oft für das Verhalten im schlimmsten Fall interessieren. Wenn f(x)
etwas "Schlechtes" wie die Prozessor- oder Speichernutzung darstellt, dann " f(x) O(upperbound)
" bedeutet " upperbound
ist das Worst-Case-Szenario der Prozessor-/Speichernutzung".
Anwendungen
Als rein mathematisches Konstrukt ist die Big-O-Notation nicht darauf beschränkt, über Verarbeitungszeit und Speicher zu sprechen. Sie können damit die Asymptotik von allem erörtern, bei dem eine Skalierung sinnvoll ist, z. B.:
- die Anzahl der möglichen Handshakes zwischen
N
Menschen auf einer Party ( (N²)
und zwar N(N-1)/2
aber wichtig ist, dass es "wie" skaliert N²
)
- Wahrscheinliche erwartete Anzahl von Personen, die virales Marketing gesehen haben, als Funktion der Zeit
- wie die Latenzzeit einer Website mit der Anzahl der Verarbeitungseinheiten in einer CPU oder GPU oder einem Computer-Cluster skaliert
- wie sich die Wärmeabgabe auf CPU-Chips in Abhängigkeit von der Anzahl der Transistoren, der Spannung usw. entwickelt.
- wie viel Zeit ein Algorithmus für die Ausführung benötigt, in Abhängigkeit von der Größe der Eingabe
- wie viel Platz ein Algorithmus zur Ausführung benötigt, in Abhängigkeit von der Größe der Eingabe
Beispiel
Im obigen Beispiel mit dem Händedruck schüttelt jeder in einem Raum die Hand des anderen. In diesem Beispiel, #handshakes (N²)
. Warum?
Ein bisschen zurück: Die Anzahl der Handshakes ist genau n-choose-2 oder N*(N-1)/2
(jede von N Personen schüttelt die Hände von N-1 anderen Personen, aber hier werden die Händedrücke doppelt gezählt, also durch 2 teilen):
Für eine sehr große Anzahl von Personen ist der lineare Term jedoch N
ist verschwindend klein und trägt effektiv 0 zum Verhältnis bei (im Diagramm: der Anteil der leeren Kästchen auf der Diagonale an der Gesamtzahl der Kästchen wird kleiner, wenn die Zahl der Teilnehmer größer wird). Daher ist das Skalierungsverhalten order N²
oder die Anzahl der Handshakes "wächst wie N²".
#handshakes(N)
1/2
N²
Es ist, als ob die leeren Kästchen auf der Diagonale des Diagramms (N*(N-1)/2 Häkchen) gar nicht vorhanden wären (N 2 Häkchen asymptotisch).
(Vorübergehende Abschweifung vom "einfachen Englisch":) Wenn Sie sich das selbst beweisen wollten, könnten Sie eine einfache Algebra auf das Verhältnis anwenden, um es in mehrere Terme aufzuteilen ( lim
bedeutet "betrachtet im Grenzwert von", ignorieren Sie es einfach, wenn Sie es noch nicht gesehen haben, es ist nur eine Schreibweise für "und N ist wirklich sehr groß"):
N²/2 - N/2 (N²)/2 N/2 1/2
lim = lim ( - ) = lim = 1/2
N N² N N² N² N 1
this is 0 in the limit of N:
graph it, or plug in a really large number for N
tl;dr: Die Anzahl der Handshakes "ähnelt" x² bei großen Werten so sehr, dass, wenn wir das Verhältnis #handshakes/x² aufschreiben würden, die Tatsache, dass wir keine genau x² Handshakes würden für eine beliebig lange Zeit nicht einmal im Dezimalsystem auftauchen.
z.B. für x=1Million, Verhältnis #Handshakes/x²: 0.499999...
Intuition aufbauen
So können wir Aussagen treffen wie...
"Für eine hinreichend große Eingabegröße=N ist es egal, wie hoch der konstante Faktor ist, wenn I doppelt die Eingabegröße ...
- ... Ich verdopple die Zeit, die ein O(N) ("lineare Zeit") Algorithmus benötigt."
N (2N) = 2( N )
- ... Ich habe die Zeit, die ein O(N²) ("quadratische Zeit") Algorithmus benötigt, verdoppelt (vervierfacht)." (z.B. ein Problem, das 100x so groß ist, braucht 100²=10000x so lange... möglicherweise unhaltbar)
N² (2N)² = 4( N² )
- ... Ich habe die Zeit, die ein O(N³) ("kubische Zeit") Algorithmus benötigt, verdoppelt (verachtfacht)." (z.B. ein Problem, das 100x so groß ist, braucht 100³=1000000x so lange... sehr unhaltbar)
cN³ c(2N)³ = 8( cN³ )
- ... Ich addiere einen festen Betrag zu der Zeit, die ein O(log(N)) ("logarithmische Zeit") Algorithmus benötigt." (billig!)
c log(N) c log(2N) = (c log(2))+( c log(N) ) = (fester Betrag)+( c log(N) )
- ... Ich ändere nicht die Zeit, die ein O(1) ("constant ime") Algorithmus benötigt." (das billigste!)
c*1 c*1
- ... Ich "verdopple" im Grunde die Zeit, die ein O(N log(N)) Algorithmus benötigt." (relativ häufig)
c 2N log(2N) / c N log(N) (hier dividieren wir f(2n)/f(n), aber wir hätten den Ausdruck wie oben massieren und cNlogN wie oben herausrechnen können)
2 log(2N)/log(N)
2 (log(2) + log(N))/log(N)
2*(1+(log 2 N) -1 ) (grundsätzlich 2 für große N; eventuell weniger als 2.000001)
(alternativ, sagen wir log(N) wird immer unter wie 17 für Ihre Daten, so dass es O(17 N), die linear ist; das ist nicht rigoros noch sinnvoll though)
- ... Ich erhöhe die Zeit lächerlich um O(2 N ) ("exponentielle Zeit") Algorithmus benötigt." (Sie würden die Zeit verdoppeln (oder verdreifachen usw.), wenn Sie das Problem nur um eine einzige Einheit vergrößern)
2 N 2 2N \= (4 N )............auf andere Weise...... 2 N 2 N+1 \= 2 N 2 1 \= 2 2 N
(für die mathematisch Interessierten: Sie können mit der Maus über die Spoiler fahren, um kleinere Nebenbemerkungen zu erhalten)
(mit Dank an https://stackoverflow.com/a/487292/711085 )
(technisch gesehen könnte der konstante Faktor in einigen esoterischeren Beispielen eine Rolle spielen, aber ich habe die Dinge oben (z.B. in log(N)) so formuliert, dass er keine Rolle spielt)
Dies sind die üblichen Wachstumsordnungen, die Programmierer und angewandte Informatiker als Bezugspunkte verwenden. Sie sehen sie die ganze Zeit. (Auch wenn man technisch gesehen denken könnte: "Die Verdopplung der Eingabe macht einen O(N)-Algorithmus 1,414 Mal langsamer", ist es besser, sich vorzustellen: "Das ist schlechter als logarithmisch, aber besser als linear").
Konstante Faktoren
In der Regel ist es egal, wie die spezifischen konstanten Faktoren lauten, da sie keinen Einfluss auf die Art und Weise haben, wie die Funktion wächst. Zum Beispiel können zwei Algorithmen beide O(N)
Zeit für die Fertigstellung, aber eine kann doppelt so langsam sein wie die andere. Normalerweise kümmert uns das nicht allzu sehr, es sei denn, der Faktor ist sehr groß, denn die Optimierung ist eine heikle Angelegenheit ( Wann ist eine Optimierung verfrüht? ); auch die bloße Auswahl eines Algorithmus mit einem besseren Big-O verbessert die Leistung oft um Größenordnungen.
Einige asymptotisch überlegene Algorithmen (z. B. ein Nicht-Vergleichs O(N log(log(N)))
sort) kann einen so großen konstanten Faktor haben (z. B. 100000*N log(log(N))
), oder einen relativ großen Overhead wie O(N log(log(N)))
mit einer versteckten + 100*N
dass sie sich selbst bei "großen Daten" kaum lohnen.
Warum O(N) manchmal das Beste ist, was man tun kann, d.h. warum wir Datenstrukturen brauchen
O(N)
Algorithmen sind in gewissem Sinne die "besten" Algorithmen, wenn Sie alle Ihre Daten lesen müssen. Die der Akt des Lesens selbst ein Bündel von Daten ist ein O(N)
Betrieb. Das Laden in den Speicher ist normalerweise O(N)
(oder schneller, wenn Sie Hardware-Unterstützung haben, oder überhaupt keine Zeit, wenn Sie die Daten bereits gelesen haben). Wenn Sie jedoch die Daten berühren oder sogar siehe bei jedem Datenelement (oder sogar bei jedem anderen Datenelement), wird Ihr Algorithmus O(N)
Zeit, um diese Suche durchzuführen. Unabhängig davon, wie lange Ihr tatsächlicher Algorithmus dauert, wird er mindestens O(N)
weil sie diese Zeit damit verbracht hat, sich alle Daten anzusehen.
Dasselbe gilt für die der Akt des Schreibens selbst . Alle Algorithmen, die N Dinge ausgeben, benötigen N Zeit, weil die Ausgabe mindestens so lang ist (z. B. ist das Ausdrucken aller Permutationen (Möglichkeiten der Neuanordnung) eines Satzes von N Spielkarten faktoriell: O(N!)
(weshalb gute Programme in diesen Fällen sicherstellen, dass eine Iteration O(1) Speicher verwendet und nicht jeden Zwischenschritt ausgibt oder speichert).
Dies motiviert die Verwendung von Datenstrukturen Datenstruktur: Eine Datenstruktur muss nur einmal gelesen werden (normalerweise O(N)
Zeit), plus eine beliebige Menge an Vorverarbeitung (z. B. O(N)
o O(N log(N))
o O(N²)
), die wir versuchen, klein zu halten. Danach nehmen Änderungen der Datenstruktur (Einfügungen/Löschungen/etc.) und Abfragen der Daten sehr wenig Zeit in Anspruch, wie z. B. O(1)
o O(log(N))
. Dann machen Sie eine große Anzahl von Abfragen! Im Allgemeinen gilt: Je mehr Arbeit Sie im Vorfeld leisten, desto weniger Arbeit müssen Sie später erledigen.
Nehmen wir an, Sie haben die Längen- und Breitengradkoordinaten von Millionen von Straßenabschnitten und möchten alle Straßenkreuzungen finden.
- Naive Methode: Wenn man die Koordinaten einer Straßenkreuzung hätte und die benachbarten Straßen untersuchen wollte, müsste man jedes Mal die Millionen von Segmenten durchgehen und jedes einzelne auf Nachbarschaft prüfen.
- Wenn Sie dies nur einmal tun müssten, wäre es kein Problem, die naive Methode der
O(N)
funktioniert nur einmal, aber wenn Sie es viele Male tun wollen (in diesem Fall, N
mal, einmal für jedes Segment), müssten wir O(N²)
Arbeit, oder 1000000²=1000000000000 Operationen. Nicht gut (ein moderner Computer kann etwa eine Milliarde Operationen pro Sekunde durchführen).
- Wenn wir eine einfache Struktur verwenden, die Hash-Tabelle genannt wird (eine schnelle Nachschlagetabelle, auch bekannt als Hash-Map oder Dictionary), zahlen wir einen geringen Preis, indem wir alles in
O(N)
Zeit. Danach dauert es im Durchschnitt nur noch eine konstante Zeit, um etwas nach seinem Schlüssel zu suchen (in diesem Fall ist unser Schlüssel die Koordinaten von Breiten- und Längengrad, gerundet zu einem Raster; wir suchen die angrenzenden Rasterräume, von denen es nur 9 gibt, was eine Konstante ist).
- Unsere Aufgabe wurde von einer undurchführbaren
O(N²)
auf ein überschaubares O(N)
und alles, was wir zu tun hatten, waren geringe Kosten für die Erstellung einer Hashtabelle.
- Analogie : Die Analogie in diesem speziellen Fall ist ein Puzzle: Wir haben eine Datenstruktur erstellt, die eine Eigenschaft der Daten ausnutzt. Wenn unsere Straßenabschnitte wie Puzzleteile sind, gruppieren wir sie nach übereinstimmender Farbe und Muster. Dies machen wir uns zunutze, um später zusätzliche Arbeit zu vermeiden (indem wir Puzzleteile gleicher Farbe miteinander vergleichen, nicht mit jedem einzelnen Puzzleteil).
Die Moral von der Geschicht': Eine Datenstruktur ermöglicht es uns, Operationen zu beschleunigen. Mehr noch: Mit fortgeschrittenen Datenstrukturen können Sie Operationen auf unglaublich clevere Weise kombinieren, verzögern oder sogar ignorieren. Für verschiedene Probleme gibt es unterschiedliche Analogien, aber bei allen geht es darum, die Daten so zu organisieren, dass eine Struktur ausgenutzt wird, die uns wichtig ist oder die wir aus buchhalterischen Gründen künstlich auferlegt haben. Wir erledigen die Arbeit im Voraus (im Grunde planen und organisieren wir), und jetzt sind wiederkehrende Aufgaben viel einfacher!
Praktisches Beispiel: Visualisierung von Wachstumsreihenfolgen während der Codierung
Die asymptotische Notation ist in ihrem Kern völlig unabhängig von der Programmierung. Die asymptotische Notation ist ein mathematischer Rahmen für die Betrachtung der Skalierung von Dingen und kann in vielen verschiedenen Bereichen eingesetzt werden. Abgesehen davon... so können Sie anwenden. asymptotische Notation zur Kodierung.
Die Grundlagen: Wann immer wir mit jedem Element in einer Sammlung der Größe A interagieren (z. B. einem Array, einer Menge, allen Schlüsseln einer Map usw.) oder A Iterationen einer Schleife durchführen, ist das ein multiplikativer Faktor der Größe A. Warum sage ich "ein multiplikativer Faktor"? Weil Schleifen und Funktionen (fast per Definition) eine multiplikative Laufzeit haben: die Anzahl der Iterationen mal die in der Schleife geleistete Arbeit (oder bei Funktionen: die Anzahl der Aufrufe der Funktion mal die in der Funktion geleistete Arbeit). (Dies gilt, wenn wir nichts Ausgefallenes tun, wie z. B. Schleifen überspringen oder die Schleife frühzeitig verlassen, oder den Kontrollfluss in der Funktion auf der Grundlage von Argumenten ändern, was sehr häufig vorkommt). Hier sind einige Beispiele für Visualisierungstechniken, mit begleitendem Pseudocode.
(hier: die x
s stehen für zeitlich konstante Arbeitseinheiten, Prozessorbefehle, Interpreter-Opcodes, was auch immer)
for(i=0; i<A; i++) // A * ...
some O(1) operation // 1
--> A*1 --> O(A) time
visualization:
|<------ A ------->|
1 2 3 4 5 x x ... x
other languages, multiplying orders of growth:
javascript, O(A) time and space
someListOfSizeA.map((x,i) => [x,i])
python, O(rows*cols) time and space
[[r*c for c in range(cols)] for r in range(rows)]
Beispiel 2:
for every x in listOfSizeA: // A * (...
some O(1) operation // 1
some O(B) operation // B
for every y in listOfSizeC: // C * (...
some O(1) operation // 1))
--> O(A*(1 + B + C))
O(A*(B+C)) (1 is dwarfed)
visualization:
|<------ A ------->|
1 x x x x x x ... x
2 x x x x x x ... x ^
3 x x x x x x ... x |
4 x x x x x x ... x |
5 x x x x x x ... x B <-- A*B
x x x x x x x ... x |
................... |
x x x x x x x ... x v
x x x x x x x ... x ^
x x x x x x x ... x |
x x x x x x x ... x |
x x x x x x x ... x C <-- A*C
x x x x x x x ... x |
................... |
x x x x x x x ... x v
Beispiel 3:
function nSquaredFunction(n) {
total = 0
for i in 1..n: // N *
for j in 1..n: // N *
total += i*k // 1
return total
}
// O(n^2)
function nCubedFunction(a) {
for i in 1..n: // A *
print(nSquaredFunction(a)) // A^2
}
// O(a^3)
Wenn wir etwas etwas Kompliziertes machen, können Sie sich vielleicht noch visuell vorstellen, was vor sich geht:
for x in range(A):
for y in range(1..x):
simpleOperation(x*y)
x x x x x x x x x x |
x x x x x x x x x |
x x x x x x x x |
x x x x x x x |
x x x x x x |
x x x x x |
x x x x |
x x x |
x x |
x___________________|
Hier kommt es auf den kleinsten erkennbaren Umriss an, den man zeichnen kann; ein Dreieck ist eine zweidimensionale Form (0,5 A^2), genauso wie ein Quadrat eine zweidimensionale Form (A^2) ist; der konstante Faktor von zwei bleibt hier im asymptotischen Verhältnis zwischen den beiden, wir ignorieren ihn jedoch wie alle Faktoren... (Es gibt einige unglückliche Nuancen dieser Technik, auf die ich hier nicht eingehe; sie kann Sie in die Irre führen).
Das bedeutet natürlich nicht, dass Schleifen und Funktionen schlecht sind; im Gegenteil, sie sind die Bausteine moderner Programmiersprachen, und wir lieben sie. Wir können jedoch sehen, dass die Art und Weise, wie wir Schleifen, Funktionen und Bedingungen mit unseren Daten (Kontrollfluss usw.) verweben, die Zeit- und Raumnutzung unseres Programms nachahmt! Wenn der Zeit- und Platzbedarf zu einem Problem wird, greifen wir auf unsere Cleverness zurück und finden einen einfachen Algorithmus oder eine Datenstruktur, die wir nicht in Betracht gezogen hatten, um die Wachstumsrate irgendwie zu reduzieren. Nichtsdestotrotz können diese Visualisierungstechniken (auch wenn sie nicht immer funktionieren) eine naive Schätzung der Worst-Case-Laufzeit liefern.
Hier ist eine weitere Sache, die wir visuell erkennen können:
<----------------------------- N ----------------------------->
x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x
x x x x x x x x x x x x x x x x
x x x x x x x x
x x x x
x x
x
Wir können dies einfach neu anordnen und sehen, dass es O(N) ist:
<----------------------------- N ----------------------------->
x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x
x x x x x x x x x x x x x x x x|x x x x x x x x|x x x x|x x|x
Oder Sie führen log(N) Durchläufe der Daten durch, was insgesamt O(N*log(N)) dauert:
<----------------------------- N ----------------------------->
^ x x x x x x x x x x x x x x x x|x x x x x x x x x x x x x x x x
| x x x x x x x x|x x x x x x x x|x x x x x x x x|x x x x x x x x
lgN x x x x|x x x x|x x x x|x x x x|x x x x|x x x x|x x x x|x x x x
| x x|x x|x x|x x|x x|x x|x x|x x|x x|x x|x x|x x|x x|x x|x x|x x
v x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x
Das hat zwar nichts damit zu tun, ist aber dennoch erwähnenswert: Wenn wir einen Hash durchführen (z. B. eine Wörterbuch-/Hashtabellensuche), ist das ein Faktor von O(1). Das ist ziemlich schnell.
[myDictionary.has(x) for x in listOfSizeA]
\----- O(1) ------/
--> A*1 --> O(A)
Wenn wir etwas sehr Kompliziertes machen, z. B. eine rekursive Funktion oder einen Divide-and-Conquer-Algorithmus, können Sie die Master-Theorem (funktioniert normalerweise), oder in lächerlichen Fällen das Akra-Bazzi-Theorem (funktioniert fast immer) schlagen Sie die Laufzeit Ihres Algorithmus auf Wikipedia nach.
Aber Programmierer denken nicht so, denn irgendwann wird die Intuition für Algorithmen zur zweiten Natur. Man fängt an, etwas Ineffizientes zu programmieren und denkt sofort: "Mache ich etwas grob ineffizient? ". Wenn die Antwort "ja" lautet UND Sie vorhersehen, dass es tatsächlich von Bedeutung ist, können Sie einen Schritt zurücktreten und sich verschiedene Tricks ausdenken, um die Dinge schneller ablaufen zu lassen (die Antwort lautet fast immer "eine Hashtabelle verwenden", selten "einen Baum verwenden" und sehr selten etwas Komplizierteres).
Amortisierte und durchschnittliche Komplexität
Es gibt auch das Konzept des "amortisierten" und/oder "durchschnittlichen Falles" (beachten Sie, dass es sich dabei um unterschiedliche Begriffe handelt).
Durchschnittlicher Fall : Dies ist nichts anderes als die Verwendung der Big-O-Notation für den Erwartungswert einer Funktion und nicht für die Funktion selbst. In dem üblichen Fall, in dem alle Eingaben als gleich wahrscheinlich angesehen werden, ist der durchschnittliche Fall einfach der Durchschnitt der Laufzeit. Bei der Quicksortierung zum Beispiel ist der schlimmste Fall, auch wenn er O(N^2)
für einige wirklich schlechte Eingaben, der durchschnittliche Fall ist der übliche O(N log(N))
(die wirklich schlechten Eingaben sind sehr gering, so gering, dass wir sie im Durchschnitt nicht bemerken).
Abgeschriebener Worst-Case : Einige Datenstrukturen können im schlimmsten Fall eine hohe Komplexität aufweisen, aber sie garantieren, dass der durchschnittliche Arbeitsaufwand besser ist als im schlimmsten Fall, wenn Sie viele dieser Operationen durchführen. Sie können zum Beispiel eine Datenstruktur haben, die normalerweise eine konstante O(1)
Zeit. Gelegentlich kommt es jedoch zu "Schluckauf" und O(N)
Zeit für eine zufällige Operation, weil er vielleicht etwas Buchhaltung oder Garbage Collection oder so etwas machen muss... aber er verspricht Ihnen, dass er, wenn er einen Schluckauf hat, für N weitere Operationen nicht mehr schluckern wird. Die Kosten im schlimmsten Fall sind immer noch O(N)
pro Vorgang, aber die amortisierten Kosten über viele Läufe es O(N)/N
= O(1)
pro Vorgang. Da die großen Operationen hinreichend selten sind, kann man davon ausgehen, dass sich die große Menge an gelegentlicher Arbeit als konstanter Faktor in den Rest der Arbeit einfügt. Wir sagen, dass die Arbeit über eine ausreichend große Anzahl von Aufrufen "amortisiert" ist, so dass sie asymptotisch verschwindet.
Die Analogie zur amortisierten Analyse:
Sie fahren ein Auto. Gelegentlich müssen Sie 10 Minuten lang zur Tankstelle fahren zur Tankstelle zu fahren und dann 1 Minute lang den Tank aufzufüllen. Wenn Sie dies jedes Mal tun würden, wenn Sie mit Ihrem Auto irgendwo hinfahren (10 (10 Minuten Fahrt zur Tankstelle, ein paar Sekunden zum Tanken Bruchteil einer Gallone), wäre das sehr ineffizient. Wenn Sie aber alle paar Tage tanken, werden die 11 Minuten, die man für die Fahrt zur Tankstelle über eine ausreichend große Anzahl von Fahrten "amortisiert", dass man sie ignorieren und so tun kann, als wären alle Fahrten vielleicht 5 % länger.
Vergleich zwischen durchschnittlichem und amortisiertem Worst-Case-Fall:
- Durchschnittlicher Fall: Wir machen einige Annahmen über unsere Eingaben; d. h. wenn unsere Eingaben unterschiedliche Wahrscheinlichkeiten haben, dann haben auch unsere Ausgaben/Zeiten unterschiedliche Wahrscheinlichkeiten (von denen wir den Durchschnitt nehmen). Normalerweise gehen wir davon aus, dass unsere Eingaben alle gleich wahrscheinlich sind (einheitliche Wahrscheinlichkeit), aber wenn die Eingaben in der Realität nicht zu unseren Annahmen über die "durchschnittliche Eingabe" passen, sind die Berechnungen der durchschnittlichen Ausgaben/Laufzeiten möglicherweise bedeutungslos. Wenn Sie jedoch mit gleichmäßig zufälligen Eingaben rechnen, ist es sinnvoll, darüber nachzudenken!
- Amortisierter Worst-Case-Fall: Wenn Sie eine amortisierte Worst-Case-Datenstruktur verwenden, wird die Leistung garantiert innerhalb des amortisierten Worst-Case liegen... irgendwann (selbst wenn die Eingaben von einem bösen Dämon ausgewählt werden, der alles weiß und versucht, Sie zu bescheißen). Normalerweise verwenden wir diese Methode, um Algorithmen zu analysieren, die in ihrer Leistung sehr "unruhig" sind und unerwartete große Schluckaufs aufweisen, aber im Laufe der Zeit genauso gut funktionieren wie andere Algorithmen. (Wenn Ihre Datenstruktur jedoch keine Obergrenzen für die Menge an ausstehender Arbeit hat, die sie aufschieben kann, könnte ein böser Angreifer Sie vielleicht dazu zwingen, die maximale Menge an aufgeschobener Arbeit auf einmal nachzuholen.
Wenn Sie allerdings einigermaßen besorgt für einen Angreifer gibt es neben der Amortisation und dem Durchschnittsfall noch viele andere algorithmische Angriffsvektoren, über die man sich Gedanken machen muss).
Sowohl der Durchschnittsfall als auch die Amortisierung sind unglaublich nützliche Werkzeuge, um über Skalierung nachzudenken und diese zu planen.
(Siehe Unterschied zwischen Durchschnittsfall und amortisierter Analyse wenn Sie an diesem Unterthema interessiert sind).
Multidimensionales Big-O
Meistens sind sich die Menschen nicht bewusst, dass mehr als eine Variable am Werk ist. Bei einem Algorithmus zur Suche nach Zeichenketten kann Ihr Algorithmus zum Beispiel Zeit benötigen O([length of text] + [length of query])
, d.h. sie ist linear in zwei Variablen wie O(N+M)
. Andere, naivere Algorithmen können sein O([length of text]*[length of query])
o O(N*M)
. Das Ignorieren mehrerer Variablen ist eines der häufigsten Versäumnisse, die ich bei der Algorithmusanalyse sehe, und kann bei der Entwicklung eines Algorithmus zum Nachteil werden.
Die ganze Geschichte
Denken Sie daran, dass Big-O nicht die ganze Geschichte ist. Sie können einige Algorithmen drastisch beschleunigen, indem Sie Caching verwenden, sie cache-oblivious machen, Engpässe vermeiden, indem Sie mit RAM statt mit Festplatten arbeiten, Parallelisierung verwenden oder die Arbeit im Voraus erledigen - diese Techniken sind oft unabhängig der "Big-O"-Notation, obwohl Sie die Anzahl der Kerne oft in der Big-O-Notation von parallelen Algorithmen sehen werden.
Denken Sie auch daran, dass Ihnen das asymptotische Verhalten aufgrund versteckter Beschränkungen Ihres Programms möglicherweise nicht wirklich wichtig ist. Sie können zum Beispiel mit einer begrenzten Anzahl von Werten arbeiten:
- Wenn Sie etwas wie 5 Elemente sortieren, sollten Sie nicht die schnelle
O(N log(N))
quicksort; Sie möchten die Einfügesortierung verwenden, die bei kleinen Eingaben gut funktioniert. Diese Situationen treten häufig bei Divide-and-Conquer-Algorithmen auf, bei denen das Problem in immer kleinere Teilprobleme aufgeteilt wird, z. B. bei rekursiver Sortierung, schnellen Fourier-Transformationen oder Matrixmultiplikation.
- Wenn einige Werte aufgrund einer verborgenen Tatsache effektiv begrenzt sind (z. B. ist der durchschnittliche menschliche Name bei vielleicht 40 Buchstaben und das menschliche Alter bei etwa 150 begrenzt). Sie können Ihrer Eingabe auch Grenzen auferlegen, um Begriffe effektiv konstant zu machen.
In der Praxis kann die relative Leistung von Algorithmen mit gleicher oder ähnlicher asymptotischer Leistung von anderen Faktoren abhängen, wie z. B.: andere Leistungsfaktoren (Quicksort und Mergesort sind beide O(N log(N))
aber Quicksort nutzt die Vorteile des CPU-Caches); Überlegungen, die nicht die Leistung betreffen, wie z. B. die Einfachheit der Implementierung; ob eine Bibliothek verfügbar ist, und wie seriös und gepflegt die Bibliothek ist.
Außerdem laufen Programme auf einem 500-MHz-Computer langsamer als auf einem 2-GHz-Computer. Wir betrachten dies nicht wirklich als Teil der Ressourcengrenzen, da wir die Skalierung in Bezug auf die Maschinenressourcen (z. B. pro Taktzyklus) und nicht pro echter Sekunde betrachten. Es gibt jedoch ähnliche Dinge, die sich "heimlich" auf die Leistung auswirken können, z. B. ob Sie unter Emulation arbeiten oder ob der Compiler den Code optimiert hat oder nicht. Diese Faktoren können dazu führen, dass einige grundlegende Operationen länger dauern (auch im Verhältnis zueinander), oder sogar einige Operationen asymptotisch beschleunigen oder verlangsamen (auch im Verhältnis zueinander). Die Auswirkungen können je nach Implementierung und/oder Umgebung gering oder groß sein. Wechseln Sie die Sprache oder den Rechner, um das bisschen zusätzliche Arbeit herauszuholen? Das hängt von hundert anderen Gründen ab (Notwendigkeit, Fähigkeiten, Kollegen, Produktivität des Programmierers, Geldwert Ihrer Zeit, Vertrautheit, Umgehungsmöglichkeiten, warum nicht Assembler oder GPU, usw.), die vielleicht wichtiger sind als die Leistung.
Die oben genannten Punkte, wie auch die Auswirkung der Wahl der verwendeten Programmiersprache, werden fast nie als Teil des konstanten Faktors betrachtet (und sollten es auch nicht); dennoch sollte man sich ihrer bewusst sein, denn manchmal (wenn auch selten) können sie die Dinge beeinflussen. Zum Beispiel ist in cpython die native Implementierung der Prioritätswarteschlange asymptotisch nicht optimal ( O(log(N))
statt O(1)
für Ihre Wahl von Insertion oder Find-min); verwenden Sie eine andere Implementierung? Wahrscheinlich nicht, denn die C-Implementierung ist wahrscheinlich schneller, und es gibt wahrscheinlich noch andere ähnliche Probleme. Es gibt Kompromisse; manchmal sind sie wichtig und manchmal nicht.
( bearbeiten : Die "Klartext"-Erklärung endet hier.)
Mathe-Zusätze
Der Vollständigkeit halber sei hier die genaue Definition der Big-O-Notation angeführt: f(x) O(g(x))
bedeutet, dass "f asymptotisch durch const*g nach oben begrenzt ist": Wenn man alles unterhalb eines endlichen Wertes von x ignoriert, gibt es eine Konstante, so dass |f(x)| const * |g(x)|
. (Die anderen Symbole sind wie folgt: genau wie O
bedeutet, bedeutet. Es gibt Varianten in Kleinbuchstaben: o
bedeutet <, und bedeutet >.) f(x) (g(x))
bedeutet beides f(x) O(g(x))
y f(x) (g(x))
(Ober- und Untergrenze von g): Es gibt einige Konstanten, so dass f immer in dem "Band" liegt, das zwischen const1*g(x)
y const2*g(x)
. Dies ist die stärkste asymptotische Aussage, die man machen kann und entspricht in etwa ==
. (Entschuldigung, ich habe mich entschieden, die Erwähnung der Symbole für absolute Werte aus Gründen der Klarheit bis jetzt hinauszuzögern; vor allem, weil ich noch nie gesehen habe, dass negative Werte in einem Informatikkontext auftauchen).
_
Die Menschen verwenden oft = O(...)
was vielleicht die korrektere 'comp-sci'-Notation ist und völlig legitim zu verwenden; "f = O(...)" heißt "f ist Ordnung ... / f ist xxx-begrenzt durch ..." und ist gedacht als "f ist ein Ausdruck, dessen Asymptotik ...". Mir wurde beigebracht, den strengeren O(...)
. bedeutet "ist ein Element von" (immer noch wie zuvor gelesen). In diesem besonderen Fall, O(N²)
enthält Elemente wie { 2 N²
, 3 N²
, 1/2 N²
, 2 N² + log(N)
, - N² + N^1.9
, ...} und ist unendlich groß, aber es ist immer noch eine Menge.
O und sind nicht symmetrisch (n = O(n²), aber n² ist nicht O(n)), sondern symmetrisch, und (da diese Beziehungen alle transitiv und reflexiv sind), ist daher symmetrisch und transitiv und reflexiv, und teilt daher die Menge aller Funktionen in Äquivalenzklassen . Eine Äquivalenzklasse ist eine Menge von Dingen, die wir als gleich betrachten. Das heißt, dass man für jede denkbare Funktion einen kanonischen/eindeutigen "asymptotischen Vertreter" der Klasse finden kann (indem man im Allgemeinen den Grenzwert... I denken ); genauso wie man alle ganzen Zahlen in ungerade oder gerade gruppieren kann, kann man alle Funktionen mit in x-ish, log(x)^2-ish, etc... gruppieren, indem man im Grunde kleinere Terme ignoriert (aber manchmal könnte man mit komplizierteren Funktionen feststecken, die separate Klassen für sich sind).
_
El =
Notation ist vielleicht die gebräuchlichere und wird sogar in Abhandlungen von weltbekannten Informatikern verwendet. Darüber hinaus ist es oft der Fall, dass Leute in einer lockeren Umgebung sagen O(...)
wenn sie meinen (...)
Dies ist technisch gesehen richtig, da die Menge der Dinge (exactlyThis)
ist eine Teilmenge von O(noGreaterThanThis)
... und es ist einfacher zu tippen ;-)
69 Stimmen
Zusammenfassung: Die obere Grenze der Komplexität eines Algorithmus. Siehe auch die ähnliche Frage Big O, wie berechnest/schätzt du das? für eine gute Erläuterung.
7 Stimmen
Die anderen Antworten sind recht gut, nur ein Detail zum Verständnis: O(log n) oder ähnlich bedeutet, dass es von der "Länge" oder "Größe" der Eingabe abhängt, nicht von dem Wert selbst. Das kann schwer zu verstehen sein, ist aber sehr wichtig. Dies ist zum Beispiel der Fall, wenn Ihr Algorithmus bei jeder Iteration Dinge in zwei Teile zerlegt.
1 Stimmen
Wenn dies ein Duplikat von etwas ist, dann ist es das: stackoverflow.com/questions/107165/big-o-for-eight-year-olds
18 Stimmen
Es gibt eine Vorlesung über die Komplexität der Algorithmen in der Vorlesung 8 des MIT-Kurses "Introduction to Computer Science and Programming". youtube.com/watch?v=ewd7Lf2dr5Q Es ist nicht ganz einfaches Englisch, aber es gibt schöne Erklärungen mit Beispielen, die leicht verständlich sind.
19 Stimmen
Big O ist eine Schätzung der schlechtesten Leistung einer Funktion unter der Annahme, dass der Algorithmus die maximale Anzahl von Iterationen durchführt.
1 Stimmen
Ich denke, Sie werden das finden: youtube.com/watch?v=6Ol2JbwoJp0 Video hilfreich.
3 Stimmen
So können wir die Effizienz unserer Lösung mit anderen Lösungen vergleichen. Einfache Zeittests sind aufgrund externer Variablen (z. B. Hardware und Problemgröße (z. B. Anzahl der zu sortierenden Objekte)) nicht möglich. Big-O ermöglicht es uns, die Vergleiche zu standardisieren.
38 Stimmen
Big-O notation explained by a self-taught programmer
1 Stimmen
Siehe dies Vorführung .
2 Stimmen
Die Big-O-Notation hat eigentlich nichts mit Algorithmen und Komplexität zu tun.
1 Stimmen
Wenn Sie wirklich etwas über die Landau-Notation lernen wollen, empfehle ich Ihnen die Seite Informatik , beginnend mit unsere Referenzfragen . Wir geben zwar nicht vor, ein mathematisches Konzept genau erklären zu können und in "einfachem Englisch", wir werden Ihnen auch keine Unwahrheiten beibringen. (Hoffentlich.)
0 Stimmen
Erklärung in einem Satz: "Eine Funktion wächst nicht schneller als eine andere".
0 Stimmen
In diesem Beitrag wird die Komplexität anhand eines konkreten Beispiels erläutert: mohalgorithmsorbit.blogspot.com/2021/01/
0 Stimmen
@HaraldSchilly "hängt von der "Länge" oder "Größe" der Eingabe ab, nicht auf den Wert selbst "? Könnten Sie mir das bitte genauer erklären?