알고리즘 분석

**알고리즘 분석**(analysis of algorithms)은 컴퓨터 과학에서 알고리즘을 실행하는데 필요한 (시간과 기억 용량과 같은) 자원의 수를 결정하는 일을 가리킨다. 대부분의 알고리즘은 임의의 길이의 입력과 함께 동작하도록 구성된다. 일반적으로 알고리즘의 효율성과 실행 시간은 단계의 수 (시간 복잡도)와 기억 위치 (공간 복잡도)에 대한 입력 길이와 관련한 함수로 나타낼 수 있다. 알고리즘 분석이 더 광범위한 계산 복잡도 이론의 중요한 부분인데, 주어진 계산 문제를 해결하는 알고리즘에 필요한 자원에 대한 이론적 견적을 제공한다. 이러한 견적들은 효율적인 알고리즘을 검색하는데 도움을 준다. 알고리즘 분석에서는 보통 점근적(asymptotic) 의미로 복잡도를 계산한다. 즉, O표기법, Ω표기법, Θ표기법과 같은 방법으로 일정 크기의 입력 자료에 대한 복잡도 함수를 구한다. 예를 들어, 이진 탐색의 단계 수는 입력 자료의 크기에 대수적으로 비례하는데, 이것을 O(\log n) 또는 대수 시간이라 한다. 점근적인 방법을 사용하는 이유는, 같은 알고리즘이라도 입력 자료의 크기에 따라 수행 시간에 차이가 나는 것을 보완하기 위해서이다. 이런 수행 시간의 차이는 상수 배로 나타나는데, 이때의 상수를 숨은 상수(hidden constant)라고 한다. 시간 효율성의 추산은 수행 단계로서 정의하는 것에 의존한다. 의미있는 분석을 하려면, 각 단계에서 걸리는 수행 시간에 상한이 있어야 한다. 예를 들어, 두 수의 덧셈을 하나의 단계라 할 때, 각 수가 지극히 크면, 덧셈이 일정한 시간 내에 완료된다고 할 수 없기 때문이다. (2자리수의 덧셈을 하는 경우와 1000자리수의 덧셈을 하는 경우를 비교하여 생각해보라.)
# 비용 모델
시간 효율 추정은 한 단계를 무엇으로 정의 하는지에 달려 있다. 실제 실행 시간과 실질적으로 부합하는지 분석하기 위하여, 한 단계 수행시 요구되는 시간은 어떤 상수 위로 유계(bounded above)를 보장해야 한다. 이때 다음을 조심해야 한다; 예를 들어 분석시 두 숫자의 덧셈 연산을 한 단계로 카운트한다고 가정하자. 이 가정은 특정 상황에서 보장 되지 않는다. 예를 들어, 매우 큰 숫자를 계산하는 경우, 하나의 덧셈 연산에 필요한 시간을 더 이상 상수(constant)로 가정할 수 없다. 두 개의 비용 모델이 일반적으로 쓰인다:publisher=Addison-Wesley Pub. Co., section 1.3pages=177–178pages=3–8pages=3–7 * 동일 비용 모델(uniform cost model) / 동일 비용 측정(uniform-cost measurement): 수반되는 수의 크기에 관계 없이 일정한 비용을 모든 기계 작동에 할당한다. * 대수 비용 모델(logarithmic cost model) / 대수 비용 측정(logarithmic-cost measurement): 수반되는 비트의 수와 비례하여 비용을 모든 기계 작동에 할당한다. 대수 비용 모델은 사용하기 더 번거롭기 때문에 암호학 분야 등의 임의 밀도 연산 알고리즘 분석과 같이 필요할 때에만 이용한다.
# 실행 시간 분석
실행 시간 분석은 입력 크기가 증가함에 따라 알고리즘의 실행 시간이 늘어나는 것을 측정하고 예측하는 이론적 분류이다. 실행 시간의 효율성은 컴퓨터 과학의 주된 관심사이다. 프로그램 하나는 실행을 마치는데 수 초, 수 시간, 수 년이 걸릴 수 있으며 이는 알고리즘의 수행 방식에 따라 결정된다. (성능 분석도 참고)
## 실험적 측정법의 단점
알고리즘들은 플랫폼-독립적(platform-independent)이기 때문에(예, 특정 알고리즘은 임의의 컴퓨터 및 운영체제에서 임의의 프로그램 언어로 구현될 수 있다), 여러 알고리즘들의 성능을 비교하기 위하여 실험적 접근법을 사용하는 것은 심각한 문제점을 가지고 있다. 크기가 n인 정렬된 리스트에서 특정 엔트리(entry)를 검색하는 프로그램을 예로 들어보자. 이 프로그램이 선형 검색 알고리즘으로 최신 컴퓨터 A에 구현되었고, 이진 검색 알고리즘으로 훨씬 느린 컴퓨터 B에 구현되어있다고 가정해보자. 두 컴퓨터에서 각자의 프로그램으로 벤치마크(Benchmark) 테스트를 수행하고 다음과 같은 결과를 얻었다: {| class="wikitable" |- ! *n* (리스트 크기) ! 컴퓨터 A 실행시간
(ns) ! 컴퓨터 B 실행시간
(ns) |- | 16 | 8 | 100,000 |- | 63 | 32 | 150,000 |- | 250 | 125 | 200,000 |- | 1,000 | 500 | 250,000 |} 이 측정법 기반으로, “컴퓨터 A는 컴퓨터에서 알고리즘을 작동하는 것은 B에서 자동하는 것 보다 훨씬 효율적이다.”라는 결론을 낼 수 있다. 그러나 만약 리스트의 입력 크기가 충분히 커진다면, 위의 결론이 잘못됐다는 것이 입증될 것이다: {| class="wikitable" |- ! *n* (리스트 크기) ! 컴퓨터 A 실행시간
(ns) ! 컴퓨터 B 실행시간
(ns) |- | 16 | 8 | 100,000 |- | 63 | 32 | 150,000 |- | 250 | 125 | 200,000 |- | 1,000 | 500 | 250,000 |- | ... | ... | ... |- | 1,000,000 | 500,000 | 500,000 |- | 4,000,000 | 2,000,000 | 550,000 |- | 16,000,000 | 8,000,000 | 600,000 |- | ... | ... | ... |- | 63,072 × 1012 | 31,536 × 1012 ns,
or 1 year | 1,375,000 ns,
or 1.375 milliseconds |} 선형 검색 프로그램이 작동하는 컴퓨터 A는 선형 증가 비율을 보이고 있다. 그 프로그램의 실행 시간은 직접적으로 입력 크기에 비례한다. 입력 크기를 두 배로 하면 실행 시간도 두 배가 되고, 네 배로 증가하여도 마찬가지이다. 반면에 이진 검색 프로그램이 작동하는 컴퓨터 B는 로그(logarithmic) 증가 비율을 보인다. 네 배 증가된 입력은 실행 시간을 단지 상수만큼 (이 예제에서는 50,000ns) 증가 시킨다. 심지어 컴퓨터 A는 표면상으로 빠르지만, 실행시간 성능 측면에서 컴퓨터 B는 컴퓨터 A를 추월한다. 이는 컴퓨터 B에서 매우 낮은 증가 비율로 알고리즘이 작동하기 때문이다.
## 증가 차수
만약 입력 크기가 n이고 함수 f(n)와 양의 상수의 곱으로 알고리즘 실행 시간의 상한선(upper bound)을 제공한다면, 수학 함수의 차수로 알고리즘의 성장 비율을 보일 수 있다. 다시 말해서, 주어진 입력 크기 n이 0와 상수 c보다 클 경우, 알고리즘의 실행시간은 c \times f(n)보다 결코 크지 않다. 이 개념은 Big O 표기법으로 주로 표현된다. 예를 들어 삽입 정렬의 실행 시간이 입력 크기에 따라 이차식으로 증가하기 때문에, 삽입 정렬은 *O*(*n*2)라 할 수 있다. Big O 표기법은 알고리즘의 최악의 경우 시나리오를 표현하는데 편리한 방법이며, 평균 경우를 표현 할 때도 활용될 수 있다. 예를 들어 퀵 정렬의 최악의 경우 시나리오는 *O*(*n*2)이지만 평균 경우 실행 시간은 *O*(*n* log *n*)이다.
## 실험적 증가 차수
실행 시간이 *t ≈ k na* 다항식의 미적분(power rule)을 따른다고 가정하면, 실행시간 \\{t_1, t_2\\}과 문제 크기 \\{n_1, n_2\\}의 실험적 측정을 통하여 계수 a를 찾을 수 있다.How To Avoid O-Abuse and Bribes url=https://web.archive.org/web/20170308175036/http://rjlipton.wordpress.com/2009/07/24/how-to-avoid-o-abuse-and-bribes/, at the blog "Gödel’s Lost Letter and P=NP" by R. J. Lipton, professor of Computer Science at Georgia Tech, recounting idea by Robert Sedgewick t_2/t_1 = (n_2/n_1)^a 이기 때문에 계수 a는 a = \log(t_2/t_1) / \log(n_2/n_1) 과 같다. 다시 말해, 실행시간-문제크기의 로그-로그 그래프에서 실험적 라인의 경사도를 측정 할 수 있다. 만약 증가 차수가 실제로 다항식의 미적분을 따른다면, 그 실험적 a 값은 다른 범위에서 상수로 유지 될 것이고, 아니면 바뀔 것이다. 위의 표 내용을 적용하면 다음과 같다: {| class="wikitable" |-_ ! *n* (리스크 크기) ! 컴퓨터 A 실행시간
(ns) ! 증가 지역 차수
(n^_) ! 컴퓨터 B 실행시간
(ns) ! 증가 지역 차수
(n^_) |- | 15 | 7 | | 100,000 | |- | 65 | 32 | 1.04 | 150,000 | 0.28 |- | 250 | 125 | 1.01 | 200,000 | 0.21 |- | 1,000 | 500 | 1.00 | 250,000 | 0.16 |- | ... | ... | | ... | |- | 1,000,000 | 500,000 | 1.00 | 500,000 | 0.10 |- | 4,000,000 | 2,000,000 | 1.00 | 550,000 | 0.07 |- | 16,000,000 | 8,000,000 | 1.00 | 600,000 | 0.06 |- | ... | ... | | ... | |} 첫 번째 알고리즘 선형 차수 증가를 보이지만 실은 미적분(power rule)을 따른다. 두 번째 알고리즘의 실험적 값들은 급격히 감소하며, 또 다른 증가 규칙을 따른다는 것을 암시한다.
## 실행시간 복잡도 평가
알고리즘의 최악의 경우 실행시간 복잡도는 알고리즘의 구조를 검토하고 단순한 가정을 하면서 계산한다. 다음 의사코드를 고려하면: 1 *get a positive integer from input* 2 **if** n > 10 3 **print** "This might take a while..." 4 **for** i = 1 **to** n 5 **for** j = 1 **to** i 6 **print** i * j 7 **print** "Done!" 주어진 컴퓨터는 이 알고리즘에 포함된 각 명령을 수행하는 이산 시간이 걸릴 것이다. 주어진 명령을 수행하기 위한 특정 시간의 양은 명령과 컴퓨터의 종류에 따라 매우 의존적일 것이다. 그러나 보편적인 컴퓨터에서는 이 시간의 양은 결정적(deterministic)일 것이다.하지만 양자 컴퓨터에서는 결정적이지 않다. 1단계에서 수행하는 연산은 *T*1 시간을 소비한다고 가정하고 k단계에는 *T*k 시간이라 가정하자. 위의 알고리즘에서 1, 2, 그리고 7 단계는 한 번씩 작동한다. 최악의 경우 평가에서, 3단계는 역시 잘 작동한다고 가정하자. 그러므로 1-3 단계와 7 단계를 실행하는데 걸리는 전체 시간은 다음과 같다: :T_1 + T_2 + T_3 + T_7. \, 4, 5, 그리고 6단계의 루프는 평가하기가 어렵다. 4단계 바깥 루프는 ( n + 1 )번 수행될 것이고, 그래서 *T*4( n + 1 ) 시간이 걸릴 것이다. 반면 안쪽 루프는 i의 영향을 받아 1부터 i까지 반복할 것이다. 첫 번째 바깥 루프에서, j는 1부터 1까지 반복하며, 안쪽 루프는 한번 반복한다. 그러므로 안쪽 루프 몸체(6 단계)는 *T*6 시간을 소비하며 안쪽 루프 테스트(5단계)는 2*T*5 시간을 소비한다. 두 번째 바깥 루프에는, j는 1부터 2까지 반복하며 이때 안쪽 루프는 2번 반복한다. 그러므로 안쪽 루프 몸체(6 단계)는 2*T*5 시간을 소비하고, 안쪽 루프 테스트(5단계)는 3*T*5 시간을 소비한다. 프로그램 수행을 위한 전체 시간은 전적으로 안쪽 루프 몸체에 달려 있으며 이를 수식으로 계산하면 다음과 같다: :T_6 + 2T_6 + 3T_6 + \cdots + (n-1) T_6 + n T_6 이는 다음과 같이 인수분해(factorization) 가능하다:1 + 2 + 3 + \cdots + (n-1) + n = \frac{n(n+1)}{2}은 수학적 귀납법으로 증명될 수 있다. :T_6 \left[ 1 + 2 + 3 + \cdots + (n-1) + n \right] = T_6 \left[ \frac{1}{2} (n^2 + n) \right] 바깥 루프 테스트가 작동하는데 걸리는 전체 시간은 비슷하게 계산 할 수 있다. :\begin{align} & 2T_5 + 3T_5 + 4T_5 + \cdots + (n-1) T_5 + n T_5 + (n + 1) T_5\\\ =\ &T;_5 + 2T_5 + 3T_5 + 4T_5 + \cdots + (n-1)T_5 + nT_5 + (n+1)T_5 - T_5 \end{align} 이는 다음과 같이 인수분해 가능하며 :\begin{align} & T_5 \left[ 1+2+3+\cdots + (n-1) + n + (n + 1) \right] - T_5 \\\ =& \left[ \frac{1}{2} (n^2 + n) \right] T_5 + (n + 1)T_5 - T_5 \\\ =& T_5 \left[ \frac{1}{2} (n^2 + n) \right] + n T_5 \\\ =& \left[ \frac{1}{2} (n^2 + 3n) \right] T_5 \end{align} 그러므로, 이 알고리즘을 위한 전체 실행 시간은 다음과 같다: :f(n) = T_1 + T_2 + T_3 + T_7 + (n + 1)T_4 + \left[ \frac{1}{2} (n^2 + n) \right] T_6 + \left[ \frac{1}{2} (n^2+3n) \right] T_5 이는 다음과 같이 축소되며 :f(n) = \left[ \frac{1}{2} (n^2 + n) \right] T_6 + \left[ \frac{1}{2} (n^2 + 3n) \right] T_5 + (n + 1)T_4 + T_1 + T_2 + T_3 + T_7 주어진 함수에서 가장 높은 차수항이 증가률을 결정하기 때문에 이를 실행시간 차수로 정의한다. 이 예제에서 n²는 가장 높은 차수이고, 그래서 f(n) = O(n²)와 같은 결론을 얻을 수 있다. 이는 다음과 같이 증명 가능하다:

