티스토리 뷰
5.4 재귀 알고리즘과 머지소트merge sort 시간복잡도
gaelim 2018. 1. 9. 18:42시작하기에 앞서
특정한 입력값 집합으로 이루어진 문제에 대한 정답은 같은 문제에 대한 작은 입력 값 집합에 대한 정답으로 축소할 수 있습니다. (이것은 부분 문제subproblem 이라는 꽤 중요한 표현입니다.)
예를 들어, 두 양의 정수 a, b의 최대공약수(GCD)를 구하는 문제를 생각해보자. 여기서 b>a. 이 문제는 더 작은 입력값인 b mod a 와 a의 최대공약수를 구하는 문제로 축소될 수 있다. 왜냐하면 gcd(b mod a, a) = gcd(a, b)이기 때문이다.
이런 문제 축소가 수행될 수 있다면, 원 문제의 정답은 이런 축소된 문제의 흐름(좀 더 엄밀하게는 축소된 문제의 나열sequence of reductions) 속에서 찾아 낼 수 있다. 계속 축소하다보면 정답을 정확하게 알 수 있는 최초의 상황을 알게된다. 예를 들어, 최대 공약수를 찾는데, 두번째 숫자가 0일 때까지 계속 문제를 축소 할 수 있다. 왜냐하면 gcd(a, 0) = a (a > 0)이기 때문이다.
이제 원 문제를 작은 입력값을 받는 문제로 축소해서 문제를 재귀적으로 풀어나가는지 다양한 문제를 봐서 풀어볼것이다.
정의
예제 1
재귀 알고리즘을 이용해 n! 을 구해보자.
n! = n * (n-1) * (n-2) ... * 1 이다. 여기서 n! 는 n* (n-1)!이다. 따라서 n!을 구하는데 부분문제는 (n-1)!을 구해야하는 것이고, (n-1)!를 구하려면 (n-2)!를 구해야한다. 이를 재귀 함수로 작성해보면 다음과 같다.
예제 3
예제 6
재귀 알고리즘을 이용한 정렬
Merge Sort 알고리즘이라고 불리는 재귀 정렬 알고리즘을 알아 볼것이다.
스도코드는 다음과 같다.
List = { a1, a2, ... an }
divide (List) ={
n = List.Size
if (n>1) {
m = n/2
List1 = {a1, a2, ... am}
List2 = {am+1, am+2, ... an}
return merge ( divide(List1) , divide(List2) )
}
else return List
}
merge (List1, List2) ={
List = empty List
while (!List1.empty() && !List2.empty()) {
if( List1.top() > List2.top() ) List.push(List2.pop())
else List.push(List1.pop())
}
while(!List1.empty()) List.push(List1.pop())
while(!List2.empty()) List.push(List2.pop())
return List
}
정렬된 두 리스트를 비교하면서 합치는데에 비교가 얼마나 발생하는가 추정해볼것이다. 어떤 두 리스트를 비교할 때, 결과적으로 숫자가 작은것이 그 상위 리스트로 합쳐진다. 만약, 두 리스트 중 한개가 비어있다면, 나머지 리스트가 순서대로 통째로 그곳에 복사된다. 왜냐하면 각 리스트들은 정렬되어있기 때문에 비교 후 남은 하나의 리스트를 그대로 상위 리스트에 넣으면 된다.
이러한 비교 작업은 최대 m+n-1 만큼 수행된다.
잘 생각해보면 당연한거다.
예로 리스트 1개 3개 크기를 상위 리스트로 합칠때, 최대 3번의 비교가가능하다. 최소는 2번이다. 왜냐하면 ceiling [ log 3 ] 이기 때문이다. 선형 탐색말고 이진탐색을 사용하면 그렇다.
하지만, 각각 m과 n 크기라면 m+n-1 이 제일가능한 비교횟수이다. 즉, m+n-1 보다 더 적게 비교할 수 없다.
merge sort의 수행복잡도를 연산하기 위해, 우선 n개의 원소를 정렬한다고 가정하자. 단, n을 2의 승수로 2^m 이라고 하자. 그래야 분석이 조금 더 쉬워진다.
맨 처음 2개로 분할할 때, 이 2 부분 문제(수열, 리스트)는 2^m-1 개의 원소를 정렬하는 것으로 바뀐다.
그리고 두 번째에는, 각 리스트를 2^(m-2) 개의 원소를 정렬하는 것이 된다. 이렇게 계속된다. 맨 처음 문제에서 트리로 점차 퍼져가며 분할되는 것을 상상해보라. 그림은 아래와 같다..
총 원소가 2^m개일 경우 리스트를 2분할 해갔을때 트리의 단말 노드의 높이가 모두 같게 된다.
어쨌든 이렇게 다음 레벨로 분할해서 내려가면 결국에는, k 번째에서는 2^(m-k) 개의 부분 수열을 정렬하게 된다. 그래서 결국엔 레벨 m 때, 2^(m-m) 즉, 1개의 부분수열을 정렬하게 되고 이런 것이 총 2^m개가 존재하게된다.
이제 m-1 수준에서, m 수준에 존재하는 각각 원소들을 합칠것이다. 아래의 그림에선, 4 수준에 존재하는 각각의 1개 원소들이 있는데, 3수준에서 처럼 2개로 합칠거다. 이때 오름차순으로 만들기위해 비교연산이 정확히 한번 수행된다.
이런 수행을 계속하게 되면, k 수준에서의 2^k개의 2^(m-k) 길이의 리스트가 2^(k-1)개로 합쳐지게 된다. 이때 길이는 2^(m-k+1)이 된다.
이때 합쳐질때의 시간복잡도는, 아까전에 서술했듯이, 양 리스트의 크기 를 더하고 거기서 -1 을 한것이다. 즉, k 수준에서의 2^(m-k) 길이의 두 리스트가 합쳐지기 위한 시간복잡도는, 최대의 경우
( 2*2^(m-k)-1 )* 2^(k-1) 이다.
왜냐하면 그렇게 합칠 리스트가 2^(k-1) 개 만큼 있기 때문이다.
그리고 수준 k가 1~m 까지 수행되므로, 다음과 같이 정리할 수 있다.
이때, 앞의 항은 m*2^m 이며 뒤 항은 2^m -1 이다.(등비수열의 합)
따라서 전체 식을 정리하면 m * 2^m - 2^m +1 이며, 맨 처음 정의한 n = 2^m 을 이용하면
m* 2^m - 2^m +1 = log(2) n * n - n + 1 임을 알 수 있다.
일반화 한 함수를 적으면
T(n) = 2T(n/2) + n 이라고 할 수있다. 우측 항의 T(n/2)는 재귀함수를 뜻하며, n의 복잡도는 n크기만큼의 배열을 정리하는데에 사용된다. 위에서 n/2+n/2-1 과 같은논리다. 다만 여기서는 비교수행도가 아닌 전체적인 복사 수행시간이다. 따라서,
T(n) = 2T(n/2) + n 는, 정확히 log(2) n 만큼 반복되며,
함수를 풀어보면, 2^(log (2) n) + n*log(2)n 임을 알수있다. *2, +n 은 log(2) n 만큼 반복된다.
따라서, T(n) = n + n log (2) n 이며 Big - O notation에 따라. O(T(n)) = n log n 이 된다.
끄읕~
참고: 머지소트 구현 merge sort implementation
'Discrete mathmatics and Problem Solving > 5 재귀와 귀납' 카테고리의 다른 글
하노이의 탑 (0) | 2019.11.10 |
---|
- Total
- Today
- Yesterday
- grafana cloud
- 이산 수학
- 명제논리
- 자바스크립트
- Discrete Mathematics
- 그라파나
- 아레나 시뮬레이션
- arena simulation
- 항해99
- javascript
- 대규모 시스템 설계 기초
- 데이터 중심 애플리케이션 설계
- 최단경로 알고리즘
- 백준
- 가상 면접 사례로 배우는 대규모 시스템 설계 기초
- Arena
- 로젠
- flutter
- Simulation
- Propositional and Predicate Logic
- 아레나
- 이산수학
- Grafana
- 시뮬레이션
- 아레나시뮬레이션
- 엄청난 인내심과 시뮬레이션을 위한 아레나 툴
- rosen
- paul wilton
- beginning javascript
- 자바스크립트 예제
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |