Was würde dazu führen, dass ein Algorithmus eine O (log n) -Komplexität aufweist?


106

Mein Wissen über Big-O ist begrenzt, und wenn Log-Terme in der Gleichung auftauchen, stört mich das noch mehr.

Kann mir vielleicht jemand in einfachen Worten erklären, was ein O(log n)Algorithmus ist? Woher kommt der Logarithmus?

Dies kam speziell auf, als ich versuchte, diese mittelfristige Übungsfrage zu lösen:

X (1..n) und Y (1..n) enthalten zwei Listen von ganzen Zahlen, die jeweils in nicht absteigender Reihenfolge sortiert sind. Geben Sie einen O (log n) -Zeitalgorithmus an, um den Median (oder die n-te kleinste Ganzzahl) aller 2n kombinierten Elemente zu ermitteln. Zum Beispiel ist X = (4, 5, 7, 8, 9) und Y = (3, 5, 8, 9, 10), dann ist 7 der Median der kombinierten Liste (3, 4, 5, 5, 7) 8, 8, 9, 9, 10). [Hinweis: Konzepte der binären Suche verwenden]


29
O(log n)kann gesehen werden als: Wenn Sie die Problemgröße verdoppeln n, benötigt Ihr Algorithmus nur eine konstante Anzahl von Schritten mehr.
Phimuemue

3
Diese Website hat mir geholfen, die Big O-Notation zu verstehen: recursive-design.com/blog/2010/12/07/…
Brad

1
Ich frage mich, warum 7 der Median des obigen Beispiels ist, aber es könnte auch 8 sein. Ein nicht so gutes Beispiel, oder?
Stryba