> 다음 증명: \left[ \frac{1}{2} (n^2 + n) \right] T_6 + \left[ \frac{1}{2} (n^2 + 3n) \right] T_5 + (n + 1)T_4 + T_1 + T_2 + T_3 + T_7 \le cn^2,\ n \ge n_0
>
> \begin{align} &\left[ \frac{1}{2} (n^2 + n) \right] T_6 + \left[ \frac{1}{2} (n^2 + 3n) \right] T_5 + (n + 1)T_4 + T_1 + T_2 + T_3 + T_7\\\ \le &( n^2 + n )T_6 + ( n^2 + 3n )T_5 + (n + 1)T_4 + T_1 + T_2 + T_3 + T_7 \ (\text{for } n \ge 0 ) \end{align}
>
> *k*는 [*T*1..*T*7] 보다 크거나 같은 상수라 가정하면
>
> \begin{align} &T;_6( n^2 + n ) + T_5( n^2 + 3n ) + (n + 1)T_4 + T_1 + T_2 + T_3 + T_7 \le k( n^2 + n ) + k( n^2 + 3n ) + kn + 5k\\\ = &2kn^2 + 5kn + 5k \le 2kn^2 + 5kn^2 + 5kn^2 \ (\text{for } n \ge 1) = 12kn^2 \end{align}
>
> 그러므로 \left[ \frac{1}{2} (n^2 + n) \right] T_6 + \left[ \frac{1}{2} (n^2 + 3n) \right] T_5 + (n + 1)T_4 + T_1 + T_2 + T_3 + T_7 \le cn^2, n \ge n_0 \text{ for } c = 12k, n_0 = 1

