Big O, wie berechnet / approximiert man es?


881

Die meisten Menschen mit einem Abschluss in CS werden sicherlich wissen, wofür Big O steht . Es hilft uns zu messen, wie gut ein Algorithmus skaliert.

Aber ich bin gespannt, wie Sie die Komplexität Ihrer Algorithmen berechnen oder approximieren.


4
Vielleicht müssen Sie die Komplexität Ihres Algorithmus nicht wirklich verbessern, aber Sie sollten es zumindest berechnen können, um zu entscheiden ...
Xavier Nodet

5
Ich fand dies eine sehr klare Erklärung für Big O, Big Omega und Big Theta: xoax.net/comp/sci/algorithms/Lesson6.php
Sam Dutton

33
-1: Seufz, ein weiterer Missbrauch von BigOh. BigOh ist nur eine asymptotische Obergrenze und kann für alles verwendet werden und ist nicht nur mit CS verbunden. Über BigOh zu sprechen, als ob es eine eindeutige gibt, ist bedeutungslos (Ein linearer Zeitalgorithmus ist auch O (n ^ 2), O (n ^ 3) usw.). Zu sagen, dass es uns hilft, die Effizienz zu messen, ist ebenfalls irreführend. Was ist mit dem Link zu den Komplexitätsklassen? Wenn Sie sich nur für Techniken zur Berechnung der Laufzeit von Algorithmen interessieren, wie ist das relevant?

34
Big-O misst nicht die Effizienz. Es misst, wie gut ein Algorithmus mit der Größe skaliert (es könnte auch für andere Dinge als die Größe gelten, aber das interessiert uns wahrscheinlich hier) - und das nur asymptotisch. Wenn Sie also kein Glück haben, einen Algorithmus mit einem "kleineren" großen - O kann langsamer sein (wenn das Big-O für Zyklen gilt) als ein anderes, bis Sie extrem große Zahlen erreichen.
ILoveFortran

4
Die Auswahl eines Algorithmus auf der Grundlage seiner Big-O-Komplexität ist normalerweise ein wesentlicher Bestandteil des Programmdesigns. Es handelt sich definitiv nicht um eine „vorzeitige Optimierung“, die in jedem Fall ein häufig missbrauchtes selektives Zitat ist.
Marquis von Lorne

Antworten:


1480

Ich werde mein Bestes tun, um es hier in einfachen Worten zu erklären, aber seien Sie gewarnt, dass dieses Thema meine Schüler einige Monate braucht, um es endlich zu verstehen. Weitere Informationen finden Sie in Kapitel 2 der Datenstrukturen und Algorithmen im Java- Buch.


Es gibt kein mechanisches Verfahren , mit dem der BigOh erhalten werden kann.

Als "Kochbuch" müssen Sie zunächst feststellen, dass Sie eine mathematische Formel erstellen, um zu zählen, wie viele Berechnungsschritte bei einer Eingabe einer bestimmten Größe ausgeführt werden , um das BigOh aus einem Code zu erhalten .

Der Zweck ist einfach: Algorithmen aus theoretischer Sicht zu vergleichen, ohne den Code ausführen zu müssen. Je geringer die Anzahl der Schritte ist, desto schneller ist der Algorithmus.

Angenommen, Sie haben diesen Code:

int sum(int* data, int N) {
    int result = 0;               // 1

    for (int i = 0; i < N; i++) { // 2
        result += data[i];        // 3
    }

    return result;                // 4
}

Diese Funktion gibt die Summe aller Elemente des Arrays zurück, und wir möchten eine Formel erstellen, um die Rechenkomplexität dieser Funktion zu zählen:

Number_Of_Steps = f(N)

Wir haben also f(N)eine Funktion, um die Anzahl der Rechenschritte zu zählen. Die Eingabe der Funktion ist die Größe der zu verarbeitenden Struktur. Dies bedeutet, dass diese Funktion wie folgt aufgerufen wird:

Number_Of_Steps = f(data.length)

Der Parameter Nnimmt den data.lengthWert an. Jetzt brauchen wir die eigentliche Definition der Funktion f(). Dies geschieht aus dem Quellcode, in dem jede interessante Zeile von 1 bis 4 nummeriert ist.

Es gibt viele Möglichkeiten, den BigOh zu berechnen. Von diesem Punkt an gehen wir davon aus, dass jeder Satz, der nicht von der Größe der Eingabedaten abhängt, eine konstante CAnzahl von Rechenschritten benötigt.

Wir werden die einzelne Anzahl von Schritten der Funktion hinzufügen, und weder die lokale Variablendeklaration noch die return-Anweisung hängen von der Größe des dataArrays ab.

Das bedeutet, dass die Zeilen 1 und 4 jeweils C Schritte umfassen, und die Funktion ist ungefähr so:

f(N) = C + ??? + C

Der nächste Teil besteht darin, den Wert der forAnweisung zu definieren . Denken Sie daran, dass wir die Anzahl der Rechenschritte zählen, was bedeutet, dass der Hauptteil der forAnweisung Nmal ausgeführt wird. Das ist das gleiche wie das Hinzufügen C, NZeiten:

f(N) = C + (C + C + ... + C) + C = C + N * C + C

Es gibt keine mechanische Regel, um zu zählen, wie oft der Körper des forausgeführt wird. Sie müssen ihn zählen, indem Sie sich ansehen, was der Code tut. Um die Berechnungen zu vereinfachen, ignorieren wir die Variableninitialisierungs-, Bedingungs- und Inkrementteile der forAnweisung.

Um das tatsächliche BigOh zu erhalten, benötigen wir die asymptotische Analyse der Funktion. Dies geschieht ungefähr so:

  1. Nehmen Sie alle Konstanten weg C.
  2. Von f()bekommen das Polynom in seiner standard form.
  3. Teilen Sie die Begriffe des Polynoms und sortieren Sie sie nach der Wachstumsrate.
  4. Behalten Sie diejenige, die größer wird, wenn Sie sich Nnähern infinity.

Unser f()hat zwei Begriffe:

f(N) = 2 * C * N ^ 0 + 1 * C * N ^ 1

Entfernen aller CKonstanten und redundanten Teile:

f(N) = 1 + N ^ 1

Da der letzte Term derjenige ist, der größer wird, wenn er f()sich der Unendlichkeit nähert (denken Sie an Grenzen ), ist dies das BigOh-Argument, und die sum()Funktion hat ein BigOh von:

O(N)

Es gibt ein paar Tricks, um einige knifflige zu lösen: Verwenden Sie Summierungen, wann immer Sie können.

Dieser Code kann beispielsweise einfach mithilfe von Summierungen gelöst werden:

for (i = 0; i < 2*n; i += 2) {  // 1
    for (j=n; j > i; j--) {     // 2
        foo();                  // 3
    }
}

