[algorithm] 빅 오, 어떻게 계산 / 근사합니까?

CS 학위를 가진 대부분의 사람들은 Big O가 무엇을 의미 하는지 확실히 알 것 입니다 . 알고리즘이 얼마나 잘 확장되는지 측정하는 데 도움이됩니다.

하지만 어떻게, 궁금 하면 계산이나 알고리즘의 복잡성을 대략?



답변

나는 여기서 간단한 용어로 설명하기 위해 최선을 다할 것이지만,이 주제는 학생들이 마침내 이해하는 데 몇 달이 걸린다는 경고를받습니다. Java 책 의 데이터 구조 및 알고리즘 의 2 장에 대한 자세한 정보를 찾을 수 있습니다 .


BigOh를 얻는 데 사용할 수있는 기계적 절차 는 없습니다 .

“요리 책”으로서, 코드에서 BigOh 를 얻으려면 먼저 어떤 크기의 입력으로 몇 단계의 계산이 수행되는지 계산하는 수학 공식을 작성하고 있음을 알아야합니다.

코드를 실행할 필요없이 이론적 인 관점에서 알고리즘을 비교하는 것이 목적이 간단합니다. 단계 수가 적을수록 알고리즘이 더 빨라집니다.

예를 들어,이 코드 조각이 있다고 가정 해 봅시다.

int sum(int* data, int N) {
    int result = 0;               // 1

    for (int i = 0; i < N; i++) { // 2
        result += data[i];        // 3
    }

    return result;                // 4
}

이 함수는 배열의 모든 요소의 합을 반환하며 해당 함수 의 계산 복잡성계산 하는 수식을 만들려고 합니다.

Number_Of_Steps = f(N)

우리가 그래서 f(N), 함수는 계산 단계의 수를 계산합니다. 함수의 입력은 처리 할 구조의 크기입니다. 이 함수는 다음과 같이 호출됩니다.

Number_Of_Steps = f(data.length)

매개 변수 Ndata.length값을 갖습니다. 이제 함수의 실제 정의가 필요합니다 f(). 이것은 소스 코드에서 이루어지며 흥미로운 각 줄의 번호는 1에서 4까지입니다.

BigOh를 계산하는 방법에는 여러 가지가 있습니다. 이 시점부터 우리는 입력 데이터의 크기에 의존하지 않는 모든 문장이 일정한 C수의 계산 단계 를 취한다고 가정 합니다.

함수의 개별 단계 수를 추가 할 예정이며 지역 변수 선언이나 return 문은 data배열 의 크기에 의존하지 않습니다 .

즉, 1 행과 4 행은 각각 C 단계의 단계를 수행하며 기능은 다음과 같습니다.

f(N) = C + ??? + C

다음 부분은 for문장 의 가치를 정의하는 것 입니다. 우리는 계산 단계의 수를 계산한다는 것을 명심하십시오. 즉, for문장 의 본문 이 실행 N시간을 의미합니다 . C, N시간 을 추가하는 것과 같습니다.

f(N) = C + (C + C + ... + C) + C = C + N * C + C

forget 본문이 몇 번이나 실행 되는지 계산하는 기계적 규칙은 없으므로 코드가 수행하는 작업을 확인하여 계산해야합니다. 계산을 단순화하기 위해 변수 초기화, 조건 및 for명령문의 증분 부분을 무시합니다 .

실제 BigOh를 얻으려면 함수 의 점근 분석 이 필요합니다 . 이것은 대략 다음과 같이 수행됩니다.

  1. 모든 상수를 제거하십시오 C.
  2. 에서 것은 f()얻가 polynomium 의의를 standard form.
  3. 폴리 노뮴의 항을 나누고 성장률에 따라 정렬합니다.
  4. N접근 할 때 더 커지는 것을 유지하십시오 infinity.

우리 f()에게는 두 가지 용어가 있습니다.

f(N) = 2 * C * N ^ 0 + 1 * C * N ^ 1

모든 C상수 및 중복 부품을 제거합니다.

f(N) = 1 + N ^ 1

