Warum ist die Rechenkomplexität O (n ^ 4)?

50
int sum = 0;
for(int i = 1; i < n; i++) {
    for(int j = 1; j < i * i; j++) {
        if(j % i == 0) {
            for(int k = 0; k < j; k++) {
                sum++;
            }
        }
    }
}

Ich verstehe nicht, wie wenn j = i, 2i, 3i ... die letzte forSchleife n-mal läuft. Ich glaube, ich verstehe einfach nicht, wie wir aufgrund der ifAussage zu diesem Schluss gekommen sind .

Bearbeiten: Ich weiß, wie man die Komplexität für alle Schleifen berechnet, außer warum die letzte Schleife i-mal basierend auf dem Mod-Operator ausgeführt wird ... Ich sehe nur nicht, wie es i ist. Grundsätzlich, warum kann ich nicht zu i * i anstatt zu i gehen?

user11452926
quelle
5
Sie können die Komplexität dieses Codes durch mehrere große Faktoren reduzieren . Hinweis : Die Summe der Zahlen 1 bis n ist ((n + 1) * n) / 2 Hinweis 2 : for (j = i; j < i *i; j += i)Dann benötigen Sie den Modultest nicht (da er jgarantiert durch teilbar ist i).
Elliott Frisch
1
Die O () - Funktion ist eine Ball-Park-Funktion, daher erhöht jede Schleife in diesem Beispiel die Komplexität. Die zweite Schleife läuft bis zu n ^ 2. if-Anweisungen werden ignoriert.
Christoph Bauer
11
@ChristophBauer ifAussagen werden absolut nicht ignoriert. Diese ifAnweisung bedeutet, dass die Komplexität O (n ^ 4) anstelle von O (n ^ 5) ist, da die innerste Schleife bei jeder Iteration der zweiten Schleife nur iZeiten anstelle von i*iZeiten ausführt .
Kaya3
1
@ kaya3 hat den Teil total verpasst. k < n^2Also ist es O (n ^ 5), aber Wissen (durch das Verstehen des if) legt O (n ^ 4) nahe.
Christoph Bauer
1
Wenn dies nicht nur eine Klassenübung ist, ändern Sie die zweite Schleife in for (int j = i; j <i * i; j + = i)
Cristobol Polychronopolis

Antworten:

49

Beschriften wir die Schleifen A, B und C:

int sum = 0;
// loop A
for(int i = 1; i < n; i++) {
    // loop B
    for(int j = 1; j < i * i; j++) {
        if(j % i == 0) {
            // loop C
            for(int k = 0; k < j; k++) {
                sum++;
            }
        }
    }
}
  • Schleife A iteriert O ( n ) Mal.
  • Schleife iteriert B O ( i 2 ) mal pro Iteration eines . Für jede dieser Iterationen:
    • j % i == 0 wird ausgewertet, was O (1) Zeit in Anspruch nimmt.
    • Bei 1 / i dieser Iterationen iteriert Schleife C j- mal und führt O (1) pro Iteration aus. Da j im Durchschnitt O ( i 2 ) ist und dies nur für 1 / i- Iterationen der Schleife B erfolgt, betragen die durchschnittlichen Kosten O ( i 2  /  i ) = O ( i ).

Wenn wir all dies zusammen multiplizieren, erhalten wir O ( n  ×  i 2  × (1 +  i )) = O ( n  ×  i 3 ). Da i im Durchschnitt O ( n ) ist, ist dies O ( n 4 ).


Der schwierige Teil davon ist zu sagen, dass die ifBedingung nur 1 / i der Zeit wahr ist :

Grundsätzlich, warum kann ich nicht zu i * i anstatt zu i gehen?

In der Tat jgeht es j < i * inicht nur um j < i. Aber die Bedingung j % i == 0ist genau dann wahr, wenn jein Vielfaches von ist i.

Die Multiples von iinnerhalb des Bereichs sind i, 2*i, 3*i, ..., (i-1) * i. Es gibt i - 1davon, so dass Schleife C i - 1trotz Iterationszeiten von Schleife B i * i - 1mal erreicht wird.

kaya3
quelle
2
In O (n × i ^ 2 × (1 + i)) warum 1 + i?
Soleil
3
Weil die ifBedingung bei jeder Iteration von Schleife B O (1) Zeit benötigt. Sie wird hier von Schleife C dominiert, aber ich habe sie oben gezählt, damit sie nur "meine Arbeit zeigt".
Kaya3
16
  • Die erste Schleife verbraucht nIterationen.
  • Die zweite Schleife verbraucht n*nIterationen. Stellen Sie sich den Fall i=ndann vor j=n*n.
  • Die dritte Schleife verbraucht nIterationen, da sie nur imal ausgeführt wird, wo isie nim schlimmsten Fall begrenzt ist.

Somit ist die Codekomplexität O (n × n × n × n).

Ich hoffe das hilft dir zu verstehen.

Mohammed Deifallah
quelle
6

Alle anderen Antworten sind richtig, ich möchte nur Folgendes ändern. Ich wollte sehen, ob die Reduzierung der Ausführungen der inneren k-Schleife ausreicht, um die tatsächliche Komplexität unten zu reduzieren. O(n⁴).Also schrieb ich Folgendes:

