본문 바로가기

프로그래밍/알고리즘

정렬 알고리즘 - 퀵 정렬

퀵 정렬 (Quick Sort)


분할 정복법

- 분할 : 배열을 아래의 조건이 되도록 두 부분으로 나눈다. (Pivot을 이용한다.)

lower part elements ≤ upper part elements

- 정복 : 각 부분을 순환적으로 정렬한다.

- 합병 : 수행하지 않는다.


퀵 정렬 수행 과정

- 정렬할 배열이 주어진다. 이 때 마지막 수를 기준 (pivot)으로 삼아둔다.

31 8 48 73 11 3 20 29 65 15(pivot)

- 기준보다 작은 수는 기준의 왼쪽에, 나머지는 기준의 오른쪽에 오도록 재배치 (분할)한다.

8 11 3 15 31 48 20 29 65 73

- 기준의 왼쪽과 오른쪽을 각각 순환적으로 정렬한다.


퀵 정렬 슈도 코드

quickSort (A[], p, r) {

if (p < r) {

q = partition (A, p, r); // 분할

quickSort (A, p, q - 1); // 피벗 기준 왼쪽 정렬

quickSort (A, q, r); // 피벗 기준 오른쪽 정렬

} // 만일 p가 r보다 크거나 같다면 정렬할 원소가 0이거나 1이라는 뜻이다.

}

partition (A[], p, r) {

배열 A의 원소들을 A[r]기준으로 양쪽으로 재배치한 후 A[r]이 자리한 인덱스를 반환해준다.

}


- 파티션의 슈도 코드

대략적인 과정으로는 피벗을 맨 뒤에 가만히 둔채로 작은 값과 큰 값을 정렬한 후 큰 값중 첫번째 인덱스와 피벗의 위치를 바꾸어주는 것이다.

i는 기준보다 작은 값들 중 마지막 값이며, j는 피벗과 비교하려는 값을 의미한다.

partition {

if A[j] >= x

j <- j + 1;

else

j <- i + 1;

exchange A[i] and A[j];

j <- j + 1;

}

- 피벗을 이용한 파티션 과정


Partition (A, p, r) {

x <- A[r];

// 초기에는 피벗보다 작은 값이 있는지 모르기 때문에 p의 최소값보다 1 더 적게 하여 아예 밖을 가리키게 한다.

i <- p - 1; 

for j <- p to r -1

if A[j] ≤ x

i <- i + 1;

exchange A[i] and A[j];

exchange A[i + 1] and A[r];

return i + 1;

}

-> 파티션 함수가 동작하는 시간은 데이터를 n개라 하였을 때 모든 데이터를 한 번씩 비교해야 하기 때문에 n - 1의 시간이 걸리게 되고, 최종적으로는 O(n)의 시간이 걸린다고 할 수 있다.


퀵 정렬이 최악의 경우가 나올 때

- 항상 한 쪽은 0개, 다른 쪽은 n - 1개로 분할되는 경우

T(n) = T(0) + T(n - 1) + Θ(n)

= T(n - 1) + Θ(n)

= T(n - 2) + T(0) + Θ(n - 1) + Θ(n)

...

= Θ(1) + Θ(2) + ... + Θ(n - 1) + Θ(n)

= Θ($n^2$)

- 이미 정렬된 입력 데이터 (만일 마지막 원소를 피벗으로 설정하였을 때)

- 최악의 경우 합병 정렬의 시간복잡도인 O(n logn)과 비교했을 때 더 효율이 떨어지는 결과라고 할 수 있다.


퀵 정렬이 최선의 경우가 나올 때

- 항상 정확하게 절반으로 분할되는 경우

T(n) = 2T(n / 2) + Θ(n)

= Θ(n logn)

- 합병 정렬과 같은 루틴을 보이게 된다.

- 퀵 정렬이 많이 쓰이는 이유는 최악의 경우에는 시간복잡도가 $n^2$임에도 실제로는 그런 상황이 거의 발생하지 않기 때문에 


평균적인 시간 복잡도

