계산 복잡도가 왜 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 ... 마지막 for루프가 n 번 실행되는 방법을 이해하지 못합니다 . 나는 우리가 그 if진술에 근거하여 어떻게 그 결론에 도달했는지 이해하지 못한다고 생각 합니다.

편집 : 마지막 루프가 mod 연산자를 기반으로 i 번 실행되는 이유를 제외하고 모든 루프의 복잡성을 계산하는 방법을 알고 있습니다. 기본적으로 j % i가 i가 아닌 i * i까지 올라갈 수없는 이유는 무엇입니까?


5
여러 가지 요인 으로이 코드의 복잡성을 줄일 수 있습니다 . 힌트 : 1에서 n까지의 숫자의 합은 ((n + 1) * n) / 2 힌트 2입니다 : for (j = i; j < i *i; j += i)그러면 계수 테스트가 필요하지 않습니다 (왜냐하면 j으로 나눌 수 있음 i).
Elliott Frisch

1
O () 함수는 볼 파크 함수이므로이 예제의 루프는 복잡성을 증가시킵니다. 두 번째 루프는 n ^ 2까지 실행됩니다. if 문은 무시됩니다.
Christoph Bauer

11
@ChristophBauer if문장은 절대 무시 되지 않습니다 . 이 if명령문은 복잡성이 O (n ^ 5) 대신 O (n ^ 4)임을 의미합니다. 왜냐하면 가장 안쪽 루프가 두 번째 루프의 각 반복에 대해 i시간 대신 시간 만 실행하기 때문 i*i입니다.
kaya3

1
@ kaya3는 부분을 완전히 놓쳤 k < n^2으므로 O (n ^ 5)이지만 지식을 이해함으로써 ifO (n ^ 4)를 제안합니다.
Christoph Bauer

1
이것이 단순한 운동이 아닌 경우, 두 번째 루프를 for (int j = i; j <i * i; j + = i)로 변경하십시오.
Cristobol Polychronopolis

답변:


49

루프 A, B 및 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++;
            }
        }
    }
}
  • 루프 A는 O ( n ) 번 반복합니다.
  • 루프 B 는 A 반복마다 O ( i 2 )를 반복 합니다. 이러한 각 반복에 대해 :
    • j % i == 0 O (1) 시간이 소요됩니다.
    • 이 반복의 1 / i 에서 루프 C는 반복 마다 O (1) 작업을 수행하여 j 번 반복합니다. 이후 j는 O이다 ( 2 평균)이 단지 1 / 위해 수행 루프의 반복 B 평균 비용은 O이다 ( I 2  /  I ) = O ( I ).

이 모든 것을 곱하면 O ( n  ×  i 2  × (1 +  i )) = O ( n  ×  i 3 )가됩니다. 이후 평균 O (에 해당 ),이 O (인 N 4 ).


이것의 까다로운 부분은 if조건이 1 / i 의 시간에 불과 하다는 것입니다 .

기본적으로 j % i가 i가 아닌 i * i까지 올라갈 수없는 이유는 무엇입니까?

실제로, j최대 j < i * i뿐만 아니라까지 올라갑니다 j < i. 그러나의 배수 인 j % i == 0경우에만 조건 이 적용됩니다 .ji

의 배수 i범위 내에서가 i, 2*i, 3*i, ..., (i-1) * i. 이 중 루프 B 반복 시간 에도 불구하고 루프 i - 1C에 도달 합니다.i - 1i * i - 1


2
O (n × i ^ 2 × (1 + i))에서 왜 1 + i?
Soleil

3
if루프 B의 모든 반복 에서 조건이 O (1) 시간이 걸리기 때문에 루프 C가 지배적이지만 위에서 계산 한 것이므로 "내 작업 표시"입니다.
kaya3

