Computer Science/알고리즘

[알고리듬] 시간복잡도 분석(피보나치, Peasant, Hanoi, Quick sort, Merge sort)

그적 2020. 5. 8. 14:54

지난주는 시간복잡도, 알고리즘을 수행하는데 걸리는 시간(running time)
어떻게 측정하는가?
알고리즘 자체가 추상적인 상태이므로, 시간 복잡도를 이용하자.

시간 복잡도 분석 = 연산의 개수 세기  >> (시간복잡도 = n에 대한 함수)
n에 대한 개수를 정확하게 세는 것은 어렵다. 따라서 최악의 경우를 따짐 -> 최대 개수에 대한 상한을 분석

◇예제. 피보나치 수열◇
 : O(mn)만큼의 한 자릿수 곱셈

피보나치 수열

(안쪽 for문)
 >> ppt 내용 인덱스 범위 지정이 잘못됐음. if문을 써서 i <n, k-i<m이 가정되어야 한다.
 >> 안쪽 for 문 : 상수 개+1번의 상한을 갖게 됨.                                                    // O(k+1)를 넘지 않는 빅오를 갖게 된다.
 >> 안쪽 for문에서 hold += hold*X[i][j]의 값이 O(mn)의 값을 가진다.
 그 이유는 i와 j의 쌍의 개수는 k와 같다. 항상 k개는 아니지만 최대한 i가 가능한 k의 개수는 n개고, j가 가능한 k의 개수는 m개이기 때문에 O(mn)의 이 알고리즘에서 가정하고 있는 기본 연산 횟수이기  때문인데 덧셈과 한 자릿수의 곱셈을 가정하고 있기에 교재에서는 O(mn)의 시간복잡도를 가진다.
(바깥 for문)
 >> n+m-1만큼의 반복문을 돌린다. n+m까지의 합 ΣO(k+1)
 >> O((n+m)²) 

※기본 연산에 따라서 시간 복잡도가 달라지는 경우가 생긴다.※


◇예제. PeasantMultiply◇

피보나치 곱

(( while x>0 인 반복문 ))
x <- [x/2] 로 인해 x값이 작아지는 점 >> 따라서 반복 횟수가 log x
y <- y+y  >> n+n만큼의 덧셈을 할 때 걸리는 시간은 n의 자릿수만큼 걸린다. 따라서 log y번의 연산.

>> 빅오에서는 log2x * logy 가 있다면 2와 같은 로그 밑 값이 필요가 없어진다.  
>> 반복 * 연산 횟수 = (log x) * (log y)

┌ ┐실링 : 안에 있는 값을 올림 // └ ┘플로어: 안에 있는 값을 버림
------------------------------------------------------------------------------------------

// Recursion을 이해하려면 Reduction에 대해서도 이해해야 한다. 동전의 양면과 같음.

"재귀"
: 무한을 유한한 텍스트로 표현하는 방법 중 하나
재귀 호출을 하는 함수 >> 재귀 함수

Recursion의 예들
예시 1)     1.  n! = {n=0,    1  }
                             {n>0,   n*(n-1)}
               2. 자연수의 정의 { 1은 자연수이다.    }
                                         { n이 자연수이면, n보다 1 큰 수도 자연수이다.  }
예시 2)    1. (Linked) List 
                     - NULL은 list이다.
                     - A가 list이면, A 앞에 노드를 연결한 전체도 list이다. 
              2. Binary Tree
                     - 공집합은 binary tree이다. (공 트리)
                     - T1과 T2가 binary tree라면... 재귀적 구조
예시 3) int factorial(int n) {if (n==1) return 0; else return n*fact(n-1);}

Reduction
- 가장 많이 사용하는 technique
- x 문제를 풀기 위해 x에 대한 알고리즘을 짜고 싶었는데, 그것을 풀기 위해 y 알고리즘을 사용하게 된다.
- y를 풀기 위한 알고리즘을 호출하는 것이지, 그것을 들여다볼 필요가 없으므로 마치 black box 같다.
- "문제 간의 관계" 개념

예시)
최솟값 찾기
1. A를 정렬한다. // 최솟값 찾기 problem에 정렬 알고리즘이 사용됨. 
2. return A[0];

