CS 학위를 가진 대부분의 사람들은 Big O가 무엇을 의미 하는지 확실히 알 것 입니다 . 알고리즘이 얼마나 잘 확장되는지 측정하는 데 도움이됩니다.
하지만 어떻게, 궁금 하면 계산이나 알고리즘의 복잡성을 대략?
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)
매개 변수 N
는 data.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
for
get 본문이 몇 번이나 실행 되는지 계산하는 기계적 규칙은 없으므로 코드가 수행하는 작업을 확인하여 계산해야합니다. 계산을 단순화하기 위해 변수 초기화, 조건 및 for
명령문의 증분 부분을 무시합니다 .
실제 BigOh를 얻으려면 함수 의 점근 분석 이 필요합니다 . 이것은 대략 다음과 같이 수행됩니다.
C
.f()
얻가 polynomium 의의를 standard form
.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은 음수로 끝납니다! 불가능하고 잘못되었습니다. 우리는 총합을 두 부분으로 나눌 필요 i
가 N / 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 규칙을 사용하여 요약을 단순화 할 수 있습니다.
w
)대수 적용 :
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²)
O(n)
여기서 n
요소 또는 개수 O(x*y)
어디로 x
및 y
배열의 차원이다. Big-oh는 "입력에 상대적"이므로 입력 내용에 따라 다릅니다.
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)입니다.
이것은 표면을 간신히 긁는 것이지만 좀 더 복잡한 알고리즘을 분석하면 증명과 관련된 복잡한 수학이 시작됩니다. 그래도 이것이 기본 사항에 익숙해지기를 바랍니다.
O(1)
. 인스턴스에 대한 C 표준 API에서, bsearch
본질적이고 O(log n)
, strlen
이다 O(n)
,하고 qsort
있다 O(n log n)
(기술적으로는 보장이 없으며, 자체의 최악의 복잡성을 가지고 퀵 O(n²)
하지만 가정 libc
저자 것은 바보 아니다, 평균 경우 복잡성이 O(n log n)
그것을 사용 O(n²)
사건 발생 가능성을 줄이는 피봇 선택 전략 ). 그리고 모두 bsearch
와 qsort
비교 기능이 병적 인 경우 악화 될 수 있습니다.
특정 문제에 대한 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!) 대신 사용되는 경우가 많습니다
x&1==1
이상을 확인 하는 데 사용하지 않습니까?
x & 1
만으로 충분하고 확인할 필요는 없습니다 == 1
.C에서는 연산자 우선 순위 덕분에x&1==1
평가 되므로 실제로 테스트와 동일합니다 ). 그래도 대답을 잘못 읽고 있다고 생각합니다. 쉼표가 아닌 세미콜론이 있습니다. 짝수 / 홀수 테스트를 위해 룩업 테이블이 필요하다고 말하는 것이 아니라 짝수 / 홀수 테스트 와 룩업 테이블을 확인하는 것이 작업이라고합니다. x&(1==1)
x&1
O(1)
작은 알림은 다음 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) 정렬 알고리즘이 가능합니다.
거의 모든 알고리즘 성능 문제를 이런 방식으로 볼 수 있음을 발견했습니다.
처음부터 시작할 수 있습니다.
우선, 데이터에 대한 특정 간단한 조작이 O(1)
시간, 즉 입력 크기와 무관 한 시간에 수행 될 수 있다는 원칙을 받아들이십시오 . C에서의 이러한 기본 연산은
이 원칙을 정당화하려면 일반적인 컴퓨터의 기계 지침 (기본 단계)에 대한 자세한 연구가 필요합니다. 설명 된 각각의 작업은 적은 수의 기계 명령으로 수행 될 수 있습니다. 종종 하나 또는 두 개의 명령 만 필요합니다. 결과적으로 C에서 여러 종류의 명령문을 O(1)
시간, 즉 입력과 관계없이 일정한 시간 내에 실행할 수 있습니다 . 이 간단한 포함
C에서, 많은 for-loop는 인덱스 변수를 어떤 값으로 초기화하고 루프 주위에서 매번 그 변수를 1 씩 증가시킴으로써 형성됩니다. 인덱스가 제한에 도달하면 for-loop가 종료됩니다. 예를 들어, for-loop
for (i = 0; i < n-1; i++)
{
small = i;
for (j = i+1; j < n; j++)
if (A[j] < A[small])
small = j;
temp = A[small];
A[small] = A[i];
A[i] = temp;
}
인덱스 변수 i를 사용합니다. 루프 주위에서 매번 i를 1 씩 증가시키고 i가 n-1에 도달하면 반복이 중지됩니다.
그러나 현재로서는 간단한 형태의 for-loop에 초점을 맞추고 최종 변수와 초기 값 의 차이를 인덱스 변수의 증가량으로 나눈 값은 루프를 돌아 다니는 횟수를 알려줍니다 . jump 문을 통해 루프를 종료 할 수있는 방법이 없다면 그 수는 정확합니다. 어떤 경우에도 반복 횟수의 상한입니다.
예를 들어, for 루프 ((n − 1) − 0)/1 = n − 1 times
는 0이 i의 초기 값이므로 n-1은 i가 도달 한 가장 높은 값이므로 (즉, i가 n-1에 도달하면 루프가 중지되고 i = n−에서 반복이 발생하지 않습니다.) 1), 루프가 반복 될 때마다 1이 i에 추가됩니다.
가장 간단한 경우, 루프 본문에 소요 된 시간이 각 반복에 대해 동일한 경우, 본문에 대한 최대 값 상한에 루프 주변 횟수를 곱할 수 있습니다 . 엄밀히 말하면 루프 인덱스를 초기화하기 위해 O (1) 시간을 추가하고 루프 인덱스를 limit과의 첫 번째 비교를 위해 O (1) 시간을 추가해야합니다. 루프보다 한 번 더 테스트하기 때문입니다. 그러나 루프를 0 번 실행할 수없는 경우 루프를 초기화하고 한도를 한 번 테스트하는 시간은 합산 규칙에 의해 제거 될 수있는 하위 항입니다.
이제이 예제를 고려하십시오.
(1) for (j = 0; j < n; j++)
(2) A[i][j] = 0;
우리는 라인 (1)에 시간이 걸린다 는 것을 알고 O(1)
있습니다. 우리는 라인 (1)에서 찾은 상한에서 하한을 빼고 1을 더함으로써 루프를 n 번 돌고 있습니다. j를 증가시키는 시간과 j를 n과 비교하는 시간을 무시할 수 있으며 둘 다 O (1)입니다. 따라서, 라인 (1)과 (2)의 실행 시간은이고 N 및 O (1)의 제품 이다 O(n)
.
마찬가지로, 우리는 라인 (2)에서 (4)로 구성된 외부 루프의 실행 시간을 바인딩 할 수 있습니다.
(2) for (i = 0; i < n; i++)
(3) for (j = 0; j < n; j++)
(4) A[i][j] = 0;
우리는 이미 라인 (3)과 (4)의 루프가 O (n) 시간을 필요로한다는 것을 확립했습니다. 따라서 외부 루프의 각 반복에 O (n) 시간이 걸린다는 결론을 내릴 때 i를 증가시키고 각 반복에서 i <n인지 여부를 테스트하기 위해 O (1) 시간을 무시할 수 있습니다.
외부 루프의 초기화 i = 0 및 조건 i <n의 (n + 1) 번째 테스트도 마찬가지로 O (1) 시간이 걸리고 무시 될 수 있습니다. 마지막으로, 우리는 외부 루프를 n 번 돌며 각 반복에 대해 O (n) 시간을 소비하여 총
O(n^2)
실행 시간을 제공합니다.
보다 실용적인 예입니다.
코드를 분석하지 않고 경험적으로 코드의 순서를 추정하려면 n 값을 늘리고 코드의 시간을 늘리는 것이 좋습니다. 로그 스케일로 타이밍을 플로팅하십시오. 코드가 O (x ^ n)이면 값은 기울기 n의 선에 있어야합니다.
이것은 코드를 연구하는 것보다 몇 가지 장점이 있습니다. 우선, 런타임이 점근 순서에 도달하는 범위에 있는지 확인할 수 있습니다. 또한 주문 O (x)라고 생각한 일부 코드는 예를 들어 라이브러리 호출에 소비 된 시간 때문에 실제로 주문 O (x ^ 2)라는 것을 알 수 있습니다.
Big O 표기법은 작업하기 쉽고 불필요한 합병증과 세부 사항을 숨기므로 유용합니다 (불필요한 정의). 분할 및 정복 알고리즘의 복잡성을 해결하는 좋은 방법 중 하나는 트리 방법입니다. 중간 프로 시저가있는 퀵 정렬 버전이 있다고 가정하면 매번 배열을 완벽하게 균형 잡힌 하위 배열로 분할합니다.
이제 작업하는 모든 배열에 해당하는 트리를 작성하십시오. 루트에는 원래 배열이 있고 루트에는 하위 배열 인 두 개의 자식이 있습니다. 하단에 단일 요소 배열이 나타날 때까지이 과정을 반복하십시오.
O (n) 시간의 중앙값을 찾고 O (n) 시간의 두 부분으로 배열을 분할 할 수 있으므로 각 노드에서 수행되는 작업은 O (k)입니다. 여기서 k는 배열의 크기입니다. 트리의 각 레벨에는 (전체적으로) 전체 배열이 포함되므로 레벨 당 작업은 O (n)입니다 (하위 배열의 크기는 n이 더 해지고 레벨 당 O (k)가 있으므로이를 추가 할 수 있습니다) . 입력을 반으로 줄 때마다 트리에는 log (n) 레벨 만 있습니다.
따라서 우리는 O (n * log (n))에 의해 작업량의 상한을 정할 수 있습니다.
그러나 Big O는 때때로 무시할 수없는 세부 정보를 숨 깁니다. 피보나치 수열 계산
a=0;
b=1;
for (i = 0; i <n; i++) {
tmp = b;
b = a + b;
a = tmp;
}
a와 b가 Java의 BigInteger이거나 임의로 많은 수를 처리 할 수있는 것으로 가정하십시오. 대부분의 사람들은 이것이 깜박임이없는 O (n) 알고리즘이라고 말합니다. 추론은 for 루프에 n 개의 반복이 있고 O (1)이 루프에서 작동한다는 것입니다.
그러나 피보나치 수는 크고 n 번째 피보나치 수는 n에서 지수이므로 저장하면 n 바이트 정도의 순서로 수행됩니다. 큰 정수로 더하기를 수행하려면 O (n)의 작업이 필요합니다. 따라서이 절차에서 수행 된 총 작업량은
1 + 2 + 3 + ... + n = n (n-1) / 2 = O (n ^ 2)
따라서이 알고리즘은 2 차 시간으로 실행됩니다!
일반적으로 덜 유용하다고 생각하지만 완전성을 위해 알고리즘의 복잡성에 대한 하한을 정의하는 Big Omega Ω 과 상한과 하한을 모두 정의 하는 Big Theta Θ도 있습니다.
알고리즘을 큰 O 표기법을 알고있는 조각으로 나누고 큰 O 연산자를 통해 결합하십시오. 그것이 내가 아는 유일한 방법입니다.
자세한 내용 은 해당 주제 의 Wikipedia 페이지 를 확인하십시오 .
내가 사용하는 알고리즘 / 데이터 구조에 대한 지식 및 / 또는 반복 중첩에 대한 빠른 개요 분석. 라이브러리 함수를 여러 번 호출 할 때 어려움이 있습니다. 여러 번 함수를 불필요하게 호출하는지 또는 어떤 구현을 사용하는지 확실하지 않을 수 있습니다. 라이브러리 함수는 Big O이든 다른 메트릭이든 문서 나 IntelliSense 에서 사용할 수있는 복잡성 / 효율성 측정 값을 가져야합니다 .
Big O를 "어떻게 계산합니까"에 관해서는, 이것은 계산 복잡도 이론의 일부입니다 . 일부 (많은) 특수한 경우에는 중첩 된 루프의 곱셈 루프 수와 같은 간단한 휴리스틱이 제공 될 수 있습니다. 당신이 원하는 모든 상한 추정이며, 너무 비관적이라면 신경 쓰지 않아도됩니다. 아마도 귀하의 질문에 관한 것입니다.
알고리즘에 대한 질문에 정말로 대답하고 싶다면 이론을 적용하는 것이 가장 좋습니다. 간단한 "최악의 사례"분석 외에도 필자는 Amortized 분석 이 실제로 매우 유용하다는 것을 알았습니다 .
마스터 방법 (또는 그 전문화 방법 중 하나)을 사용하는 것 외에도 실험적으로 알고리즘을 테스트합니다. 이것은 특정 복잡성 등급이 달성되었음을 증명할 수 없지만 수학적 분석이 적절하다는 확신을 줄 수 있습니다. 이러한 확신을 돕기 위해 실험과 함께 코드 적용 도구를 사용하여 모든 경우를 연습하고 있습니다.
아주 간단한 예로 .NET 프레임 워크의 목록 정렬 속도에 대한 온 전성 검사를 원한다고 가정합니다. 다음과 같은 내용을 작성한 다음 Excel에서 결과를 분석하여 n * log (n) 곡선을 초과하지 않았는지 확인할 수 있습니다.
이 예제에서는 비교 횟수를 측정하지만 각 샘플 크기에 필요한 실제 시간을 조사하는 것도 신중합니다. 그러나 알고리즘 만 측정하고 테스트 인프라의 아티팩트를 포함하지 않도록주의해야합니다.
int nCmp = 0;
System.Random rnd = new System.Random();
// measure the time required to sort a list of n integers
void DoTest(int n)
{
List<int> lst = new List<int>(n);
for( int i=0; i<n; i++ )
lst[i] = rnd.Next(0,1000);
// as we sort, keep track of the number of comparisons performed!
nCmp = 0;
lst.Sort( delegate( int a, int b ) { nCmp++; return (a<b)?-1:((a>b)?1:0)); }
System.Console.Writeline( "{0},{1}", n, nCmp );
}
// Perform measurement for a variety of sample sizes.
// It would be prudent to check multiple random samples of each size, but this is OK for a quick sanity check
for( int n = 0; n<1000; n++ )
DoTest(n);
메모리 자원이 제한되어있는 경우 우려 할 수있는 공간 복잡성을 허용하는 것을 잊지 마십시오. 예를 들어, 일정한 공간 알고리즘을 원하는 누군가가 기본적으로 알고리즘이 사용하는 공간의 양이 코드 내부의 요소에 의존하지 않는다고 말하는 방법을들을 수 있습니다.
때때로 복잡성은 호출 횟수, 루프 실행 빈도, 메모리 할당 빈도 등으로 인해 발생할 수 있습니다.
마지막으로 big O는 최악의 경우, 최상의 경우 및 상각의 경우에 사용될 수 있으며 일반적으로 알고리즘이 얼마나 나쁜지를 설명하는 데 사용되는 최악의 경우입니다.
종종 간과되는 것은 알고리즘 의 예상되는 동작입니다. 알고리즘의 Big-O를 변경하지는 않지만 "조기 최적화.. .."와 관련이 있습니다.
알고리즘의 예상되는 동작은 매우 멍청한 것입니다. 알고리즘이 가장 많이 볼 수있는 데이터에서 얼마나 빠르게 작동하는지 예상 할 수 있습니다.
예를 들어 목록에서 값을 검색하는 경우 O (n)이지만 대부분의 목록에 값이 있다는 것을 알고 있으면 알고리즘의 일반적인 동작이 더 빠릅니다.
실제로이를 정리하려면 "입력 공간"의 확률 분포를 설명 할 수 있어야합니다 (목록을 정렬해야하는 경우 해당 목록이 얼마나 자주 정렬됩니까? 얼마나 자주 전체가 반전됩니까? 종종 대부분 정렬되어 있습니까?) 항상 아는 것이 가능하지는 않지만 때로는 그렇습니다.
좋은 질문입니다!
면책 조항 :이 답변에는 허위 진술이 포함되어 있습니다. 아래 의견을 참조하십시오.
Big O를 사용하는 경우 더 나쁜 경우에 대해 이야기하고 있습니다 (나중에 무엇을 의미하는지 더 자세히 설명). 또한 평균 사례에는 대문자 세타가 있고 최상의 경우에는 큰 오메가가 있습니다.
Big O에 대한 공식적인 정의를 보려면 이 사이트를 확인하십시오 : https://xlinux.nist.gov/dads/HTML/bigOnotation.html
f (n) = O (g (n))은 모든 n ≥ k에 대해 0 ≤ f (n) ≤ cg (n)과 같이 양의 상수 c와 k가 있음을 의미합니다. c와 k의 값은 함수 f에 대해 고정되어야하며 n에 의존해서는 안됩니다.
자 이제 "최고의 경우"와 "최악의 경우"복잡성이 무엇을 의미합니까?
이것은 아마도 예제를 통해 가장 명확하게 설명됩니다. 예를 들어 선형 검색을 사용하여 정렬 된 배열에서 숫자를 찾는 경우 최악의 경우 배열 의 마지막 요소 를 검색 하기로 결정한 경우 배열에 항목이있는 단계만큼 많은 단계가 필요합니다. 최상의 경우는 우리가 검색 할 때 될 첫 번째 요소 우리가 처음 확인 후 할 것이기 때문이다.
이러한 형용사- 복잡성 복잡성 의 핵심은 특정 변수의 크기 측면에서 가상의 프로그램이 완료되는 데 걸리는 시간을 그래프로 나타낼 수 있는 방법을 찾고 있다는 것입니다. 그러나 많은 알고리즘의 경우 특정 크기의 입력에 대해 단일 시간이 없다고 주장 할 수 있습니다. 이것은 함수의 기본 요구 사항과 상반되므로 모든 입력은 하나 이상의 출력을 가져서는 안됩니다. 그래서 우리 는 알고리즘의 복잡성을 설명하기 위해 여러 함수를 고안했습니다. 이제 n 크기의 배열을 검색하는 것은 배열에서 찾고있는 내용과 n에 비례하여 다양한 시간이 걸릴 수 있지만 최선의 경우를 사용하여 알고리즘에 대한 유익한 설명을 만들 수 있습니다 최악의 클래스.
죄송합니다. 글이 잘못 작성되어 기술 정보가 부족합니다. 그러나 시간 복잡성 수업을 더 쉽게 생각할 수 있기를 바랍니다. 일단 이것에 익숙해지면 프로그램을 통해 구문 분석하고 데이터 구조를 기반으로 배열 크기 및 추론에 의존하는 for-loops와 같은 것을 찾는 간단한 문제가됩니다. 최악의 경우.
프로그래밍 방식 으로이 문제를 해결하는 방법을 모르지만 사람들이하는 첫 번째 작업은 수행 된 작업 수에서 특정 패턴에 대한 알고리즘을 샘플링한다는 것입니다.
여기서 f (x)가 수행 된 연산 수에 대한 공식 (위에서 설명한 4n ^ 2 + 2n + 1) 인 f (x)를 단순화하면 다음에서 big-O 값 [O (n ^ 2)을 얻습니다. 케이스]. 그러나 이것은 프로그램에서 Lagrange 보간을 설명해야하며 구현하기 어려울 수 있습니다. 실제 큰 O 값이 O (2 ^ n)이고 O (x ^ n)과 같은 것이 있으면이 알고리즘을 프로그래밍 할 수 없을 것입니다. 그러나 누군가 나를 잘못 증명하면 코드를 제공하십시오. . . .
Big-O를 약간 다른 측면으로 설명하고 싶습니다.
Big-O는 단지 프로그램의 복잡성을 비교하는 것입니다. 즉, 입력이 증가 할 때 얼마나 빨리 성장하고 있으며 조치를 수행하는 데 소요되는 정확한 시간이 아닙니다.
빅오 공식에서 IMHO를 사용하면 더 복잡한 방정식을 사용하지 않는 것이 좋습니다 (다음 그래프의 수식을 사용하면됩니다). 그러나 다른 정확한 공식을 사용할 수도 있습니다 (예 : 3 ^ n, n ^ 3, .. .) 그러나 그 이상은 때때로 오도 할 수 있습니다! 가능한 한 간단하게 유지하는 것이 좋습니다.
여기서 알고리즘에 대한 정확한 공식을 얻고 싶지 않다는 것을 다시 강조하고 싶습니다. 우리는 입력이 커질 때 어떻게 성장하는지 보여주고 다른 알고리즘과 비교할 수 있습니다. 그렇지 않으면 벤치마킹과 같은 다른 방법을 사용하는 것이 좋습니다.