분할 정복
본문선택 정렬은 남은 원소 가운데 가장 작은 것을 찾아 앞으로 보내기를 되풀이한다. 원소 1000개는 금방 정렬하지만, 100만 개를 정렬하려면 1초에 1억 번 비교하는 컴퓨터로도 한 시간 넘게 걸린다. 그런데 리스트를 반으로 나누어 각각 정렬한 뒤 합치는 병합 정렬은 같은 100만 개를 0.2초 만에 정렬한다. 나누고 합치는 일을 더했는데 왜 오히려 수만 배 빨라질까?
3.4.1나누고, 풀고, 합치기
분할 정복은 문제를 같은 모양의 작은 문제들로 나누고, 각각을 재귀로 푼 뒤, 그 답을 합쳐 전체의 답을 만든다. 병합 정렬은 리스트를 반으로 나누어 왼쪽 절반과 오른쪽 절반을 각각 정렬하고, 정렬된 두 리스트를 합친다. 원소가 하나뿐인 리스트는 이미 정렬되어 있으므로 더 나누지 않는다. 정렬된 두 리스트를 합칠 때는 두 리스트의 맨 앞 원소만 견주어 작은 것을 꺼내면 되므로, 원소 수만큼만 비교한다. 이렇게 합치는 일이 싸기 때문에 분할 정복이 빛난다.
원소 \(n\)개를 반씩 나누기를 되풀이하면 \(\log_2 n\)단계 만에 하나씩 쪼개진다. 각 단계에서 합치는 비교는 모두 합해 \(n\)번 정도이므로 전체 비교 횟수는 약 \(n\log_2 n\)이다. 선택 정렬은 \(\dfrac{n(n-1)}{2}\)번 비교한다.
그림 1에서 원소 수를 바꾸어 보자. 원소 1000개는 선택 정렬이 약 50만 번, 병합 정렬이 약 1만 번 비교한다. 원소 100만 개는 선택 정렬이 약 5000억 번으로 한 시간 넘게 걸리지만, 병합 정렬은 약 2000만 번으로 0.2초면 끝난다. 원소가 1000배 늘 때 선택 정렬의 일은 100만 배 늘지만, 병합 정렬의 일은 2000배만 는다. 반으로 나누는 일을 스무 번만 해도 100만 개가 하나씩으로 쪼개지기 때문이다.1
3.4.2분할 정복이 통하는 조건
분할 정복이 효율적이려면 두 조건이 맞아야 한다. 첫째, 부분 문제의 크기가 일정한 비율로 줄어야 한다. 반씩 줄면 \(\log n\)단계면 끝나지만, 하나씩만 줄면 \(n\)단계가 걸린다. 둘째, 부분 문제의 답을 합치는 일이 싸야 한다. 정렬된 두 리스트는 \(n\)번이면 합칠 수 있지만, 합치는 일이 \(n^2\)번 걸린다면 나누는 의미가 없다. 정렬된 리스트에서 값을 찾는 이진 탐색은 절반을 버리고 한쪽만 풀면 되므로 합치는 일조차 없어 \(\log n\)번이면 끝난다. 거듭제곱 \(a^n\)도 \(a^{n/2}\)를 한 번 구해 제곱하면 \(\log n\)번의 곱셈으로 구한다.
3.4.3예제 풀이
정렬된 두 리스트 [2, 5, 8, 9]와 [1, 3, 7, 10]을 병합 정렬의 방식으로 합쳐라. 몇 번 비교하는가? 원소 8개를 병합 정렬할 때 나누는 단계는 몇 단계이며, 비교 횟수는 대략 몇 번인가?
맨 앞끼리 견준다. 2와 1에서 1, 2와 3에서 2, 5와 3에서 3, 5와 7에서 5, 8과 7에서 7, 8과 10에서 8, 9와 10에서 9를 꺼내고, 남은 10을 붙인다. 결과는 [1, 2, 3, 5, 7, 8, 9, 10]이고, 비교는 7번이다. 원소 8개를 합치는 데 많아야 7번이면 된다.
원소 8개는 8 → 4 → 2 → 1로 \(\log_2 8 = 3\)단계 만에 하나씩 쪼개진다. 단계마다 합치는 비교가 많아야 8번 안쪽이므로 전체 비교는 약 \(8 \times 3 = 24\)번이다. 선택 정렬은 \(8 \times 7 \div 2 = 28\)번으로, 원소가 적을 때는 별 차이가 없다. 그런데 원소가 100만 개가 되면 2000만 번과 5000억 번으로 벌어진다. 즉 분할 정복의 이득은 문제가 클수록 커진다. 그래서 대부분의 프로그래밍 언어의 정렬 함수는 이런 분할 정복 정렬을 쓴다.
아래 탐구 카드에서 병합 정렬과 이진 탐색을 직접 짜 보고, 원소 수를 늘리며 실행 시간을 재어 보자.
- 병합 정렬은 1945년 폰 노이만이 처음 만든 것으로 알려져 있다. 원소를 견주어 정렬하는 어떤 방법도 최악의 경우 약 \(n\log_2 n\)번보다 적게 비교할 수는 없다는 것이 증명되어 있다.
분할 정복은 문제를 같은 모양의 작은 문제로 나누어 풀고 그 답을 합치며, 병합 정렬은 반씩 나누기를 \(\log_2 n\)단계 하고 단계마다 \(n\)번쯤 비교해 합쳐 약 \(n\log_2 n\)번이면 정렬을 끝낸다. 부분 문제가 일정한 비율로 줄고 합치는 일이 쌀 때 큰 이득이 있다. 그래서 처음 질문의 100만 개는 선택 정렬로는 5000억 번 비교해 한 시간이 넘게 걸리지만, 병합 정렬로는 2000만 번이면 되어 0.2초로 준다.