Selection(A[0 ... n-1, k] // 배열 A에서 k번째로 작은 값찾기
1. A를 정렬한다.
2. return A[k-1]

Selection is reduced to Sorting >> selection문제를 풀기 위해 sorting 알고리즘을 subroutine으로 활용함
                                                   >> 우리가 푼 알고리즘이 더 큰 알고리즘을 풀기 위한 building blocks으로 사용되기도 한다.


■Recursion■
: Recursion은 reduction의 한 종류
: instance(입력) 개수가 하나 이상이라면, 사이즈를 줄기 위해 같은 문제로 reduction 하거라.
: "문제의 사이즈를 줄여라"

int factorial(int n)
{
   if(n==0) return 0;
   else return n*factorial(n-1); // 이게 함수 본인이라는 것을 잊어라.
} // 그저 나는 fact함수가 (n-1)!이라는 것만 생각하라.  


■Peasant Multiplication■
//곱셈 문제
x*y :  if x=0  return 0
       else if  x=even  return  (플로어 x/2) * (y+y)
       else  x=odd   return  ((플로어 x/2) * (y+y)) +y

// 알고리즘 
PeasantMultiply(x,y) : 
if { x=0 return 0 } 
else { 
   x' <- 플로어x/2 
   y' <- 플로어y/y 
   prod <- PeasantMultiply(x', y') 
   if x==odd 
       prod <- prod+y 
   return prod 



■Hanoi Tower■
 - n개의 disc와 3개의 Peg(막대기)
 - 하노이 규칙 : 큰 disc가 작은 disc 위에 올라가서는 안된다.
1. n-1개의 disc를 peg1에서 peg2로 옮긴다.(by recursion)
2. disc n을 peg1에서 peg3로 옮긴다.
3. n-1개의 disc를 peg2에서 peg3로 옮긴다.(by recursion)

n-1개를 옮기는 것은 원래 instacne보다 작은 하노이 탑 문제의 instance
즉, 같은 문제로의 reduction -> recursion

Hanoi(n, src, dis, tmp)
if n>0 //base case
  Hanoi(n-1, src, tmp, dst) 
  move disk n from src to dst  
  Hanoi(n-1, tmp, dst, src)

재귀 함수가 동작 하는 알고리즘을 짜려면 어떻게 하는가? 작은 수부터 해봐라
base case : instance
recursion step : reduce/simpler instances

Simplify & Delegate
- Simplify : 더 작은 instance를 만들고
- Delegate : 던져버리고 잊어버리자 -> 내가 하는 일이 아님. recursion으로 만들어라

왜 동작하는가? 이건 수학적 귀납법에 의해서 설명 가능함.

----------------------------------------
Hanoi Tower : 시간 복잡도 분석
move 연산을 몇 번 수행하게 될까?
n=1일 때 1번/ n=2일 때 2번/ n=3일 때 7번/...

if n>0 //base case
Hanoi(n-1, src, tmp, dst) // T(n-1)
move disk n from src to dst // 1
Hanoi(n-1, tmp, dst, src) // T(n-1)
----------------------------------------
점화식 >> T(n)을 Hanoi(n, src, dst, tmp) 함수를 실행했을 때의 move  연산의 횟수라고 하자.
     if n=0, return 0 
     else n>0, return 2*T(n-1)+1
이제 우리가 알고 있는 T(n)=2*T(n-1)+1의 점화식을 풀면 T(n) = 2ⁿ-1이고, 여기서 무한대의 값만 취급해주면 빅오 표기법으로, O(2ⁿ)이라는 시간 복잡도를 가진다. // 굉장히 큰 사이즈를 가짐.

■Mergesort■
: Recursion을 이용하는 정렬 알고리즘
: EDVAC에 구현된 최초의 컴퓨터 프로그램 중 하나

1. 주어진 배열을 절반으로 나눈다.
2. 두 subarray를 각각 정렬한다. >> by recursion
3. 정렬된 두 subarray를 합쳐서 하나의 정렬된 배열로 만든다.

MergeSort  :                               //basecase는 n=1이거나 공집합일 경우. 
if n>1 
   m <- n/2 
MergeSort(A[1 ... m]) 
MergeSort(A[m+1 .. n]) 
Merge(A[1 ... n], m)               //따로 함수를 만들어서 구현함. 


Merge : 
i <- 1;  j <- m+1                       // i와 j는 인덱스 취급 
for  k <- 1  to  n                          화살표가 가리키는 위치의 값들을 비교하여 작은 것을 앞으로. 
     if j>n 
          B[k] <- A[i];  i <- i+1; 
     else if i>m 
          B[k] <- A[i];  i <- j+1; 
     else if A[i] <A[i];   
          B[k] <- A[i];  i <- i+1; 
     else 
          B[k] <- A[j];  j <- j+1; 


(정확성에 대한 증명) >> 수학적 귀납법
proof by induction
- (base case) n=1일 때,
- (induction step) n>1일 때, 
     // (★induction High processes★라고 부름.)
    <Assume> MergeSort가 n-1개 이하의 원소를 가진 배열은 정확하게 정렬한다고 가정하자.
     n>1이므로 m<n이고 n-m<n이므로 ... 
두 번의 recursive call은 올바르게 동작하여 두 개의 정렬된 배열을 만든다.
이걸 가지고 Merge 하니까 전체가 정렬될 것이다.

두 조건(n=1일 때, n>1일 때)이 만족하므로 수학적 귀납법에 의해 어떤 자연수 n에 대해 정렬할 수 있다.
recursion : button up
 
시간 복잡도
T(n)을 MergeSort(A[1 ... n])의 시간복잡도 함수라고 정의하자
Merge sort
if n>1
m <- n/2
MergeSort(A[1 ... m])     // T(n)
MergeSort(A[m+1 .. n]) // T(n-m-1)
Merge(A[1 ... n], m)       // O(n)

T(n) = T(실링(n/2)) + T(플로어(n/2))+O  //플로어랑 실링은 무시할 수 있음
T(n) = 2*T(n/2) + O(n) >> 여기서 빅오는 c(상수)xn
      ≤ 2*T(n/2) + c(상수)xn = T(n)O(n log n) //이걸 어떻게 풀까?

----------------------------------------

Quick Sort
: Mergesort와 다르게 recursion 하기 전에 분류 작업이다.

알고리즘
1. 주어진 배열의 원소 중에 pivot을 고른다.
2. Partition : Pivot을 기준으로 작은 것과 큰 것을 분류
   pivot보다 작은 건 앞으로, 큰 건 뒤로 분류
3. 이 두 subarray를 각각 정렬한다.

/*정렬*/ 
QuickSort(A[1 ... n]): 
if (n>1) 
   Choose a pivot element A[p] 
r <- Parttition(A,p) 
QuickSort(A[1 ... r-1]) 
QuickSort(A[r ... n]) 

/*분할*/ 
Partition(A[1 ... n], p): 
 swap A[p] <-> A[n] 
 l <- 0 
 for i<-1 to n-1 
    if A[i] < A[n] 
       l <- l+1 
       swap A[l] <-> A[i] 
 swap A[n] <-> A[l+1] 
 return l+1 


명확성
Mergesort와 같은 방식으로 증명 가능
By iduction

시간 복잡도
T(n) = T(r-1) + T(n-r) + O(n)
최악의 경우 r=1이거나 r=n //pivot이 맨 끝으로 지정됐을 경우.
T(n) = T(n-1) + O(n) = O(n²)
 >> 평균적인 분석을 하면 QuickSort는 대개 O(n log n) 시간이 걸린다.


-----------------------------------------------------------
/*정리*/

- 팩토리얼 함수  >>  T(n) = T(n-1)+c
- 하노이 타워 >>  T(n) = 2*T(n=1)+1
- Merge sort >>  T(n) = 2*T(n/2) + O(n)
- Peasant Multi >>  T(n) = T(n/2) + O(log y)

Peasant Multiplication에서.. 최초에 나온 알고리즘은 반복문으로 되어 있었습니다.
그 반복 횟수가 log x 번이고 그때마다 O(log y) 번의 연산을 하니까 곱하면 됩니다.