Divide and Conqure Algorithm(2)
퀵 정렬
- 특정 원소를 기준으로 주어진 배열을 두 부분배열로 분할, 각 부분배열에 대해서 퀵 정렬을 순환적으로 적용하는 방식
- 오름차순으로 정렬한다고 가정
-
피벗
: 주어진 배열을 두 부분배열로 분할할 때 기준이 되는 특정 원소
- 보통 주워진 배열의 첫번째 원소로 지정한다.
개념과 원리
피벗이 제자리를 잡도록 하여 정렬하는 방식
왼쪽 부분배열의 모든 값 < **피벗** < 오른쪽 부분배열의 모든 값
- 분할: 피벗을 기준으로 주어진 배열을 두 부분배열로 분할한다.
- 정복: 두 부분배열에 대해서 퀵 정렬ㄹ을 순환적으로 적용하여 각 부분배열을 정렬한다.
- 결합: 필요 없음
알고리즘
피벗 위치를 중심으로 분할함수 실행 > 중간 index를 중심으로 다시 중간 index로 퀵정렬 호출 반복
QuickShort(A[], n)
{
if (n>1) {
pivot = Partition(A[0..n-1], n); // 두 부분배열로 분할
QuickSort(A[0...pivot-1], pivot); // 왼쪽 부분배열에 대한 순환 호출
QuickSort(A[pivot+1...n-1], n-pivot-1); // 오른쪽 부분배열에 대한 순환 호출
}
}
분할함수
int Partiction(A[], n)
{
Left = 1; Right = n-1;
while ( Left < Right ) { //피벗 A[0]
//피벗보다 큰 값의 위치를 찾음
while ( Left < n && A[Left] < A[0] ) Left++;
//피벗보다 작은 값의 위치를 찾음
while ( Right > 0 && A[Right] >= A[0] ) Right--;
if ( Left < Right ) 교환(A[Left] ⇔ A[Right])
else 교환(A[0] ⇔ A[Right]) }
return Right; // 분할된 값 반환
}
분할함수 Partition() 수행시간
→ O(n) 혹은 theta(n)
⇒데이터 계수에 선행적으로 비례하는 시간
퀵 정렬 QuickSort() 수행시간
T(n) = T(a) + T(b) + O(n)
→ T(n) = 오른쪽 정렬 시간 + 왼쪽 정렬 시간 + 피봇분할시간
T(1) = O(n)
분할된 두 부분배열의 크기에 따라 성능시간이 달라짐
성능분석
최악의 경우
-
배열이 항상 0:n 또는 n-1:0으로 분할되는 경우
→ 피벗만 제자리 잡고 왼쪽 오른쪽이 부분배열되는 경우
피벗이 늘 가장 최댓값이나 가장 최솟값이 될 때
즉, 극심한 균형적 분할
입력 데이터가 정렬된 경우, 그리고 피벗을 배열의 처음 원소로 정한 경우
T(n) = T(n-1) + O(n)
→ T(n) = O(n2)
최선의 경우
-
배열이 항상 n/2:n/2로 분할되는 경우
→ 피벗을 중심으로 항상 동일한 크기의 두 부분배열로 분할되는 경우
피벗이 항상 부분배열의 중간값이 되는 경우
합병정렬
- 분할: 배열을 동일한 크기의 두 개의 부분배열로 분할하고(왼쪽 오른쪽) - 입력크기 n인 배열을 n/2인 두 부분배열로 분할
- 정복: 각각의 부분배열을 순환적으로 정렬한 후,
- 결합: 정렬된 두 부분배열을 합병하여 하나의 정렬된 배열을 만듦
→ 더이상 분할이 불가능할 때까지 자르고(n/2 반복) 이후 각각을 합병
- 재귀
MergeSort(A[], n)
{
if (n > 1) {
Mid = n/2;
B[0...Mid-1] = MergeSort(A[0...Mid-1], Mid); // 왼쪽 부분배열
C[0...n-Mid-1] = MergeSort(A[Mid...n-1], n-Mid);// 오른쪽 부분배열
A[0...n-1]=Merge(B[0...Mid-1], C[0...n-Mid-1], Mid, n-Mid);
}
return A;
}
- 반복
Merge (B[], C[], n, m) {
i = j = k = 0;
while ( i<n && j<m ) {
if ( B[i] <= C[j] ) A[k++] = B[i++];
else
A[k++] = C[j++];
//A[n+m-1] ← B[n] + C[m]
//비교해서 작은 값을 A[]로 이동
}
for ( ; i<n; i++) A[k++] = B[i]; //남은 B[]의 데이터 → A[] for ( ; j<m; j++) A[k++] = C[j]; //남은 C[]의 데이터 → A[] return A[0..n+m-1];
}
- 합병함수 Merge() 수행 시간
- B[n/2] + C[n/2] → A[n]
-
비교횟수:
n/2 ~ (n/2 + n/2 - 1 = n - 1)
⇒ n/2 (최소) ~ n-1 (최대) 비교
⇒
O(n)
- 입력 데이터 개수 n만큼의 추가 저장 장소(B[] + c[])가 필요
- 점화식
⇒ T(n) = T(n/2) + T(n/2) + O(n)
, T(1) = 1
(왼쪽 정렬 + 오른쪽 정렬 + 합병)
⇒ T(n) = 2T(n/2) + O(n)
⇒ T(n) = O(nlogn)
-
비순환적 합병 정렬 (반복문)
→ 분할 과정 X 합병만을 반복해서 합병병렬 수행
선택문제
: n개의 원소가 임의의 순서로 저장된 배열A[0..n-1]에서 i번째로 작은 원소를 찾는 문제
-
직관적 방법
1)오름차순으로 정렬한 후 i번째 원소를 찾는 방법 →
O(nlogn)
2)최솟값을 찾는 과정을 i번 반복(i-1번째까지는 최솟값 찾은 후 삭제) →
O(in)
- 알고리즘 1) → 최악 O(n2), 평균 O(n)
- 알고리즘 2) → 최악 O(n), 평균 O(n)
1) 최솟값을 찾은 후 최댓값 찾기 (or 반대)
-
n - 1번 비교 + n - 2번 비교
⇒ 2n - 3번의 비교 ⇒
O(n)
2) 모든 원소를 두 개씩 짝을 이루어 동시에 최솟값/최댓값 비교
-
3/2n - 2번 비교
FindMinMax (A[], n, min, max) { if (A[0]<A[1]) { min = A[0]; max = A[1]; } else { min = A[1]; max = A[0]; } for (i=2; i<n; i+=2) { if (A[i]<A[i+1]) { small = A[i]; large = A[i+1]; } else { small = A[i+1]; large = A[i]; } if (small < min) min = small; if (large > max) max = large; } }
정리!
i번째로 작은 원소 찾기 > 최악 O(n2) 평균 O(n)
- 알고리즘 1
-
퀵 정렬의 분할함수 Partiion()을 순환적으로 적용하는 방식
1) i = pivot
2) i < pivot → 왼쪽 부분배열 순환적용
3) i > pivot → 오른쪽 부분배열 순환적용
- 개념 원리
- 분할: 피벗 기준으로 두 부분배열 분할, i가 피벗의 p와 같으면 피벗 값 반환 종료
- 정복: 인덱스 i가 포함된 부분배열에 대해서 알고리즘을 순환적으로 호출
- 결합: x
-
예제 알고리즘
int Selection (A[], n, i) // 1 ≤ i ≤ n { Left = 0; Right = n-1; p = Partition(A, n); // 0 ≤ p ≤ n-1 if (i == p+1) return A[p]; else if ( i<p+1 ) Selection(A[Left..p-1], (p-1)-Left+1, i); else Selection(A[p+1..Right], Right-(p+1)+1, i-p-1); }
- 성능분석
- 최악의 경우 = 퀵 정렬의 최악의 경우
- 분할함수가 항상 하나의 부분배열만 생성하는 경우
- 최악의 경우 = 퀵 정렬의 최악의 경우
-