마지막 항은 f()무한대에 도달 할 때 ( 한도를 생각할 때) 커지는 용어이므로 BigOh 인수이며 sum()함수의 BigOh는 다음과 같습니다.

O(N)

까다로운 문제를 해결하기위한 몇 가지 요령 이 있습니다. 가능하면 합계를 사용하십시오 .

예를 들어,이 코드는 요약을 사용하여 쉽게 해결할 수 있습니다.

for (i = 0; i < 2*n; i += 2) {  // 1
    for (j=n; j > i; j--) {     // 2
        foo();                  // 3
    }
}

가장 먼저 물어볼 것은의 실행 순서입니다 foo(). 평소에는이어야하지만 O(1)교수에 대해 문의해야합니다. size와 관계 O(1)없이 (거의 대부분) 상수를 의미 C합니다 N.

for문장 번호 하나에 문이 까다 롭습니다. 색인이에서 끝나는 동안 2 * N증분은 2 씩 이루어집니다. 즉, 첫 번째 단계는 단계별로 for실행되므로 N카운트를 2로 나눕니다.

f(N) = Summation(i from 1 to 2 * N / 2)( ... ) =
     = Summation(i from 1 to N)( ... )

문장 번호 두 사람은 그 값에 의존하기 때문에도 까다 롭습니다 i. 살펴보십시오 : 인덱스 i는 0, 2, 4, 6, 8, …, 2 * N 값을 취하고 두 번째 for는 실행됩니다 : N은 첫 번째 것을 N 곱하기 N-2를 두 번째로 N-4 세 번째는 … N / 2 단계까지, 두 번째는 for절대로 실행되지 않습니다.

공식에서 이는 다음을 의미합니다.

f(N) = Summation(i from 1 to N)( Summation(j = ???)(  ) )

다시, 우리는 걸음 수를 세고 있습니다. 정의에 따르면 모든 합계는 항상 하나에서 시작하여 하나보다 크거나 같은 수로 끝나야합니다.

