[ALGO] Lect6. 코드블록 단위의 복잡도 분석

5 minutes to read

강의 정보
강의명: [집콕]인공지능을 위한 알고리즘과 자료구조: 이론, 코딩, 그리고 컴퓨팅 사고
교수: 성균관대학교 소프트웨어학과 허재필 교수님
사이트: http://www.kmooc.kr/courses/course-v1:SKKUk+SKKU_46+2021_T1/course/



이번 시간에는 알고리즘에서 블록 단위의 시간/공간 복잡도를 분석하는 방법에 대해 배운다.

코드 블록 단위의 복잡도 분석

Instruction

의 개념을 배우고, Code Sequence가 있을 때 전체 Complexity를 표현하는 방법에 대해 배운다.


Motivation

알고리즘을 분석하는 목적은 코드 블록 단위로 다양한 파라미터에 대한 asymptotic run time 혹은 asymptotic memory requirements를 결정하기 위함이다.


■ Asymptotic Behavior of Algorithms

알고리즘의 Asymptotic Behavior이란, scale(n)에 따라 알고리즘이 어떻게 동작하는지에 관한 것이다.


● 예시: 알고리즘 A, B에 대한 complexitiy가 각각 $f_A(n)=Θ(n^2)$, $f_B(n)=Θ(n\log_2n))$인 경우

  • $n=2k$일 때

    \[f_A(n) = (2k)^2 = 4k^2\\ f_B(n) = (2k)\log_2(2k) = 2k(\log_2(k)+\log_2(2)) = 2k\log(k)+2k\]
  • $n=10k$일 때

    \[f_A(n) = (10k)^2 = 100k^2\\ f_B(n) = (10k)\log_2(10k) = 10k(\log_2(k)+\log_2(10)) = 10k\log(k)+33.2k\]

위 예시를 통해 n이 5배가 될 때, $f_A$는 25배로 증가하고 $f_B$는 약 5배로 증가함을 알 수 있다.


Machine Instructions

프로그램이 어떤 Instruction으로 구성되었는지를 알면 프로세서에 따라 어느정도의 시간이 소요되는지를 계산할 수 있다.

프로세서(CPU,GPU)는 한정된 숫자의 연산(e.g., 덧셈, 뺄셈, 곱셈, 나눗셈)만을 수행할 수 있다. 이러한 연산을 Instruction이라고 하며, Instruction set은 프로세서에 따라 다르다. 따라서 프로그램을 컴파일 할 때는 꼭 타겟(e.g., ARM, x86)을 지정해주어야 machine이 이해할 수 있는 언어로 번역되어 동작가능해진다.


Constant 수행시간을 갖는 연산

아래의 연산들은 Machine Instruction에 매핑되어있기 때문에 fixed number of cycle에 수행될 수 있다. 즉, Constant 수행시간$Θ(1)$)을 갖는다.

  • Retieving/Storing variables from memory : 메모리로부터 변수 검색 및 저장

  • Variable assignment (=) : 메모리에 특정값 할당

  • Integer Operations (+, -, *, /, %, ++, –) : 덧셈/뺄셈/곱셈/나눗셈 등의 산술 연산

  • Logical Operations (&&, ││, !): AND/OR와 같은 논리 연산

  • Bitwise Operations (&, │, ^, ~)

  • Relational Operations (==, !=, <, <=, =>, >) : 값의 비교

  • Memory Allocation and Deallocation (new, delete) : 메모리 할당


■ 예시1. Swap Algorithm

int tmp = a;
a = b;
b = tmp;

위 알고리즘은 3줄의 코드로 구성되어있지만 각각이 Θ(1)의 수행시간을 가지므로, 알고리즘이 Θ(1) 시간을 갖는다고 말할 수 있다.


■ 예시2. AVL 트리의 노드 재정렬

Tree_node *lrl = left->right->left;
Tree_node *lrl = left->right->right;
parent = left->right;
parent->left = left;
parent->right = this;
left->right = lrl;
left = lrr;

위 알고리즘 역시 모든 코드가 Θ(1)의 수행시간을 가지므로 Θ(1)의 시간을 갖는 알고리즘이라고 말할 수 있다.


시간복잡도의 Dominant Term

가령 어떤 알고리즘이 Θ(1)의 시간복잡도를 갖는 코드와 Θ(n)의 시간복잡도를 갖는 코드로 구성되어있을 때, 이 알고리즘은 Θ(1+n)=Θ(n)의 복잡도를 갖는다고 말한다. 즉, 다항식으로 구성된 시간복잡도는 dominant term으로 구성된 시간복잡도라고 말할 수 있다.


