Hesaplama karmaşıklığı neden 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++;
            }
        }
    }
}

J = i, 2i, 3i ... ne zaman son fordöngü n kez çalışır nasıl anlamıyorum . Sanırım bu sonuca dayanarak bu sonuca nasıl geldiğimizi anlamıyorum if.

Düzenleme: Son döngü neden mod operatörüne göre i kez yürüttüğü dışında tüm döngüler için karmaşıklığı hesaplamak biliyorum ... Sadece nasıl i görmüyorum. Temel olarak, neden j% i i yerine i * i'ye gidemiyorum?


5
Bu kodun karmaşıklığını birden çok büyük faktörle azaltabilirsiniz . İpucu : 1'den n'ye kadar olan sayıların toplamı ((n + 1) * n) / 2 İpucu 2 : for (j = i; j < i *i; j += i)o zaman modül testine ihtiyacınız yoktur (çünkü jbölünebilir olması garanti edilir i).
Elliott Frisch

1
O () işlevi bir bilyeli park işlevidir, bu nedenle bu örnekteki herhangi bir döngü karmaşıklığa katkıda bulunur. İkinci döngü n ^ 2'ye kadar çalışıyor. if-ifadeleri yoksayılır.
Christoph Bauer

11
@ChristophBauer ififadeleri kesinlikle göz ardı edilmez . Bu ififade, karmaşıklığın O (n ^ 5) yerine O (n ^ 4) olduğu anlamına gelir, çünkü en içteki döngünün , ikinci döngünün her yinelemesi için isüreler yerine yalnızca zamanları çalıştırmasına neden olur i*i.
kaya3

1
@ kaya3 bölümü tamamen kaçırdı. k < n^2Yani O (n ^ 5) ama bilgi (anlayarak if) O (n ^ 4) önerir.
Christoph Bauer

1
Bu sadece bir sınıf egzersizi değilse, ikinci döngüyü (int j = i; j <i * i; j + = i) olarak değiştirin
Cristobol Polychronopolis

Yanıtlar:


49

A, B ve C döngülerini etiketleyelim:

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++;
            }
        }
    }
}
  • Döngü A, O ( n ) kez tekrar eder.
  • Döngü B , A'nın her yinelemesi başına O ( i 2 ) kez tekrarlar . Bu iterasyonların her biri için:
    • j % i == 0 O (1) zaman alan değerlendirilir.
    • Açık 1 / i , bu iterasyon, döngü Cı yineler j yineleme başına O (1) çalışma durumu, kat. Yana j O (bir i 2 ortalama), ve bu sadece 1 / için yapılır i döngü B adımlama, ortalama maliyeti O olduğu ( I 2  /  i ) = O ( i ).

Tüm bunları bir araya getirerek O ( n  ×  i 2  × (1 +  i )) = O ( n  ×  i 3 ) elde ederiz . Yana ı ortalama O (verildi n ), bu yağ (bir n- 4 ).


Bunun zor kısmı, ifdurumun sadece zamanın 1 / i olduğunu söylüyor :

Temel olarak, neden j% i i yerine i * i'ye gidemiyorum?

Aslında, sadece yukarı değil, jyukarı doğru gider . Ancak koşul , yalnızca ve katları ise doğrudur .j < i * ij < ij % i == 0ji

Katları iaralığında olan i, 2*i, 3*i, ..., (i-1) * i. Bunlardan bazıları vardır i - 1, bu nedenle i - 1döngü B yineleme i * i - 1sürelerine rağmen döngü C'ye erişilir .


2
O (n × i ^ 2 × (1 + i)) 'de neden 1 + i?
Soleil

3
Çünkü ifkoşul, B döngüsünün her yinelemesinde O (1) zaman alır. Burada C döngüsüne hakimdir, ancak yukarıda saydım, bu yüzden sadece "çalışmamı gösteriyor".
kaya3