for (int n = 1; n < 363; ++n) {
    int sum = 0;
    for(int i = 1; i < n; ++i) {
        for(int j = 1; j < i * i; ++j) {
            if(j % i == 0) {
                for(int k = 0; k < j; ++k) {
                    sum++;
                }
            }
        }
    }

    long cubic = (long) Math.pow(n, 3);
    long hypCubic = (long) Math.pow(n, 4);
    double relative = (double) (sum / (double) hypCubic);
    System.out.println("n = " + n + ": iterations = " + sum +
            ", n³ = " + cubic + ", n⁴ = " + hypCubic + ", rel = " + relative);
}

Nachdem dies ausgeführt wurde, wird klar, dass die Komplexität tatsächlich ist n⁴. Die letzten Ausgabezeilen sehen folgendermaßen aus:

n = 356: iterations = 1989000035, n³ = 45118016, n = 16062013696, rel = 0.12383254507467704
n = 357: iterations = 2011495675, n³ = 45499293, n = 16243247601, rel = 0.12383580700180696
n = 358: iterations = 2034181597, n³ = 45882712, n = 16426010896, rel = 0.12383905075183874
n = 359: iterations = 2057058871, n³ = 46268279, n = 16610312161, rel = 0.12384227647628734
n = 360: iterations = 2080128570, n³ = 46656000, n = 16796160000, rel = 0.12384548432498857
n = 361: iterations = 2103391770, n³ = 47045881, n = 16983563041, rel = 0.12384867444612208
n = 362: iterations = 2126849550, n³ = 47437928, n = 17172529936, rel = 0.1238518469862343

Dies zeigt, dass der tatsächliche relative Unterschied zwischen dem tatsächlichen n⁴und der Komplexität dieses Codesegments ein Faktor ist, der asymptotisch gegenüber einem Wert um 0.124...(tatsächlich 0,125) ist. Obwohl es uns nicht den genauen Wert gibt, können wir Folgendes ableiten:

Zeitkomplexität ist, n⁴/8 ~ f(n)wo fIhre Funktion / Methode ist.

  • Die Wikipedia-Seite zur Big O-Notation gibt in den Tabellen der 'Familie der Bachmann-Landau-Notationen' an, dass die ~Definition der Grenze der beiden Operandenseiten gleich ist. Oder:

    f ist asymptotisch gleich g

(Ich habe 363 als ausgeschlossene Obergrenze gewählt, da dies n = 362der letzte Wert ist, für den wir ein vernünftiges Ergebnis erhalten. Danach überschreiten wir den langen Raum und der relative Wert wird negativ.)

Der Benutzer kaya3 hat Folgendes herausgefunden:

Die asymptotische Konstante beträgt übrigens genau 1/8 = 0,125; Hier ist die genaue Formel über Wolfram Alpha .

TreffnonX
quelle
5
Natürlich ist O (n⁴) * 0,125 = O (n⁴). Das Multiplizieren der Laufzeit mit einem positiven konstanten Faktor ändert nichts an der asymptotischen Komplexität.
Ilmari Karonen
Das ist wahr. Ich habe jedoch versucht, die tatsächliche Komplexität und nicht die obere Schätzung widerzuspiegeln. Da ich keine andere Syntax zum Ausdrücken der Zeitkomplexität als die O-Notation gefunden habe, habe ich darauf zurückgegriffen. Es ist jedoch nicht 100% sinnvoll, es so zu schreiben.
TreffnonX
Sie können die Little-O-Notation verwenden, um zu sagen, dass die Zeitkomplexität ist n⁴/8 + o(n⁴), aber es ist trotzdem möglich, n⁴/8 + O(n³)mit Big O einen strengeren Ausdruck zu geben .
Kaya3
@TreffnonX big OH ist ein mathematisch solides Konzept. Was Sie also tun, ist grundlegend falsch / bedeutungslos. Natürlich können Sie mathematische Konzepte neu definieren, aber das ist eine große Dose Würmer, die Sie dann öffnen. Die Art und Weise, es in einem strengeren Kontext zu definieren, ist die von kaya3 beschriebene. Sie gehen eine Reihenfolge "niedriger" und definieren es auf diese Weise. (Obwohl Sie in der Mathematik normalerweise den Kehrwert verwenden).
paul23
Du hast Recht. Ich habe mich wieder korrigiert. Dieses Mal verwende ich das asymtotische Wachstum in Richtung derselben Grenze, wie sie in den Notationen der Familie Bachmann-Landau auf en.wikipedia.org/wiki/Big_O_notation#Little-o_notation definiert ist . Ich hoffe, dass dies jetzt mathematisch korrekt genug ist, um keine Revolte
auszulösen
2

Entfernen ifund modulieren, ohne die Komplexität zu ändern

Hier ist die ursprüngliche Methode:

public static long f(int n) {
    int sum = 0;
    for (int i = 1; i < n; i++) {
        for (int j = 1; j < i * i; j++) {
            if (j % i == 0) {
                for (int k = 0; k < j; k++) {
                    sum++;
                }
            }
        }
    }
    return sum;
}