■ 예시: Θ(n) 시간 복잡도를 갖는 알고리즘

int *Increase_Capacity(int *_array, int _n, int _delta)
{
	int *array_old = _array;
	_array = new int [_n + _delta];
    
    for(int i=0; i<_n; i++)
		_array[i] = array_old[i];
    
   	delete[] array_old;
    return _array;
}

위 알고리즘은 위의 두 줄과 아래의 두 줄은 모두 Θ(1)의 시간복잡도를 갖지만, 3~4번째 줄(for문)이 machine instruction을 _n회 반복하므로 Θ(n)의 시간복잡도를 갖는다. 따라서 $Θ(1+n+1)=Θ(n)$의 시간복잡도를 갖는 알고리즘이라고 말할 수 있다.

이전에 배웠던 Weak Ordering을 이용하면 코드 시퀀스의 복잡도를 dominant term만으로 이용하여 설명할 수 있다.


알고리즘 복잡도 분석

프로그램 언어에서 사용하는 Control Statement(e.g., if, for)에 대해 Complexity를 계산하는 방법에 대해 배운다.


■ Control Statement의 종류

  • Conditional Statements : if, switch
  • Condition-controlled Loops : for, while, do-while
  • Count-controlled Loops : for i from 1 to 10 do … end do;
  • Collection-controlled Loops : foreach (int i in array) {…}


Conditional Statements

Conditional Statement의 대표적인 예로는 `if`, `switch`가 있다.


image-20210824180909051

Conditional Statement의 runtime은 다음과 같이 구성된다.

  • condition (test)에 대한 runtime

  • 실행될 body에 대한 runtim


일반적인 condition(e.g., n>5)에 대한 runtime은 Θ(1)이지만, condition에서 function call을 하는 경우는 constant runtime을 가지지 않을 수도 있다.


■ 예시1. Factorial Algorithm

int Factorial(int _n)
{
	if( _n == 0)
        return 1;
    else
        return _n * Factorial( _n -1 );
}

_n=0인 조건을 제외하면 언제나 recursive하게 함수를 호출한다는 것을 명확하게 알 수 있다. 따라서 complexity를 비교적 쉽게 계산할 수 있다.


■ 예시2. Find Max Algorithm

template <typename Type>
Type Find_Max(Type *_array, int _n)
{
    Type maxVal = _array[0];
    for(int i=1; i<_n; i++)
    {
        if(_array[i] > maxVal)
            maxVal = _array[i];  // ·······ⓐ
    }
    return maxVal;
}

위 코드에서 ⓐ 코드가 몇 번 실행되는지는 데이터 _array에 따라 달라진다.

  1. _array가 작은 것부터 큰 것까지 순서대로 정렬된 경우

    for문을 돌 때마다 매번 최대값이 갱신되어 ⓐ가 언제나 실행된다.

  2. _array가 큰 것부터 작은 것까지 순서대로 정렬된 경우

    _array[0]이 최대값이므로 ⓐ는 한 번도 실행되지 않는다.


예시2와 같이 데이터의 분포에 따라 코드의 시행횟수가 달라지는 경우가 있기 때문에 이러한 변수까지 고려하여 알고리즘의 complexity를 계산하기는 어렵다. 따라서 이러한 경우는 최악의 경우(모든 데이터에 대해서 한 번씩 수행될 때)와 평균적인 경우(일반적인 데이터가 주어졌을때, 혹은 랜덤 분포를 갖는 데이터일 때)에 대해서 논할 수 있다.


Switch

switch문에서 case는 변수가 아닌 상수에 대해서만 프로그래밍이 가능하다. 따라서 미리 정의된 machine instruction에 의해, 바로 해당 값의 binary로 점프할 수 있도록 최적화 되어있기때문에 if-else문에 비해 switch문이 조금 더 빠르다.


Conditional-controlled Loops

Conditional-controlled Loop(반복문)의 대표적인 예로는 for, while, do-while가 있다.


image-20210824182833365

Conditional-controlled Loop의 runtime은 다음과 같이 구성된다.

  • Initialization에 대한 runtime

  • Condition Check에 대한 runtime

  • Increment에 대한 runtime


for(i=0; i<n; i++)인 Conditional-controlled Loop에 대한 runtime은 다음과 같다.

  • Initialization, Condition Check, Increment에 대한 runtime은 모두 Θ(1)이다.
  • bodybreak이나 return이 없다면, Conditional-controlled Loop는 Ω(n)의 runtime을 갖는다(즉, 최소 n이 소비됨).
  • body의 runtime이 Θ(f(n))이면, Loop는 Θ(nf(n))의 runtime을 갖는다.
  • body의 runtime이 O(f(n))이면, Loop는 O(nf(n))의 runtime을 갖는다 (즉, 최악의 경우 nf(n)이 소비됨)