이 알고리즘을 분석하는 더 정교한 방법은 [*T*1..*T*7]가 모두 하나의 단위시간으로 선언하는 것이다. 이는 알고리즘의 실행 시간이 다음과 같이 간소화 되는 것을 확인할 수 있다:이 접근 방식은 위의 접근 방식과 달리 각 루프를 종료하는 루프 테스트에 소비되는 일정한 시간을 무시하지만 이러한 누락이 최종 결과에 영향을 미치지 않는다는 것은 자명하다.

> 4+\sum_{i=1}^n i\leq 4+\sum_{i=1}^n n=4+n^2\leq5n^2 \ (\text{for } n \ge 1) =O(n^2).

## 다른 자원들의 증가 비율 분석
위의 실행시간 분석 방법은 메모리 공간(memory space)과 같은 다른 자원의 증가 비율을 예측하는데 활용될 수 있다. 예를 들어 컴퓨터 파일 크기에 기반하여 프로그램의 메모리 사용량의 관리 및 재할당을 수행하는 다음 의사코드를 고려해보자: **while**(*file still open*) **let** n = *size of file* **for** *every 100,000 kilobytes of increase in file size* *double the amount of memory reserved* 여기서 파일 크기 n 이 커질수록 지수적 증가 비율로 메모리를 소모할 것이며, 이는 O(2n) 차수이다. 이는 매우 빠르고 관리 불가능한 메모리 자원 소모 증가 비율이다.
# 중요성
알고리즘 분석은 실제로 중요하다. 비효율적인 알고리즘의 우발적이거나 또는 고의가 아니 사용은 시스템 성능에 중요한 영향을 끼칠 것이다. 시간에 민감한 응용에서, 너무 오래 동작하는 알고리즘은 기한이 지나거나 쓸모없는 결과를 만들 수 있다. 비효율적인 알고리즘 역시 비경제적인 계산 파워나 저장 공간을 요구하게 될 것이며 이 역시 실질적으로 쓸모 없어진다.
# 상수 인자
특히 초급 단계에서 알고리즘 분석은 일반적으로 점근적(asymptotic) 성능에 초점을 맞춘다, 그러나 실제 응용에서 상수 요소들은 중요하며 실제 데이터들은 항상 크기가 제한되 있다. 다룰수 있는 메모리의 크기가 다음과 같이 제한되어 있다; 32-bit 장치는 232 = 4 GiB 그리고 64-bit 장치는 264 = 16 EiB. 그러므로 제한된 크기가 주어 졌다면 시간 또는 공간의 증가 차수는 상수 요소로 대신 할 수 있다. 이로써 모든 실제 알고리즘들은 충분히 큰 상수 또는 충분히 작은 데이터에 대하여 O(1)이다. 이 해석은 매우 극도로 느리게 성장하는 함수에 매우 유용하다. (이진) 반복 알고리즘(iterated logarithm) (log*)는 모든 실제 데이터 (265536 bits)에서 5보다 작다; (이진) log-log (log log *n*) 은 거의 모든 실제 데이터 (264 bits)에서 6보다 작다; 그리고 이진 log (log *n*)은 거의 모든 실제 데이터 (264 bits)에서 64보다 작다. 실제 데이터에서 상수가 아닌 복잡도를 가지는 알고리즘은 상수 복잡도를 가지는 알고리즘 보다 더 효율적이다.