Wenn Sie durch das ifund modulo verwirrt sind , können Sie sie einfach umgestalten, jindem Sie direkt von inach 2*inach 3*i... springen :

public static long f2(int n) {
    int sum = 0;
    for (int i = 1; i < n; i++) {
        for (int j = i; j < i * i; j = j + i) {
            for (int k = 0; k < j; k++) {
                sum++;
            }
        }
    }
    return sum;
}

Um die Berechnung der Komplexität noch einfacher zu gestalten, können Sie eine Zwischenvariable einführen j2, sodass jede Schleifenvariable bei jeder Iteration um 1 erhöht wird:

public static long f3(int n) {
    int sum = 0;
    for (int i = 1; i < n; i++) {
        for (int j2 = 1; j2 < i; j2++) {
            int j = j2 * i;
            for (int k = 0; k < j; k++) {
                sum++;
            }
        }
    }
    return sum;
}

Sie können Debugging oder Old-School verwenden, System.out.printlnum zu überprüfen, ob das i, j, kTriplett bei jeder Methode immer gleich ist.

Ausdruck in geschlossener Form

Wie von anderen erwähnt, können Sie die Tatsache verwenden, dass die Summe der ersten n Ganzzahlen gleich ist n * (n+1) / 2(siehe Dreieckszahlen ). Wenn Sie diese Vereinfachung für jede Schleife verwenden, erhalten Sie:

public static long f4(int n) {
    return (n - 1) * n * (n - 2) * (3 * n - 1) / 24;
}

Es ist offensichtlich nicht die gleiche Komplexität wie der ursprüngliche Code, aber es gibt die gleichen Werte zurück.

Wenn Sie die ersten Begriffe googeln, können Sie feststellen, dass 0 0 0 2 11 35 85 175 322 546 870 1320 1925 2717 3731sie in "Stirling-Zahlen der ersten Art: s (n + 2, n)" erscheinen. , mit zwei 0s am Anfang hinzugefügt. Dies bedeutet, dass dies sumdie Stirling-Nummer der ersten Art ist s(n, n-2) .

Eric Duminil
quelle
0

Werfen wir einen Blick auf die ersten beiden Schleifen.

Der erste ist einfach, er schleift von 1 nach n. Der zweite ist interessanter. Es geht von 1 bis ich im Quadrat. Sehen wir uns einige Beispiele an:

e.g. n = 4    
i = 1  
j loops from 1 to 1^2  
i = 2  
j loops from 1 to 2^2  
i = 3  
j loops from 1 to 3^2  

Insgesamt haben die i and j loopskombinierten 1^2 + 2^2 + 3^2.
Es gibt eine Formel für die Summe der ersten n Quadrate n * (n+1) * (2n + 1) / 6, die ungefähr ist O(n^3).

Sie haben eine letzte k loopSchleife von 0 bis jgenau dann, wenn j % i == 0. Da jgeht von 1 bis i^2, j % i == 0gilt imal. Da die i loopIterationen vorbei sind n, haben Sie eine zusätzliche O(n).

Sie haben also O(n^3)aus i and j loopsund ein anderer O(n)aus k loopfür eine Gesamtsumme vonO(n^4)

Silviu Burcea
quelle
Ich weiß, wie man die Komplexität für alle Schleifen berechnet, außer warum die letzte Schleife i-mal basierend auf dem Mod-Operator ausgeführt wird ... Ich sehe nur nicht, wie es i ist. Grundsätzlich, warum kann ich nicht zu i * i anstatt zu i gehen?
user11452926
1
@ user11452926 Nehmen wir an, das i war 5. j würde in der 2. Schleife von 1 auf 25 gehen. Jedoch j % i == 0nur, wenn j 5, 10, 15, 20 und 25 ist. 5-mal, wie der Wert von i. Wenn Sie die Zahlen von 1 bis 25 in einem Quadrat von 5 x 5 notieren würden, würde nur die 5. Spalte die durch 5 teilbaren Zahlen enthalten. Dies funktioniert für eine beliebige Anzahl von i. Zeichnen Sie ein Quadrat von n mal n mit den Zahlen 1 bis n ^ 2. Die n-te Spalte enthält die durch n teilbaren Zahlen. Sie haben n Zeilen, also n Zahlen von 1 bis n ^ 2, teilbar durch n.
Silviu Burcea
Vielen Dank! macht Sinn! Was wäre, wenn es eine willkürliche Zahl wie 24 statt 25 wäre? Funktioniert der quadratische Trick immer noch?
user11452926
25 kommt, wenn iTreffer 5, also die jSchleifen von 1 bis 25, können Sie keine beliebige Zahl wählen. Wenn Ihre 2. Schleife zu einer festen Zahl gehen würde, z. B. 24, i * iwäre dies eine konstante Zahl und wäre nicht daran gebunden n, so wäre es O(1). Wenn Sie über j < i * ivs. nachdenken j <= i * i, wird das nicht viel nn-1O(n)
Silviu Burcea