f(N) = Summation(i from 1 to N)( Summation(j = 1 to (N - (i - 1) * 2)( C ) )

(우리는이 가정하는 foo()것입니다 O(1)및 소요 C단계를.)

여기에 문제가 있습니다. i값을 N / 2 + 1위로 가져 가면 내부 Summation은 음수로 끝납니다! 불가능하고 잘못되었습니다. 우리는 총합을 두 부분으로 나눌 필요 iN / 2 + 1있습니다.

f(N) = Summation(i from 1 to N / 2)( Summation(j = 1 to (N - (i - 1) * 2)) * ( C ) ) + Summation(i from 1 to N / 2) * ( C )

중추적 인 순간 이래로 i > N / 2내부 for는 실행되지 않으며 우리는 그 몸체에서 일정한 C 실행 복잡성을 가정합니다.

이제 일부 ID 규칙을 사용하여 요약을 단순화 할 수 있습니다.

  1. 합산 (w에서 1에서 N까지) (C) = N * C
  2. 합산 (1에서 N까지) (A (+/-) B) = 합산 (1에서 N까지) (A) (+/-) 합산 (1에서 N까지) (B)
  3. 합산 (w는 1에서 N까지) (w * C) = C * 합산 (w는 1에서 N) (w) (C는 상수이며, 독립 w)
  4. 합산 (w에서 1에서 N까지) (w) = (N * (N + 1)) / 2

대수 적용 :

f(N) = Summation(i from 1 to N / 2)( (N - (i - 1) * 2) * ( C ) ) + (N / 2)( C )

f(N) = C * Summation(i from 1 to N / 2)( (N - (i - 1) * 2)) + (N / 2)( C )

f(N) = C * (Summation(i from 1 to N / 2)( N ) - Summation(i from 1 to N / 2)( (i - 1) * 2)) + (N / 2)( C )

f(N) = C * (( N ^ 2 / 2 ) - 2 * Summation(i from 1 to N / 2)( i - 1 )) + (N / 2)( C )

=> Summation(i from 1 to N / 2)( i - 1 ) = Summation(i from 1 to N / 2 - 1)( i )

f(N) = C * (( N ^ 2 / 2 ) - 2 * Summation(i from 1 to N / 2 - 1)( i )) + (N / 2)( C )

f(N) = C * (( N ^ 2 / 2 ) - 2 * ( (N / 2 - 1) * (N / 2 - 1 + 1) / 2) ) + (N / 2)( C )

=> (N / 2 - 1) * (N / 2 - 1 + 1) / 2 =

   (N / 2 - 1) * (N / 2) / 2 =

   ((N ^ 2 / 4) - (N / 2)) / 2 =

   (N ^ 2 / 8) - (N / 4)

f(N) = C * (( N ^ 2 / 2 ) - 2 * ( (N ^ 2 / 8) - (N / 4) )) + (N / 2)( C )

f(N) = C * (( N ^ 2 / 2 ) - ( (N ^ 2 / 4) - (N / 2) )) + (N / 2)( C )

f(N) = C * (( N ^ 2 / 2 ) - (N ^ 2 / 4) + (N / 2)) + (N / 2)( C )

f(N) = C * ( N ^ 2 / 4 ) + C * (N / 2) + C * (N / 2)

f(N) = C * ( N ^ 2 / 4 ) + 2 * C * (N / 2)

f(N) = C * ( N ^ 2 / 4 ) + C * N

f(N) = C * 1/4 * N ^ 2 + C * N

그리고 BigOh는 다음과 같습니다.

O(N²)


답변

Big O는 알고리즘의 시간 복잡성에 대한 상한을 제공합니다. 일반적으로 데이터 세트 (목록) 처리와 함께 사용되지만 다른 곳에서 사용할 수 있습니다.

C 코드에서 어떻게 사용되는지 몇 가지 예입니다.

n 개의 요소가 있다고 가정 해 봅시다.

int array[n];

배열의 첫 번째 요소에 액세스하려면 배열의 크기에 관계없이 O (1)이됩니다. 첫 번째 항목을 가져 오는 데 항상 일정한 시간이 걸립니다.

x = array[0];

목록에서 번호를 찾으려면 다음을 수행하십시오.

for(int i = 0; i < n; i++){
    if(array[i] == numToFind){ return i; }
}

우리는 우리의 숫자를 찾기 위해 전체 목록을 살펴 봐야하므로 이것은 O (n)입니다. Big-O가 알고리즘의 상한을 설명하기 때문에 Big-O는 여전히 O (n)입니다. 루프를 통해 첫 번째 시도와 실행을 한 번 시도 할 수 있습니다 (오메가는 하한, 세타는 엄격한 경계) .

중첩 루프에 도달하면 :

for(int i = 0; i < n; i++){
    for(int j = i; j < n; j++){
        array[j] += 2;
    }
}

외부 루프 (O (n))의 각 패스에 대해 우리는 전체 목록을 다시 거쳐야하므로 n은 n을 제곱으로 남겨 두어야하기 때문에 이것은 O (n ^ 2)입니다.

이것은 표면을 간신히 긁는 것이지만 좀 더 복잡한 알고리즘을 분석하면 증명과 관련된 복잡한 수학이 시작됩니다. 그래도 이것이 기본 사항에 익숙해지기를 바랍니다.


답변

특정 문제에 대한 Big O 시간을 계산하는 방법을 아는 것이 유용하지만 일반적인 경우를 알고 있으면 알고리즘에서 의사 결정을 내리는 데 많은 도움이 될 수 있습니다.

가장 일반적인 사례는 다음과 같습니다 . http://en.wikipedia.org/wiki/Big_O_notation#Orders_of_common_functions :

O (1)-숫자가 짝수인지 홀수인지 확인 일정한 크기의 룩업 테이블 또는 해시 테이블 사용

O (logn)-이진 검색을 사용하여 정렬 된 배열에서 항목 찾기

O (n)-정렬되지 않은 목록에서 항목 찾기; 두 개의 n 자리 숫자 추가

O (n 2 )-두 개의 n 자리 숫자에 간단한 알고리즘을 곱합니다. 2 개의 n × n 행렬을 추가하는 단계; 버블 정렬 또는 삽입 정렬

O (n 3 )-간단한 알고리즘으로 2 개의 n × n 행렬 곱하기

O (c n )-동적 프로그래밍을 사용하여 출장중인 판매원 문제에 대한 정확한 솔루션 찾기; 무차별 대입을 사용하여 두 개의 논리 문이 동등한 지 판별

O (n!)-무차별 검색을 통해 이동하는 세일즈맨 문제 해결

O (n n )-점근 적 복잡성을 위해 더 간단한 공식을 도출하기 위해 O (n!) 대신 사용되는 경우가 많습니다


답변

작은 알림은 다음 big O표기법을 나타 내기 위해 사용되는 점근 (문제의 크기가 무한대로 증가 할 때입니다,), 복잡성을 하고 그것이 일정을 숨 깁니다.

이것은 O (n)의 알고리즘과 O (n 2 ) 의 알고리즘 사이 에서 가장 빠른 알고리즘이 항상 첫 번째 알고리즘이 아니라는 것을 의미합니다 (크기> n의 문제에 대해 첫 번째 알고리즘은 항상 n의 값이 존재하지만) 가장 빠른).