16
  • İlk döngü nyinelemeleri tüketir .
  • İkinci döngü n*nyinelemeleri tüketir . i=nO zaman ne zaman hayal edin j=n*n.
  • Üçüncü döngü nyinelemeleri tüketir, çünkü en kötü durumda yalnızca bağlı iolduğu zamanlar yürütülür .in

Böylece, kod karmaşıklığı O (n × n × n × n) olur.

Umarım bu anlamanıza yardımcı olur.


6

Diğer tüm cevaplar doğrudur, sadece aşağıdakileri değiştirmek istiyorum. İç k döngüsünün yürütmelerinin azaltılmasının aşağıdaki gerçek karmaşıklığı azaltmak için yeterli olup olmadığını görmek istedim O(n⁴).Bu yüzden aşağıdakileri yazdım:

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);
}

Bunu yaptıktan sonra, aslında karmaşıklığın olduğu açıktır n⁴. Son çıktı satırları şöyle görünür:

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

Bu, gerçek n⁴ve bu kod segmentinin karmaşıklığı arasındaki gerçek göreceli farkın , etrafındaki bir değere 0.124...(aslında 0.125) asimptotik bir faktör olduğudur . Bize kesin değeri vermese de, aşağıdakileri çıkarabiliriz:

Zaman karmaşıklığı olduğu n⁴/8 ~ f(n)yerdef işlevinizin / yönteminizin .

  • Big O notasındaki wikipedia sayfası, 'Bachmann Ailesi-Landau notasyonları' tablolarında ~ , iki işlenen tarafının sınırını tanımlayan eşittir. Veya:

    f asimptotik olarak g'ye eşittir

(363'ü hariç tutulan üst sınır olarak seçtim, çünkü n = 362 mantıklı bir sonuç elde ettiğimiz son değer. Bundan sonra uzun alanı aştık ve göreceli değer negatif oluyor.)

Kaya3 kullanıcısı şunları anladı:

Bu arada asimptotik sabit tam olarak 1/8 = 0.125'tir; İşte Wolfram Alpha ile tam formül .


5
Tabii ki, O (n⁴) * 0.125 = O (n⁴). Çalışma zamanını pozitif bir sabit faktörle çarpmak, asimtotik karmaşıklığı değiştirmez.
Ilmari Karonen

Bu doğru. Ancak üst sınır tahminini değil, gerçek karmaşıklığı yansıtmaya çalışıyordum. O gösterimi dışında zaman karmaşıklığını ifade etmek için başka bir sözdizimi bulamadığım için, buna geri düştüm. Ancak bu şekilde yazmak% 100 mantıklı değil.
TreffnonX

Sen kullanabilirsiniz küçük-o notasyonu zaman karmaşıklığı olduğunu söylemek n⁴/8 + o(n⁴), ancak daha katı bir ifade vermek mümkün n⁴/8 + O(n³)zaten büyük O ile.
kaya3

@TreffnonX big OH, matematiksel sağlam bir kavramdır. Yani yaptığınız şey temelde yanlış / anlamsız. Tabii ki matematiksel kavramları yeniden tanımlamakta özgürsünüz, ancak o zaman açtığınız büyük bir solucan kutusu. Daha katı bir bağlamda tanımlamanın yolu, kaya3'ün tanımladığı şeydir, bir sipariş "daha düşük" olur ve onu bu şekilde tanımlarsınız. (Gerçi matematikte normal olarak karşılık verirsiniz).
paul23

Haklısın. Kendimi tekrar düzelttim. Bu sefer, asimtotik büyümeyi en.wikipedia.org/wiki/Big_O_notation#Little-o_notation adresindeki Bachmann-Landau Ailesi notasyonlarında tanımlananla aynı sınıra doğru kullanıyorum . Umarım bu artık isyanı kışkırtmayacak kadar matematiksel olarak doğrudur;)
TreffnonX

2

ifKarmaşıklığı değiştirmeden kaldırın ve modulo yapın

İşte orijinal yöntem:

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;
}

