Veranstaltungsadresse: 21-tägige CSDN-Lernherausforderung
Artikelverzeichnis
- Algorithmen und ihre Komplexität
-
- 1. Einführung in den Algorithmus
- 2. Algorithmuseffizienz
Algorithmen und ihre Komplexität
1. Einführung in den Algorithmus
1.1 Was ist der Algorithmus?
Eine heute allgemein akzeptierte Definition eines Algorithmus lautet:
Ein Algorithmus ist eine Beschreibung der Schritte zur Lösung eines bestimmten Problems, die in einem Computer als endliche Folge von Anweisungen verkörpert werden und jede Anweisung eine oder mehrere Operationen darstellt。
In der realen Welt gibt es viele Arten von Problemen. Algorithmen werden entwickelt, um Probleme zu lösen, daher gibt es viele Arten von Algorithmen. Es gibt keinen allgemeinen Algorithmus, der alle Probleme lösen kann.
1.2 Eigenschaften des Algorithmus
Algorithmen haben fünf grundlegende Eigenschaften.
Eingabe-Ausgabe: Ein Algorithmus hat null oder mehr Eingaben und mindestens eine oder mehrere Ausgaben.
Endlichkeit: Nachdem der Algorithmus eine endliche Anzahl von Schritten ausgeführt hat, endet er automatisch ohne Endlosschleife und jeder Schritt wird innerhalb einer akzeptablen Zeit abgeschlossen.
Deterministisch: Jeder Schritt des Algorithmus hat eine eindeutige Bedeutung ohne Mehrdeutigkeit.
Machbarkeit: Jeder Schritt des Algorithmus muss machbar sein, das heißt, jeder Schritt kann durch eine endliche Anzahl von Ausführungen abgeschlossen werden.
1.3 Anforderungen an das Algorithmendesign
Es gibt vier Grundanforderungen für das Algorithmusdesign.
Korrektheit: Dies bedeutet, dass der Algorithmus zumindest keine Mehrdeutigkeit bei der Eingabe, Ausgabe und Verarbeitung aufweisen sollte und die Anforderungen des Problems korrekt widerspiegeln und die richtige Antwort erhalten kann.
Lesbarkeit: Ein weiterer Zweck des Algorithmusdesigns besteht darin, das Lesen, Verstehen und Kommunizieren zu erleichtern.
Robustheit: Wenn die Eingabedaten illegal sind, kann der Algorithmus auch eine relevante Verarbeitung durchführen, anstatt abnormale oder unerklärliche Ergebnisse zu erzeugen.
Hohe Zeiteffizienz und geringe Speicherkapazität: Nutzen Sie begrenzte Ressourcen, um die Zeit- und Platzeffizienz zu maximieren.
2. Algorithmuseffizienz
2.1 Messmethode
2.1.1 Post-Event-Statistikmethode
Diese Methode besteht hauptsächlich darin, die Laufzeit von Programmen, die von verschiedenen Algorithmen kompiliert wurden, mithilfe von Computer-Timern anhand entworfener Testprogramme und -daten zu vergleichen, um die Effizienz des Algorithmus zu bestimmen.
Tatsächlich weist diese Methode schwerwiegende Mängel auf:
- Es muss im Voraus auf der Grundlage des Algorithmus programmiert werden, was normalerweise viel Zeit und Mühe kostet. Wenn sich nach der Programmierung herausstellt, dass der Algorithmus schlecht ist, wäre das dann nicht eine Geldverschwendung?
- Der Zeitvergleich hängt zu einem großen Teil von Umgebungsfaktoren wie Computerhardware und -software ab , was manchmal die Vor- und Nachteile des Algorithmus selbst verdeckt. Im Vergleich zu den vorherigen 386-, 486- und anderen Großelterncomputern ist ein aktueller Quad-Core-Prozessor-Computer in der Berechnungsgeschwindigkeit des Verarbeitungsalgorithmus völlig unvergleichlich. Selbst wenn es sich um denselben Computer handelt, sind die CPU-Auslastung und die Speicherauslastung unterschiedlich, was zu geringfügigen Unterschieden führt. Daher ist es sinnlos, einfach die Zeit zu vergleichen.
- Das Design von Algorithmentestdaten ist schwierig. Wie viele Daten verwenden wir zum Testen? Wie oft kann es getestet werden? Das sind schwer zu beurteilende Fragen.
2.1.2 Voranalyse- und Schätzmethode
Vor der Computerprogrammierung wird die Effizienz des Algorithmus anhand statistischer Methoden geschätzt.
Wie lange es dauert, bis ein in einer höheren Programmiersprache geschriebenes Programm auf einem Computer ausgeführt wird, hängt hauptsächlich von folgenden Faktoren ab:
Artikel (1) ist natürlich die Grundlage des Algorithmus, Artikel (2) muss von der Software unterstützt werden und Artikel (4) hängt von der Hardwareleistung ab. Das heißt,Unabhängig von Umgebungsfaktoren hängt die Laufzeit eines Programms von der Qualität des Algorithmus und dem Eingabemaßstab des Problems ab。
Dauert die Ausführung jeder Codeanweisungsoperation während der Ausführung des Programms eine bestimmte Zeit? Je mehr Operationen ausgeführt werden, desto länger wird die Zeit verbraucht. Mit anderen Worten, die Laufzeit des Programms hängt davon ab die grundlegenden Vorgänge, die Zeit verbrauchen. Proportional zur Anzahl der Ausführungen.
Es ist uns egal, welche Sprache zum Schreiben des Programms verwendet wird und auch nicht, auf welchem Computer diese Programme ausgeführt werden. Uns interessiert nur der Algorithmus, den es implementiert. Unabhängig von Änderungen des Schleifenindex und von Operationen wie Schleifenbedingungen, Variablendeklarationen und Druckergebnissen ist es daher bei der Analyse der Laufzeit eines Programms am wichtigsten, sich das Programm als Algorithmus oder Algorithmus vorzustellen Eine Reihe von Schritten, die von der Programmiersprache unabhängig sind. Schätzen Sie anhand der Anzahl der Grundoperationen die Laufzeit des Vergleichsprogramms und messen Sie so die Zeiteffizienz des Algorithmus .
Zum Beispiel:
int sum(int n)
{
int i = 0;
int sum = 0;
for(i = 1; i < n; i++)
{
sum += i; //执行n次
}
return sum;
}
int print(int n)
{
int i = 0;
int j = 0;
int m = 0;
for(i = 0; i < n; i++)
{
for(j = 0; j < n; j++)
{
printf("%d ", m); //nxn次
m++; //nxn次
}
printf("\n"); //n次
}
}
Wenn wir die Laufzeit eines Algorithmus analysieren, ist es wichtig, die Anzahl der Grundoperationen mit der Eingabeskala zu verknüpfen, das heißt, die Anzahl der Grundoperationen muss als Funktion der Eingabeskala ausgedrückt werden, hier setzen wir f( n), n ist die Grundanzahl der Operationen.
2.2 Asymptotisches Wachstum von Funktionen
Welcher der folgenden Algorithmen ist schneller? Nicht unbedingt.
Wenn n = 1 ist, ist die Effizienz von Algorithmus A nicht so gut wie die von B, und wenn n = 2, ist die Effizienz beider gleich, aber mit zunehmendem n wird Algorithmus A immer besser als B, also wir kann sagen, dass Algorithmus A insgesamt besser ist als B.
Asymptotisches Wachstum von Funktionen
Wir haben festgestellt, dass mit zunehmendem n letzteres +3 oder +1 tatsächlich keinen Einfluss auf die Effizienz des endgültigen Algorithmus hat, sodass wir diese Konstanten tatsächlich ignorieren können .
Schauen Sie sich das zweite Beispiel an
Wenn n<=3, ist Algorithmus C schlechter als D, aber wenn n>3, ist Algorithmus C immer besser als D und übertrifft am Ende sogar die Leistung bei weitem. Unabhängig davon, ob die folgende Konstante entfernt oder die mit n multiplizierte Konstante entfernt wird, ändert sich die Zeiteffizienzlücke zwischen den Algorithmen C und D kaum. Das heißt, die Konstante multipliziert mit dem Term höchster Ordnung ist nicht so wichtig und kann ignoriert werden.
Schauen Sie sich das dritte Beispiel an.
Wenn n=1, sind die Ergebnisse der Algorithmen E und F gleich, aber wenn n>1, werden die Vorteile des Algorithmus E hervorgehoben und mit zunehmendem n immer offensichtlicher. Das heißt, wenn der Index des Elements höchster Ordnung groß ist, nimmt die Anzahl der Operationsausführungen schneller zu und die Zeiteffizienz des Algorithmus wird geringer.
Schauen Sie sich das vierte Beispiel an.
Wie Sie sehen, ist der Algorithmus H mit zunehmendem n völlig unvergleichbar mit den beiden anderen, von denen keiner in der gleichen Größenordnung liegt. Allerdings tendiert Algorithmus G mit zunehmendem n allmählich zum Algorithmus I, und die Lücke zwischen ihnen ist im Vergleich zur aktuellen Größe völlig vernachlässigbar. Das heißt, bei der Beurteilung der Effizienz eines Algorithmus können Konstanten und andere Nebenterme ignoriert werden, und die Reihenfolge des Hauptterms (der Term höchster Ordnung) sollte beachtet werden.
Um zu beurteilen, ob ein Algorithmus gut ist oder nicht, ist es unmöglich, nur anhand einer kleinen Datenmenge ein genaues Urteil zu fällen. **Ein bestimmter Algorithmus wird mit zunehmendem n immer besser als ein anderer Algorithmus oder immer schlechter als ein anderer Algorithmus. **Dies ist die theoretische Grundlage, die wir verwenden, um die Zeiteffizienz des Algorithmus abzuschätzen.
2.3 Zeitkomplexität
2.3.1 Definition
Bei der Algorithmusanalyse ist die Gesamtzahl der Ausführungen T(n) der Anweisung eine Funktion der Eingabeskala n des Problems. Anschließend wird die Variation von T(n) mit n analysiert und die Größe von T(n) bestimmt ).
Die Zeitkomplexität des Algorithmus, dh die Zeitmessung des Algorithmus, ist proportional zu T(n) und wird als T(n) = O(f(n)) bezeichnet. Dies bedeutet, dass mit zunehmendem n die Wachstumsrate der Ausführungszeit des Algorithmus mit der Wachstumsrate von f(n) übereinstimmt , was als asymptotische Zeitkomplexität des Algorithmus oder kurz Zeitkomplexität bezeichnet wird. wobei f(n) eine Funktion der Problemeingabegröße n ist.
Eine solche Notation, die das große O() verwendet, um die Zeitkomplexität widerzuspiegeln, wird als Big-O-Notation bezeichnet .
Im Allgemeinen ist mit zunehmendem n der Algorithmus mit dem langsamsten Wachstum von T(n) der optimale Algorithmus.
2.3.2 Wie man die Big-O-Ordnung herleitet
Tatsächlich ist das, was wir im vorherigen Abschnitt zusammengefasst haben, die Methode.
Ableitungsmethode
1. Ersetzen Sie alle additiven Konstanten durch die Konstante 1
2. Nur die Elemente mit der höchsten Ordnung werden beibehalten und die anderen werden entfernt
3. Wenn der Term höchster Ordnung existiert und sein Koeffizient nicht 1 ist, entfernen Sie seinen Koeffizienten
**Es ist zu beachten, dass die Reihenfolge hier nicht nur auf dem Index basiert, sondern auch auf der Wachstumsrate von f(n). Je höher die Wachstumsrate, desto höher die Ordnung und je niedriger die niedrigere Ordnung. **Daher muss die Wachstumsratenbeziehung einiger mathematischer Funktionen klar sein.
Tatsächlich bedeuten die Elemente niedriger Ordnung nicht, dass sie keine Wirkung haben, sondern dass die Auswirkungen im Vergleich zu Elementen höherer Ordnung vernachlässigbar sind, da wir uns die Größenänderung ansehen möchten, z. B. n und n 2 , nicht gleich groß Ja, die Lücke ist signifikant, während n und 2n immer noch in der gleichen Größenordnung liegen.
2.3.3 Erläuterung der Common Big O Order
2.3.3.1 Konstante Ordnung
int main()
{
int sum = 0;//执行1次
int i = 0;//执行1次
for(i = 0; i < 10; i++)//执行11次
{
sum += i;//执行10次
}
printf("%d", sum);//执行1次
return 0;
}
Die Anzahl der Ausführungen des obigen Codes ist eine feste Konstante und hat nichts mit n zu tun. Wir nennen es eine Zeitkomplexität von O(1), auch bekannt als konstante Ordnung .
Unabhängig von der Konstante schreiben wir sie als O(1).
2.3.3.2 Lineare Ordnung
Um die Komplexität des Algorithmus zu analysieren, besteht einer der Schlüssel darin, die Funktionsweise der Schleifenstruktur zu analysieren.
int add(int n)
{
int i = 0;
int sum = 0;
for(i = 0; i < n; i++)
{
sum += i;
}
return sum;
}
Der Code im Schleifenkörper muss n-mal ausgeführt werden, und das Element höchster Ordnung ist n, sodass leicht zu erkennen ist, dass die zeitliche Komplexität O(n) ist.
2.3.3.3 Logarithmische Ordnung
void mul(int n)
{
int cnt = 1;
while(cnt < n)
{
cnt *= 2;
}
}
Solange cnt kleiner als n ist, tritt es weiterhin in die Schleife ein und multipliziert mit 2, bis es größer als n ist. In diesem Prozess gehen wir davon aus, dass x 2 multipliziert wird, dann gibt es 2 x = n, also ist, x = log 2 n . Die Zeitkomplexität beträgt also O(logn). Beachten Sie , dass logn die Abkürzung für log 2 n ist .
2.3.3.4 Quadratische Ordnung
Das Folgende ist ein sehr einfaches Beispiel, die Zeitkomplexität beträgt O(n 2 ).
for(i = 0; i < n; i++)
{
for(j = 0; j < n; j++)
{
//执行O(1)的操作
}
}
Im Allgemeinen entspricht die zeitliche Komplexität einer Schleife der Komplexität des Schleifenkörpers multipliziert mit der Anzahl der Schleifendurchläufe.
for(i = 0; i < n; i++)
{
for(j = 0; j < m; j++)
{
//执行O(1)的操作
}
}
Ist die Zeitkomplexität hier nur O(m*n)? Nicht unbedingt, es hängt von der relativen Größe von m und n ab:
Wenn m>>n, ist die Zeitkomplexität O(m 2 )
Wenn n>>m, ist die Zeitkomplexität O(n 2 )
Wenn m und n ungefähr gleich groß sind, beträgt die Zeitkomplexität O(m*n)
Schauen Sie sich dieses Beispiel noch einmal an:
for(i = 0; i < n; i++)
{
for(j = 0; j <= i; j++)
{
//执行O(1)的操作
}
}
Wenn i = 0, wird die innere Schleife einmal ausgeführt, wenn i = 1, wird die innere Schleife zweimal ausgeführt. Wenn i = n-1, wird die innere Schleife n-mal ausgeführt, also insgesamt der Ausführungen ist 1+2+3 +...+n, die Summe ist (n+1)n/2, also 1/2n 2 +1/2n, und die Zeitkomplexität ist O(n 2 ).
Hier ist ein Beispiel für den Aufruf einer Funktion:
void fun(int cnt)
{
int j = 0;
for(j = cnt; j < n; j++)
{
//执行O(1)的操作
}
}
int main()
{
int i = 0;
int j = 0;
int n = 0;
scanf("%d", &n);
fun(n);//执行次数为n
for(i = 0; i < n; i++)
{
fun(i);//执行次数为n*n
}
for(i = 0; i < n; i++)//执行次数为n(n+1)/2
{
for(j = i; j < n; j++)
{
//执行O(1)的操作
}
}
return 0;
}
Ist es auf den ersten Blick etwas kompliziert? Tatsächlich ist es immer noch quadratisch. Lassen Sie es uns analysieren.
Achten Sie nicht auf die konstante Anzahl der Male. Der erste fun(n) ruft die Fun-Funktion einmal auf und wird n-mal ausgeführt. Die erste for-Schleife ruft die Fun-Funktion in der Schleife auf. Ein Aufruf ist n-mal, und die Gesamtanzahl der Anrufe n-mal, also n*n-mal.
Bei der zweiten for-Schleife handelt es sich um eine verschachtelte Schleife. Beachten Sie, dass die Initialisierungsbedingung der inneren Schleife j=i ist. Wenn i=0, wird die innere Schleife n-mal ausgeführt. Wenn i=1, ist die innere Schleife n-1 Mal ausgeführt... Wenn i=n-2, wird die innere Schleife zweimal ausgeführt, wenn i=n-1, wird die innere Schleife einmal ausgeführt, die Gesamtzahl der Ausführungen beträgt n+n-1+n- 2+...+2+1, Summe Holen Sie sich n(n+1)/2.
Die Anzahl der Programmausführungen beträgt also etwa 3/2n 2 +3/2n und die Zeitkomplexität beträgt O(n 2 ).
2.3.3.5 Gemeinsame Zeitkomplexität
Sortierung von niedrig nach hoch:
O (1) < O(logn) < O(n) < O(nlongn) < O(n 2 ) < O(n 3 ) < O(2 n ) < O(n!) < O(n n )
Was wir am häufigsten diskutieren, sind tatsächlich die folgenden zeitlichen Komplexitäten
2.3.4 Worst Case und Durchschnittsfall
2.3.4.1 Drei Situationen
In dem zuvor erwähnten Beispiel kann die Anzahl der Ausführungen tatsächlich geschätzt werden, da sie relativ fest ist. Es gibt jedoch einige Algorithmen, deren Ausführungszeiten ungewiss sind, und es gibt derzeit mehrere mögliche Situationen.
- Bester Fall: Mindestanzahl von Läufen (Untergrenze) für jede Eingabegröße
- Schlimmster Fall: maximale Anzahl von Läufen für jede Eingabegröße (Obergrenze)
- Durchschnittsfall: erwartete Anzahl von Läufen für jede Eingabegröße
Beispiel: Suchen Sie nach Daten x in einem Array der Länge N
Bester Fall: 1 Fund.
Schlimmster Fall: N Funde.
Durchschnittlicher Fall: N/2 Funde
In der Praxis besteht die allgemeine Sorge vor dem schlimmsten Fall des Algorithmus, dem sogenanntenZeitkomplexität bezieht sich auf die Zeitkomplexität im ungünstigsten FallBeispielsweise beträgt die zeitliche Komplexität der Suche nach Daten im Array im obigen Beispiel O(N).
2.3.4.2 Durchschnittliche Zeitkomplexität (wird nur in Sonderfällen verwendet)
Durchschnittliche Zeitkomplexität, auch bekannt als „gewichtete durchschnittliche Zeitkomplexität“, „erwartete Zeitkomplexität“, warum wird sie Gewichtung genannt? Da zur Berechnung der durchschnittlichen Zeitkomplexität normalerweise die Wahrscheinlichkeit berücksichtigt werden muss, ist bei der Berechnung der durchschnittlichen Zeitkomplexität ein „gewichteter Wert“ erforderlich, um die durchschnittliche Zeitkomplexität tatsächlich zu berechnen.
Nehmen Sie ein Beispiel zur Analyse
// n 表示数组 arr 的长度
int find(int[] arr, int n, int x)
{
int i = 0;
int pos = -1;
for (i = 0; i < n; i++)
{
if (arr[i] == x)
{
pos = i;
break;
}
}
return pos;
}
Der Code ist sehr einfach, was bedeutet, dass in einem Array nach der Zahl Zeitkomplexität im schlimmsten Fall Grad ist O(n).
Wie wird die durchschnittliche Komplexität berechnet?
Lassen Sie mich zunächst über die einfache Durchschnittsberechnungsformel sprechen :
Im obigen Code gibt es viele Möglichkeiten herauszufinden, ob x ausgeführt wird. Beispielsweise beträgt die Anzahl der Ausführungen 1, 2, 3 ... n, und die Anzahl der Ausführungen unter jeder Möglichkeit ist unterschiedlich. Addieren Sie die Anzahl der Ausführungen unter allen möglichen Ausführungen: 1 + 2 + 3 +······+ n + n ( 第二个 n 表示当 x 不存在的情况下遍历 arr 需要的执行次数
), außerdem gibt es n + 1 mögliche Fälle (die zusätzliche 1 wird nicht gefunden), dann ist das Ergebnis :
In der Notation der großen O-Ordnung ist es O(n).
Diese Formel drückt aus: Berechnen Sie die Summe der Anzahl der Ausführungen in allen möglichen Situationen und dividieren Sie sie dann durch die Anzahl der möglichen Situationen. Um es ganz klar auszudrücken: Das ist ein absolutes Durchschnittsergebnis. Gibt an, dass die Wahrscheinlichkeit, dass jedes Ergebnis eintritt, 1/(n+1) beträgt.
Wie wird die gewichtete Durchschnittsformel zur Berechnung verwendet?
Hier gibt es 2 Wahrscheinlichkeiten:
- Die Wahrscheinlichkeit, ob sich die x-Variable im Array befindet, gibt es in zwei Fällen – rein und raus, also beträgt die Wahrscheinlichkeit 1/2
- Die Wahrscheinlichkeit, dass die x-Variable an einer bestimmten Position im Array erscheint. Es gibt n Fälle, sie erscheint jedoch nur einmal, sodass die Wahrscheinlichkeit 1/n beträgt
Wert" aller Ausführungszeiten.
Wie kann der gewichtete Wert verwendet werden, um die „Komplexität“ des obigen Codes zu berechnen?
(1+2+3+...+n+n)/2n
, das heißt, ersetzen Sie den Nenner durch 2n und berechnen Sie (3n+1)/4
.
In der Big-O-Notation beträgt die Zeitkomplexität O(n).
Um die genaue durchschnittliche Zeitkomplexität zu berechnen, muss dieser „Gewichtswert“ genau berechnet werden. Der Gewichtswert wird durch den Datenbereich und den Datentyp beeinflusst. Daher ist es notwendig, Parameter im tatsächlichen Betrieb anzupassen.
2.3.5 Beispielübungen
2.3.5.1 Binäre Suche
int BinarySearch(int* a, int n, int x)
{
assert(a);
int begin = 0;
int end = n-1;
// [begin, end]:begin和end是左闭右闭区间,因此有=号
while (begin <= end)
{
int mid = begin + ((end-begin)/2);
if (a[mid] < x)
begin = mid+1;
else if (a[mid] > x)
end = mid-1;
else
return mid;
}
return -1
}
Die Anzahl der binären Suchvorgänge ist tatsächlich ungewiss. Im besten Fall kann sie einmal gefunden werden. Im schlimmsten Fall muss sie in zwei Teile geteilt werden, bis links > rechts. Es gibt n Zahlen. Jedes Mal, wenn die Zahl geteilt wird Die Hälfte der Zahlen wird gekürzt und schließlich auf gekürzt. Es ist nur noch eine Zahl übrig. Unter der Annahme, dass x Mal gekürzt wird, muss jede Kürzung n durch 2 geteilt werden, sodass am Ende n/2 x =1 vorhanden sind ist, dass die Anzahl der Ausführungen x=log 2 n ist und die Zeitkomplexität O( logn) ist.
2.3.5.2 Faktorielle Rekursion
// 计算阶乘递归Fac的时间复杂度?
long long Fac(size_t N)
{
if(0 == N)
return 1;
else
return Fac(N-1)*N;
}
Es scheint, dass die Rekursion noch nie erwähnt wurde. Wie hoch wird Ihrer Meinung nach die zeitliche Komplexität sein?
Jedes Mal, wenn Sie die Funktion eingeben, wird die Funktion nach der N-1-Ersetzung einmal aufgerufen, bis der Wert von N 0 wird, also f(N-1), f(N-2)...f(3), f (2), f(1), f(0) so viele Funktionen, die Anzahl der Ausführungen beträgt N und die Zeitkomplexität beträgt O(n).
2.3.5.3 Fibonacci-Rekursion
// 计算斐波那契递归Fib的时间复杂度?
long long Fib(size_t N)
{
if(N < 3)
return 1;
else
return Fib(N-1) + Fib(N-2);
}
Dies ist komplizierter. Es ist am besten, zu wissen, was ein Binärbaum ist (später erfahren). Einfach ausgedrückt ist ein Binärbaum eine Baumstruktur mit höchstens zwei Zweigen pro Zweig, wie in der Abbildung dargestellt
Welche Beziehung besteht zwischen Binärbäumen und der Fibonacci-Rekursion? Der rekursive Funktionsaufrufprozess von Fibonacci kann tatsächlich durch einen Binärbaum dargestellt werden, wie in der Abbildung gezeigt
Das ist sehr intuitiv. Es ist offensichtlich, dass dieser Algorithmus viele wiederholte Berechnungen hat, sodass die Effizienz des Algorithmus mit zunehmendem n sehr gering sein sollte. Wie berechnet man also die Zeitkomplexität?
Achten Sie auf das Bild. Die Anzahl der Ausführungen jeder Ebene kann berechnet werden, indem die Anzahl der Funktionsaufrufe in dieser Ebene gezählt wird. Es gibt bestimmte Regeln: Sie sind alle exponentiell mal 2, zum Beispiel die erste Ebene 2 0 und die zweite Schicht ist 2 1 , die dritte Schicht ist 2 2 und so weiter, die n-te Schicht ist 2 n-1 , also ergibt sich die Zeitkomplexität – O(2 n ).
2.4 Weltraumkomplexität
Die Zeitkomplexität misst hauptsächlich, wie schnell ein Algorithmus ausgeführt wird, während die Raumkomplexität hauptsächlich den zusätzlichen Platz misst, der für die Ausführung eines Algorithmus erforderlich ist.
In den Anfängen der Computerentwicklung verfügten Computer über eine sehr geringe Speicherkapazität. Daher mache ich mir große Sorgen über die Komplexität des Weltraums. Mit der rasanten Entwicklung der Computerindustrie hat die Speicherkapazität von Computern jedoch ein sehr hohes Niveau erreicht. Daher müssen wir der räumlichen Komplexität eines Algorithmus keine besondere Aufmerksamkeit mehr schenken.
Die Raumkomplexität des Algorithmus wird durch Berechnen der Größe des vom Algorithmus benötigten Hilfsraums realisiert, und die Berechnung erfolgt durch die Anzahl der Variablen. Die Berechnungsregeln für die Raumkomplexität ähneln grundsätzlich denen für die Zeitkomplexität. Es wird auch die asymptotische Big- O- Notation verwendet , die wie folgt aufgezeichnet wird: S(n)=O(f(n)), n ist die Eingabeskala des Problems und f (n) ist der Wert der n-Funktion.
Wenn ein Programm auf einer Maschine ausgeführt wird, muss es im Allgemeinen nicht nur die eigenen Anweisungen, Konstanten, Variablen und Eingabedaten des Programms speichern, sondern auch die Speichereinheit für Datenoperationen speichern. Es ist lediglich erforderlich, den erforderlichen Algorithmus zu analysieren zur Umsetzung . Hilfsaggregate stehen zur Verfügung.
Der von der Funktion zur Laufzeit benötigte Stapelspeicherplatz (Speicherparameter, lokale Variablen, einige Registerinformationen usw.) wurde während der Kompilierung bestimmt, sodass die Speicherplatzkomplexität hauptsächlich durch den von der Funktion zur Laufzeit explizit angeforderten zusätzlichen Speicherplatz bestimmt wird .
2.4.1 Beispiel 1
// 计算BubbleSort的空间复杂度?
void BubbleSort(int* a, int n)
{
assert(a);
for (size_t end = n; end > 0; --end)
{
int exchange = 0;
for (size_t i = 1; i < end; ++i)
{
if (a[i-1] > a[i])
{
Swap(&a[i-1], &a[i]);
exchange = 1;
}
}
if (exchange == 0)
break;
}
}
Ist es O(n)? Hat a nicht n Elemente? Beachten Sie, dass es sich beim a-Array nicht um einen zusätzlichen Hilfsraum handelt, der entsprechend den Anforderungen des Algorithmusdesigns geöffnet wird! Es ist ein wesentlicher Raum, da der von uns entwickelte Blasensortierungsalgorithmus auf diesem Array arbeiten soll, anstatt dass der Blasensortierungsalgorithmus ein Array öffnen muss. Aus dieser Sicht ist der zusätzliche Raum, den wir eröffnen, höchstens eine konstante Zahl, sodass die Raumkomplexität O (1) beträgt.
2.4.2 Beispiel 2
// 计算Fibonacci的空间复杂度?
// 返回斐波那契数列的前n项
long long* Fibonacci(size_t n)
{
if(n==0)
return NULL;
long long * fibArray = (long long *)malloc((n+1) * sizeof(long long));
fibArray[0] = 0;
fibArray[1] = 1;
for (int i = 2; i <= n ; ++i)
{
fibArray[i] = fibArray[i - 1] + fibArray [i - 2];
}
return fibArray;
}
Dies ist eine iterative Methode, mit der die ersten n Elemente der Fibonacci-Folge gefunden werden. Auf den Heap wird ein fibArray-Array angewendet, sodass die Raumkomplexität O(n) beträgt.
2.4.3 Beispiel 3
// 计算阶乘递归Fac的空间复杂度?
long long Fac(size_t N)
{
if(N == 0)
return 1;
return Fac(N-1)*N;
}
Die zeitliche Komplexität dieser Rekursion wurde bereits erwähnt. Tatsächlich werden während des Rekursionsprozesses n+1 Stapelrahmen erstellt, und einer wird für jeden Funktionsaufruf geöffnet. Es wird nur ein Funktionsparameter N erstellt (die Details des Stapels nicht). betroffen) und insgesamt n+1 N erstellen, sodass die Raumkomplexität O(n) ist.
2.4.4 Beispiel 4
// 计算斐波那契递归Fib的时间复杂度?
long long Fib(size_t N)
{
if(N < 3)
return 1;
else
return Fib(N-1) + Fib(N-2);
}
Schauen wir uns noch das zuvor verwendete Bild an. Wenn Sie f (n) aufrufen, werden zunächst f (n-1) und f (n-2) aufgerufen. Wird zuerst f (n-1) aufgerufen? Nach dem Aufruf von f(n-1) wird f(n-2) aufgerufen. Durch den Aufruf von f(n-1) wird ein Stapelrahmen erstellt, oder? Dann wird der Stapelrahmen nach dem Aufruf von f(n-1) zerstört. und der Raum Geben Sie es an das System zurück und rufen Sie dann f(n-2) auf. Müssen Sie nicht einen Stapelrahmen erstellen, welcher Raum verwendet wird? Der vom zuvor zerstörten f(n-1) verwendete Block wird verwendet, was bedeutet, dass die Funktionsstapelrahmen von f(n-1) und f(n-2) unter Verwendung desselben Raumblocks erstellt werden. Ähnlich, wie in der Abbildung gezeigt:
Es gibt also nur n Funktionsstapelrahmen, die hin und her verwendet werden, und jeder Funktionsstapelrahmen erstellt eine konstante Anzahl von Variablen, sodass die Raumkomplexität O(n) ist.
Wenn der für die Ausführung des Algorithmus erforderliche Hilfsraum eine Konstante für die Menge der Eingabedaten ist, spricht man davon, dass der Algorithmus in situ arbeitet und die Raumkomplexität (1) beträgt.