왼쪽 부분배열의 모든 값 < 피벗 < 오른쪽 부분배열의 모든 값
/* n: 배열의 길이 */
// 배열 값 교환
static void swap(int[] arr, int idx1, int idx2) {
int temp = arr[idx1];
arr[idx1] = arr[idx2];
arr[idx2] = temp;
}
// 두 부분배열로 분할
static int partition(int[] arr, int left, int right) {
int x = arr[left]; // 피벗
int pl = left+1; // 왼쪽 커서
int pr = right; // 오른쪽 커서
// 1-1. 피벗 x를 기준으로 배열 arr 분할
while (pl <= pr){
// 피벗 x보다 큰 값의 위치 찾기
while (pl <= right && arr[pl] < x) {
pl++;
}
// 피벗 x보다 작은 값의 위치 찾기
while (pr >= left+1 && arr[pr] > x) {
pr--;
}
if (pl <= pr) {
swap(arr, pl++, pr--); // arr[pl]과 arr[pr] 교환
}
}
// 1-2. 피벗 위치 조정
swap(arr, left, pr);
return pr; // 피벗 위치 반환
}
static void quickSort(int[] arr, int left, int right) {
if(left < right) {
int x = partition(arr, left, right); // 1. 배열 분할 후 피벗 위치 얻음
quickSort(arr, left, x-1); // 2. 왼쪽 부분배열에 대한 순환 호출
quickSort(arr, x+1, right); // 3. 오른쪽 부분배열에 대한 순환 호출
}
}
1. partition()
메소드를 실행하여 배열을 두 부분배열로 분할하고 피벗 위치를 얻어온다. 피벗은 현재 배열의 맨 처음 요소로 지정한다(x = arr[left]
). 피벗을 제외한 나머지 부분을 정렬하기 위해 피벗의 다음 요소를 왼쪽 커서로(arr[left+1]
), 배열의 맨 마지막 요소를 오른쪽 커서로(arr[right]
) 지정한다.
1-1. 왼쪽 커서는 피벗(이하 x
)보다 큰 값을 찾을 때까지 오른쪽으로 이동하고(pl++
), 오른쪽 커서는 x
보다 작은 값을 찾을 때까지 왼쪽으로 이동한다(pl--
). x
를 기준으로 왼쪽은 보다 작은 값, 오른쪽은 x
보다 큰 값이 와야하므로, 방금 찾은 두 값(arr[pl]
, arr[pr]
)의 위치를 조정해줘야 한다. 왼쪽 커서와 오른쪽 커서의 위치가 x
를 기준으로 교차하지 않은 상태라면(pl <= pr
), arr[pl]
와 arr[pr]
를 서로 교환해준다. 이 과정은 왼쪽 커서와 오른쪽 커서의 위치가 교차하지 않는 동안 반복한다.
1-2. 왼쪽 커서와 오른쪽 커서의 위치가 x
를 기준으로 교차한 상태라면(pl > pr
), 현재 피벗(arr[left]
)과 오른쪽 커서의 값(arr[pr]
)을 교환해주어 피벗을 알맞은 위치로 이동시켜주고, 오른쪽 커서의 값은 배열의 맨 앞으로 이동하여 다음 피벗으로 사용할 수 있도록 한다. 이 과정이 끝나면, 현재 피벗의 위치를 반환한다.
2. 얻어온 피벗의 위치를 기준으로 배열의 크기가 0이나 1이 될때까지 왼쪽 배열을 과정 1을 반복하여 정렬한다. 왼쪽 배열의 범위는 원래 배열의 맨 앞 요소(arr[0]
)부터 피벗의 바로 앞 요소(arr[x-1]
)까지이다.
3. 얻어온 피벗의 위치를 기준으로 배열의 크기가 0이나 1이 될때까지 오른쪽 배열을 과정 1을 반복하여 정렬한다. 오른쪽 배열의 범위는 피벗의 바로 다음 요소(arr[x+1]
분할된 오른쪽 배열의 마지막 요소(arr[right]
) 까지이다.
4. 위의 모든 과정을 수행하면 정렬이 완료된다.
Partition()
) 수행 시간Θ(n)
QuickSort()
) 수행 시간static void quickSort(int[] arr, int left, int right) {
if(left < right) {
int x = partition(arr, left, right); // T(n)
quickSort(arr, left, x-1); // T(left)
quickSort(arr, x+1, right); // T(right)
}
}
피벗만 제자리를 잡고 나머지 모든 원소가 하나의 부분배열이 되는 경우
피벗이 항상 부분배열의 최솟값 또는 최댓값이 되는 경우
입력데이터가 정렬된 상태 + 피벗을 배열의 처음 원소로 정한 경우
10
20
30
40
50
=> 0 : n-1
10
20
30
40
50
=> 1:4 → 4
10
20
30
40
50
=> 2:3 → 3
10
20
30
40
50
=> 3:2 → 2
10
20
30
40
50
=> 4:1 → 1
10
20
30
40
50
50
40
30
20
10
=> n-1 : 0
10
40
30
20
50
=> 4:1 → 4
10
40
30
20
50
=> 3:2 → 3
10
20
30
40
50
=> 2:3 → 2
10
20
30
40
50
=> 1:4 → 1
10
20
30
40
50
- (n-1) + (n-2) + (n-3) + ... + 1 = n(n-1)/2
- T(n) = T(n-1) + T(0) + Θ(n) (n>0), T(0)=0
T(n) = T(n-1) + Θ(n)
∴ 시간복잡도 O(n^2)
n/2:n/2
피벗을 중심으로 항상 동일한 크기의 두 부분배열로 분할되는 경우
피벗이 항상 부분배열의 중간값이 되는 경우
30
45
35
40
20
15
25
20
25
15
30
40
35
45
=> 4
15
20
25
30
40
35
45
=> 2
15
20
25
30
35
40
45
=> 2
15
20
25
30
35
40
45
T(n) = T(⌊n-2⌋) + T(⌊n-2⌋) + Θ(n) (n>1), T(1)=1
T(n) = 2T(n-2) + Θ(n)
∴ 시간복잡도 O(nlogn)
피벗이 동일한 확률로 분할 후 배열의 어느 곳에나 위치하는 경우
📖 참고
- Bohyoh Shibata. (2020). Do it! 자료구조와 함께 배우는 알고리즘 입문 - 자바편(강민 옮김). 이지스퍼블리싱
- 이관용. "알고리즘 3강. 분할정복 알고리즘 (1)" 방송통신대학교, 2022년.