분할정복
분할정복(divide and conquer)은 큰 문제를 같은 종류의 더 작은 문제로 나누고(분할), 작은 문제를 풀고(정복), 그 답을 합쳐서(결합) 원래 문제의 답을 만드는 알고리즘 설계 방식입니다. 작은 문제는 같은 방법을 재귀적으로 적용해 풀고, 더 나눌 수 없을 만큼 작아지면 바로 답을 내는 기저 사례에서 멈춥니다.
병합 정렬, 이진 탐색, 거듭제곱의 빠른 계산이 모두 이 방식입니다. 분할정복의 효율은 얼마나 잘 나누고 얼마나 싸게 합치느냐에 달려 있고, 시간은 점화식으로 계산합니다. 이 토픽에서는 병합 정렬이 쪼개졌다가 합쳐지는 과정, 지수를 절반씩 줄이는 거듭제곱, 점화식을 재귀 트리로 펼쳐 비용을 더하는 방법을 봅니다.
핵심 개념
분할 · 정복 · 결합
문제를 작게 나누고, 각 부분 문제를 재귀로 풀고, 부분 해를 합쳐 전체 해를 만듭니다. 세 단계 중 무엇이 어려운지는 문제마다 다릅니다.
핵심 개념
기저 사례
더 나눌 수 없을 만큼 작은 문제는 바로 답을 냅니다. 병합 정렬에서는 원소가 하나뿐인 조각이 이미 정렬된 상태라는 점이 기저 사례입니다.
핵심 개념
병합 정렬
배열을 절반으로 나누어 각각 정렬한 뒤, 정렬된 두 조각을 앞에서부터 비교하며 하나로 합칩니다. 나누기는 쉽고 합치기에 일이 몰려 있는 형태입니다.
핵심 개념
점화식
T(n) = 2T(n/2) + n처럼 문제 크기 n의 시간을 더 작은 크기의 시간과 나누고 합치는 비용으로 적은 식입니다. 병합 정렬의 시간이 이 식에서 나옵니다.
핵심 개념
재귀 트리
점화식을 호출 구조로 펼친 트리입니다. 단계마다 비용을 구해 모두 더하면 전체 시간이 나옵니다. 병합 정렬은 단계마다 n이고 단계가 log n개입니다.
핵심 개념
마스터 정리
T(n) = aT(n/b) + f(n) 꼴의 점화식을 풀어 (a ≥ 1, b > 1이고 f(n)이 세 경우 중 하나에 맞으면) 시간을 바로 알려 주는 정리입니다. 병합 정렬은 a = 2, b = 2, f(n) = n이라 Θ(n log n)이 됩니다.
분할정복의 배경
병합 정렬은 1945년 존 폰 노이만(John von Neumann)이 고안한 것으로 알려져 있고, 컴퓨터를 위해 만들어진 초기의 정렬 알고리즘 중 하나입니다. 분할정복 방식은 이후 빠른 곱셈과 행렬 곱셈 같은 문제에서도 점근적 개선을 이끌었습니다. 1962년 카라추바(Karatsuba)는 큰 수의 곱셈을 세 번의 절반 크기 곱셈으로 줄여 n^(log2 3) ≈ n^1.585 시간에 하는 방법을 보였고, 1969년 슈트라센(Strassen)은 행렬 곱셈을 n^(log2 7) ≈ n^2.81 시간에 하는 방법을 보였습니다.
숫자로 확인하기
자주 하는 오해
면접에서 이렇게 물어요
면접 질문
분할정복의 세 단계는?
분할(문제를 작게 나눔), 정복(부분 문제를 재귀로 풂), 결합(부분 해를 합침)입니다. 더 나눌 수 없으면 기저 사례로 바로 답을 냅니다.
면접 질문
병합 정렬의 시간·공간 복잡도는?
시간은 기본 병합 정렬에서 모든 경우에 O(n log n)이고 공간은 임시 배열 때문에 O(n)입니다. 절반씩 나누는 단계가 log n개이고 단계마다 합치는 비용이 n이기 때문입니다.
면접 질문
분할정복과 동적 계획법의 차이는?
분할정복은 겹치지 않는 부분 문제를 풀어 합치고, 동적 계획법은 겹치는 부분 문제의 결과를 저장해 같은 계산을 반복하지 않습니다. 부분 문제가 겹치는지가 기준입니다.
면접 질문
x^n을 어떻게 빠르게 계산하나요?
x^n = x^⌊n/2⌋ × x^⌊n/2⌋로 지수를 절반씩 줄이고, n이 홀수이면 x를 한 번 더 곱합니다. 곱셈이 O(log n)번 필요합니다.
실제 시스템에서는
함께 보면 좋은 토픽
참고 자료
Python — 병합 정렬
count = [0] # 비교 횟수 def merge_sort(a): if len(a) <= 1: # 기저 사례: 이미 정렬됨 return a mid = len(a) // 2 left = merge_sort(a[:mid]) # 분할 + 정복 right = merge_sort(a[mid:]) return merge(left, right) # 결합 def merge(left, right): out, i, j = [], 0, 0 while i < len(left) and j < len(right): count[0] += 1 if left[i] <= right[j]: out.append(left[i]) i += 1 else: out.append(right[j]) j += 1 return out + left[i:] + right[j:] # 남은 조각을 이어 붙인다 print(merge_sort([38, 27, 43, 3, 9, 82, 10, 5]), count[0]) # [3, 5, 9, 10, 27, 38, 43, 82] 17
분할정복은 세 단계입니다. 문제를 더 작은 같은 문제로 나누고(분할), 각 부분을 재귀로 풀고(정복), 부분의 답을 합쳐 전체의 답을 만듭니다(결합). 병합 정렬에서 분할은 배열을 절반으로 가르는 일이고, 정복은 재귀 호출이며, 결합이 정렬된 두 조각을 하나로 합치는 merge 함수입니다. 길이가 1 이하인 조각은 이미 정렬된 상태라 재귀를 멈추는 기저 사례가 됩니다. merge는 두 조각의 맨 앞 원소를 비교해 작은 쪽을 꺼내므로 두 조각 길이의 합만큼의 시간이 듭니다. 시각화의 8개 원소 배열에서는 합치기 3단계에 비교가 4번, 6번, 7번 들어 모두 17번이고, 전체 시간은 O(n log n)입니다. 합치기에 임시 배열이 필요해서 추가 공간은 O(n)입니다. 같은 값이 있을 때 left[i] <= right[j]처럼 왼쪽을 먼저 꺼내면 원래 순서가 유지되는 안정 정렬이 됩니다. 시각화의 병합 정렬 탭이 보여 주는 과정과 같습니다.
Python — 분할정복 거듭제곱
def power(x, n): if n == 0: # 기저 사례 return 1 half = power(x, n // 2) # 지수를 절반으로 return half * half * x if n % 2 else half * half print(power(2, 13), power(3, 5), power(2, 0)) # 8192 243 1 def power_iter(x, n): # 같은 아이디어를 반복문으로 result = 1 while n: if n & 1: result *= x x *= x n >>= 1 return result print(power_iter(2, 13)) # 8192
x^n을 x를 n번 곱해서 구하면 곱셈이 n - 1번 필요하지만, x^n = x^⌊n/2⌋ × x^⌊n/2⌋로 지수를 절반씩 줄이면 호출이 log n 단계만 쌓입니다. n이 홀수이면 x를 한 번 더 곱합니다. 2^13은 호출이 13, 6, 3, 1, 0의 다섯 단계로 쌓이고 곱셈을 7번 해서 8192가 됩니다(단순하게 곱하면 12번). 부분 문제가 하나뿐이라 결합 비용이 상수이고 점화식이 T(n) = T(n/2) + O(1)이 되어 O(log n)입니다. 아래의 반복문 버전은 같은 아이디어를 지수의 비트를 아래부터 읽는 방식으로 옮긴 것이고, 재귀 깊이 없이 추가 변수 O(1)개로 계산합니다. 시각화의 거듭제곱 탭이 보여 주는 호출 쌓임과 같습니다.
Java — 병합 정렬
static int[] mergeSort(int[] a) { if (a.length <= 1) return a; int mid = a.length / 2; int[] left = mergeSort(Arrays.copyOfRange(a, 0, mid)); int[] right = mergeSort(Arrays.copyOfRange(a, mid, a.length)); int[] out = new int[a.length]; int i = 0, j = 0, k = 0; while (i < left.length && j < right.length) { out[k++] = left[i] <= right[j] ? left[i++] : right[j++]; } while (i < left.length) out[k++] = left[i++]; while (j < right.length) out[k++] = right[j++]; return out; } // mergeSort(new int[]{38, 27, 43, 3, 9, 82, 10, 5}) → [3, 5, 9, 10, 27, 38, 43, 82]
자바 버전도 같은 구조입니다. 배열을 절반으로 복사해 재귀로 정렬한 뒤 앞에서부터 비교하며 합치고, 한쪽이 먼저 끝나면 남은 쪽을 그대로 이어 붙입니다. 비교 횟수는 파이썬 예와 같이 17번입니다. 분할정복이 항상 빠른 것은 아닙니다. 조각을 나누고 합치는 비용이 있고, 부분 문제가 서로 겹치면 같은 문제를 여러 번 푸는 낭비가 생깁니다. 그런 경우에는 결과를 저장해 두는 동적 계획법이 알맞습니다.
Q1
분할 정복의 세 단계를 순서대로 나열한 것은?
Q2
병합 정렬(Merge Sort)의 시간 복잡도는?
Q3
x^n(거듭제곱)을 분할 정복으로 계산하면 곱셈 횟수는 대략?