숨겨진 상수는 구현에 따라 크게 달라집니다!

또한 경우에 따라 런타임은 입력 크기 n의 결정적 기능이 아닙니다 . 예를 들어 빠른 정렬을 사용하여 정렬을 수행하십시오. n 개의 요소 배열을 정렬하는 데 필요한 시간은 일정하지 않지만 배열의 시작 구성에 따라 다릅니다.

다른 시간 복잡성이 있습니다.

  • 최악의 경우 (보통 가장 의미있는 것은 아니지만 알아내는 것이 가장 간단합니다)
  • 평균 사례 (보통 이해하기 훨씬 어렵습니다 …)

좋은 소개는 R. Sedgewick과 P. Flajolet 의 알고리즘 분석 소개 입니다.

당신이 말하는 것처럼 premature optimisation is the root of all evil, 그리고 (가능한 경우)를 프로파일 링 코드를 최적화 할 때 정말 항상 사용되어야한다. 알고리즘의 복잡성을 결정하는 데 도움이 될 수도 있습니다.


답변

여기에서 답을 보았을 때 우리 대부분은 실제로 알고리즘을 보고 알고리즘의 순서를 근사하고 대학에서 생각했던 마스터 방법 과 같은 알고리즘 을 계산하는 대신 상식을 사용 한다고 결론을 내릴 수 있다고 생각합니다. 그렇기 때문에 교수님조차도 나중에 계산하는 대신 실제로 생각해 보라고 권유 했습니다.

또한 재귀 함수에 대해 수행되는 방법을 추가하고 싶습니다 .

( scheme code ) 와 같은 함수가 있다고 가정하십시오 .

(define (fac n)
    (if (= n 0)
        1
            (* n (fac (- n 1)))))

주어진 숫자의 계승을 재귀 적으로 계산합니다.

첫 번째 단계는 이 경우 에만 함수 본문의 성능 특성을 시도하고 결정하는 것입니다. 본문에서는 특별한 것이 없으며 곱셈 (또는 값 1의 반환) 만 수행됩니다.

따라서 본문성능은 다음과 같습니다. O (1) (일정한).

다음 으로 재귀 호출 횟수를 결정하십시오 . 이 경우 n-1 재귀 호출이 있습니다.

너무 재귀 호출의 성능은 다음과 같습니다 O (N-1) (우리는 사소한 부분을 던져로 순서는, n은).

그런 다음이 두 가지를 합하면 전체 재귀 함수의 성능을 얻습니다.

1 * (n-1) = O (n)


베드로 , 제기 된 문제 에 답 하십시오. 여기에 설명 된 방법은 실제로이를 잘 처리합니다. 그러나 이것은 여전히 근사치 이며 수학적으로 정답이 아닙니다. 여기에 설명 된 방법은 우리가 대학에서 가르친 방법 중 하나이며, 올바르게 기억한다면이 예제에서 사용했던 계승보다 훨씬 고급 알고리즘에 사용되었습니다.
물론 그것은 모두 함수 본문의 실행 시간과 재귀 호출 수를 얼마나 잘 추정 할 수 있는지에 달려 있지만 다른 방법에서도 마찬가지입니다.