Eğer karıştı ediyorsanız ifve modülo, sadece birlikte, onları planı ayrı jdoğrudan atlama iiçin 2*iiçin 3*i...:

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;
}

Karmaşıklığı hesaplamayı daha da kolaylaştırmak için, bir ara j2değişken tanıtabilirsiniz , böylece her döngü değişkeni her yinelemede 1 artar:

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;
}

Üçüzün her yöntemde her zaman aynı System.out.printlnolup olmadığını kontrol etmek için hata ayıklama veya eski okul kullanabilirsiniz i, j, k.

Kapalı form ifadesi

Başkaları tarafından belirtildiği gibi, ilk n tamsayıların toplamının eşit olduğu gerçeğini kullanabilirsiniz n * (n+1) / 2( üçgen sayılara bakın ). Bu sadeleştirmeyi her döngü için kullanırsanız şunları elde edersiniz:

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

Açıkçası orijinal kodla aynı karmaşıklık değildir , ancak aynı değerleri döndürür.

İlk terimleri google, o fark edebilirsiniz 0 0 0 2 11 35 85 175 322 546 870 1320 1925 2717 3731görünür "birinci türden Stirling sayıları: s (n + 2, n)." , 0başında iki s eklenir. Bu demektir sumolan birinci türden Stirling sayısı s(n, n-2) .


0

İlk iki döngüye bir göz atalım.

Birincisi basit, 1'den n'ye döngü. İkincisi daha ilginç. 1 den i kareye gider. Hadi bazı örneklere bakalım:

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  

Toplamda, i and j loopsbirleşik var 1^2 + 2^2 + 3^2.
İlk n karenin toplamı için bir formül var n * (n+1) * (2n + 1) / 6, bu kabacaO(n^3) .

Size son var k loop0 ile döngüler jancak ve ancak j % i == 0. j1'den 1'e gittiğinden beri i^2, zamanlar j % i == 0için geçerlidir i. Yana i loopüzerinde dolaşır n, bir ekstra var O(n).

Sahip olduğunuz Yani O(n^3)gelen i and j loopsve başka O(n)gelen k loopbir genel toplamO(n^4)


Son döngü neden mod operatörüne göre i kez yürüttüğü dışında tüm döngüler için karmaşıklığı hesaplamak biliyorum ... Sadece nasıl i görmüyorum. Temel olarak, neden j% i i yerine i * i'ye gidemiyorum?
user11452926

1
@ user11452926 diyelim ki i 5 idi. j, 2. döngüde 1'den 25'e giderdi. Ancak, j % i == 0sadece j 5, 10, 15, 20 ve 25 olduğunda. İ'nin değeri gibi. Eğer 5 x 5 karede 1'den 25'e kadar olan sayıları yazacak olursanız, sadece 5. sütun 5 ile bölünebilen sayıları içerecektir. 1 - n ^ 2 arasındaki sayıları kullanarak n ile n karesini çizin. N'inci sütun n'ye bölünebilen sayıları içerecektir. N satırınız var, bu nedenle 1'den n ^ 2'ye kadar n sayısı n'ye bölünebilir.
Silviu Burcea

Teşekkürler! mantıklı! Ya 25 yerine 24 gibi rasgele bir sayı olsaydı, kare hile hala işe yarayacak mı?
user11452926

25, i5'e geldiğinde gelir , bu nedenle j1'den 25'e kadar olan döngüler, rasgele bir sayı seçemezsiniz. 2. döngünüz sabit bir sayıya, örneğin 24 yerine giderse i * i, bu sabit bir sayı olur ve bağlanmaz n, öyle olur O(1). Eğer j < i * ivs hakkında düşünürseniz j <= i * i, bu çok önemli olmayacaktır, olduğu gibi nve n-1operasyonlar, ama Big-oh gösterimi, her ikisi deO(n)
Silviu Burcea
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.