Das erste, was Sie gefragt werden mussten, ist die Reihenfolge der Ausführung von foo(). Während das Übliche sein soll, müssen O(1)Sie Ihre Professoren danach fragen. O(1)bedeutet (fast, meistens) konstant C, unabhängig von der Größe N.

Die forAussage zum ersten Satz ist schwierig. Während der Index bei endet 2 * N, erfolgt die Erhöhung um zwei. Das bedeutet, dass der erste fornur in NSchritten ausgeführt wird und wir die Anzahl durch zwei teilen müssen.

f(N) = Summation(i from 1 to 2 * N / 2)( ... ) = 
     = Summation(i from 1 to N)( ... )

Der Satz Nummer zwei ist noch schwieriger , da es auf dem Wert abhängt i. Schauen Sie mal: Der Index i nimmt die Werte an: 0, 2, 4, 6, 8, ..., 2 * N, und der zweite wird forausgeführt: N mal der erste, N - 2 der zweite, N - 4 die dritte ... bis zur N / 2-Stufe, auf der die zweite fornie ausgeführt wird.

Auf der Formel bedeutet das:

f(N) = Summation(i from 1 to N)( Summation(j = ???)(  ) )

Wieder zählen wir die Anzahl der Schritte . Und per Definition sollte jede Summierung immer bei eins beginnen und bei einer Zahl enden, die größer oder gleich eins ist.

