본 시리즈는 프로그래밍 알고리즘의 개념을 정리하고 실습을 진행해보는 시리즈입니다.
분할 정복(Divide and Conquer)는 문제를 작은 단위로 나누어 해결하는 방식으로, 특히 복잡한 문제를 간단한 문제들로 나누어 처리하고, 그 결과를 결합하여 최종 해결책을 구하는 알고리즘 설계 기법입니다.
분할 정복 기법의 과정은 세 단계로 요약할 수 있습니다:
분할(Divide): 주어진 문제를 해결하기 쉬운 작은 문제들로 재귀적으로 나누는 단계입니다. 문제의 크기가 줄어들면서 다루기 쉬워지거나 기저 사례(base case)에 도달하게 됩니다.
정복(Conquer): 나누어진 작은 문제들을 개별적으로 해결합니다. 작은 문제들은 종종 재귀적으로 처리되며, 문제의 크기가 충분히 작아지면 더 이상 나눌 필요 없이 해결됩니다.
결합(Combine): 해결된 작은 문제들을 다시 결합하여 원래 문제의 최종 해답을 얻습니다. 이 과정에서 병합이나 조합이 이루어지며, 문제가 분할되었던 방식에 따라 결합 방식도 달라집니다.
Merge Sort (합병 정렬):
Quick Sort (퀵 정렬):
Binary Search (이진 탐색):
Closest Pair of Points (최근접 두 점 문제):
분할 정복 기법의 장점:
분할 정복 기법의 단점:
분할 정복(Divide and Conquer)은 다양한 알고리즘에서 핵심 기법으로 사용됩니다.
Merge Sort는 배열을 분할하여 재귀적으로 정렬하고, 다시 병합하여 최종 정렬된 배열을 만드는 대표적인 분할 정복 알고리즘입니다.
동작 원리:
시간 복잡도:
log n
이 소요되고, 병합하는 과정에서 O(n) 시간이 걸리므로 전체 복잡도는 O(n log n)입니다.// Java로 구현된 Merge Sort 예시
public class MergeSort {
public static void mergeSort(int[] arr, int left, int right) {
if (left < right) {
int middle = (left + right) / 2;
mergeSort(arr, left, middle);
mergeSort(arr, middle + 1, right);
merge(arr, left, middle, right);
}
}
public static void merge(int[] arr, int left, int middle, int right) {
int n1 = middle - left + 1;
int n2 = right - middle;
int[] leftArr = new int[n1];
int[] rightArr = new int[n2];
for (int i = 0; i < n1; i++) leftArr[i] = arr[left + i];
for (int j = 0; j < n2; j++) rightArr[j] = arr[middle + 1 + j];
int i = 0, j = 0, k = left;
while (i < n1 && j < n2) {
if (leftArr[i] <= rightArr[j]) arr[k++] = leftArr[i++];
else arr[k++] = rightArr[j++];
}
while (i < n1) arr[k++] = leftArr[i++];
while (j < n2) arr[k++] = rightArr[j++];
}
public static void main(String[] args) {
int[] arr = {9, 2, 5, 1, 7};
mergeSort(arr, 0, arr.length - 1);
System.out.println(Arrays.toString(arr));
}
}
Quick Sort는 피벗(Pivot)을 선택한 후 피벗보다 작은 값과 큰 값을 각각 좌우로 분할하여 재귀적으로 정렬하는 알고리즘입니다.
동작 원리:
시간 복잡도:
# Python으로 구현된 Quick Sort 예시
def quick_sort(arr):
if len(arr) <= 1:
return arr
pivot = arr[len(arr) // 2]
left = [x for x in arr if x < pivot]
middle = [x for x in arr if x == pivot]
right = [x for x in arr if x > pivot]
return quick_sort(left) + middle + quick_sort(right)
arr = [10, 5, 2, 3, 7]
sorted_arr = quick_sort(arr)
print(sorted_arr)
Binary Search는 정렬된 배열에서 특정 값을 빠르게 찾는 알고리즘으로, 분할 정복을 활용하여 배열을 절반씩 나누면서 탐색 범위를 줄여갑니다.
동작 원리:
시간 복잡도: O(log n)
// Java로 구현된 Binary Search 예시
public class BinarySearch {
public static int binarySearch(int[] arr, int target, int left, int right) {
if (left > right) return -1;
int mid = (left + right) / 2;
if (arr[mid] == target) return mid;
else if (arr[mid] > target) return binarySearch(arr, target, left, mid - 1);
else return binarySearch(arr, target, mid + 1, right);
}
public static void main(String[] args) {
int[] arr = {1, 3, 5, 7, 9};
int target = 5;
int index = binarySearch(arr, target, 0, arr.length - 1);
System.out.println("Index of target: " + index);
}
}
분할 정복 기법을 사용한 알고리즘들은 시간 복잡도와 공간 복잡도 측면에서 효율적인 성능을 보입니다.
시간 복잡도:
공간 복잡도:
특징:
시간 복잡도:
공간 복잡도:
특징:
시간 복잡도:
공간 복잡도:
특징:
알고리즘 | 시간 복잡도 (평균) | 시간 복잡도 (최악) | 공간 복잡도 | 특징 |
---|---|---|---|---|
Merge Sort | O(n log n) | O(n log n) | O(n) | 항상 일정한 성능 보장, 안정 정렬 |
Quick Sort | O(n log n) | O(n²) | O(log n) | 제자리 정렬, 피벗 선택에 따라 성능 변동 |
Binary Search | O(log n) | O(log n) | O(1) | 정렬된 배열에서만 사용, 빠른 탐색 |
이 표에서 볼 수 있듯이, 각 알고리즘은 문제의 성격에 따라 다르게 사용됩니다.
이번 포스팅에서는 분할 정복(Divide and Conquer) 알고리즘의 개념과 주요 알고리즘들을 살펴보았습니다.
특히 Merge Sort, Quick Sort, Binary Search와 같은 대표적인 알고리즘들이 어떻게 분할 정복 기법을 활용하여 효율적으로 동작하는지, 그 원리와 복잡도를 살펴보았습니다.
분할 정복은 정렬과 탐색뿐만 아니라 최적화 문제, 그래프 문제 등 다양한 문제에서도 응용될 수 있는 강력한 도구입니다.
앞으로의 포스팅에서는 탐색 알고리즘, 최적화 알고리즘 등 다른 다양한 문제 해결 기법들을 다루어볼 예정입니다.