분할정복 알고리즘 (1)
분할정복 방법의 원리
분할 정복 방법의 원리
순환적으로 문제를 푸는 하향식 접근 방법
- 주어진 문제의 입력을 더 이상 나루 수 없을 때까지 두 개 이상의 작은 문제로 순환적으로 분할하고, 이렇게 분할된 작은 문제들을 각각 해결한 후 그 해를 결합하여 원래 문제의 해를 구하는 방식
특징
- 분할된 작은 문제는 원래 문제와 동일
- 단, 입력 크기만 작아짐
- 분할된 문제는 서로 독립적
- 순환적 분할 및 결과 결합이 가능
- 분할된 작은 문제는 원래 문제와 동일
각 순환 호출 시의 처리 과정
- 분할: 주어진 문제를 여러 개의 작은 문제로 분할
- 정복: 작은 문제들을 순환적으로 분할. 만약 작은 문제가 더 이상 분할되지 않을 정도로 크기가 충분히 작다면 순환 호출없이 작은 문제에 대한 해를 구함
- 결합: 작은 문제에 대해 정복된 해를 결합하여 원래 문제의 해를 구함
이진 탐색
개념과 원리
정렬된 상태의 데이터에 대해 적용 가능한 효과적인 탐색 방법
- 오름차순으로 정렬되었다고 가정
탐색 방법
- 배열의 가운데 원소와 탐색키 x를 비교
- 탐색키 = 가운데 원소 -> 탐색 성공
- 탐색키 < 가운데 원소 -> 이진 탐색(크기 1/2의 왼쪽 부분배열) 순환 호출
- 탐색키 > 가운데 원소 -> 이진 탐색(크기 1/2의 오른쪽 부분배열) 순환 호출
- 탐색을 반복할 때마다 대상 원소의 개수가 1/2씩 감소
- 배열의 가운데 원소와 탐색키 x를 비교
분할: 배열의 가운데 원소를 기준으로 왼쪽과 오른쪽 부분배열로 분할. 탐색키와 가운데 원소가 같으면 해당 원소의 배열 인덱스를 반환 종료
정복: 탐색키 x가 가운데 원소보다 작으면 왼쪽 부분배열을 대상으로 이진 탐색을 순환 호출, 크면 오른쪽 부분배열을 대상으로 이진 탐색을 순환 호출
결합: 부분배열에 대한 탐색 결과가 직접 반환되므로 결합이 불필요
알고리즘 순환 형태
function BinarySearch(arr, left, right, x) {if (left > right) return -1;const mid = Math.floor((left + right) / 2);if (x === arr[mid]) return mid;else if (x < mid) BinarySearch(arr, left, mid - 1, x);else BinarySearch(arr, mid + 1, right, x);}
알고리즘 반복 형태
function BinarySearch(arr, n, x) {let left = 0;let right = n - 1;while (left <= right) {const mid = Math.floor((left + right) / 2);if (x === arr[mid]) return mid;else if (x < mid) right = mid - 1;else left = mid + 1;}return -1;}
이진 탐색에서의 분할과 비교
입력 크기 n 일때 최대 분할 횟수: n / 2^k = 1 -> k = log n
최대 비교 횟수: 최대 분할 횟수 + 1
성능 분석
- T(n) = 입력 크기 n에 대한 탐색 과정에서의 모든 비교 횟수의 합 = 맨 바깥 수준에서의 비교 횟수 + 순환 호출에서의 비교 횟수: T(n) = logn
특징
입력이 정렬된 리스트에 대해서만 적용 가능
삽입/삭제 연산 시 데이터의 정렬 상태 유지가 필요
- 평균 n/2개의 데이터 이동이 발생 -> 삽입/삭제가 빈번한 응용에는 부적합
퀵 정렬
개념과 원리
특정 원소를 기준으로 주어진 배열을 두 부분배열로 분할하고 각 부분배열에 대해서 퀵 정렬을 순환적으로 적용하는 방식
- 오름차순으로 정렬한다고 가정
피벗
- 두 부분배열로 분할할 떄 기준이 되는 특정 원소
- 보통 주어진 배열의 첫 번쨰 원소로 지정
- 두 부분배열로 분할할 떄 기준이 되는 특정 원소
피벗이 제자리를 잡도록 하여 정렬하는 방식
- 왼쪽 부분배열의 모든 값 < 피벗 < 오른쪽 부분배열의 모든 값
분할: 피벗을 기준으로 주어진 배열을 두 부분배열로 분할
정복: 두 부분배열에 대해서 퀵 정렬을 순환적으로 적용하여 각 부분배열을 정렬
결합: 필요 없음
알고리즘 퀵 정렬
function QuickSort(arr, n) {if (n > 1) {const pivot = Partition(arr[0, ... , n - 1], n); // 두 부분배열로 분할QuickSort(arr[0, ... , pivot - 1], pivot); // 왼쪽 부분배열에 대한 순환 호출QuickSort(arr[pivot + 1, ... , n - 1], n - pivot -1); // 오른쪽 부분배열에 대한 순환 호출}}function Partition(arr, n) {let left = 1;let right = n- 1;while(left < right) { // 피벗 arr[0]while(left < n && arr[left] < arr[0]) left++;while(right > 0 && arr[right] >= a[0]) right--;if (left < right) swap(arr[left], arr[right]);else swap(arr[0], arr[right])}return right;}
성능 분석
- 퀵 정렬 QuickSort 수행 시간
- 한 번의 분할 Partition + 두 번의 QuickSort 순환 호출
성능 분석 최악의 경우
피벗만 제자리를 잡고, 나머지 모든 원소가 하나의 부분배열로 분할되는 경우
극심한 불균형적 분할
- 피벗만 제자리를 잡고, 나머지 모든 원소가 하나의 부분배열이 되는 경우
- 피벗이 항상 부분배열의 최솟값 또는 최댓값이 되는 경우
- 입력 데이터가 정렬된 경우 그리고 피벗을 배열의 처음 원소로 지정한 경우
- T(n) = O(n^2)
성능 분석 최선의 경우
피벗을 중심으로 항상 동일한 크기이 두 부분배열로 분할되는 경우
가장 균형적인 분할
- 피벗을 중심으로 항상 동일한 크기의 두 부분배열로 분할되는 경우
- 피벗이 항상 부분배열의 중간값이 되는 경우
- T(n) = O(nlogn)
성능 분석 평균적인 경우
- 부분 배열의 모든 분할 비율에 따른 수행 시간의 평균
- 피벗은 동일한 확률로서 분할 후 배열의 어느 곳에나 위치 가능
- T(n) = O(nlogn)
특징
최선/평균의 경우 -> O(nlogn)
최악의 경우 -> O(n^2)
- 피벗 선택의 임의성만 보장되면 평균적인 성능을 보일 가능성이 매우 높음
- 배열에서 임의로 값을 선택해서 배열의 처음 원소와 서로 교환한 후 정렬 수행
- 피벗 선택의 임의성만 보장되면 평균적인 성능을 보일 가능성이 매우 높음