O표기법으로 표시는 아래와 같이 세가지로 나눌수 있다.
- 최악의 성능 경우 (Worst-case analysis)
- 최고의 성능 경우 (Best-case analysis)
- 평균의 성능 경우 (Average-case analysis)
하지만, O 표기법은 표시할 때 에는 아래와 같이 최악경우를 생각하고 많은 관심을 가진다고 한다.
- 많은 알고리즘들이 실행 할 경우, 최악의 성능을 가지고 동작을 한다고 한다.
- 많은 알고리즘들이 최상의 경우를 가지고 분석을 하면, 별 도움이 되지 않는다고 한다.
- 평균의 경우, 이 평균을 정확하게 계산하기가 힘이 들며, 평균성능을 계산이 불가능할때도 있기 때문이다.
- 최악의 경우는 알고리즘의 성능의 한계를 표시해주며, 더 나쁘게 실행되지 않는다고 보장한다.
1.1 기본 표기법의 결론
보통 최악의 성능 경우 (Worst-case analysis)로 측정을 한다.
- 예외사항이 존재 - 평균 성능의 경우 (Average-case analysis)
예를 들면, 평균의 성능경우 근거로 해야 할 경우가 필요한데, 무작위 알고리즘들은
평균성능으로 해야한다고 한다. 왜냐하면, 일반적인 확률 원칙을 적용해야하기 때문이다.
그 대표적인 예로 Quick sort이며, 무작위 알고리즘으로, 평균성능(Average-case analysis)을 가지고 판단해야 한다고 한다.
아래사이트 가면, Worst-case analysis, Best-case analysis ,Average-case analysis 접근이 나온다.
https://en.wikipedia.org/wiki/Quicksort
참조: C로 구현한 알고리즘
https://en.wikipedia.org/wiki/Computational_complexity_theory#Best.2C_worst_and_average_case_complexity
2. Big O 표기법 과 시간복잡도의 관계
Big O 표기법 기반으로 시간의 복잡도를 알수 있으며, 이에 관련된 기본 규칙이 존재한다.
아래는 X축은 N번 말하며, Y축은 시간의 복잡도 (Time complexity) 를 표시를 해준다.
시간의 복잡도는 Big O 표기법 기준으로 나온 값을 말하며, Big O표기값과 동일하다고 생각하며 되겠다 하지만 아래와 같은 몇가지 규칙이 존재한다.
X축 O 표시법의 입력
Y축 O 표시법의 시간복잡도
상위 표로 알수 있는 것은 O표기법의 시간의 복잡도를 알수 있다.
간단히 보면, 1은 복잡하지 않지만, N! 과 2^N, N^2 복잡도가 복잡해지는 것을 알수 있다.
https://en.wikipedia.org/wiki/Time_complexity
2.1 시간복잡도의 기본규칙 (Time complexity's Rules)
Big O notation을 이용하여 Time complexity를 계산할 경우, 아래와 같이 기본 규칙들이 존재한다.
- 기본 규칙 1
O(c) = O(1) 모두동일
- 기본 규칙 2
O(cT) = cO(T) = O(T) 모두동일
- 기본규칙 3
한작업이 O(N) 실행 후 다른 한 작업 O(N^2) 일 실행 될 경우, O(N^2)만 선택
이유는 아래에 설명
O(T1) + O(T2) = O( T1 + T2) = max( O(T1), O(T2) ) 동일
e.g O(N) + O(N^2) = O(N^2) 로 선택
- 기본규칙 4
안쪽 루프를 O(N2) 하고 이를 T2로 한다고 하면,
O(T1)O(T2) =O(T1 * T2) 동일
e.g O(N) * O(N) = O(N^2), O(N) * O(N-1) = O( N* (N-1) )
2.2 시간복잡도 규칙의 적용의 예 (Examples)
시간의 복잡도(Time Complexity) 규칙을 설명을 했고, 이를 이용하여 실제 작업에 적용하여 이 규칙의 원리에 대해 파악해보자.
예를 들면, 작업 A, B, C가 존재 한다고 하고 각각의 작업, 즉 Program의 Big O 표기법을 이용하여, Time Complexity를 측정을 해보자.
- 작업 A
int i=0,j=0; int test=0; // 1번째 작업 , N *N 작업 for (i=0;i< n; i++) for (j=0;j< n; j++) test++; // 2번째 작업 for (i=0;i< n; i++) for (j=0;j< n; j++) test++; // 3번째 작업 for (i=0;i< n; i++) for (j=0;j< n; j++) test++;
- 작업 B
int i=0; int test=0; // 1번째 작업 , N번 작업 for (i=0;i< n; i++) test++; // 2번째 작업 for (i=0;i< n; i++) test++; // 3번째 작업 for (i=0;i< n; i++) test++; .... // 10번째 작업 for (i=0;i< n; i++) test++;
- 작업 C
O(10) : 항상 10번 반복하는 작업 존재. 이 작업이 1개 존재 할 경우,
위 전체작업을 표시 하면 아래와 같지만, 위의 기본규칙 1, 2, 3 정리하면 아래와 같이 정리된다.
O(3N^2 + 10N + 10) = O(3N^2) = O(N^2) 와 거의 동일
이 규칙에서 핵심이 되는 기본규칙 3번의 원리를 분석하고 넘어가겠다.
위와 같이 되는 이유를 개별작업시간과 전체작업시간을 백분율로 계산을 해보자.
(개별작업시간/전체작업시간) * 100
여기서 개별작업시간을 간단히 백분율로 분석을 해보자.
위와 같이 N이 증가 할수록 작업 A에 시간의 복잡도가 최대값인 작업 A에만 편중이 되는 것을 확인 할수 있다.
N 값에 증가 값에 따라 N의 복잡도가 N^2에 편중이 되므로, 다른 것들은 거의 무의미 해지기 때문에 이 규칙이 적용이된다.
O(3N^2 + 10N + 10) = O(3N^2) = O(N^2) 와 작업 A 거의동일
위와 같이 일반 작업처럼 기본공식 3 통해 단순화 시킬수 있겠지만, 위와 같이 N이 증가할 경우를 고려했기 때문에 단순화하여 가능하지만,
만약 정확한 비교와 다른 오차를 생각 한다면, 예를 들면 N의 값이 정해져 있다고 하고, 다항식의 늘어날 경우,
다른 작업 값도 무시할수는 없을 것이다.
2.3 시간복잡도 2번째 예제 (수열)
C로 구현한 알고리즘에서 삽입정렬을 보다가, 오랜만에 보니, 일반수학에 대해 잊어버려서
n + (n -1) + (n -2) + ... 1 합의 계산을 구하는 공식을 아래와 같이 적음
아래공식은 위의 합을 구할려면, 일반적인 규칙을 찾아야 하며,
아래에 예제에 적용
https://ko.wikipedia.org/wiki/%EC%88%98%EC%97%B4
아래의 예제는 원래 C로 구현한 알고리즘의 삽입정렬이지만, 간단하게 이해하기 쉽도록 표기
자세한내용은 책을 참조.
상위를 소스를 분석하여, 시간복잡도를 계산해보면, 바깥루프는 반복횟수가 (n-1) 안쪽 루프는 바깥루프의 의존적이다.
이제 바깥루프와 안쪽루프에 실제 값을 넣어 i, j에 i=1, j=0 부터 넣어 봤을때,
안쪽루프의 계산되는 부분(test++) 의 안쪽루프의 계산되는 부분의 반복횟수를이 직접 보자.
n : 바깥루프의 반복횟수, size
i j 반복횟수(계산되는부분)
1 0 1
2 1 2
3 2 3
4 3 4
5 4 5
(n-1) (n-2) (n-1)
안쪽루프의 계산되는 반복횟수를 분석을 해보면 1, 2, 3, 4 5 ,6 ~ (n-1) 증가되는 것을 알 수 있으며,
이는 수열과 동일구조이며 이것을 시간의 복잡도로 표시하면, 1 ~ (n-1) 합으로 표시가 가능하게되어진다.
수열의 합공식을 적용하면, (n-1)n/2 을 해주면 된다.
이를 간소화 하면, O((n^2)/2- n/2) 표시가 가능하며, 이는 O(n^2)와 동일하게 된다.
참조: C로 구현한 알고리즘
https://en.wikipedia.org/wiki/Time_complexity
2.4 시간복잡도 3번째 예제 (재귀함수 or LOOP )
N : N=2 이상의 변수
X : Loop의 최종 목표값 or 초기 값
수열과 같이 증가 와 감수되는 규칙을 알고 배열 순서가 바뀌는 것을 알면 되겠다.
값을 대입을 해보고 간단히 분석해보고 계산하자
우선 재귀들의 구성을 보면, input값에는 return value의 영향을 받지 않는다.
그래서 input 중심으로 분석한다.
개별 함수의 Time Complexity 계산을 해보자. T(n)으로 표시해도 좋다.
검증을 원하면, 전역변수를 넣어 count를 해서 점검을 해보자.
아래에서 재귀에서 O(1)은 재귀의 종료조건을 말하고, 이는 반복횟수에도 포함도 가능
Input 5, 4, 3, 2, 1, 0: -1
O(1) not working
O(n) = O(1) + O(n) = O(n) or O(n+1)
상위 규칙을 적용하면, O(n) 겠지만, 정확히 하기 위해서 O(n+1)
Input 15 10 5, 0: -1
O(1) not working
O(n) = O(n)/5 + O(1) = 1/5* O(n) + O(1) = O(n)
Input 125, 25, 5, 1 , 0
O(1) not working
반복횟수가 n+1 가 되는 이유는 5^0 = 1 도 포함해야 하기 때문
O(n) = O(Log5(N+1)) + O(1) = O(Log5(N+1))
재귀의 확장성을 알기위해서 등비수열과의 연관성도 알아두고, 어떻게 확장이 되어가는지 정확한 이해를 하자.
예를 들면, 재귀함수의 전체 반복횟수를 알아보기위해, 이 구조를 완전 2진 트리라고 생각하자.
시작조건 N=5 입력
종료조건 (N==0 이면 종료)
전체 반복횟수 (N+1) = 6 번
상위 형태는 등비수열의 형태이며, 이를 3배씩 변경하면 그 효과 역시 동일하다.
물론 위의 조건이 약간씩 상이하게 변경이 될 수 있지만, 그 근본은 동일하다고 생각한다.
- 등비수열의 구조 와 합의 공식
- 1st 함수 분석
재귀함수-A 와 아래 첫번째 재귀함수는 동일한 구조이며, 이를 다시 한번보자.
Input n=5, 반복횟수 : n+1
Input 5, 4, 3, 2, 1, 0: -1
O(1) not working
O(n) = O(1) + O(n) or O(n+1)
- 2nd 함수 분석
두번째 재귀 함수호출수는 반복횟수 동일하지만 함수가 2배식 확장되어, 함수 종료 조건은 한번만 실행되어진다.
n=5 일 경우 , 반복횟수 : n+1
2^1 + 2^2 + 2^3 + 2^4 + 2^5 + 1 = 63
O(1)
n은 감소하면서 두번식 확장하면서 커진다.
합의 공식 대입: (1-2^6)/(1-2) = -63/-1 = 63 (r=2 , a=1 , n+1 대입)
n=10 일 경우 , 반복횟수 : n+1
2^1 + 2^2 + 2^3 + 2^4 + 2^5 ... 2^10 + 1 = 2047
합의 공식 대입: (1-2^11)/(1-2) = -2047/-1 = 2047 (r=2 , a=1 , n+1 대입)
이공식을 다음과 같이 변경 가능하다.
O(n) = O(2^(n+1)) - 1
N 대신 2^(N+1)를 넣고 첫번째 재귀함수와 동일하게 반복횟수는 n+1 이다.
n을 5을 넣으면, 함수 호출수는 2^(n+1) -1 = 64 - 1번 = 63
n을 10을 넣으면 함수 호출수는 2^(n+1) -1 = 2048 -1 번 = 2047
- 3rd 함수 분석
세번째 재귀 함수호출수는 반복횟수 동일하지만 함수가 3배식 확장되어, 함수 종료 조건은 한번만 실행되어진다. .
n=5 일 경우 , 반복횟수 : n+1
3^1 + 3^2 + 3^3 + 3^4 + 3^5 + 1 = 364
O(1)
n은 감소하면서 3배식 확장하면서 커진다.
합의 공식 대입: (1-3^6)/(1-3) = -728/-2 = 364 (r=2 , a=1 , n+1 대입)
상위 공식을 아래와 같이 변경 가능하다.
O(n) = O(3^(n+1) - 1 ) / 2
n=5 ( 3^(5+1) - 1 ) / 2 = 364
n=10 ( 3^(10+1) - 1 ) / 2 = 88573
- 4th 함수 분석
네번째 재귀 함수호출수는 반복횟수 동일하지만 함수가 4배식 확장되어, 함수 종료 조건은 한번만 실행되어진다.
상위와 같은 등비수열의 합이며, 동일하게 아래와 같이 일반화하여 쉽게 구할수 있다.
O(n) = O(4^(n+1) -1 ) / 3
n=5 (4^(5+1 ) -1) / 3 = 1365
n=10 (4^(10+1) -1) / 3 = 1398101
다시 재귀함수-B를 보자
Input n=15, 반복횟수 : n/5+1 = 4
Input 15 10 5, 0
work_1 8 (7.5) 5 3(2.5) 1 = 17
아래의 소스를 동작해보면 cnt 16 으로 마지막 0 에서 cnt는 동작이 안됨 (n=0)
O(n) = O(n)/5 + O(1) = O(n/5)+1 안에 다른 작업이 반복이 된다.
http://stackoverflow.com/questions/13467674/determining-complexity-for-recursive-functions-big-o-notation
2.5 시간복잡도 4번째 예제
Big O 표기법이라는 것이 기본규칙을 사용하여 일반화하여 구성을 하면 정확한 측정은 힘들어진다.
하지만 기본규칙은 항상 모든 알고리즘에서 빠른 계산과 쉬운 계산을 목표로 생각으로 구성을 해야겠다.
n=5
input: 5 ,4 ,3 ,2 ,1
O(n)
n=8
input: 8 , 4 , 2 , 1
O(logN)
n=5
input : (5,4,3,2,1) , (5,4,3,2,1) , (5,4,3,2,1), (5,4,3,2,1) , (5,4,3,2,1)
O(N^2);
n=4
input : (4,2,1) , (4,2,1) , (4,2,1), (4,2,1) : O(LogN) 을 O(n) 반복
O(NLogN);
n=4
input : (4,2,1) , (3,1) , (2,1), (1) : O(LogN) 을 O(n)반복
O(NLogN);
n=4
input : (4,3,2,1) , (4,3,2,1) , (4,3,2,1) : O(n)을 LogN 만큼 반복.
O(NLogN);
n=4
input : (4,3,2,1) , (2,1) , (1) : O(n)을 O(LogN)만큼 반복 ( O(n)의 값이 적용 )
O(NLogN);
input : (4,2,1) , (4,2,1) , (4,2,1) : O(LogN)을 O(LogN)만큼 반복 (두번째 LogN은 동일)
O(LogLogN);
input : (4,2,1) , (2,1) , (1) : : O(LogN)을 O(LogN)만큼 반복 (두번째는 LogN는 감소)
O(LogLogN);
https://www.quora.com/How-can-we-check-for-the-complexity-log-n-and-n-log-n-for-an-algorithm
3. O 표기법의 주요 함수 내용 정리
기본적으로, O표기법으로 알아내는 법은 Input을 넣어 대입하고 어떻게 동작이 되는지를 파악을 해보면 된다고 생각한다.
그리고 그 변화에 규칙성이 알아내어 규칙성을 찾아 일반적인 함수를 적용 할수 있다면 적용하는 것이다.
여기서 이산수학의 필요성이 필요한 것 같다.
(사실 고등학교 수학지식이면 충분한 것 같다)
3.1 일반적인 O 표기법
아래와 같이 반복 routine이 있다 할지라도, 100 * 200에서 항상 동일하게 끝난다.
아래는 항상 일정한 시간을 갖는 routine이다.
아래 작업을 두개의 일정한 값을 유지하는 작업이 2개 ( if 와 for 반복문)
O(c)+O(1) = O(1) C는 100 * 200
3.2 일반적인 반복 O 표기법
3.3 2의 지수 O 표기법
아래의 피보나치 수열을 계산과정을 보면 2의 지수 형태로 진행이 되는 것을 알 수 있다.
예를 들면, number를 5를 넣으면, 두번을 매번 재귀를 매번 호출하고,
재귀된 함수도 1 값이 될때 까지 2번씩 반복이 된다.
이는 2의 지수 함수 동일하다.
아래의 예제를 보면 쉽게 이해가 간다.
https://ko.wikipedia.org/wiki/%EB%8F%99%EC%A0%81_%EA%B3%84%ED%9A%8D%EB%B2%95
3.4 Binary Log 함수 O 표기법
O표기법에서 사용하는 일반적인 Log의 아래값은 2이며, Log 함수의 기본적인 성질을 알아두자.
b^0 = 1 이를 Logb(1) = 0 변환하면 Log Rule 1번
쉽게 생각하면 b의 지수의 값을 구하려는 함수라고 생각하면 된다.
현재 O 표기법에서는 b의 값은 2이다,
고등학교때, 그냥 Log하면 Log b는 10인 걸로 기억을 하는데 처음 혼동이 되어서 간단히 적어본다.
4번 규칙처럼 Log 함수에서 M의 지수를 사용할 경우, N * logb(M) 변환이 가능하다.
아래내용은 로그함수의 기본정의 규칙을 정리 다시 복습 해보자.
기본로그함수내용
https://en.wikipedia.org/wiki/Logarithm
https://en.wikipedia.org/wiki/Binary_logarithm
로그관련 기본공식
http://mathbang.net/597
http://www.purplemath.com/modules/logrules.htm
Binary Search 와 Binary Traversal은 다르며, Binary Search인 경우는
보통 현재 노드를 비교하여, 저장될 노드를 비교하여 찾아가는 구조이기에,
둘중 하나만 선택하여 찾아 호출하는 구조
n/b^3 의 의미 부터 파악해보면, layer가 내려갈 수록 1/b씩 곱하는것이다.
이 전체를 순회를 한다고 가정하면, 마지막 layer에 도착하기 위해서는 n/b^3의 확률이 걸린다. N=8이다.
더 할 수 있겠다. 그리고, Log에서 덧셈을 곳셈으로 변환하고, 그 곳셈에서 나온값을
N의 지수가 나오기때문에 아래와 같이 간단히 정리를 할 수 있겠다.
= (log n+log(n-1)+...+log 2)
= (log n+log(n-1)+...+log 2)+(log n+log(n-1)+...+log 2)
= O(n*log n)
https://ko.wikipedia.org/wiki/%ED%9E%99_%EC%A0%95%EB%A0%AC
https://en.wikipedia.org/wiki/Heapsort
https://www.quora.com/How-can-we-check-for-the-complexity-log-n-and-n-log-n-for-an-algorithm
int i=0; int test=0; // 1번째 작업 , 항상 동일 for (i=0;i< 10; i++) test++;
위 전체작업을 표시 하면 아래와 같지만, 위의 기본규칙 1, 2, 3 정리하면 아래와 같이 정리된다.
- 전체 작업 A , B, C 가 존재하며 이 작업을 규칙을 사용하여 간소화해보자.
- 작업 C는 기본규칙의 1번의 동일 작업이다.
- 기본규칙 3번에 의해 MAX값만 사용 ( 간소화)
- 기본규칙 2번에 의해 앞에 상수는 의미가 없어진다.
O(3N^2 + 10N + 10) = O(3N^2) = O(N^2) 와 거의 동일
이 규칙에서 핵심이 되는 기본규칙 3번의 원리를 분석하고 넘어가겠다.
위와 같이 되는 이유를 개별작업시간과 전체작업시간을 백분율로 계산을 해보자.
(개별작업시간/전체작업시간) * 100
여기서 개별작업시간을 간단히 백분율로 분석을 해보자.
- N=10일 경우,
- 작업 A 실행시간 = O(3N^2) / O(3N^2 + 10N + 10) * 100 = 73.2%
- 작업 B 실행시간 = O(10N) / O(3N^2 + 10N + 10) * 100 = 24.4%
- 작업 C 실행시간 = O(10) / O(3N^2 + 10N + 10) * 100 = 2.4%
- N=100 일 경우,
- 작업 A 실행시간 = O(3N^2) / O(3N^2 + 10N + 10) * 100 = 96.7%
- 작업 B 실행시간 = O(10N) / O(3N^2 + 10N + 10) * 100 = 3.2%
- 작업 C 실행시간 = O(10) / O(3N^2 + 10N + 10) * 100 = 0.1% 이하
위와 같이 N이 증가 할수록 작업 A에 시간의 복잡도가 최대값인 작업 A에만 편중이 되는 것을 확인 할수 있다.
N 값에 증가 값에 따라 N의 복잡도가 N^2에 편중이 되므로, 다른 것들은 거의 무의미 해지기 때문에 이 규칙이 적용이된다.
O(3N^2 + 10N + 10) = O(3N^2) = O(N^2) 와 작업 A 거의동일
위와 같이 일반 작업처럼 기본공식 3 통해 단순화 시킬수 있겠지만, 위와 같이 N이 증가할 경우를 고려했기 때문에 단순화하여 가능하지만,
만약 정확한 비교와 다른 오차를 생각 한다면, 예를 들면 N의 값이 정해져 있다고 하고, 다항식의 늘어날 경우,
다른 작업 값도 무시할수는 없을 것이다.
2.3 시간복잡도 2번째 예제 (수열)
C로 구현한 알고리즘에서 삽입정렬을 보다가, 오랜만에 보니, 일반수학에 대해 잊어버려서
n + (n -1) + (n -2) + ... 1 합의 계산을 구하는 공식을 아래와 같이 적음
아래공식은 위의 합을 구할려면, 일반적인 규칙을 찾아야 하며,
- 기본 수열의 합 공식
- 가장 끝수와 가장 첫수를 더하면 항상 (n+1) 동일
- 위의 합의 전체 반복은 N이며, 2개짝으로 했기에, 2로 나눈다.
아래에 예제에 적용
https://ko.wikipedia.org/wiki/%EC%88%98%EC%97%B4
아래의 예제는 원래 C로 구현한 알고리즘의 삽입정렬이지만, 간단하게 이해하기 쉽도록 표기
자세한내용은 책을 참조.
int SimpleTest ( int size) { int i=0,j=0; int test=0; for(i=1; i< size; i++) { j= i-1; while(j>=0){ test ++; j--; } } return 0; }
상위를 소스를 분석하여, 시간복잡도를 계산해보면, 바깥루프는 반복횟수가 (n-1) 안쪽 루프는 바깥루프의 의존적이다.
이제 바깥루프와 안쪽루프에 실제 값을 넣어 i, j에 i=1, j=0 부터 넣어 봤을때,
안쪽루프의 계산되는 부분(test++) 의 안쪽루프의 계산되는 부분의 반복횟수를이 직접 보자.
n : 바깥루프의 반복횟수, size
i j 반복횟수(계산되는부분)
1 0 1
2 1 2
3 2 3
4 3 4
5 4 5
(n-1) (n-2) (n-1)
안쪽루프의 계산되는 반복횟수를 분석을 해보면 1, 2, 3, 4 5 ,6 ~ (n-1) 증가되는 것을 알 수 있으며,
이는 수열과 동일구조이며 이것을 시간의 복잡도로 표시하면, 1 ~ (n-1) 합으로 표시가 가능하게되어진다.
수열의 합공식을 적용하면, (n-1)n/2 을 해주면 된다.
이를 간소화 하면, O((n^2)/2- n/2) 표시가 가능하며, 이는 O(n^2)와 동일하게 된다.
참조: C로 구현한 알고리즘
https://en.wikipedia.org/wiki/Time_complexity
2.4 시간복잡도 3번째 예제 (재귀함수 or LOOP )
- LOOP와 사칙연산 간의 반복횟수 규칙
N : N=2 이상의 변수
X : Loop의 최종 목표값 or 초기 값
- Loop 와 + N 일경우, 곱하기로 변하지만, 반복횟수 X/N
- Loop 와 - N 일경우, 나누기로 변하지만, 반복횟수 X/N
- Loop 와 * N 일경우, 지수로 변하지만, 반복횟수 LogNX
- Loop 와 / N 일경우, 로그로 변하지만, 반복횟수 LogNX
수열과 같이 증가 와 감수되는 규칙을 알고 배열 순서가 바뀌는 것을 알면 되겠다.
- LOOP or 재귀의 세부 분석의 예제
값을 대입을 해보고 간단히 분석해보고 계산하자
우선 재귀들의 구성을 보면, input값에는 return value의 영향을 받지 않는다.
그래서 input 중심으로 분석한다.
개별 함수의 Time Complexity 계산을 해보자. T(n)으로 표시해도 좋다.
검증을 원하면, 전역변수를 넣어 count를 해서 점검을 해보자.
아래에서 재귀에서 O(1)은 재귀의 종료조건을 말하고, 이는 반복횟수에도 포함도 가능
- 재귀 함수-A (일반 LOOP)
Input 5, 4, 3, 2, 1, 0: -1
O(1) not working
O(n) = O(1) + O(n) = O(n) or O(n+1)
상위 규칙을 적용하면, O(n) 겠지만, 정확히 하기 위해서 O(n+1)
static int cnt=0; int recursiveFun1(int n) { cnt++; if (n <= 0) return 1; // +1 else return 1 + recursiveFun1(n-1); //N } int loopFun1(int n) { int i; for(i=n;i >0; i-=1 ) { cnt++; //N } return 1; }
- 재귀 함수-B (LOOP의 X/N의 예제)
Input 15 10 5, 0: -1
O(1) not working
O(n) = O(n)/5 + O(1) = 1/5* O(n) + O(1) = O(n)
static int cnt=0; int recursiveFun2(int n) { cnt++; if (n <= 0) return 1; //+1 else return 1 + recursiveFun2(n-5); //N/5 } int loopFun2(int n) { int i; for(i=n;i > 0; i-=5 ) //N/5 { cnt++; } return 1; }
- 재귀 함수-C (LOOP의 LOGNX)
Input 125, 25, 5, 1 , 0
O(1) not working
반복횟수가 n+1 가 되는 이유는 5^0 = 1 도 포함해야 하기 때문
O(n) = O(Log5(N+1)) + O(1) = O(Log5(N+1))
static int cnt=0; int recursiveFun3(int n) { cnt++; if (n <= 0) return 1; // +1 else return 1 + recursiveFun3(n/5); //Log5N } int loopFun3(int n) { int i; for(i=n;i > 0; i/=5 ) //Log5N { cnt++; } return 1; }
- 재귀 함수-D (재귀의 확장성 과 등비수열 )
재귀의 확장성을 알기위해서 등비수열과의 연관성도 알아두고, 어떻게 확장이 되어가는지 정확한 이해를 하자.
예를 들면, 재귀함수의 전체 반복횟수를 알아보기위해, 이 구조를 완전 2진 트리라고 생각하자.
시작조건 N=5 입력
종료조건 (N==0 이면 종료)
전체 반복횟수 (N+1) = 6 번
- 002 번 재귀호출 (처음호출)
- 004 번 재귀호출
- 008 번 재귀호출
- 016 번 재귀호출
- 032 번 재귀호출
- 001 번 재귀호출 ( 64개 중에 종료조건이 맞아 종료)
상위 형태는 등비수열의 형태이며, 이를 3배씩 변경하면 그 효과 역시 동일하다.
물론 위의 조건이 약간씩 상이하게 변경이 될 수 있지만, 그 근본은 동일하다고 생각한다.
- 등비수열의 구조 와 합의 공식
- 등비수열구조 : Sn = a + ar + ar^2 + ar^3 .. + ar^(n-1)
- 합의공식 : a(1-r^n) / (1-r)
- 1st 함수 분석
재귀함수-A 와 아래 첫번째 재귀함수는 동일한 구조이며, 이를 다시 한번보자.
Input n=5, 반복횟수 : n+1
Input 5, 4, 3, 2, 1, 0: -1
O(1) not working
O(n) = O(1) + O(n) or O(n+1)
- 2nd 함수 분석
두번째 재귀 함수호출수는 반복횟수 동일하지만 함수가 2배식 확장되어, 함수 종료 조건은 한번만 실행되어진다.
n=5 일 경우 , 반복횟수 : n+1
2^1 + 2^2 + 2^3 + 2^4 + 2^5 + 1 = 63
O(1)
n은 감소하면서 두번식 확장하면서 커진다.
합의 공식 대입: (1-2^6)/(1-2) = -63/-1 = 63 (r=2 , a=1 , n+1 대입)
n=10 일 경우 , 반복횟수 : n+1
2^1 + 2^2 + 2^3 + 2^4 + 2^5 ... 2^10 + 1 = 2047
합의 공식 대입: (1-2^11)/(1-2) = -2047/-1 = 2047 (r=2 , a=1 , n+1 대입)
이공식을 다음과 같이 변경 가능하다.
O(n) = O(2^(n+1)) - 1
N 대신 2^(N+1)를 넣고 첫번째 재귀함수와 동일하게 반복횟수는 n+1 이다.
n을 5을 넣으면, 함수 호출수는 2^(n+1) -1 = 64 - 1번 = 63
n을 10을 넣으면 함수 호출수는 2^(n+1) -1 = 2048 -1 번 = 2047
- 3rd 함수 분석
세번째 재귀 함수호출수는 반복횟수 동일하지만 함수가 3배식 확장되어, 함수 종료 조건은 한번만 실행되어진다. .
n=5 일 경우 , 반복횟수 : n+1
3^1 + 3^2 + 3^3 + 3^4 + 3^5 + 1 = 364
O(1)
n은 감소하면서 3배식 확장하면서 커진다.
합의 공식 대입: (1-3^6)/(1-3) = -728/-2 = 364 (r=2 , a=1 , n+1 대입)
상위 공식을 아래와 같이 변경 가능하다.
O(n) = O(3^(n+1) - 1 ) / 2
n=5 ( 3^(5+1) - 1 ) / 2 = 364
n=10 ( 3^(10+1) - 1 ) / 2 = 88573
- 4th 함수 분석
네번째 재귀 함수호출수는 반복횟수 동일하지만 함수가 4배식 확장되어, 함수 종료 조건은 한번만 실행되어진다.
상위와 같은 등비수열의 합이며, 동일하게 아래와 같이 일반화하여 쉽게 구할수 있다.
O(n) = O(4^(n+1) -1 ) / 3
n=5 (4^(5+1 ) -1) / 3 = 1365
n=10 (4^(10+1) -1) / 3 = 1398101
static int cnt=0; // 전역변수로 한번 점검해보자 void recursiveFun4_1(int n, int m, int o) { cnt++; if (n <= 0){ return 1; }else{ recursiveFun4_1(n-1, m+1, o); // 동일한 함수를 1번 호출 1배식 확장 } } void recursiveFun4_2(int n, int m, int o) { cnt++; if (n <= 0){ return 1; }else{ recursiveFun4_2(n-1, m+1, o); // 동일한 함수를 2번 호출하여 2배식 확장 recursiveFun4_2(n-1, m, o+1); } } void recursiveFun4_3(int n, int m, int o) { cnt++; if (n <= 0){ return 1; }else{ recursiveFun4_3(n-1, m+1, o); // 동일한 함수를 2번 호출하여 3배식 확장 recursiveFun4_3(n-1, m, o+1); recursiveFun4_3(n-1, m, o+1); } } void recursiveFun4_4(int n, int m, int o) { cnt++; if (n <= 0){ return 1; }else{ recursiveFun4_3(n-1, m+1, o); // 동일한 함수를 2번 호출하여 4배식 확장 recursiveFun4_3(n-1, m, o+1); recursiveFun4_3(n-1, m, o+1); recursiveFun4_3(n-1, m, o+1); } }
- 재귀 함수-E
다시 재귀함수-B를 보자
Input n=15, 반복횟수 : n/5+1 = 4
Input 15 10 5, 0
work_1 8 (7.5) 5 3(2.5) 1 = 17
아래의 소스를 동작해보면 cnt 16 으로 마지막 0 에서 cnt는 동작이 안됨 (n=0)
O(n) = O(n)/5 + O(1) = O(n/5)+1 안에 다른 작업이 반복이 된다.
static int cnt=0; // 전역변수로 한번 점검해보자 int recursiveFun5(int n) { int i; for (i = 0; i < n; i += 2) { //work_1 cnt++; } if (n <= 0) return 1; else return 1 + recursiveFun5(n-5); }
http://stackoverflow.com/questions/13467674/determining-complexity-for-recursive-functions-big-o-notation
2.5 시간복잡도 4번째 예제
Big O 표기법이라는 것이 기본규칙을 사용하여 일반화하여 구성을 하면 정확한 측정은 힘들어진다.
하지만 기본규칙은 항상 모든 알고리즘에서 빠른 계산과 쉬운 계산을 목표로 생각으로 구성을 해야겠다.
- Example-1
x = n while ( x > 0 ) { x = x - 1 }
n=5
input: 5 ,4 ,3 ,2 ,1
O(n)
- Example-2
x = n while ( x > 0 ) { x = x / 2 }
n=8
input: 8 , 4 , 2 , 1
O(logN)
- Example-3
x = n while ( x > 0 ) { y = n while ( y > 0 ) { y = y - 1 } x = x - 1 }
n=5
input : (5,4,3,2,1) , (5,4,3,2,1) , (5,4,3,2,1), (5,4,3,2,1) , (5,4,3,2,1)
O(N^2);
- Example-4
x = n while ( x > 0 ) { y = n while ( y > 0 ) { y = y / 2 } x = x - 1 }
n=4
input : (4,2,1) , (4,2,1) , (4,2,1), (4,2,1) : O(LogN) 을 O(n) 반복
O(NLogN);
- Example-5
x = n while ( x > 0 ) { y = x while ( y > 0 ) { y = y / 2 } x = x - 1 }
n=4
input : (4,2,1) , (3,1) , (2,1), (1) : O(LogN) 을 O(n)반복
O(NLogN);
- Example-6
x = n while ( x > 0 ) { y = n while ( y > 0 ) { y = y - 1 } x = x / 2 }
n=4
input : (4,3,2,1) , (4,3,2,1) , (4,3,2,1) : O(n)을 LogN 만큼 반복.
O(NLogN);
- Example-7
x = n while ( x > 0 ) { y = x while ( y > 0 ) { y = y - 1 } x = x / 2 }
n=4
input : (4,3,2,1) , (2,1) , (1) : O(n)을 O(LogN)만큼 반복 ( O(n)의 값이 적용 )
O(NLogN);
- Example-8
x = n while ( x > 0 ) { y = n while ( y > 0 ) { y = y / 2 } x = x / 2 }n=4
input : (4,2,1) , (4,2,1) , (4,2,1) : O(LogN)을 O(LogN)만큼 반복 (두번째 LogN은 동일)
O(LogLogN);
- Example-9
x = n while ( x > 0 ) { y = x while ( y > 0 ) { y = y / 2 } x = x / 2 }n=4
input : (4,2,1) , (2,1) , (1) : : O(LogN)을 O(LogN)만큼 반복 (두번째는 LogN는 감소)
O(LogLogN);
https://www.quora.com/How-can-we-check-for-the-complexity-log-n-and-n-log-n-for-an-algorithm
3. O 표기법의 주요 함수 내용 정리
기본적으로, O표기법으로 알아내는 법은 Input을 넣어 대입하고 어떻게 동작이 되는지를 파악을 해보면 된다고 생각한다.
그리고 그 변화에 규칙성이 알아내어 규칙성을 찾아 일반적인 함수를 적용 할수 있다면 적용하는 것이다.
여기서 이산수학의 필요성이 필요한 것 같다.
(사실 고등학교 수학지식이면 충분한 것 같다)
3.1 일반적인 O 표기법
- O(1) : 항상 일정한 시간을 갖는것을 의미한다.
아래와 같이 반복 routine이 있다 할지라도, 100 * 200에서 항상 동일하게 끝난다.
아래는 항상 일정한 시간을 갖는 routine이다.
아래 작업을 두개의 일정한 값을 유지하는 작업이 2개 ( if 와 for 반복문)
O(c)+O(1) = O(1) C는 100 * 200
int index = 5; int item = list[index]; if (condition true) then perform some operation that runs in constant time else perform some other operation that runs in constant time for i = 1 to 100 for j = 1 to 200 perform some operation that runs in constant time
3.2 일반적인 반복 O 표기법
- O(N) : n은 보통 n번의 반복이며, for or while loop로 생각하면되겠다.
int index = 5; int item = list[index]; for i = 1 to n perform some operation that runs in n time
- O(N^2): n번을 반복을 하고 다시 n번을 반복 이중반복이라고 생각하면되겠다.
int index = 5; int item = list[index]; for i = 1 to n for j = 1 to n perform some operation that runs in n * n time
- O(N^3): 쉽게 3중반복이라고 생각하면되겠다.
3.3 2의 지수 O 표기법
- O(2^N): 2의 지수이며, input data set이 두배로 확장되는 것을 의미한다.
int Fibonacci(int number) { if (number <= 1) return number; return Fibonacci(number - 2) + Fibonacci(number - 1); }
아래의 피보나치 수열을 계산과정을 보면 2의 지수 형태로 진행이 되는 것을 알 수 있다.
예를 들면, number를 5를 넣으면, 두번을 매번 재귀를 매번 호출하고,
재귀된 함수도 1 값이 될때 까지 2번씩 반복이 된다.
이는 2의 지수 함수 동일하다.
아래의 예제를 보면 쉽게 이해가 간다.
https://ko.wikipedia.org/wiki/%EB%8F%99%EC%A0%81_%EA%B3%84%ED%9A%8D%EB%B2%95
3.4 Binary Log 함수 O 표기법
O표기법에서 사용하는 일반적인 Log의 아래값은 2이며, Log 함수의 기본적인 성질을 알아두자.
- 지수와 Log의 관계
b^0 = 1 이를 Logb(1) = 0 변환하면 Log Rule 1번
쉽게 생각하면 b의 지수의 값을 구하려는 함수라고 생각하면 된다.
현재 O 표기법에서는 b의 값은 2이다,
고등학교때, 그냥 Log하면 Log b는 10인 걸로 기억을 하는데 처음 혼동이 되어서 간단히 적어본다.
- Log 기본규칙 (Rules)
- logb1 = 0 , Logb(b) = b;
- logb(mn) = logb(m) + logb(n)
- logb(m/n) = logb(m) – logb(n)
- logb(m^n) = n · logb(m)
4번 규칙처럼 Log 함수에서 M의 지수를 사용할 경우, N * logb(M) 변환이 가능하다.
아래내용은 로그함수의 기본정의 규칙을 정리 다시 복습 해보자.
기본로그함수내용
https://en.wikipedia.org/wiki/Logarithm
https://en.wikipedia.org/wiki/Binary_logarithm
로그관련 기본공식
http://mathbang.net/597
http://www.purplemath.com/modules/logrules.htm
- O(logN) : log는 2를 말하며, 보통 binary search or Binomial heap 말한다.
Binary Search 와 Binary Traversal은 다르며, Binary Search인 경우는
보통 현재 노드를 비교하여, 저장될 노드를 비교하여 찾아가는 구조이기에,
둘중 하나만 선택하여 찾아 호출하는 구조
http://stackoverflow.com/questions/2307283/what-does-olog-n-mean-exactly |
n/b^3 의 의미 부터 파악해보면, layer가 내려갈 수록 1/b씩 곱하는것이다.
이 전체를 순회를 한다고 가정하면, 마지막 layer에 도착하기 위해서는 n/b^3의 확률이 걸린다. N=8이다.
- O(NlogN) : log는 2를 말하며, LogN에 N번의 반복을 하는 것 같다.
더 할 수 있겠다. 그리고, Log에서 덧셈을 곳셈으로 변환하고, 그 곳셈에서 나온값을
N의 지수가 나오기때문에 아래와 같이 간단히 정리를 할 수 있겠다.
= (log n+log(n-1)+...+log 2)
= (log n+log(n-1)+...+log 2)+(log n+log(n-1)+...+log 2)
= O(n*log n)
https://ko.wikipedia.org/wiki/%ED%9E%99_%EC%A0%95%EB%A0%AC
https://en.wikipedia.org/wiki/Heapsort
https://www.quora.com/How-can-we-check-for-the-complexity-log-n-and-n-log-n-for-an-algorithm