f(N) = Summation(i from 1 to N)( Summation(j = 1 to (N - (i - 1) * 2)( C ) )

(Wir gehen davon aus, dass dies Schritte foo()sind O(1)und Cunternehmen.)

Wir haben hier ein Problem: Wenn ider Wert N / 2 + 1nach oben geht, endet die innere Summierung mit einer negativen Zahl! Das ist unmöglich und falsch. Wir müssen die Summe in zwei Teile teilen, da dies der entscheidende Punkt ist, den der Moment ibenötigt N / 2 + 1.

f(N) = Summation(i from 1 to N / 2)( Summation(j = 1 to (N - (i - 1) * 2)) * ( C ) ) + Summation(i from 1 to N / 2) * ( C )

Seit dem entscheidenden Moment i > N / 2wird das Innere fornicht ausgeführt, und wir gehen von einer konstanten Komplexität der C-Ausführung in seinem Körper aus.

Jetzt können die Summierungen mithilfe einiger Identitätsregeln vereinfacht werden:

  1. Summe (w von 1 bis N) (C) = N * C.
  2. Summation (w von 1 bis N) (A (+/-) B) = Summation (w von 1 bis N) (A) (+/-) Summation (w von 1 bis N) (B)
  3. Summation (w von 1 bis N) (w * C) = C * Summation (w von 1 bis N) (w) (C ist eine Konstante, unabhängig von w)
  4. Summe (w von 1 bis N) (w) = (N * (N + 1)) / 2

Algebra anwenden:

f(N) = Summation(i from 1 to N / 2)( (N - (i - 1) * 2) * ( C ) ) + (N / 2)( C )

f(N) = C * Summation(i from 1 to N / 2)( (N - (i - 1) * 2)) + (N / 2)( C )

f(N) = C * (Summation(i from 1 to N / 2)( N ) - Summation(i from 1 to N / 2)( (i - 1) * 2)) + (N / 2)( C )

f(N) = C * (( N ^ 2 / 2 ) - 2 * Summation(i from 1 to N / 2)( i - 1 )) + (N / 2)( C )

=> Summation(i from 1 to N / 2)( i - 1 ) = Summation(i from 1 to N / 2 - 1)( i )

f(N) = C * (( N ^ 2 / 2 ) - 2 * Summation(i from 1 to N / 2 - 1)( i )) + (N / 2)( C )

f(N) = C * (( N ^ 2 / 2 ) - 2 * ( (N / 2 - 1) * (N / 2 - 1 + 1) / 2) ) + (N / 2)( C )

=> (N / 2 - 1) * (N / 2 - 1 + 1) / 2 = 

   (N / 2 - 1) * (N / 2) / 2 = 

   ((N ^ 2 / 4) - (N / 2)) / 2 = 

   (N ^ 2 / 8) - (N / 4)

f(N) = C * (( N ^ 2 / 2 ) - 2 * ( (N ^ 2 / 8) - (N / 4) )) + (N / 2)( C )

f(N) = C * (( N ^ 2 / 2 ) - ( (N ^ 2 / 4) - (N / 2) )) + (N / 2)( C )

f(N) = C * (( N ^ 2 / 2 ) - (N ^ 2 / 4) + (N / 2)) + (N / 2)( C )

f(N) = C * ( N ^ 2 / 4 ) + C * (N / 2) + C * (N / 2)

f(N) = C * ( N ^ 2 / 4 ) + 2 * C * (N / 2)

f(N) = C * ( N ^ 2 / 4 ) + C * N

f(N) = C * 1/4 * N ^ 2 + C * N

Und das BigOh ist:

O(N²)

6
@arthur Das wäre O (N ^ 2), da Sie eine Schleife zum Durchlesen aller Spalten und eine zum Lesen aller Zeilen einer bestimmten Spalte benötigen würden.
Abhishek Dey Das

@arthur: Es kommt darauf an. Es ist , O(n)wo ndie Anzahl der Elemente ist, oder O(x*y)wo xund ysind die Abmessungen des Arrays. Big-oh ist "relativ zur Eingabe", es hängt also davon ab, was Ihre Eingabe ist.
Mooing Duck

1
Tolle Antwort, aber ich stecke wirklich fest. Wie wird aus Summation (i von 1 bis N / 2) (N) (N ^ 2/2)?
Parsa

2
@ParsaAkbari In der Regel ist die Summe (i von 1 bis a) (b) a * b. Dies ist nur eine andere Art, b + b + ... (a-mal) + b = a * b zu sagen (per Definition für einige Definitionen der ganzzahligen Multiplikation).
Mario Carneiro

Nicht so relevant, aber nur um Verwirrung zu vermeiden, gibt es einen kleinen Fehler in diesem Satz: "Der Index i nimmt die Werte an: 0, 2, 4, 6, 8, ..., 2 * N". Index i steigt tatsächlich auf 2 * N - 2, die Schleife stoppt dann.
Albert

201

Big O gibt die Obergrenze für die zeitliche Komplexität eines Algorithmus an. Es wird normalerweise in Verbindung mit der Verarbeitung von Datensätzen (Listen) verwendet, kann aber auch an anderer Stelle verwendet werden.

Einige Beispiele für die Verwendung in C-Code.

Angenommen, wir haben ein Array von n Elementen

int array[n];

Wenn wir auf das erste Element des Arrays zugreifen möchten, ist dies O (1), da es egal ist, wie groß das Array ist, es dauert immer dieselbe konstante Zeit, um das erste Element zu erhalten.

x = array[0];

Wenn wir eine Nummer in der Liste finden wollten:

for(int i = 0; i < n; i++){
    if(array[i] == numToFind){ return i; }
}

Dies wäre O (n), da wir höchstens die gesamte Liste durchsehen müssten, um unsere Nummer zu finden. Das Big-O ist immer noch O (n), obwohl wir unsere Zahl möglicherweise beim ersten Versuch finden und einmal durch die Schleife laufen, weil Big-O die Obergrenze für einen Algorithmus beschreibt (Omega steht für Untergrenze und Theta für Enge). .

Wenn wir zu verschachtelten Schleifen kommen:

for(int i = 0; i < n; i++){
    for(int j = i; j < n; j++){
        array[j] += 2;
    }
}

Dies ist O (n ^ 2), da wir für jeden Durchgang der äußeren Schleife (O (n)) die gesamte Liste erneut durchgehen müssen, damit sich die n multiplizieren und uns n Quadrat erhalten.

Dies kratzt kaum an der Oberfläche, aber wenn Sie komplexere Algorithmen analysieren, kommt die komplexe Mathematik mit Beweisen ins Spiel. Ich hoffe, dies macht Sie zumindest mit den Grundlagen vertraut.


Tolle Erklärung! Wenn also jemand sagt, sein Algorithmus habe eine O (n ^ 2) -Komplexität, bedeutet dies, dass er verschachtelte Schleifen verwendet?
Navaneeth KN

2
Nicht wirklich, jeder Aspekt, der zu n Quadratzeiten führt, wird als n ^ 2 betrachtet
asyncwait

@NavaneethKN: Sie werden nicht immer sehen die verschachtelte Schleife, als Funktionsaufrufe tun können> O(1)selbst zu arbeiten. In den C-Standard-APIs zum Beispiel bsearchist O(log n), strlenist O(n)und qsortist O(n log n)(technisch hat es keine Garantien und Quicksort selbst hat eine Worst-Case-Komplexität von O(n²), aber vorausgesetzt, Ihr libcAutor ist kein Idiot, ist seine durchschnittliche Case-Komplexität O(n log n)und verwendet eine Pivot-Auswahlstrategie, die die Wahrscheinlichkeit verringert, den O(n²)Fall zu treffen ). Und beides bsearchund qsortkann schlimmer sein, wenn die Komparatorfunktion pathologisch ist.
ShadowRanger

95

Es ist zwar hilfreich zu wissen, wie Sie die Big O-Zeit für Ihr spezielles Problem ermitteln können, aber einige allgemeine Fälle können Ihnen dabei helfen, Entscheidungen in Ihrem Algorithmus zu treffen.

Hier sind einige der häufigsten Fälle, die aus http://en.wikipedia.org/wiki/Big_O_notation#Orders_of_common_functions entfernt wurden :

O (1) - Bestimmen, ob eine Zahl gerade oder ungerade ist; Verwenden einer Nachschlagetabelle oder Hash-Tabelle mit konstanter Größe

O (logn) - Suchen eines Elements in einem sortierten Array mit einer binären Suche

O (n) - Suchen eines Elements in einer unsortierten Liste; Hinzufügen von zwei n-stelligen Zahlen

O (n 2 ) - Multiplizieren von zwei n-stelligen Zahlen mit einem einfachen Algorithmus; Hinzufügen von zwei n × n Matrizen; Blasensortierung oder Einfügesortierung

O (n 3 ) - Multiplizieren von zwei n × n Matrizen mit einem einfachen Algorithmus

O (c n ) - Finden der (genauen) Lösung für das Problem des Handlungsreisenden mithilfe dynamischer Programmierung; Bestimmen, ob zwei logische Anweisungen mit Brute Force äquivalent sind

O (n!) - Lösung des Problems des Handlungsreisenden durch Brute-Force-Suche

O (n n ) - Wird häufig anstelle von O (n!) Verwendet, um einfachere Formeln für die asymptotische Komplexität abzuleiten


Warum nicht verwenden x&1==1, um auf Seltsamkeit zu prüfen?
Samy Bencherif

2
@SamyBencherif: Dies wäre eine typische Methode zur Überprüfung (tatsächlich x & 1würde nur das Testen ausreichen, es muss nicht überprüft werden == 1; in C x&1==1wird dies x&(1==1) aufgrund der Vorrangstellung des Bedieners bewertet , also ist es tatsächlich dasselbe wie das Testen x&1). Ich denke, Sie verstehen die Antwort falsch. Dort gibt es ein Semikolon, kein Komma. Es heißt nicht, dass Sie eine Nachschlagetabelle für gerade / ungerade Tests benötigen, sondern dass gerade / ungerade Tests und das Überprüfen einer Nachschlagetabelle O(1)Operationen sind.
ShadowRanger

Ich weiß nichts über den Anspruch auf Verwendung im letzten Satz, aber wer das tut, ersetzt eine Klasse durch eine andere, die nicht gleichwertig ist. Die Klasse O (n!) Enthält, ist aber streng größer als O (n ^ n). Die tatsächliche Äquivalenz wäre O (n!) = O (n ^ ne ^ {- n} sqrt (n)).
bedingte

43

Kleine Erinnerung: Die big ONotation wird verwendet, um die asymptotische Komplexität zu bezeichnen ( dh wenn die Größe des Problems unendlich wird), und sie verbirgt eine Konstante.

Dies bedeutet, dass zwischen einem Algorithmus in O (n) und einem in O (n 2 ) der schnellste nicht immer der erste ist (obwohl es immer einen Wert von n gibt, so dass für Probleme mit einer Größe> n der erste Algorithmus ist das schnellste).

Beachten Sie, dass die versteckte Konstante sehr stark von der Implementierung abhängt!

In einigen Fällen ist die Laufzeit auch keine deterministische Funktion der Größe n der Eingabe. Nehmen wir zum Beispiel die Sortierung mit der Schnellsortierung: Die Zeit, die zum Sortieren eines Arrays von n Elementen benötigt wird, ist keine Konstante, sondern hängt von der Startkonfiguration des Arrays ab.

Es gibt verschiedene zeitliche Komplexitäten:

  • Schlimmster Fall (normalerweise am einfachsten herauszufinden, wenn auch nicht immer sehr aussagekräftig)
  • Durchschnittlicher Fall (normalerweise viel schwieriger herauszufinden ...)

  • ...

Eine gute Einführung ist eine Einführung in die Analyse von Algorithmen von R. Sedgewick und P. Flajolet.

Wie Sie sagen, premature optimisation is the root of all evilund (wenn möglich) Profiling sollte bei der Optimierung von Code immer verwendet werden. Es kann Ihnen sogar helfen, die Komplexität Ihrer Algorithmen zu bestimmen.


3
In der Mathematik bedeutet O (.) Eine Obergrenze und Theta (.) Bedeutet, dass Sie eine Ober- und Untergrenze haben. Ist die Definition in CS tatsächlich anders oder handelt es sich nur um einen häufigen Missbrauch der Notation? Nach der mathematischen Definition ist sqrt (n) sowohl O (n) als auch O (n ^ 2), so dass es nicht immer einige n gibt, nach denen eine O (n) -Funktion kleiner ist.
Douglas Zare

28

Wenn wir die Antworten hier sehen, können wir daraus schließen, dass die meisten von uns tatsächlich die Reihenfolge des Algorithmus annähern, indem sie ihn betrachten und den gesunden Menschenverstand verwenden, anstatt ihn beispielsweise mit der Master-Methode zu berechnen, wie wir es an der Universität gedacht hatten. Vor diesem Hintergrund muss ich hinzufügen, dass sogar der Professor uns (später) ermutigt hat, tatsächlich darüber nachzudenken , anstatt es nur zu berechnen.

Außerdem möchte ich hinzufügen, wie es für rekursive Funktionen gemacht wird :

Angenommen, wir haben eine Funktion wie ( Schema-Code ):

(define (fac n)
    (if (= n 0)
        1
            (* n (fac (- n 1)))))

die rekursiv die Fakultät der gegebenen Zahl berechnet.

Der erste Schritt besteht darin, nur in diesem Fall zu versuchen, das Leistungsmerkmal für den Funktionskörper zu bestimmen. Im Körper wird nichts Besonderes getan, nur eine Multiplikation (oder die Rückgabe des Wertes 1).

Die Leistung für den Körper ist also: O (1) (konstant).

Versuchen Sie als nächstes, dies für die Anzahl der rekursiven Aufrufe zu bestimmen . In diesem Fall haben wir n-1 rekursive Aufrufe.

Die Leistung für die rekursiven Aufrufe lautet also: O (n-1) (Reihenfolge ist n, da wir die unbedeutenden Teile wegwerfen).

Setzen Sie dann diese beiden zusammen und Sie haben dann die Leistung für die gesamte rekursive Funktion:

1 * (n-1) = O (n)


Peter , um deine aufgeworfenen Fragen zu beantworten ; Die hier beschriebene Methode handhabt dies tatsächlich recht gut. Beachten Sie jedoch, dass dies immer noch eine Annäherung und keine vollständige mathematisch korrekte Antwort ist. Die hier beschriebene Methode ist auch eine der Methoden, die uns an der Universität beigebracht wurden, und wenn ich mich recht erinnere, wurde sie für weitaus fortgeschrittenere Algorithmen verwendet als die Fakultät, die ich in diesem Beispiel verwendet habe.
Natürlich hängt alles davon ab, wie gut Sie die Laufzeit des Funktionskörpers und die Anzahl der rekursiven Aufrufe abschätzen können, aber das gilt auch für die anderen Methoden.


Sven, ich bin mir nicht sicher, ob Ihre Art, die Komplexität einer rekursiven Funktion zu beurteilen, für komplexere funktionieren wird, z. B. eine Suche / Summierung / etwas von oben nach unten in einem Binärbaum. Sicher, Sie könnten über ein einfaches Beispiel nachdenken und die Antwort finden. Aber ich denke, Sie müssten tatsächlich etwas Mathe für rekursive machen?
Peteter

3
+1 für die Rekursion ... Auch diese ist wunderschön: "... sogar der Professor hat uns zum Nachdenken angeregt ..." :)
TT_

