Loading [MathJax]/jax/element/mml/optable/MathOperators.js
본문 바로가기

[알고리듬] 2주차: 시간복잡도

Time Complexity

알고리듬의 수행시간을 '이론적으로' 분석

-> 실제로 수행해보고 시간을 측정하는 게 아니다

 

실제 수행시간은 환경에 따라 달라진다.

- 무슨 코드로 작성했는가?

- 컴파일러 버전은 무엇인가?

- 알고리즘을 수행하는 기계의 스펙은 어떻게 되는가?

- 입력으로 주어지는 데이터의 크기

- 다른 프로세스가 점유한 자원은 얼마나 되는가?

- etc.

 

-> 이런 모든 환경적 요인들은 고정되었다고(모두 같다고) 가정한다.

 

추상화

환경적 요인의 조합은 거의 무한

-> 매번 모든 알고리즘을 구현할 수는 없다.

 

통상적인 어떤 환경에서 알고리즘 A가 알고리즘 B보다 빨랐다면 다른 환경에서도 그렇지 않을까?

-> 환경적 요인을 고려할 필요가 없다: 추상화

Ex) C로 짰든 Java로 짰든 Python으로 짰든 Bubble sort는 다 같은 Bubble sort

 

알고리즘은 추상적 개념이고 어떻게 기술하냐에 따라 C 함수일 수도 있고 Java method일 수도 있는 것

 

 

n에 대한 함수

시간복잡도는 n에 대한 함수, 수행되는 기본 연산의 개수

알고리즘의 수행시간은 수행되는 기본 연산의 개수에 비례할 것

-> n에 대한 증가함수

 

 

시간 복잡도 분석

n에 대한 함수 f(n)

분기문(Ex: if)이 많아지면 정확한 f(n)을 알기가 어렵다

-> 근사화/단순화된 case에 대해 분석

 

ⓐ Best-case Analysis

ⓑ Worst-case Analysis

ⓒ Average-case Analysis

 

Best-case Analysis: 별 의미 없어서 잘 안 함

최선의 경우에 1초 걸린다 -> ?

 

Worst-case Analysis: 최악의 경우에 1분이 걸린다

= 어떤 입력이 주어지든 간에 1분 안에는 끝난다

-> 상한

 

 

Ex) Selection sort

1
2
3
4
5
6
7
8
9
10
11
12
13
void selectionSort(int A[], int n) {
    for (int i = 0; i < n - 1; i++) {
        int indexMin = i;
 
        for (int j = i + 1; i < n; j++) {
            if (A[j] < A[indexMin])
                indexMin = j;
        }
        int temp = A[indexMin];
        A[indexMin] = A[i];
        A[i] = temp;
    }
}
cs

* 초기화 연산은 기본 연산 개수에서 제외

 

2행: 3개

< ++

 

5행: 2개 (초기화 부분의 + 연산은 좀 애매함)

< ++

 

6행: 3개

[](Indexing operation) <

 

7행: 1개 (6행 조건이 참일 때만)

=

=> 6~7행: 3~4개

 

9행 ~ 11행: 6

[]

[] = []

[] =

 

수행되는 연산의 개수는 6

반복문에서 연산의 개수를 셀 때는 안쪽 반복문부터

 

안쪽 for문(5~8행)

반복 할 때마다 5~6회 -> 최악의 경우는 "6회"

i+1 n -> 반복 횟수는 (ni)번. i는 바깥쪽 for문에서 정해짐

 

바깥쪽 for 문

2행: 3

안쪽 for문: 6

9~11행: 6

-> n2i=0{6(ni)+10}=3n2+13n16

가 시간복잡도 (n에 대한 함수)

여기에 환경적인 요인에 따른 비례상수가 붙음

 

 

Big-O Notation

그냥 시간복잡도를 나타내기에 적절하니까 쓰는 것일 뿐 시간복잡도와 별개임

(둘은 아예 다른 개념으로 시간복잡도가 없어도 빅오표기법은 있음)

 

원래는 그냥 두 함수의 관계를 나타내기 위한 표기법

여기서는 함수의 증가속도를 비교하기 위한 표기법

 

f(n)=O(g(n)) (f(n)O(g(n))와 일맥상통)

함수 f의 증가속도가 g보다 빠르지 않다(= 비슷하거나 느리다)

Ex)3n2+13n16=O(n2)

 

f(n)=Ω(g(n)) (f(n)Ω(g(n))와 일맥상통)

함수 f의 증가속도가 g보다 느리지 않다(= 비슷하거나 빠르다)

Ex)3n2+13n16=O(n2)

 

f(n)=Θ(g(n))

함수 f의 증가속도가 g와 비슷하다

 

 

 