16
  • 첫 번째 루프는 n반복을 사용합니다.
  • 두 번째 루프는 n*n반복을 사용합니다. i=n그렇다면 그때를 상상해보십시오 j=n*n.
  • 세 번째 루프는 최악의 경우 경계가 지정된 시간 n만 실행되므로 반복을 소비 합니다.iin

따라서, 코드 복잡도는 O (n × n × n × n)이다.

이것이 당신이 이해하는 데 도움이되기를 바랍니다.


6

다른 모든 대답은 정확합니다. 다음을 수정하고 싶습니다. 내부 k 루프의 실행 감소가 실제 복잡도를 낮추기에 충분했는지 확인하고 싶었습니다 O(n⁴)..

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

이것을 실행 한 후에는 복잡성이 실제로 분명 n⁴합니다. 마지막 출력 행은 다음과 같습니다.

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

이것이 보여주는 것은, n⁴이 코드 세그먼트의 실제 와 복잡성 사이의 실제 상대적인 차이는 주위의 값 0.124...(실제 0.125)에 대해 점근 적 요인이라는 것 입니다. 그것이 우리에게 정확한 가치를 제공하지는 않지만 다음과 같이 추론 할 수 있습니다.

시간 복잡도는 기능 / 방법 이 n⁴/8 ~ f(n)있는 곳 f입니다.

  • Big O 표기법에 대한 Wikipedia-page는 'Bachmann-Landau 표기법의 가족'표 ~에서 두 피연산자 측의 한계가 동일하다는 것을 나타냅니다. 또는:

    f는 무증상으로 g와 같습니다.

(나는 n = 362합리적인 결과를 얻는 마지막 값 이기 때문에 363을 제외 된 상한으로 선택했습니다 . 그 후 우리는 긴 공간을 초과하고 상대 값은 음수가됩니다.)

kaya3 사용자는 다음을 알아 냈습니다.

그런데 점근 상수는 정확히 1/8 = 0.125입니다. Wolfram Alpha를 통한 정확한 공식은 다음과 같습니다 .


5
물론 O (n⁴) * 0.125 = O (n⁴)입니다. 런타임에 양의 상수 요소를 곱해도 점근 적 복잡성은 바뀌지 않습니다.
Ilmari Karonen

사실입니다. 그러나 나는 상한 추정치가 아니라 실제 복잡성을 반영하려고했습니다. O- 표기법 이외의 시간 복잡성을 표현하는 다른 구문을 찾지 못했기 때문에 그에 대해 다시 생각했습니다. 그러나 이렇게 작성하는 것은 100 % 현명하지 않습니다.
TreffnonX

little-o 표기법 을 사용 하여 시간 복잡성이이라고 말할 수는 n⁴/8 + o(n⁴)있지만 n⁴/8 + O(n³)어쨌든 큰 O 로 더 엄격한 표현 을 할 수 있습니다 .
kaya3

@TreffnonX big OH는 수학적으로 견고한 개념입니다. 그래서 당신이하고있는 일은 근본적으로 잘못되었거나 의미가 없습니다. 물론 수학적 개념을 자유롭게 재정의 할 수는 있지만 그때는 벌레가 크게 늘어납니다. 보다 엄격한 맥락에서 정의하는 방법은 kaya3에서 설명한 것입니다. 순서를 "더 낮게"바꾸어 정의하십시오. (수학에서는 일반적으로 왕복 운동을 사용합니다).
paul23

당신이 올바른지. 나는 다시 교정했다. 이번에는 en.wikipedia.org/wiki/Big_O_notation#Little-o_notation 의 Bachmann-Landau 제품군에 정의 된 것과 동일한 제한을 향해 비대칭 성장을 사용합니다 . 나는 이것이 반란을
일으키지

2

if복잡성을 변경하지 않고 제거 및 모듈로

원래 방법은 다음과 같습니다.

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

당신이 혼동하는 경우 if와 모듈로, 당신은 단지와 함께, 그들을 멀리 리팩토링 할 수 j에서 직접 점프 i2*i3*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;
}

