나는 알고리즘엔 엄청 약하다. 솔직히 아직도 잘 모르겠다.

그래서 이번에 알고리즘 관련 복습을 할 겸, 내 알고리즘이 어느 정도의 효율을 가지고 있는지,

어떻게 측정하는지를 써보려고 한다.

 

(주의! 이건 필자가 공부한 걸 바탕으로 적어 내려 가는 것이기 때문에 틀릴 수 있음을 미리 경고합니다.)

(주의! 이 글은 C# 언어 기준입니다.)

 

시간 복잡도 표기법?

보통 알고리즘을 얘기하면 아래의 표기법으로 많이 얘기한다.

각각의 표기법은

빅오: 상한선을 표현

빅오메가: 하한선을 표현

빅세타: 상한선과 하한선 사이를 표현

이렇게 된다.

 

이걸 현실에 사용한다면 어떻게 표현할 수 있을까?

예를 들어, 누군가와 게임 내에 있는 어떤 기능의 알고리즘의 효율에 대해 이야기한다고 가정해보자.

A: "이 알고리즘은 효율이 어느 정도인가?"

(빅오 기준일 경우)B: "최악의 경우 ($n^2$) 정도의 시간 복잡도를 가집니다."

(빅오메가 기준일 경우)B: "값이 잘 주어질 때 (1) 정도의 시간 복잡도를 가집니다."

(빅세타 기준일 경우)B: "평균적으로 ($\log n$) 시간 복잡도를 가집니다."

 

개인적인 생각으론 빅오메가는 쓸 일이 없어 보인다.

물론 쓰이긴 할 것이다! 자신의 알고리즘에서 최악의 경우가 너무 나빠서 상사한테 마치 괜찮은 알고리즘인 것 마냥 숨길 때 쓰일 것이다.

 

내 알고리즘의 시간 복잡도 구하기

필자는 수학을 매우 못한다...

그래서 가장 계산하기 쉬운 기본적인 예시만 들어서 설명하겠다.

 

일단, 시간 복잡도를 계산할 때 중요한 요소와 규칙이 있다.

중요한 요소

  • 조건문 (if)
  • 반복문 (for, while, foreach)
  • 재귀 호출

규칙

  • 시간 복잡도에서 상수값은 무시된다.
  • 실제 개발자가 짜 놓은 코드를 수행하는 것은 상수 시간으로 간주한다.

예시

1.

void Function(int n)
{
  for (int i = 0; i < n; ++i)
  {
    // 필요한 코드 수행
  }
}

이 코드의 시간 복잡도는 O(n)이다.

간단하게, 코드를 n번 반복했기 때문이다.

 

2.

void Function(int n)
{
  for (int i = 0; i < n; ++i)
  {
    for (int j = 0; j < n; ++j)
    {
      // 필요한 코드 수행
    }
  }
}

이 코드는 n * n번 수행하기 때문에, 시간 복잡도는 O($n^2$) 이다.

 

3.

void Function(int n)
{
  // 짝수값마다 코드를 수행
  for (int i = 0; i < n; i += 2)
  {
    // 코드 수행
  }
}

이 코드는 i를 2씩 더해서 $\frac n2$번 코드를 반복하기 때문에, O($\frac n2)$일 것 같지만...!

$\frac n2$은 $\frac {1}{2} n$이기 때문에

시간 복잡도에서는 상수를 무시하므로 그냥 O(n)이다.

 

4. 

void Function(int n)
{
  for (int i = 0; i < n; ++i)
  {
    // 홀수 일 때만 코드를 수행하도록 함
    if (n % 0)
    {
    	continue;
    }
    
    // 코드 수행
  }
}

이 코드는 예시 3번과 유사한데, i += 2를 하는 대신 if을 넣어서 짝수 케이스를 무시하도록 하고 있다.

이것은 어떻게 계산할까?

 

if문은 if조건문 코드와 내부 코드 else if의 조건문, else 내의 코드 등등을 다 더하는 계산을 한다.

그리고 코드 한 줄 한 줄은 앞서 얘기한 대로 상수 실행 값으로 처리한다.

 

if조건문의 코드 실행 시간을 $c_0$, continue 코드의 실행 시간을 $c_1$,  그 아래 나머지 코드 실행 시간을 $c_2$이라고 하면,

if문의 실행시간을 이것을 다 더한 $(c_0 + c_1 + c_2)$이 되고, 이것을 for 반복문으로 n번 반복하니,

이 코드의 복잡도는 3f(n) = $(c_0 + c_1 + c_2) * n$ 이 된다.

그리고 시간 복잡도를 따질 때, 상수는 무시되므로 이 예시의 시간 복잡도는 O(n)이 된다.

 

5.

void Function(int n)
{
  if (n == 0)
  {
     return;
  }

  Function(n / 2);
}

여기서부턴 조금 계산이 어려워진다.

재귀 함수가 나올 때 공식의 모습은 함수 공식 안에 함수 공식을 또 호출하는 모양이 나온다.

그래서 위 예시의 함수는, $f(n) = f(\frac {1}{2}n) + c_0$이다. (상수 값 $c_0$는 if문 처리 시간을 뜻한다.)

 

여기서 $f(\frac {1}{2}n)$는 다시 $f(\frac {1}{2}n) = f(\frac {1}{4}n) + c_0$ 으로 풀어쓸 수 있는데,

이걸 위 첫 공식에 대입 하면 $f(n) = f(\frac {1}{4}n) + c_0 + c_0$이 된다.

그리고 또 여기서 한번더 풀어쓰면 $f(n) = f(\frac {1}{8}n) + c_0 + c_0 + c_0$이 된다.

 

그럼 만약에 여기서 또 이 함수를 풀어쓰고, 거기서 또 풀어쓰고를 무한히 반복하면?

$f(n) = f(\frac {n}{2^k}) + kc_0$ 와 같은 모습이 나올 것이다.

 

그런데, 코드 로직의 한계상 $2^k$는 n보다 2배 혹은 2배보다 작은 수가 된다.

예를 들어, n = 3일 경우,

  1. Function(3 / 2) => 1
  2. Function(1 / 2) => 0
  3. 0 == 0 이므로 리턴

이렇게 Function(n / 2)가 총 2번 반복하므로, 우리는 n < $2^k$ 2n임을 알 수 있다.

여기서 머리가 나쁜 나 같은 사람은 범위 계산엔 쥐약이므로 $2^k$ = 2n인 경우만 따져보자.

 

$2^k$를 2n으로 치환하면, $f(\frac {n}{2n}) + kc_0$가 되는데, $\frac {n}{2n}$을 계산하면, $f(2) + kc_0$이다. 

그럼 남은 것은 $f(2)$와 $kc_0$인데, $f(2)$와 $c_0$는 상수이기 때문에 버려진다.

그럼 남은 것은 k뿐인데, k는 어떻게 구하냐? 우리는 여기서 지수의 로그 변환 공식을 기억해야 한다.

 

'$2^a = b$는 $a = log_2 b$ 이다.'

이 공식과 아까 $2^k = 2n$을 섞으면, 우리는 k의 값을 알아낼 수 있다.

$2^k = 2n$은 $k = log_2 2n$

 

그럼 이를 다시 $kc_0$에 넣으면, $f(n) = log_2 2n$이 된다, 여기서 로그의 성질에 의해 진수는 더하기로 분리가 가능하므로,

$f(n) = log_2 2 + log_2 n$ => $f(n) = 1 + log_2 n$이 된다. 헉헉

거의 다 왔다! 여기서 상수인 1을 버리면?

$f(n) = log_2 n$

마지막으로! 여기서 밑인 2를 상용로그로 생략하면?

$f(n) = log n$

 

드디어 구했다!

이로써 우리는 예제의 시간 복잡도가 $log n$임을 구하게 되었다.

 

그런데!

우리는 아까 $2^k$ = 2n일 때만 구하였다. $2^k$ < 2n 일 때는 어떻게 할까?

이 경우에도 결과적으로 똑같다. $f(\frac {n}{2^k})$를 $f(\frac {n}{2n})$로 치환해서 상수 값으로 버릴 때,

어쨋꺼나 $2^k$는 n보다 크고 2n보다 작은 수므로, 결국엔 n으로 나눠져 상수 값으로 버려질 값이기 때문이다.

 

하지만, 실제 실행 횟수는 정확히 $log n$값이 아닌 정수로 딱딱 떨어지는 횟수이므로 빅오로는 표기하기 애매하다.

그래서 이 경우에는 평균 시간 복잡도인 빅세타로 표기해야 한다.

 

결국, 이 예제의 정확한 시간 복잡도는

Θ(log n)

 

마무리하며...

나는 솔직히 알고리즘의 시간 복잡도를 써본 적 없다. 아마 앞으로도 평생 쓸 일이 없을 것이라 생각하지만, 한번쯤 복습하는 것도 나쁘지 않다고 생각한다. 왜인지 회사에서는 이런걸 쓰지도 않음에도 꼭 면접 볼 때 이것 관련 지식을 요구하므로...

 

 

p.s 오늘 하루 종일 예제 5에 대해 어떻게 계산하는지 구글링을 엄청 했는데,

다들 마스터 정리의 기본 예제인 '$T(n) = 2T(\frac {1}{2}n) + O(n)$은 Θ(n log n)이다.' 만 주구장창 대충 설명하고 있고

왜 그렇게 되는지 하나하나 자세하게 쉽게 설명하는 곳은 없었다.

그래서 이해하느라 애먹었다. :(

+ Recent posts