Ja das ist so gut Ich neige dazu, es so zu denken, höher der Begriff in O (..), mehr die Arbeit, die Sie / Maschine tun. Es zu denken, während man sich auf etwas bezieht, mag eine Annäherung sein, aber diese Grenzen sind es auch. Sie sagen Ihnen nur, wie sich die zu erledigende Arbeit erhöht, wenn die Anzahl der Eingaben erhöht wird.
Abhinav Gauniyal

26

Wenn Ihre Kosten ein Polynom sind, behalten Sie einfach den Term höchster Ordnung ohne seinen Multiplikator bei. Z.B:

O ((n / 2 + 1) * (n / 2)) = O (n 2 /4 + n / 2) = O (n 2 /4) = O (n 2 )

Das funktioniert wohlgemerkt nicht für unendliche Serien. Es gibt kein einziges Rezept für den allgemeinen Fall, obwohl für einige häufige Fälle die folgenden Ungleichungen gelten:

O (log N ) <O ( N ) <O ( N log N ) <O ( N 2 ) <O ( N k ) <O ( en ) <O ( n !)


8
sicherlich O (N) <O (NlogN)
jk.

22

Ich denke darüber in Bezug auf Informationen nach. Jedes Problem besteht darin, eine bestimmte Anzahl von Bits zu lernen.

Ihr grundlegendes Werkzeug ist das Konzept der Entscheidungspunkte und ihrer Entropie. Die Entropie eines Entscheidungspunkts ist die durchschnittliche Information, die er Ihnen gibt. Wenn ein Programm beispielsweise einen Entscheidungspunkt mit zwei Zweigen enthält, ist seine Entropie die Summe der Wahrscheinlichkeit jedes Zweigs multipliziert mit log 2 der inversen Wahrscheinlichkeit dieses Zweigs. So viel lernen Sie, wenn Sie diese Entscheidung treffen.

Beispielsweise hat eine ifAnweisung mit zwei Zweigen, die beide gleich wahrscheinlich sind, eine Entropie von 1/2 * log (2/1) + 1/2 * log (2/1) = 1/2 * 1 + 1/2 * 1 = 1. Die Entropie beträgt also 1 Bit.

Angenommen, Sie durchsuchen eine Tabelle mit N Elementen, z. B. N = 1024. Dies ist ein 10-Bit-Problem, da log (1024) = 10 Bit ist. Wenn Sie es also mit IF-Anweisungen durchsuchen können, deren Ergebnisse gleich wahrscheinlich sind, sollten 10 Entscheidungen getroffen werden.

Das bekommen Sie mit der binären Suche.

Angenommen, Sie führen eine lineare Suche durch. Sie schauen sich das erste Element an und fragen, ob es das ist, das Sie wollen. Die Wahrscheinlichkeiten sind 1/1024, dass es ist, und 1023/1024, dass es nicht ist. Die Entropie dieser Entscheidung ist 1/1024 * log (1024/1) + 1023/1024 * log (1024/1023) = 1/1024 * 10 + 1023/1024 * ungefähr 0 = ungefähr 0,01 Bit. Du hast sehr wenig gelernt! Die zweite Entscheidung ist nicht viel besser. Deshalb ist die lineare Suche so langsam. Tatsächlich ist die Anzahl der zu lernenden Bits exponentiell.