답변

비용이 다항식 인 경우 승수없이 가장 높은 항을 유지하십시오. 예 :

O ((N / 2 + 1) * (N / 2)) = O (n은 2 / 4 + N / 2) = O (n은 2 / 4) = O (N 2 )

이것은 무한한 시리즈에서는 작동하지 않습니다. 일반적인 경우에는 단일 레시피가 없지만 일반적인 경우에는 다음과 같은 불평등이 적용됩니다.

O (log N ) <O ( N ) <O ( N log N ) <O ( N 2 ) <O ( N k ) <O (e n ) <O ( n !)


답변

나는 정보의 관점에서 그것에 대해 생각합니다. 모든 문제는 특정 비트 수를 학습하는 것으로 구성됩니다.

기본 도구는 의사 결정 지점과 엔트로피의 개념입니다. 결정 포인트의 엔트로피는 그것이 당신에게 줄 평균 정보입니다. 예를 들어, 프로그램에 두 개의 분기가있는 결정 지점이 포함 된 경우 엔트로피는 각 분기의 확률에 해당 분기 의 역 확률에 대한 로그 2 를 곱한 값의 합입니다 . 그것은 그 결정을 실행함으로써 얼마나 많은 것을 배우는지입니다.

예를 들어, if두 가지가 모두 같을 가능성이 있는 명령문은 1/2 * log (2/1) + 1/2 * log (2/1) = 1/2 * 1 + 1/2 * 1의 엔트로피를 갖습니다. = 1. 따라서 엔트로피는 1 비트입니다.

N = 1024와 같은 N 개의 항목 테이블을 검색한다고 가정하십시오. log (1024) = 10 비트이기 때문에 이것은 10 비트 문제입니다. 따라서 결과가 같은 IF 문으로 검색 할 수 있으면 10 가지 결정이 필요합니다.

이것이 이진 검색으로 얻는 것입니다.

선형 검색을 수행한다고 가정하십시오. 첫 번째 요소를보고 원하는 요소인지 묻습니다. 확률은 1/1024이고 그렇지 않은 경우 1023/1024입니다. 그 결정의 엔트로피는 1 / 1024 * log (1024/1) + 1023/1024 * log (1024/1023) = 1/1024 * 10 + 1023/1024 * 약 0 = 약 0.01 비트입니다. 당신은 아주 조금 배웠습니다! 두 번째 결정은 그리 나쁘지 않습니다. 이것이 선형 검색이 너무 느린 이유입니다. 실제로 배우는 데 필요한 비트 수는 기하 급수적입니다.

인덱싱을 수행한다고 가정하십시오. 테이블이 많은 빈으로 미리 정렬되어 있고 키의 모든 비트 중 일부를 사용하여 테이블 항목에 직접 색인을 생성한다고 가정하십시오. 1024 개의 빈이있는 경우 엔트로피는 모든 1,024 개의 가능한 결과에 대해 1/1024 * log (1024) + 1/1024 * log (1024) + …입니다. 이는 1/1024 * 10 곱하기 1024 개의 결과 또는 해당 인덱싱 작업의 10 비트 엔트로피입니다. 그렇기 때문에 색인 검색이 빠릅니다.

이제 정렬에 대해 생각하십시오. N 개의 아이템이 있고리스트가 있습니다. 각 항목에 대해 목록에서 항목이있는 위치를 검색 한 다음 목록에 추가해야합니다. 따라서 정렬은 기본 검색 단계 수의 약 N 배를 차지합니다.

따라서 거의 동등한 결과를 갖는 이진 결정을 기반으로 정렬은 모두 O (N log N) 단계를 수행합니다. 인덱싱 검색을 기반으로하는 경우 O (N) 정렬 알고리즘이 가능합니다.

거의 모든 알고리즘 성능 문제를 이런 방식으로 볼 수 있음을 발견했습니다.