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!
O(log n)
kann gesehen werden als: Wenn Sie die Problemgröße verdoppelnn
, benötigt Ihr Algorithmus nur eine konstante Anzahl von Schritten mehr.