Angenommen, Sie führen eine Indizierung durch. Angenommen, die Tabelle ist in viele Bins vorsortiert, und Sie verwenden einige der Bits im Schlüssel, um direkt auf den Tabelleneintrag zu indizieren. Wenn 1024 Bins vorhanden sind, beträgt die Entropie 1/1024 * log (1024) + 1/1024 * log (1024) + ... für alle 1024 möglichen Ergebnisse. Dies sind 1/1024 * 10 mal 1024 Ergebnisse oder 10 Entropiebits für diese eine Indizierungsoperation. Deshalb ist die Indizierungssuche schnell.

Denken Sie jetzt über das Sortieren nach. Sie haben N Elemente und Sie haben eine Liste. Für jedes Element müssen Sie suchen, wo sich das Element in der Liste befindet, und es dann zur Liste hinzufügen. Das Sortieren dauert also ungefähr das N-fache der Anzahl der Schritte der zugrunde liegenden Suche.

Sortierungen, die auf binären Entscheidungen mit ungefähr gleich wahrscheinlichen Ergebnissen basieren, erfordern alle ungefähr O (N log N) Schritte. Ein O (N) -Sortieralgorithmus ist möglich, wenn er auf der Indizierungssuche basiert.

Ich habe festgestellt, dass fast alle algorithmischen Leistungsprobleme auf diese Weise betrachtet werden können.


Beeindruckend. Haben Sie hilfreiche Referenzen dazu? Ich denke, dieses Zeug ist hilfreich für mich beim Entwerfen / Refaktorieren / Debuggen von Programmen.
Jesvin Jose

3
@aitchnyu: Für das, was es wert ist, habe ich ein Buch geschrieben, das dieses und andere Themen behandelt. Es ist längst vergriffen, aber Kopien werden zu einem vernünftigen Preis angeboten. Ich habe versucht, GoogleBooks dazu zu bringen, es zu greifen, aber im Moment ist es ein wenig schwierig herauszufinden, wer das Urheberrecht hat.
Mike Dunlavey

21

Fangen wir von vorne an.

Akzeptieren Sie zunächst das Prinzip, dass bestimmte einfache Operationen an Daten rechtzeitig ausgeführt werden können, dh in einer O(1)Zeit, die unabhängig von der Größe der Eingabe ist. Diese primitiven Operationen in C bestehen aus

  1. Arithmetische Operationen (zB + oder%).
  2. Logische Operationen (z. B. &&).
  3. Vergleichsoperationen (zB <=).
  4. Strukturzugriffsoperationen (z. B. Array-Indizierung wie A [i] oder Zeiger nach dem Operator ->).
  5. Einfache Zuordnung wie das Kopieren eines Wertes in eine Variable.
  6. Ruft Bibliotheksfunktionen auf (z. B. scanf, printf).

Die Begründung für dieses Prinzip erfordert eine detaillierte Untersuchung der Maschinenanweisungen (primitiven Schritte) eines typischen Computers. Jede der beschriebenen Operationen kann mit einer kleinen Anzahl von Maschinenanweisungen ausgeführt werden; oft werden nur ein oder zwei Anweisungen benötigt. Infolgedessen können verschiedene Arten von Anweisungen in C zeitlich ausgeführt O(1)werden, dh in einer konstanten Zeitdauer, die von der Eingabe unabhängig ist. Diese einfachen gehören

  1. Zuweisungsanweisungen, die keine Funktionsaufrufe in ihren Ausdrücken enthalten.
  2. Aussagen lesen.
  3. Schreiben Sie Anweisungen, für die keine Funktionsaufrufe erforderlich sind, um Argumente auszuwerten.
  4. Die Sprunganweisungen brechen, setzen fort, gehen zu und geben den Ausdruck zurück, wobei der Ausdruck keinen Funktionsaufruf enthält.

In C werden viele for-Schleifen gebildet, indem eine Indexvariable auf einen bestimmten Wert initialisiert und diese Variable jedes Mal um die Schleife um 1 erhöht wird. Die for-Schleife endet, wenn der Index eine Grenze erreicht. Zum Beispiel die for-Schleife

for (i = 0; i < n-1; i++) 
{
    small = i;
    for (j = i+1; j < n; j++)
        if (A[j] < A[small])
            small = j;
    temp = A[small];
    A[small] = A[i];
    A[i] = temp;
}

verwendet die Indexvariable i. Es erhöht i jedes Mal um die Schleife um 1, und die Iterationen hören auf, wenn i n - 1 erreicht.

Konzentrieren Sie sich im Moment jedoch auf die einfache Form der for-Schleife, bei der die Differenz zwischen dem End- und dem Anfangswert geteilt durch den Betrag, um den die Indexvariable erhöht wird, angibt, wie oft wir die Schleife durchlaufen . Diese Anzahl ist genau, es sei denn, es gibt Möglichkeiten, die Schleife über eine Sprunganweisung zu verlassen. es ist in jedem Fall eine Obergrenze für die Anzahl der Iterationen.

Zum Beispiel iteriert die for-Schleife ((n − 1) − 0)/1 = n − 1 times, da 0 der Anfangswert von i ist, n - 1 der höchste von i erreichte Wert ist (dh wenn i n - 1 erreicht, stoppt die Schleife und es tritt keine Iteration mit i = n - auf 1) und 1 wird bei jeder Iteration der Schleife zu i addiert.

Im einfachsten Fall, wenn die im Schleifenkörper verbrachte Zeit für jede Iteration gleich ist, können wir die Big-Oh-Obergrenze für den Körper mit der Anzahl der Male um die Schleife multiplizieren . Genau genommen müssen wir dann die O (1) -Zeit addieren, um den Schleifenindex zu initialisieren, und die O (1) -Zeit für den ersten Vergleich des Schleifenindex mit dem Grenzwert , da wir einmal mehr testen, als wir die Schleife umgehen. Sofern es nicht möglich ist, die Schleife nullmal auszuführen, ist die Zeit zum Initialisieren der Schleife und zum einmaligen Testen des Grenzwerts ein Term niedriger Ordnung, der durch die Summierungsregel gelöscht werden kann.


Betrachten Sie nun dieses Beispiel:

(1) for (j = 0; j < n; j++)
(2)   A[i][j] = 0;

Wir wissen, dass Zeile (1)O(1) Zeit braucht . Es ist klar, dass wir die Schleife n-mal umgehen, wie wir bestimmen können, indem wir die Untergrenze von der Obergrenze in Zeile (1) subtrahieren und dann 1 addieren. Da der Körper, Zeile (2), O (1) Zeit benötigt, wir können die Zeit zum Inkrementieren von j und die Zeit zum Vergleichen von j mit n vernachlässigen, die beide auch O (1) sind. Somit ist die Laufzeit der Zeilen (1) und (2) das Produkt aus N und O (1) , das ist O(n).

In ähnlicher Weise können wir die Laufzeit der äußeren Schleife, die aus den Linien (2) bis (4) besteht, begrenzen

