4. 퀵 정렬 (Quick Sort)

2022. 2. 1. 22:29프로그래머스

지난 포스팅에서 살펴본 선택, 버블, 삽입 정렬은 모두 시간복잡도 O(N^2)을 가졌습니다. 그렇기 때문에 데이터의 갯수가 많아지면 일반적인 상황에서 사용하기 매우 어렵습니다.

 

이번에 다룰 퀵 정렬(Quick Sort)은 대표적인 분할정복(divide and conquer) 알고리즘으로, 시간복잡도 O(N*logN)을 가져 매우 빠른편으로, 일반적인 상황에서 사용하기 용이합니다.

 

다음의 숫자들을 오름차순으로 정렬하는 프로그램을 작성하시오.
1 10 5 8 7 6 4 3 2 9

 

문제는 동일합니다. 숫자들을 arr 배열이라고 합시다.

퀵 정렬에서는 기준값(피벗) 을 정해놓고 진행합니다. 보통 첫 번째 원소인 1 을 피벗으로 정합니다.

 

                                           <-j
(1) 10  5  8  7  6  4  3  2  9
     i->

 

P1보다 큰 숫자를 왼쪽부터 찾고, 1보다 작은 숫자를 오른쪽부터 찾습니다.

    1보다 큰 숫자의 인덱스: i, 1보다 작은 숫자의 인덱스 : j

 

i > j인 경우, 피벗의 자리와 arr[j]값을 바꿉니다. 그 후 피벗의 왼쪽 배열과 오른쪽 배열에서 다시 P를 실행합니다.

i <= j인 경우, P를 반복합니다.

 

소스코드를 보는게 이해가 빠를 것 같습니다. 윗 부분만으로는 이해하기가 어렵습니다.

 

소스코드

public class TestMain {

    public void quickSort(int start, int end, int[] arr){
        if(start >= end) return;

        int i = start + 1;
        int j = end;
        int key =start;

        while(i<=j){
            while(i<=end && arr[i] <= arr[key]){ // arr[key] 보다 더 큰값 발견
                i++;
            }
            while(j>start && arr[j] >= arr[key]){ // arr[key] 보다 더 작은값 발견
                j--;
            }
            if(i > j){
                int temp = arr[j];
                arr[j] = arr[key];
                arr[key] = temp;
            }
            else{
                int temp = arr[i];
                arr[i] = arr[j];
                arr[j] = temp;
            }
        }

        quickSort(start, j-1, arr);
        quickSort(j+1, end, arr);
    }

    public static void main(String[] args) {

        TestMain t = new TestMain();
        int[] arr1 = {1,10,5,8,7,6,4,3,2,9};
        int[] arr2 = {2,4,6,8,10,5,7,9,1,3};

        t.quickSort(0,9,arr1);
        t.quickSort(0,9,arr2);

        for (int i : arr1) {
            System.out.print(i + "  ");
        }
        System.out.println();
        for (int i : arr2) {
            System.out.print(i+"  ");
        }
    }
}

 

위 소스코드에서

arr[i] > arr[key] 

arr[j] < arr[key] 입니다. (애초에 i와 j의 정의가 이러하니까요.)

 

그런데 i < j 인 경우에는 그저 arr[i]값과 arr[j] 값을 바꿔주기만 하면 됩니다. 애초에 arr[i] > arr[j] 인데,

더 큰 값이 배열의 앞에 위치하기 때문입니다. 이 경우는 while문을 통해 다시 한번 탐색을 합니다.

 

그렇게 가다가 i > j 인 순간이 옵니다. 이 경우는 특별합니다. 이 의미는

(1) : i를 기점으로, i 왼쪽의 숫자들은 pivot 값보다 전부 작다. (i 에서 처음 pivot 보다 큰값이 나타나기 때문

(2) : j를 기점으로, j 오른쪽의 숫자들 중에는 pivot 값보다 전부 크다. (j 에서 처음 작은 값을 발견.)

 

(1) 과 (2) 에 의해서 j 왼쪽의 숫자들이 모두 pivot 보다 작다는 결론이 나옵니다. ( i > j 이기 때문)

j 왼쪽은 pivot 값보다 작고, j 오른쪽은 pivot 값보다 크므로 pivot 이 위치할 곳은 인덱스 j 가 됩니다.

 

따라서 arr[key] 와 arr[j] 의 위치를 바꿔줍니다. 

 

우리는 위에서 arr[key] 가 전체 arr에서 봤을 때 정확한 위치에 위치시켰습니다. 따라서 quickSort 의 범위를 j 왼쪽과 j 오른쪽으로 분리하여 실행을 해줍니다. 정렬이 완료될 때까지.

이렇게 나눠서 실행하는 것을 (divide and conquer) 분할정복이라고 부릅니다.

 

 

시간복잡도

퀵 정렬은 기본적으로 N번 탐색하되 반씩 쪼개 들어가므로 O(N*logN) 입니다.

 

하지만 최악의 경우, 퀵 정렬의 시간복잡도는 O(N^2) 까지도 늘어납니다...

1  2  3  4  5  6  7  8  9  10

위 배열의 경우, divide and conquer를 하여도 연산 횟수가 N + N-1 + ...... + 1 이기 때문입니다. 

배열을 분할하여도 큰 의미가 없게 됩니다.

 

이처럼 퀵 정렬은 평균 시간복잡도는 O(N*logN) 으로

앞서 말한 3개의 정렬 알고리즘보다는 월등히 빠르나, 최악의 경우 O(N^2)이라는 단점이 있습니다.

'프로그래머스' 카테고리의 다른 글

단어 정렬 - 백준 1181  (0) 2022.02.16
5. 병합 정렬( merge sort)  (0) 2022.02.13
3. 삽입 정렬(Insertion Sort)  (0) 2022.01.31
2. 버블 정렬(Bubble Sort)  (0) 2022.01.31
1. 선택 정렬 (Selection Sort)  (0) 2022.01.31