복잡성을 계산하기 쉽도록 중간 j2변수를 도입하여 모든 반복 변수가 각 반복마다 1 씩 증가하도록 할 수 있습니다 .

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

System.out.println이를 확인하기 위해 디버깅 또는 구식을 사용할 수 있습니다i, j, k 하여 각 방법에서 항상 트리플렛이 동일한 지 있습니다.

닫힌 양식 표현

다른 사람들이 언급했듯이 첫 번째 n 정수 의 합이 같다는 사실을 사용할 수 있습니다 n * (n+1) / 2( 삼각 숫자 참조 ). 모든 루프에이 단순화를 사용하면 다음을 얻을 수 있습니다.

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

분명히 하지 원래의 코드와 같은 복잡하지만 같은 값을 반환한다.

첫 번째 조건을 구글하면, 당신은 알 수 0 0 0 2 11 35 85 175 322 546 870 1320 1925 2717 3731에 나타나는 "첫 번째 종류의 스털링 번호 : S (N + 2, N)." 0시작 부분에 2 개가 추가되었습니다. 그것은 첫 번째 종류sum스털링 번호 임을 의미합니다 s(n, n-2).


0

처음 두 개의 루프를 살펴 보겠습니다.

첫 번째는 간단합니다 .1에서 n까지 반복됩니다. 두 번째는 더 흥미 롭습니다. 1에서 i 제곱으로갑니다. 몇 가지 예를 보자.

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  

총합은 i and j loops을가집니다 1^2 + 2^2 + 3^2.
첫 번째 n 제곱의 합에 대한 공식 n * (n+1) * (2n + 1) / 6이 있습니다.O(n^3) .

마지막 k loop으로 0에서 jif까지만 반복 됩니다 j % i == 0. 부터 j1까지 간다 i^2, j % i == 0마찬가지입니다 i시간. i loop반복 이 반복 되기 때문에 n하나 더 추가 할 수 있습니다 O(n).

당신은 그래서 O(n^3)에서 i and j loops다른 O(n)에서 k loop의 그랜드 총O(n^4)


마지막 루프가 mod 연산자를 기반으로 i 번 실행되는 이유를 제외하고 모든 루프에 대한 복잡성을 계산하는 방법을 알고 있습니다 ... 나는 그것이 어떻게 i인지 보지 못합니다. 기본적으로 j % i가 i가 아닌 i * i까지 올라갈 수없는 이유는 무엇입니까?
user11452926

1
@ user11452926 i가 5라고 가정 해 봅시다. j는 두 번째 루프에서 1에서 25로 갈 것입니다. 그러나 j % i == 0j가 5, 10, 15, 20 및 25 일 때만 i의 값과 같습니다. 5 x 5 정사각형에서 1에서 25까지의 숫자를 적어두면 5 번째 열에 만 5로 나눌 수있는 숫자가 포함됩니다. 1에서 n ^ 2까지의 숫자를 사용하여 n에 n의 제곱을 그립니다. n 번째 열에는 n으로 나눌 수있는 숫자가 포함됩니다. n 개의 행이 있으므로 1에서 n ^ 2까지의 n 숫자는 n으로 나눌 수 있습니다.
Silviu Burcea

감사! 맞는 말이다! 25가 아닌 24와 같은 임의의 숫자라면, 제곱 트릭은 여전히 ​​작동합니까?
user11452926

25는 i5 를 칠 때 발생 하므로 j1에서 25까지 루프 되므로 임의의 숫자를 선택할 수 없습니다. 두 번째 루프가 고정 번호 (예 : 대신 24) i * i로 이동하면 상수가되고에 묶이지 n않을 것 O(1)입니다. 에 대해 당신이 만약있는 거 생각 j < i * ij <= i * i, 그 것이다별로 중요하지,이있을 것 같은 nn-1운영,하지만 빅 - 오 표기법, 모두 수단에O(n)
실비 Burcea
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.