(2) for (i = 0; i < n; i++)
(3)     for (j = 0; j < n; j++)
(4)         A[i][j] = 0;

Wir haben bereits festgestellt, dass die Schleife der Linien (3) und (4) O (n) Zeit benötigt. Somit können wir die O (1) -Zeit vernachlässigen, um i zu inkrementieren und zu testen, ob i <n in jeder Iteration ist, was zu dem Schluss führt, dass jede Iteration der äußeren Schleife O (n) -Zeit benötigt.

Die Initialisierung i = 0 der äußeren Schleife und der (n + 1) -te Test der Bedingung i <n benötigen ebenfalls O (1) Zeit und können vernachlässigt werden. Schließlich beobachten wir, dass wir n-mal um die äußere Schleife herumgehen und für jede Iteration O (n) -Zeit nehmen, was eine Gesamtlaufzeit ergibt O(n^2).


Ein praktischeres Beispiel.

Geben Sie hier die Bildbeschreibung ein


Was ist, wenn eine goto-Anweisung einen Funktionsaufruf enthält? So etwas wie Schritt 3: if (M.step == 3) {M = Schritt 3 (erledigt, M); } Schritt 4: if (M.step == 4) {M = Schritt 4 (M); } if (M.step == 5) {M = step5 (M); gehe zu Schritt 3; } if (M.step == 6) {M = step6 (M); gehe zu Schritt 4; } return cut_matrix (A, M); Wie würde dann die Komplexität berechnet? Wäre es eine Addition oder eine Multiplikation? Wenn man bedenkt, dass Schritt 4 n ^ 3 und Schritt 5 n ^ 2 ist.
Taha Tariq

14

Wenn Sie die Reihenfolge Ihres Codes empirisch schätzen möchten, anstatt den Code zu analysieren, können Sie eine Reihe ansteigender Werte für n und die Zeit Ihres Codes eingeben. Zeichnen Sie Ihre Timings auf einer Protokollskala. Wenn der Code O (x ^ n) ist, sollten die Werte auf eine Steigungslinie n fallen.

Dies hat mehrere Vorteile gegenüber dem bloßen Studium des Codes. Zum einen können Sie sehen, ob Sie sich in dem Bereich befinden, in dem sich die Laufzeit ihrer asymptotischen Reihenfolge nähert. Möglicherweise stellen Sie auch fest, dass ein Code, von dem Sie dachten, er sei die Reihenfolge O (x), tatsächlich die Reihenfolge O (x ^ 2) ist, beispielsweise aufgrund der Zeit, die für Bibliotheksaufrufe aufgewendet wurde.


Nur um diese Antwort zu aktualisieren: en.wikipedia.org/wiki/Analysis_of_algorithms , dieser Link hat die Formel, die Sie benötigen. Viele Algorithmen folgen einer Potenzregel. Wenn dies bei Ihnen der Fall ist, können wir mit 2 Zeitpunkten und 2 Laufzeiten auf einer Maschine die Steigung auf einem Log-Log-Plot berechnen. Welches ist a = log (t2 / t1) / log (n2 / n1), dies gab mir den Exponenten für den Algorithmus in, O (N ^ a). Dies kann mit der manuellen Berechnung unter Verwendung des Codes verglichen werden.
Christopher John

1
Hallo, schöne Antwort. Ich habe mich gefragt, ob Ihnen eine Bibliothek oder Methodik bekannt ist (ich arbeite zum Beispiel mit Python / R), um diese empirische Methode zu verallgemeinern, dh verschiedene Komplexitätsfunktionen an einen Datensatz mit zunehmender Größe anzupassen und herauszufinden, welche relevant sind. Danke
Agenis

10

Grundsätzlich werden in 90% der Fälle nur Schleifen analysiert. Haben Sie einfache, doppelte oder dreifach verschachtelte Schleifen? Die Sie haben O (n), O (n ^ 2), O (n ^ 3) Laufzeit.

Sehr selten (es sei denn, Sie schreiben eine Plattform mit einer umfangreichen Basisbibliothek (wie zum Beispiel die .NET BCL oder die CL von C ++)) werden Sie auf etwas stoßen, das schwieriger ist, als nur Ihre Schleifen zu betrachten (für Anweisungen, while, goto, usw...)


1
Kommt auf die Schleifen an.
Kelalaka

8

Die Big O-Notation ist nützlich, da sie einfach zu bearbeiten ist und unnötige Komplikationen und Details verbirgt (für eine Definition von unnötig). Eine gute Möglichkeit, die Komplexität von Divide- und Conquer-Algorithmen zu ermitteln, ist die Baummethode. Angenommen, Sie haben eine Version von Quicksort mit der Medianprozedur, sodass Sie das Array jedes Mal in perfekt ausbalancierte Subarrays aufteilen.

Erstellen Sie nun einen Baum, der allen Arrays entspricht, mit denen Sie arbeiten. An der Wurzel haben Sie das ursprüngliche Array, die Wurzel hat zwei untergeordnete Elemente, die die Subarrays sind. Wiederholen Sie diesen Vorgang, bis Sie unten einzelne Elementarrays haben.

Da wir den Median in O (n) -Zeit finden und das Array in O (n) -Zeit in zwei Teile teilen können, ist die an jedem Knoten geleistete Arbeit O (k), wobei k die Größe des Arrays ist. Jede Ebene des Baums enthält (höchstens) das gesamte Array, sodass die Arbeit pro Ebene O (n) beträgt (die Größen der Subarrays addieren sich zu n, und da wir O (k) pro Ebene haben, können wir dies addieren). . Es gibt nur log (n) Ebenen im Baum, da jedes Mal, wenn wir die Eingabe halbieren.

Daher können wir den Arbeitsaufwand durch O (n * log (n)) nach oben begrenzen.

Big O verbirgt jedoch einige Details, die wir manchmal nicht ignorieren können. Erwägen Sie die Berechnung der Fibonacci-Sequenz mit

a=0;
b=1;
for (i = 0; i <n; i++) {
    tmp = b;
    b = a + b;
    a = tmp;
}

und nehmen wir einfach an, dass a und b BigInteger in Java sind oder etwas, das beliebig große Zahlen verarbeiten kann. Die meisten Leute würden sagen, dass dies ein O (n) -Algorithmus ist, ohne zusammenzuzucken. Der Grund dafür ist, dass Sie n Iterationen in der for-Schleife haben und O (1) neben der Schleife arbeiten.

Aber Fibonacci-Zahlen sind groß, die n-te Fibonacci-Zahl ist in n exponentiell, so dass nur das Speichern in der Größenordnung von n Bytes erfolgt. Das Durchführen einer Addition mit großen ganzen Zahlen erfordert O (n) Arbeit. Der Gesamtaufwand für dieses Verfahren beträgt also

1 + 2 + 3 + ... + n = n (n-1) / 2 = O (n ^ 2)