■ 예시1.

int sum = 0;
for(int i=0; i<n; i++)
{
    sum += 1;	// Θ(1)
}

위 코드는 $Θ(n·1)=Θ(n)$의 run time을 갖는다.


■ 예시2.

int sum = 0;
for(int i=0; i<n; i++)
{
    for(int j=0; j<n; j++)
        sum += 1;	// Θ(1)
}

위 코드는 $Θ(n·n·1)=Θ(n^2)$의 run time을 갖는다.


■ 예시3.

for(int i=0; i<n; i++)
{
    // search through an array of size m
    // O(m);
}

위 코드는 $O(n·m)$의 run time을 갖는다.


■ 예시4.

for(int i=0; i<n; i++){    // code which is Θ(f(i,n))}

위 코드의 run time은 다음과 같다.

\[Θ(1 + \sum_{i=0}^{n-1}{(1+f(i,n))})\] ※ 첫번째 항은 initialization(i=0)에 대한 run time Θ(1)을 의미한다.


■ 예시5.

int sum = 0;for(int i=0; i<n; i++){    for(int j=0; j<i; j++)    {        sum += i+j; // Θ(1)    } // Θ(1+i·1)} // Θ(1+SUMⁿ(1+i·1))

위 코드의 run time은 다음과 같다.

\(Θ(1+\sum_{i=0}^{n-1}{(1+i)}) \\ = Θ(1+n+\sum_{i=0}^{n-1}{i}) \\ = Θ(1+n+\frac{n(n-1)}{2}) \\ = Θ(n^2)\)

Serial Statements

■ 예시1. 적합한 Notation

  • $\text{size}=n$인 random list로부터 maximum entry를 찾는 경우에는
    모든 값을 조사해야하므로 $\text{run time}=Θ(n)$이다.
  • $\text{size}=n$인 random list로부터 특정값(particular entry)를 찾는 경우
    최악의 경우 모든 값을 조사해야하므로 $\text{run time}=O(n)$이다.


■ 예시2. leading term 기준으로 Notation 결정

  • $O(n) + O(n^2) + O(n^4) = O(n+n^2+n^4) = O(n^4)$
  • $O(n) + Θ(n^2) = Θ(n^2)$
  • $O(n^2) + Θ(n) = O(n^2)$
  • $O(n^2) + Θ(n^2) = Θ(n^2)$

코드에 따라서는 Notation이 섞여있을 수 있다. 이런 경우 leading term 기준으로 Notation을 표기한다.


Functions

■ 개념

Function(혹은 subroutine)이란 별도로 분리되어 존재하는 코드를 의미한다. 가령, mathmatical functions과 같이 반복적인 연산을 하거나 initialization과 같이 연결된 tasks를 모아서 함수로 만들 수 있다.


■ Function Call

Function Call을 하면, 어떤 프로그램의 binary로 점프를 하여 해당 부분을 수행한 뒤, 다시 본래의 프로그램 binary위치로 복귀하게 된다. 즉, Function Call의 과정은 다음과 같다.

  1. 함수를 구동하기 위한 적절한 환경을 구축한다.
  2. 함수에 넣어 줄 파라미터를 처리한다.
  3. subroutine으로 점프한다.
  4. subroutine을 실행한다.
  5. return value를 처리한다.
  6. clean up

위의 일련의 과정은 어떻게보면 Overhead이지만, 최신 프로세서는 이러한 과정을 constant time에 해줄 수 있는 instruction을 가지고 있다. 따라서 function call에 대한 overhead는 Ω(1)이다 (즉, 최대 constant time).


■ 함수 안에서 발생하는 run time

함수 안에서 발생하는 run time은 $T_{f(n)}$ 혹은 $T(N)$이라고 표현한다.

※ 이때 N은 함수가 받아들이는 파라미터의 사이즈를 의미한다. ※


References

  1. [집콕]인공지능을 위한 알고리즘과 자료구조: 이론, 코딩, 그리고 컴퓨팅 사고-비선형 자료구조, K-MOOC: http://www.kmooc.kr/courses/course-v1:SKKUk+SKKU_46+2021_T1/courseware/1158909c057347478d7386a2b7e97214/e9d736660cad4f76bcb9a05974fe0bf2/1?activate_block_id=block-v1%3ASKKUk%2BSKKU_46%2B2021_T1%2Btype%40vertical%2Bblock%4052b62ff6da2147c9a8a65a6056681d91