- 일반적인 다른 정렬들에 경우에는 평균 시간 복잡도를 구하기 어렵다는 이유로 잘 계산하지 않는다. 하지만 퀵 정렬에 경우에는 알고리즘이 비교적 단순하기 때문에 평균 시간 복잡도를 구하기에 어려움이 적다.

- A(n) = $\sum _{ \forall InputInstanceIOfSizeN }^{  }{ p(I)T(I) } $

(p(I) : I가 입력으로 들어올 확률 / T(I) : I를 정렬하는 데 걸리는 시간)

데이터가 n개라고 할 때 가능한 모든 입력 I에 대해 p(I)와 T(I)의 곱의 합들이다.

- 위의 식에서 정확한 p(I)값을 모른다는 문제점이 있다.

- 문제점을 해결하기 위해 p(I)에 대한 가정을 한 후 분석을 수행하게 된다.

- Ex) 모든 입력 인스턴스가 동일한 확률을 가진다고 할 때 $p(I) = \frac { 1 }{ n! }$


Pivot을 선택하는 방법들

- 첫번째 값이나 마지막 값을 피벗으로 선택

1) 이미 정렬된 데이터나 거꾸로 정렬된 데이터가 최악의 경우

2) 현실의 데이터는 무작위가 아니기 때문에 (거꾸로)정렬된 데이터가 입력으로 들어올 가능성은 매우 높다.

3) 이 방식은 좋은 방식이라고 할 수 없음

- "Median of Three"

1) 첫번째 값과 마지막 값, 그리고 가운데 값 중에 중간값(median)을 피벗으로 삼는다.

2) 최악의 경우가 발생했을 때의 시간복잡도는 위의 방식과 차이가 없다.

- Randomized Quicksort

1) 피벗을 무작위 위치에서 선택한다.

2) no worst case instance, but worst case execution

3) 평균 시간복잡도는 O(n logn)이 된다.


코드 구현

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.Arrays;
import java.util.StringTokenizer;
 
//2019-05-27
//퀵 정렬을 구현한 프로그램이다.
//합병정렬과 마찬가지로 ArrayOutOfBoundException으로 인해
//배열의 처음과 끝 인덱스를 계산하는데 시간을 많이 소요하였다.
public class QuickSort {
    static BufferedWriter bw;
    static int[] inputArray;
    
    public static void main(String[] args) throws Exception {
        init();
        sorting(inputArray, 1, inputArray.length);
        printArray(inputArray);
    }
    
    static void init() throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        bw = new BufferedWriter(new OutputStreamWriter(System.out));
        
        bw.write("정렬하고자 하는 수를 입력해주세요 : ");
        bw.flush();
        
        int index = 0;
        String input = br.readLine();
        StringTokenizer st = new StringTokenizer(input);
        inputArray = new int[st.countTokens()];
        
        while (st.hasMoreTokens())
            inputArray[index++= Integer.parseInt(st.nextToken());
    }
    
    static void printArray(int[] array) throws Exception {
        bw.write("정렬된 결과 배열은 " + Arrays.toString(array) + "입니다.");
        bw.close();
    }
    
    static void sorting(int[] inputArray, int start, int end) {
        int middle;
        
        if (start < end) {
            middle = partition(inputArray, start, end);
            sorting(inputArray, start, middle - 1);
            sorting(inputArray, middle, end);
        }
    }
    
    static int partition(int[] array, int start, int end) {
        int comp = array[end - 1];
        int i = start - 1;
        
        for (int j = start - 1; j < end; j++) {
            if (array[j] < comp) {
                change(array, i, j);
                i++;
            }
        }
        
        change(array, i, end - 1);
        
        return i + 1//피벗의 위치는 i + 1번째에 위치
    }
    
    static void change(int[] array, int a, int b) {
        int tmp = array[a];
        array[a] = array[b];
        array[b] = tmp;
    }
}
 
cs


'프로그래밍 > 알고리즘' 카테고리의 다른 글

힙 정렬에 대해(2)  (0) 2019.05.24
힙 정렬에 대해(1)  (0) 2019.05.23
정렬 알고리즘 - 합병 정렬  (0) 2019.05.20
기초 정렬 알고리즘  (0) 2019.05.18
Recursion 응용(4)  (0) 2019.05.17