Dieser Algorithmus läuft also in quadratischer Zeit!


1
Sie sollten sich nicht darum kümmern, wie die Zahlen gespeichert werden, es ändert nichts daran, dass der Algorithmus mit einer Obergrenze von O (n) wächst.
Mikek3332002

8

Allgemein weniger nützlich, denke ich, aber der Vollständigkeit halber gibt es auch ein Big Omega Ω , das eine Untergrenze für die Komplexität eines Algorithmus definiert, und ein Big Theta Θ , das sowohl eine Ober- als auch eine Untergrenze definiert.


7

Teilen Sie den Algorithmus in Teile auf, für die Sie die große O-Notation kennen, und kombinieren Sie ihn durch große O-Operatoren. Das ist der einzige Weg, den ich kenne.

Weitere Informationen finden Sie auf der Wikipedia-Seite zu diesem Thema.


7

Vertrautheit mit den von mir verwendeten Algorithmen / Datenstrukturen und / oder schnelle Analyse der Iterationsverschachtelung. Die Schwierigkeit besteht darin, dass Sie eine Bibliotheksfunktion möglicherweise mehrmals aufrufen. Oft können Sie sich nicht sicher sein, ob Sie die Funktion manchmal unnötig aufrufen oder welche Implementierung sie verwenden. Möglicherweise sollten Bibliotheksfunktionen ein Komplexitäts- / Effizienzmaß haben, sei es Big O oder eine andere Metrik, die in der Dokumentation oder sogar in IntelliSense verfügbar ist .


6

Das "Wie berechnet man Big O?" Ist Teil der rechnergestützten Komplexitätstheorie . Für einige (viele) Sonderfälle können Sie möglicherweise einige einfache Heuristiken verwenden (z. B. das Multiplizieren der Anzahl der Schleifen für verschachtelte Schleifen), insbesondere wenn alles, was Sie wollen, eine Schätzung der Obergrenze ist und es Ihnen nichts ausmacht, wenn sie zu pessimistisch ist - worum es bei Ihrer Frage wahrscheinlich geht.

Wenn Sie Ihre Frage für einen Algorithmus wirklich beantworten möchten, können Sie die Theorie am besten anwenden. Neben der simplen "Worst-Case" -Analyse habe ich die Amortized-Analyse in der Praxis als sehr nützlich empfunden.


6

Für den ersten Fall wird die innere Schleife n-imal ausgeführt , sodass die Gesamtzahl der Ausführungen die Summe für den iÜbergang von 0zu n-1(weil niedriger als, nicht niedriger als oder gleich) der ist n-i. Du bekommst endlich n*(n + 1) / 2so O(n²/2) = O(n²).

Für die 2. Schleife iist zwischen 0und nfür die äußere Schleife enthalten; dann wird die innere Schleife ausgeführt, wenn sie jstreng größer als ist n, was dann unmöglich ist.


5

Zusätzlich zur Verwendung der Master-Methode (oder einer ihrer Spezialisierungen) teste ich meine Algorithmen experimentell. Dies kann nicht beweisen, dass eine bestimmte Komplexitätsklasse erreicht wird, kann jedoch die Gewissheit geben, dass die mathematische Analyse angemessen ist. Um diese Sicherheit zu gewährleisten, verwende ich in Verbindung mit meinen Experimenten Tools zur Codeabdeckung, um sicherzustellen, dass ich alle Fälle ausführe.

Als sehr einfaches Beispiel sagen Sie, Sie wollten eine Überprüfung der Geschwindigkeit der Listensortierung des .NET Frameworks durchführen. Sie können Folgendes schreiben und dann die Ergebnisse in Excel analysieren, um sicherzustellen, dass sie eine n * log (n) -Kurve nicht überschreiten.

In diesem Beispiel messe ich die Anzahl der Vergleiche, aber es ist auch ratsam, die tatsächliche Zeit zu untersuchen, die für jede Stichprobengröße erforderlich ist. Dann müssen Sie jedoch noch vorsichtiger sein, dass Sie nur den Algorithmus messen und keine Artefakte aus Ihrer Testinfrastruktur einbeziehen.

int nCmp = 0;
System.Random rnd = new System.Random();

// measure the time required to sort a list of n integers
void DoTest(int n)
{
   List<int> lst = new List<int>(n);
   for( int i=0; i<n; i++ )
      lst[i] = rnd.Next(0,1000);

   // as we sort, keep track of the number of comparisons performed!
   nCmp = 0;
   lst.Sort( delegate( int a, int b ) { nCmp++; return (a<b)?-1:((a>b)?1:0)); }

   System.Console.Writeline( "{0},{1}", n, nCmp );
}


// Perform measurement for a variety of sample sizes.
// It would be prudent to check multiple random samples of each size, but this is OK for a quick sanity check
for( int n = 0; n<1000; n++ )
   DoTest(n);

4

Vergessen Sie nicht, auch Platzkomplexitäten zu berücksichtigen, die ebenfalls Anlass zur Sorge geben können, wenn die Speicherressourcen begrenzt sind. So können Sie beispielsweise jemanden hören, der einen Algorithmus mit konstantem Speicherplatz wünscht. Dies bedeutet im Grunde, dass der vom Algorithmus belegte Speicherplatz nicht von Faktoren im Code abhängt.

Manchmal hängt die Komplexität davon ab, wie oft etwas aufgerufen wird, wie oft eine Schleife ausgeführt wird, wie oft Speicher zugewiesen wird usw. Ein weiterer Teil zur Beantwortung dieser Frage.

Schließlich kann großes O für Worst-Case-, Best-Case- und Amortisationsfälle verwendet werden, bei denen im Allgemeinen der Worst-Case verwendet wird, um zu beschreiben, wie schlecht ein Algorithmus sein kann.


4

Was oft übersehen wird, ist das erwartete Verhalten Ihrer Algorithmen. Es ändert nichts am Big-O Ihres Algorithmus , bezieht sich jedoch auf die Aussage "Vorzeitige Optimierung ...".

Das erwartete Verhalten Ihres Algorithmus ist - sehr niedergeschlagen - wie schnell Sie erwarten können, dass Ihr Algorithmus mit Daten arbeitet, die Sie am wahrscheinlichsten sehen.

Wenn Sie beispielsweise nach einem Wert in einer Liste suchen, ist dies O (n). Wenn Sie jedoch wissen, dass die meisten Listen, die Sie sehen, Ihren Wert im Voraus haben, ist das typische Verhalten Ihres Algorithmus schneller.

Um es wirklich zu verstehen, müssen Sie in der Lage sein, die Wahrscheinlichkeitsverteilung Ihres "Eingabebereichs" zu beschreiben (wenn Sie eine Liste sortieren müssen, wie oft wird diese Liste bereits sortiert? Wie oft wird sie vollständig umgekehrt? Wie oft ist es meistens sortiert?) Es ist nicht immer machbar, dass Sie das wissen, aber manchmal tun Sie es.