증가속도

n일 때 어느 함수가 더 빨리 증가하는가?

= n이 작을 때는 무시

 

사진에서 표시된 부분은 저렇게 나타낼 수 있다.

 

 

 

극한에 의한 정의

f(n)=O(g(n))(f(n) \leq O(g(n)$)

limng(n)f(n)>0일 때

무한대로 발산: g(n)의 증가속도가 훨씬 빠름을 의미

양수로 수렴:

 

f(n)=Ω(g(n)) (f(n)Ω(g(n))

limnf(n)g(n)>0일 때

무한대로 발산: g(n)의 증가속도가 훨씬 빠름을 의미

양수로 수렴:

 

f(n)=Θ(g(n))

f(n)=O(g(n)) 이고 f(n)=Ω(g(n)) 일 때

limnf(n)g(n)이 0이 아닌 양수로 수렴할 때

 

Ex) f(n)=3n2+3n16

f(n)=O(n): 틀림

f(n)=O(n2), f(n)=O(n3), f(n)=O(n100): 맞음

f(n) = O(n^3)같은 건 별 의미는 없지만 '틀린' 표현은 아님

 

f(n) = \Omega(n), f(n) = \Omega(n^2): 맞음

f(n) = \Omega(n^3), f(n) = \Omega(n^{100}): 틀림

\because \; f(n) \geq O(g(n)

 

 

f(n) = \Theta(n^2): 맞음

f(n) = \Theta(n), f(n) = \Theta(n^3), f(n) = \Theta(n^{100}): 틀림

 

f(n) = O(12n^2+3n): 맞음

f(n) = \Omega(972n - 10): 맞음

f(n) = \Theta(0.2n^2)

헷갈릴 때는 극한으로 계산해보면 됨

 

 

증가속도에 따른 함수의 서열

 

1 \lt \lt \log n

\lt \lt \lt n \lt n\log n \lt n^2 \lt n^3

\lt \lt \lt 2^n

\lt \lt n! \lt n^n

 

Poly-logarithm

물론 1\log{n} 사이에도 무수히 많은 함수가 있다.

근데 \log({\log{n}}), \log{(\log({\log{n}}))},\;\cdots 같은 식이라 딱히 의미가..?

이런 걸 Poly-logarithm(\log 함수의 다항식)이라 함

 

Polynomial

①과 ② 사이에는 n^{0.5} = \sqrt{n}이나 n^{0.1}같은 게 있음

의외로 n^{0.0001}(\log{n})^{1000000}보다 빠르게 증가함

 

Exponential

의외로 2^n보다 1.1^n이 빨리 증가함

교수님피셜) 이래서 복리를 해야 된다

 

④ 얘네보다 더 빠른 것도 많지만(Ex: 2^{2^{2^{2^n}}}) 제일 빠른 걸 구하는 건 별 의미가 없다

 

 

 

왜 Big-O notation인가?

ⓐ 일단 최악의 경우 = 수행 연산 수의 상한

ⓑ 연산의 갯수를 정확히 세는 건 어려움

ⓒ 추상적인 대상으로써 알고리즘에서는 상수는 덜 중요하다.

기술 언어에 따라 세부 연산이 다르긴 하지만 결국 비슷함

- 세부 연산의 수행속도 차이가 있긴 하지만 환경 요인에 대한 상수로 이해한다.

 

대부분은 연산의 정확한 개수로 세는 것보다는 'n이 클 때 증가하는 경향이 어떤가?'가 더 중요하다.

-> Big-O notation이 적절한 표기법

 

1
2
3
4
5
6
7
8
9
10
11
12
13
void selectionSort(int A[], int n) {
    for (int i = 0; i < n - 1; i++) {
        int indexMin = i;
 
        for (int j = i + 1; i < n; j++) {
            if (A[j] < A[indexMin])
                indexMin = j;
        }
        int temp = A[indexMin];
        A[indexMin] = A[i];
        A[i] = temp;
    }
}
cs

 

6~8행의 연산 횟수는 고정임 -> c_1회라고 치자

-> 바로 바깥쪽 for 문이 (n-i)번이니까 c_1(n-i)

 

9~11행의 연산 횟수도 고정 -> 바깥쪽 for문 한 번 당 c-2회라고 치자

 

=> 총 연산 횟수 = \sum^{n-2}_{i=0} c_1(n-i) + c_2 \; = \; O(n^2)

 

Big-O notation에서는 이런식으로 대략적으로 세면 됨

더 loose하게 하면 안쪽 for문은 최대 n번이고 바깥쪽 for 문이 n번이니까

대충 n \times n 해서 n^2번 -> O(n^2)