13
Eine gute Möglichkeit, über O (log (n)) - Algorithmen nachzudenken, besteht darin, dass sie in jedem Schritt die Größe des Problems um die Hälfte reduzieren. Nehmen Sie das Beispiel der binären Suche: In jedem Schritt überprüfen Sie den Wert in der Mitte Ihres Suchbereichs und teilen den Bereich in zwei Hälften. Danach entfernen Sie eine der Hälften aus Ihrem Suchbereich und die andere Hälfte wird zu Ihrem Suchbereich für den nächsten Schritt. Und so halbiert sich in jedem Schritt Ihr Suchbereich in der Größe, also O (log (n)) Komplexität des Algorithmus. (Die Reduzierung muss nicht genau um die Hälfte sein, sie kann um ein Drittel, um 25%, einen konstanten Prozentsatz
betragen

Vielen Dank, Leute, die an einem früheren Problem arbeiten und bald darauf zurückkommen werden. Ich schätze die Antworten sehr!
Wir

Antworten:


290

Ich muss zustimmen, dass es ziemlich seltsam ist, wenn Sie zum ersten Mal einen O (log n) -Algorithmus sehen ... woher kommt dieser Logarithmus? Es stellt sich jedoch heraus, dass es verschiedene Möglichkeiten gibt, einen Protokollbegriff in Big-O-Notation anzuzeigen. Hier sind ein paar:

Wiederholt durch eine Konstante teilen

Nimm eine beliebige Zahl n; sagen wir, 16. Wie oft können Sie n durch zwei teilen, bevor Sie eine Zahl kleiner oder gleich eins erhalten? Für 16 haben wir das

16 / 2 = 8
 8 / 2 = 4
 4 / 2 = 2
 2 / 2 = 1

Beachten Sie, dass dies in vier Schritten abgeschlossen werden muss. Interessanterweise haben wir auch das log 2 16 = 4. Hmmm ... was ist mit 128?

128 / 2 = 64
 64 / 2 = 32
 32 / 2 = 16
 16 / 2 = 8
  8 / 2 = 4
  4 / 2 = 2
  2 / 2 = 1

Dies dauerte sieben Schritte und log 2 128 = 7. Ist das ein Zufall? Nee! Dafür gibt es einen guten Grund. Angenommen, wir teilen eine Zahl n durch 2 i-mal. Dann erhalten wir die Zahl n / 2 i . Wenn wir nach dem Wert von i auflösen wollen, wobei dieser Wert höchstens 1 ist, erhalten wir

n / 2 i ≤ 1

n ≤ 2 i

log 2 n ≤ i

Mit anderen Worten, wenn wir eine ganze Zahl i so wählen, dass i ≥ log 2 n ist, dann haben wir nach dem Teilen von n in zwei i-Hälften einen Wert, der höchstens 1 beträgt. Das kleinste i, für das dies garantiert ist, ist ungefähr log 2 n Wenn wir also einen Algorithmus haben, der durch 2 dividiert, bis die Zahl ausreichend klein wird, können wir sagen, dass er in O (log n) -Schritten endet.

Ein wichtiges Detail ist, dass es keine Rolle spielt, durch welche Konstante Sie n teilen (solange sie größer als eins ist). Wenn Sie durch die Konstante k dividieren, sind log k n Schritte erforderlich, um 1 zu erreichen. Daher benötigt jeder Algorithmus, der die Eingabegröße wiederholt durch einen Bruchteil dividiert, O (log n) -Iterationen, um zu beenden. Diese Iterationen können viel Zeit in Anspruch nehmen, sodass die Nettolaufzeit nicht O (log n) sein muss, sondern die Anzahl der Schritte logarithmisch ist.

Wo kommt das her? Ein klassisches Beispiel ist die binäre Suche , ein schneller Algorithmus zum Durchsuchen eines sortierten Arrays nach einem Wert. Der Algorithmus funktioniert folgendermaßen:

  • Wenn das Array leer ist, geben Sie zurück, dass das Element nicht im Array vorhanden ist.
  • Andernfalls:
    • Schauen Sie sich das mittlere Element des Arrays an.
    • Wenn es dem gesuchten Element entspricht, geben Sie Erfolg zurück.
    • Wenn es größer ist als das Element, nach dem wir suchen:
      • Werfen Sie die zweite Hälfte des Arrays weg.
      • Wiederholen
    • Wenn es weniger als das Element ist, nach dem wir suchen:
      • Werfen Sie die erste Hälfte des Arrays weg.
      • Wiederholen

Zum Beispiel, um im Array nach 5 zu suchen

1   3   5   7   9   11   13

Wir würden uns zuerst das mittlere Element ansehen:

1   3   5   7   9   11   13
            ^

Da 7> 5 und das Array sortiert ist, wissen wir, dass die Zahl 5 nicht in der hinteren Hälfte des Arrays liegen kann, sodass wir sie einfach verwerfen können. Diese Blätter

1   3   5

Nun schauen wir uns hier das mittlere Element an:

1   3   5
    ^

Da 3 <5 ist, wissen wir, dass 5 nicht in der ersten Hälfte des Arrays erscheinen kann, also können wir das Array der ersten Hälfte zum Verlassen werfen

        5

Wieder schauen wir uns die Mitte dieses Arrays an:

        5
        ^

Da dies genau die Zahl ist, nach der wir suchen, können wir berichten, dass sich tatsächlich 5 im Array befindet.

Wie effizient ist das? Nun, bei jeder Iteration werfen wir mindestens die Hälfte der verbleibenden Array-Elemente weg. Der Algorithmus stoppt, sobald das Array leer ist oder wir den gewünschten Wert finden. Im schlimmsten Fall ist das Element nicht vorhanden, daher halbieren wir die Größe des Arrays so lange, bis uns die Elemente ausgehen. Wie lange dauert das? Nun, da wir das Array immer wieder halbieren, werden wir höchstens in O (log n) -Iterationen ausgeführt, da wir das Array nicht mehr als O (log n) -Zeiten halbieren können, bevor wir es ausführen aus Array-Elementen.

Algorithmen, die der allgemeinen Technik des Teilens und Eroberens folgen (das Problem in Stücke schneiden, diese Teile lösen und dann das Problem wieder zusammensetzen), enthalten aus demselben Grund logarithmische Terme - Sie können ein Objekt nicht weiter einschneiden halb mehr als O (log n) mal. Vielleicht möchten Sie die Zusammenführungssortierung als gutes Beispiel dafür betrachten.

Werte jeweils eine Ziffer verarbeiten

Wie viele Ziffern enthält die Basis-10-Nummer n? Nun, wenn die Zahl k Ziffern enthält, dann haben wir die größte Ziffer als Vielfaches von 10 k . Die größte k-stellige Zahl ist 999 ... 9, k-mal, und dies ist gleich 10 k + 1 - 1. Wenn wir also wissen, dass n k Ziffern enthält, dann wissen wir, dass der Wert von n ist höchstens 10 k + 1 - 1. Wenn wir nach k in n auflösen wollen, erhalten wir

n ≤ 10 k + 1 - 1

n + 1 ≤ 10 k + 1

log 10 (n + 1) ≤ k + 1

(log 10 (n + 1)) - 1 ≤ k

Daraus ergibt sich, dass k ungefähr der Logarithmus zur Basis 10 von n ist. Mit anderen Worten ist die Anzahl der Stellen in n O (log n).

Lassen Sie uns zum Beispiel über die Komplexität des Hinzufügens von zwei großen Zahlen nachdenken, die zu groß sind, um in ein Maschinenwort zu passen. Angenommen, wir haben diese Zahlen in Basis 10 dargestellt und nennen die Zahlen m und n. Eine Möglichkeit, sie hinzuzufügen, besteht in der Grundschulmethode: Schreiben Sie die Zahlen jeweils eine Ziffer aus und arbeiten Sie dann von rechts nach links. Um beispielsweise 1337 und 2065 hinzuzufügen, schreiben wir zunächst die Zahlen als

    1  3  3  7
+   2  0  6  5
==============

Wir addieren die letzte Ziffer und tragen die 1:

          1
    1  3  3  7
+   2  0  6  5
==============
             2

Dann addieren wir die vorletzte ("vorletzte") Ziffer und tragen die 1:

       1  1
    1  3  3  7
+   2  0  6  5
==============
          0  2

Als nächstes fügen wir die vorletzte ("vorletzte") Ziffer hinzu:

       1  1
    1  3  3  7
+   2  0  6  5
==============
       4  0  2

Schließlich fügen wir die vorletzte Ziffer ("preantepenultimate" ... ich liebe Englisch) hinzu:

       1  1
    1  3  3  7
+   2  0  6  5
==============
    3  4  0  2

Wie viel Arbeit haben wir getan? Wir erledigen insgesamt O (1) Arbeit pro Ziffer (dh eine konstante Menge an Arbeit), und es gibt O (max {log n, log m}) Gesamtziffern, die verarbeitet werden müssen. Dies ergibt eine Gesamtkomplexität von O (max {log n, log m}), da wir jede Ziffer in den beiden Zahlen besuchen müssen.

Viele Algorithmen erhalten einen O (log n) -Term, wenn sie in einer Basis jeweils eine Ziffer arbeiten. Ein klassisches Beispiel ist die Radix-Sortierung , bei der Ganzzahlen jeweils eine Ziffer sortiert werden. Es gibt viele Varianten der Radix-Sortierung, aber sie laufen normalerweise in der Zeit O (n log U), wobei U die größtmögliche Ganzzahl ist, die sortiert wird. Der Grund dafür ist, dass jeder Durchgang der Sortierung O (n) Zeit benötigt und insgesamt O (log U) -Iterationen erforderlich sind, um jede der O (log U) -Ziffern der größten zu sortierenden Zahl zu verarbeiten. Viele fortschrittliche Algorithmen, wie Gabows Algorithmus für kürzeste Wege oder die Skalierungsversion des Ford-Fulkerson-Max-Flow-Algorithmus , weisen in ihrer Komplexität einen Protokollbegriff auf, da sie jeweils eine Ziffer bearbeiten.


Bei Ihrer zweiten Frage, wie Sie dieses Problem lösen, sollten Sie sich diese verwandte Frage ansehen, in der eine fortgeschrittenere Anwendung untersucht wird. Angesichts der allgemeinen Struktur der hier beschriebenen Probleme können Sie jetzt besser verstehen, wie Sie über Probleme nachdenken, wenn Sie wissen, dass das Ergebnis einen Protokollbegriff enthält. Ich würde daher davon abraten, die Antwort zu prüfen, bis Sie sie gegeben haben einige Gedanken.

Hoffe das hilft!


8

Wenn wir über Big-Oh-Beschreibungen sprechen, sprechen wir normalerweise über die Zeit , die benötigt wird, um Probleme einer bestimmten Größe zu lösen . Und für einfache Probleme wird diese Größe normalerweise nur durch die Anzahl der Eingabeelemente charakterisiert, und das wird normalerweise als n oder N bezeichnet. (Offensichtlich ist das nicht immer der Fall - Probleme mit Graphen werden oft durch die Anzahl der Eckpunkte V und charakterisiert Anzahl der Kanten, E; aber im Moment werden wir über Listen von Objekten sprechen, mit N Objekten in den Listen.)

Wir sagen, dass ein Problem "groß ist - Oh von (irgendeine Funktion von N)", wenn und nur wenn :

Für alle N> einige beliebige N_0 gibt es eine Konstante c, so dass die Laufzeit des Algorithmus kleiner als diese Konstante c-mal ist (eine Funktion von N.)

Mit anderen Worten, denken Sie nicht an kleine Probleme, bei denen der "ständige Aufwand" beim Einrichten des Problems von Bedeutung ist, sondern an große Probleme. Und wenn es um große Probleme, Big-Oh (einige Funktion von N) Mittel zu denken , dass die Laufzeit ist noch immer weniger als einige konstanten Zeiten diese Funktion. Immer.

Kurz gesagt, diese Funktion ist eine Obergrenze bis zu einem konstanten Faktor.

"Big-Oh von log (n)" bedeutet also dasselbe, was ich oben gesagt habe, außer dass "eine Funktion von N" durch "log (n)" ersetzt wird.

Ihr Problem fordert Sie also auf, über die binäre Suche nachzudenken. Denken wir also darüber nach. Nehmen wir an, Sie haben beispielsweise eine Liste von N Elementen, die in aufsteigender Reihenfolge sortiert sind. Sie möchten herausfinden, ob in dieser Liste eine bestimmte Nummer vorhanden ist. Eine Möglichkeit , das zu tun , das ist nicht eine binäre Suche ist nur jedes Element der Liste zu scannen und sehen , ob es Ihre Zielnummer ist. Sie könnten Glück haben und es beim ersten Versuch finden. Im schlimmsten Fall überprüfen Sie jedoch N zu verschiedenen Zeiten. Dies ist keine binäre Suche, und es ist kein großes Oh von log (N), da es keine Möglichkeit gibt, es in die oben skizzierten Kriterien zu zwingen.

Sie können diese beliebige Konstante als c = 10 auswählen. Wenn Ihre Liste N = 32 Elemente enthält, ist dies in Ordnung: 10 * log (32) = 50, was größer als die Laufzeit von 32 ist. Wenn jedoch N = 64 , 10 * log (64) = 60, was weniger als die Laufzeit von 64 ist. Sie können c = 100 oder 1000 oder eine Unmenge auswählen, und Sie werden immer noch einige N finden, die diese Anforderung verletzen. Mit anderen Worten, es gibt kein N_0.

Wenn wir jedoch eine binäre Suche durchführen, wählen wir das mittlere Element aus und führen einen Vergleich durch. Dann werfen wir die Hälfte der Zahlen weg und machen es immer wieder und so weiter. Wenn Ihr N = 32 ist, können Sie dies nur ungefähr fünfmal tun, was log (32) ist. Wenn Ihr N = 64, können Sie dies nur tun , etwa 6 - mal, etc. Nun Sie können diese beliebige Konstante c holen, so dass die Anforderung immer für große Werte von N. erfüllt ist

Bei all dem Hintergrund bedeutet O (log (N)) normalerweise, dass Sie eine Möglichkeit haben, eine einfache Sache zu tun, die Ihre Problemgröße halbiert. Genau wie bei der binären Suche oben. Sobald Sie das Problem halbiert haben, können Sie es immer wieder halbieren. Entscheidend ist jedoch, dass Sie keinen Vorverarbeitungsschritt ausführen können, der länger als diese O-Zeit (log (N)) dauern würde. So können Sie beispielsweise Ihre beiden Listen nicht in eine große Liste mischen, es sei denn, Sie finden einen Weg, dies auch in der Zeit O (log (N)) zu tun.

(HINWEIS: Fast immer bedeutet Log (N) Log-Base-Two, was ich oben annehme.)


4

In der folgenden Lösung werden alle Zeilen mit einem rekursiven Aufruf auf der Hälfte der angegebenen Größen der Unterarrays von X und Y ausgeführt. Andere Zeilen werden in einer konstanten Zeit ausgeführt. Die rekursive Funktion ist T (2n) = T (2n / 2) + c = T (n) + c = O (lg (2n)) = O (lgn).

Sie beginnen mit MEDIAN (X, 1, n, Y, 1, n).

MEDIAN(X, p, r, Y, i, k) 
if X[r]<Y[i]
    return X[r]
if Y[k]<X[p]
    return Y[k]
q=floor((p+r)/2)
j=floor((i+k)/2)
if r-p+1 is even
    if X[q+1]>Y[j] and Y[j+1]>X[q]
        if X[q]>Y[j]
            return X[q]
        else
            return Y[j]
    if X[q+1]<Y[j-1]
        return MEDIAN(X, q+1, r, Y, i, j)
    else
        return MEDIAN(X, p, q, Y, j+1, k)
else
    if X[q]>Y[j] and Y[j+1]>X[q-1]
        return Y[j]
    if Y[j]>X[q] and X[q+1]>Y[j-1]
        return X[q]
    if X[q+1]<Y[j-1]
        return MEDIAN(X, q, r, Y, i, j)
    else
        return MEDIAN(X, p, q, Y, j, k)

3

Der Log-Begriff taucht in der Analyse der Algorithmuskomplexität sehr häufig auf. Hier einige Erklärungen:

1. Wie repräsentieren Sie eine Zahl?

Nehmen wir die Nummer X = 245436. Diese Notation von "245436" enthält implizite Informationen. Diese Informationen explizit machen:

X = 2 * 10 ^ 5 + 4 * 10 ^ 4 + 5 * 10 ^ 3 + 4 * 10 ^ 2 + 3 * 10 ^ 1 + 6 * 10 ^ 0

Welches ist die Dezimalerweiterung der Zahl. Die Mindestmenge an Informationen, die wir zur Darstellung dieser Zahl benötigen, beträgt 6 Ziffern. Dies ist kein Zufall, da jede Zahl unter 10 ^ d in d- Ziffern dargestellt werden kann.

Wie viele Ziffern sind also erforderlich, um X darzustellen? Das entspricht dem größten Exponenten von 10 in X plus 1.

==> 10 ^ d> X
==> log (10 ^ d)> log (X)
==> d * log (10)> log (X)
==> d> log (X) // Und log erscheint wieder ...
==> d = Boden (log (x)) + 1

Beachten Sie auch, dass dies die präziseste Art ist, die Nummer in diesem Bereich zu kennzeichnen. Jede Reduzierung führt zu Informationsverlust, da eine fehlende Ziffer 10 anderen Zahlen zugeordnet werden kann. Beispiel: 12 * kann 120, 121, 122,…, 129 zugeordnet werden.

2. Wie sucht man in (0, N - 1) nach einer Zahl?

Mit N = 10 ^ d verwenden wir unsere wichtigste Beobachtung:

Die minimale Informationsmenge zur eindeutigen Identifizierung eines Werts in einem Bereich zwischen 0 und N - 1 = log (N) Ziffern.

Dies bedeutet, dass bei der Suche nach einer Zahl in der Ganzzahlzeile zwischen 0 und N - 1 mindestens log (N) versucht, sie zu finden. Warum? Jeder Suchalgorithmus muss bei der Suche nach der Nummer eine Ziffer nach der anderen auswählen.

Die Mindestanzahl an Stellen, die ausgewählt werden muss, ist log (N). Daher ist die minimale Anzahl von Operationen, die ausgeführt werden, um nach einer Zahl in einem Raum der Größe N zu suchen, log (N).

Können Sie die Komplexität der binären Suche, der ternären Suche oder der Deka-Suche erraten?
Sein O (log (N))!

3. Wie sortieren Sie eine Reihe von Zahlen?

Wenn Sie aufgefordert werden, eine Reihe von Zahlen A in ein Array B zu sortieren, sieht es folgendermaßen aus: ->

Elemente permutieren

Jedes Element im ursprünglichen Array muss seinem entsprechenden Index im sortierten Array zugeordnet werden. Für das erste Element haben wir also n Positionen. Um den entsprechenden Index in diesem Bereich von 0 bis n - 1 korrekt zu finden, benötigen wir… log (n) -Operationen.

Das nächste Element benötigt Protokolloperationen (n-1), das nächste Protokoll (n-2) usw. Die Summe ergibt sich:

==> log (n) + log (n - 1) + log (n - 2) +… + log (1)

Mit log (a) + log (b) = log (a * b),

==> log (n!)

Dies kann an nlog (n) - n angenähert werden .
Welches ist O (n * log (n))!

Daher schließen wir, dass es keinen Sortieralgorithmus geben kann, der besser als O (n * log (n)) ist. Und einige Algorithmen mit dieser Komplexität sind die beliebte Zusammenführungssortierung und Heap-Sortierung!

Dies sind einige der Gründe, warum log (n) in der Komplexitätsanalyse von Algorithmen so häufig auftaucht. Das gleiche kann auf Binärzahlen erweitert werden. Ich habe hier ein Video dazu gemacht.
Warum erscheint log (n) während der Analyse der Algorithmuskomplexität so oft?

Prost!


2

Wir nennen die Zeitkomplexität O (log n), wenn die Lösung auf Iterationen über n basiert, wobei die in jeder Iteration geleistete Arbeit einen Bruchteil der vorherigen Iteration darstellt, da der Algorithmus auf die Lösung hinarbeitet.


1

Kann noch nicht kommentieren ... Nekro ist es! Die Antwort von Avi Cohen ist falsch. Versuchen Sie:

X = 1 3 4 5 8
Y = 2 5 6 7 9

Keine der Bedingungen ist wahr, also schneidet MEDIAN (X, p, q, Y, j, k) beide Fünfer. Dies sind nicht abnehmende Sequenzen, nicht alle Werte sind unterschiedlich.

Versuchen Sie auch dieses Beispiel mit gerader Länge mit unterschiedlichen Werten:

X = 1 3 4 7
Y = 2 5 6 8

Jetzt schneidet MEDIAN (X, p, q, Y, j + 1, k) die vier.

Stattdessen biete ich diesen Algorithmus an und nenne ihn mit MEDIAN (1, n, 1, n):

MEDIAN(startx, endx, starty, endy){
  if (startx == endx)
    return min(X[startx], y[starty])
  odd = (startx + endx) % 2     //0 if even, 1 if odd
  m = (startx+endx - odd)/2
  n = (starty+endy - odd)/2
  x = X[m]
  y = Y[n]
  if x == y
    //then there are n-2{+1} total elements smaller than or equal to both x and y
    //so this value is the nth smallest
    //we have found the median.
    return x
  if (x < y)
    //if we remove some numbers smaller then the median,
    //and remove the same amount of numbers bigger than the median,
    //the median will not change
    //we know the elements before x are smaller than the median,
    //and the elements after y are bigger than the median,
    //so we discard these and continue the search:
    return MEDIAN(m, endx, starty, n + 1 - odd)
  else  (x > y)
    return MEDIAN(startx, m + 1 - odd, n, endy)
}
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.