4

tolle Frage!

Haftungsausschluss: Diese Antwort enthält falsche Aussagen, siehe Kommentare unten.

Wenn Sie das Big O verwenden, sprechen Sie über den schlimmsten Fall (mehr dazu später). Zusätzlich gibt es Kapital Theta für den Durchschnittsfall und ein großes Omega für den besten Fall.

Auf dieser Website finden Sie eine schöne formale Definition von Big O: https://xlinux.nist.gov/dads/HTML/bigOnotation.html

f (n) = O (g (n)) bedeutet, dass es positive Konstanten c und k gibt, so dass 0 ≤ f (n) ≤ cg (n) für alle n ≥ k ist. Die Werte von c und k müssen für die Funktion f festgelegt werden und dürfen nicht von n abhängen.


Ok, was meinen wir nun mit "Best-Case" - und "Worst-Case" -Komplexität?

Dies wird wahrscheinlich am deutlichsten anhand von Beispielen veranschaulicht. Wenn wir beispielsweise die lineare Suche verwenden, um eine Zahl in einem sortierten Array zu finden, ist der schlimmste Fall, wenn wir uns entscheiden, nach dem letzten Element des Arrays zu suchen, da dies so viele Schritte erfordern würde, wie Elemente im Array vorhanden sind. Der beste Fall wäre, wenn wir nach dem ersten Element suchen, da wir nach der ersten Prüfung fertig wären.

Der Sinn all dieser Adjektiv- Fall-Komplexitäten besteht darin, dass wir nach einer Möglichkeit suchen, die Zeitspanne, die ein hypothetisches Programm bis zur Fertigstellung durchläuft, in Bezug auf die Größe bestimmter Variablen grafisch darzustellen. Für viele Algorithmen kann man jedoch argumentieren, dass es für eine bestimmte Eingabegröße keine einzige Zeit gibt. Beachten Sie, dass dies der Grundvoraussetzung einer Funktion widerspricht. Jede Eingabe sollte nicht mehr als eine Ausgabe haben. Wir haben uns also mehrere Funktionen ausgedacht, um die Komplexität eines Algorithmus zu beschreiben. Obwohl das Durchsuchen eines Arrays der Größe n je nach dem, wonach Sie im Array suchen, und proportional zu n unterschiedlich lange dauern kann, können wir eine informative Beschreibung des Algorithmus im besten Fall und im Durchschnittsfall erstellen und Worst-Case-Klassen.

Entschuldigung, das ist so schlecht geschrieben und es fehlen viele technische Informationen. Aber hoffentlich wird es einfacher, über Zeitkomplexitätsklassen nachzudenken. Sobald Sie sich mit diesen vertraut gemacht haben, müssen Sie nur noch Ihr Programm analysieren und nach For-Loops suchen, die von der Arraygröße und den Überlegungen abhängen, die auf Ihren Datenstrukturen basieren. Welche Art von Eingabe würde zu trivialen Fällen führen und welche Eingabe würde sich ergeben im schlimmsten Fall.


1
Das ist falsch. Big O bedeutet "Obergrenze", nicht der schlimmste Fall.
Samy Bencherif

1
Es ist ein weit verbreitetes Missverständnis, dass Big-O sich auf den schlimmsten Fall bezieht. Wie hängen O und Ω mit dem schlechtesten und besten Fall zusammen?
Bernhard Barker

1
Das ist irreführend. Big-O bedeutet Obergrenze für eine Funktion f (n). Omega bedeutet Untergrenze für eine Funktion f (n). Es hängt überhaupt nicht mit dem besten oder schlechtesten Fall zusammen.
Tasneem Haider

1
Sie können Big-O als Obergrenze für den besten oder den schlechtesten Fall verwenden, aber ansonsten ja, keine Beziehung.
Samy Bencherif

2

Ich weiß nicht, wie ich das programmgesteuert lösen soll, aber das erste, was die Leute tun, ist, dass wir den Algorithmus für bestimmte Muster in der Anzahl der durchgeführten Operationen abtasten, sagen wir 4n ^ 2 + 2n + 1, wir haben 2 Regeln:

  1. Wenn wir eine Summe von Begriffen haben, wird der Begriff mit der größten Wachstumsrate beibehalten, wobei andere Begriffe weggelassen werden.
  2. Wenn wir ein Produkt aus mehreren Faktoren haben, werden konstante Faktoren weggelassen.

Wenn wir f (x) vereinfachen, wobei f (x) die Formel für die Anzahl der durchgeführten Operationen ist (4n ^ 2 + 2n + 1, wie oben erläutert), erhalten wir hier den Big-O-Wert [O (n ^ 2) Fall]. Dies müsste jedoch die Lagrange-Interpolation im Programm berücksichtigen, die möglicherweise schwer zu implementieren ist. Und was wäre, wenn der echte Big-O-Wert O (2 ^ n) wäre und wir möglicherweise so etwas wie O (x ^ n) hätten, sodass dieser Algorithmus wahrscheinlich nicht programmierbar wäre. Aber wenn mir jemand das Gegenteil beweist, gib mir den Code. . . .


2

Für Code A wird die äußere Schleife für n+1Zeiten ausgeführt. Die Zeit '1' bedeutet den Prozess, der prüft, ob ich die Anforderung noch erfülle. Und die innere Schleife läuft nmal, n-2mal .... Also , 0+2+..+(n-2)+n= (0+n)(n+1)/2= O(n²).

Für Code B wird, obwohl die innere Schleife nicht einspringen und foo () ausführen würde, die innere Schleife n-mal ausgeführt, abhängig von der Ausführungszeit der äußeren Schleife, die O (n) ist.


1

Ich möchte das Big-O in einem etwas anderen Aspekt erklären.

Big-O dient nur dazu, die Komplexität der Programme zu vergleichen, dh wie schnell sie wachsen, wenn die Eingaben zunehmen, und nicht die genaue Zeit, die für die Ausführung der Aktion aufgewendet wird.

IMHO in den Big-O-Formeln sollten Sie keine komplexeren Gleichungen verwenden (Sie können sich nur an die in der folgenden Grafik halten). Sie können jedoch auch andere präzisere Formeln verwenden (wie 3 ^ n, n ^ 3, .. .) aber mehr als das kann manchmal irreführend sein! Also besser so einfach wie möglich zu halten.

Geben Sie hier die Bildbeschreibung ein

Ich möchte noch einmal betonen, dass wir hier keine genaue Formel für unseren Algorithmus erhalten wollen. Wir wollen nur zeigen, wie es wächst, wenn die Eingaben wachsen, und mit den anderen Algorithmen in diesem Sinne vergleichen. Andernfalls verwenden Sie besser verschiedene Methoden wie das Benchmarking.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.