Study

[데이터구조 & 알고리즘] Quick Sort - 퀵 정렬 개요

Devailean 2021. 2. 25. 16:23

배열을 정렬하는 데는 여러 가지 방법이 있다.

정렬이란 순서대로 나열하는 것을 의미한다.

 

[3, -2, -1, 0, 2, 4, 1]

위 배열을 정렬하면 

[-2, -1, 0, 1, 2, 3, 4]

가 된다.

 

그 중 Quick Sort(퀵 정렬)에 대해 알아보자.

퀵 정렬은 pivot을 정해 배열을 분할하여 정렬하는 방식이다.

 

 

코드는 아래와 같다.

 

def qs(arr, l, r):
	if l >= r: // 작거나 같으면 정렬할 필요 없음
    	 return
    p = partition(arr, l, r)
    
    qs(arr, l, p - 1)
    qs(arr, p + 1, r)

 

l, r은 정렬할 범위를 나타내는 인덱스이다.

l = r 이면 요소가 같아 정렬할 필요가 없고,

r < l 이면 정렬할 요소가 없다.

두 가지 경우에는 반환하여 끝난다.

 

그 다음 재귀함수(recurssive function)를 사용하여 정렬해준다.

3가지 인수(arr, l, r)를 받는 파티션 함수(분할)를 사용한다.

 

def qs(arr, l, r):
	if l >= r: // 작거나 같으면 정렬할 필요 없음
    	 return
    p = partition(arr, l, r)
    // 위의 파티션 함수 설명

파티션 함수는 첫 번째로 pivot을 정한다. pivot은 주로 마지막 인덱스인 r로 정해진다.

 

그러면 pivot보다 작은 숫자큰 숫자가 그룹으로 나누어지고

작은 숫자pivot의 왼쪽에 위치하게 되며

큰 숫자pivot의 오른쪽에 위치하게 된다.

[3, -2, -1, 0, 2, 4, 1]
// partition(arr,0,7) 함수 실행 후
[-2, -1, 0, 1, 2, 4, 3]

위를 보면 pivot이 1인 경우

왼쪽에는 1보다 작은 숫자, 오른쪽에는 1보다 큰 숫자가 오는 것을 볼 수 있다.

두 그룹 내 순서는 신경쓰지 않는다.

그 뒤 pivot(1)의 인덱스를 반환한다(위의 경우 3).

 

p = partition(arr, l, r)
// 반환된 인덱스 값을 p에 할당

이 함수의 실행이 끝나면 pivot의 위치가 확정된다.

파티션 함수를 실행한 뒤에는 양 옆 그룹 내 순서를 정렬해주기만 하면 된다.

 

위의 그림과 같이 pivot이 1일때 파티션을 나누면 (1)과 (2)의 그룹으로 나누어지고

재귀함수로 (1)그룹과 (2)그룹의 순서를 정렬해준다.

 

이제 다시 퀵정렬 코드를 살펴보자.

def qs(arr, l, r):
	if l >= r: // 작거나 같으면 정렬할 필요 없음
    	 return
    p = partition(arr, l, r)
    //l,r 범위에서 pivot을 기준으로 큰 수와 작은 수를 나누어 pivot의 인덱스 값을 p에 할당
    
    
    qs(arr, l, p - 1) //사진의 1부분 정렬(재귀함수), 위에서 반환된 피벗 왼쪽 정렬
    qs(arr, p + 1, r) //사진의 2부분 정렬(재귀함수), 위에서 반환된 피벗 오른쪽 정렬

위와 같은 방식으로 재귀함수가 호출되어 전체 배열이 정렬된다.

pivot을 정하여 양쪽 그룹을 나누고, 또 각 양쪽 그룹마다 pivot을 정하여 그룹을 나누어 가다보면

전체 배열이 정렬된다는 원리이다.

 

그럼 partition 함수는 어떻게 작동하는지 알아보자. 

그림과 같이

10이 피벗일 때 i,j 인덱스를 사용한다.

l 부터 r-1(피벗 직전)까지 배열의 요소를 검사한다.

i까지는 피벗보다 작은 그룹, j까지는 피벗과 같거나 큰 그룹

즉, i는 피벗보다 작은 숫자를 가르켜야 하고 j는 피벗보다 큰 숫자를 가르켜야 한다.

 

만약 j가 피벗보다 작은 숫자를 가르키면 j가 가르키는 요소와 i의 다음 [i+1] 인덱스의 요소와 바꾼다.

그렇게 i과 j를 비교해 나가면 두 그룹으로 나뉜다. 

마지막으로 피벗을 i+1 배열의 인덱스(피벗보다 작은 요소 다음 인덱스)와 바꿔주면 피벗은 중간으로 들어가게 된다.

 

배열, l, r의 3개의 인수를 받는 partition 함수의 코드를 살펴보자.

def partition(arr, l, r):
	pivot arr[r]
    i = l - 1;
    for j from l upto r - 1;
    	if arr[j] < pivot:
        	i += 1
            swap arr[i] and arr[j]
    swap arr[i+1] and arr[r]
    return i + 1 //index of the pivot

 

 

퀵정렬 재귀함수가 이해되지 않는다면 다음 이미지를 참조하자.

아래 이미지는 내가 작성한 방식과는 약간 다른다.

피벗을 기준으로 양쪽에서 줄어들며 서로 비교하는 방식인데 딱히 다른 건 없다.

중요한건 피벗을 기준으로 작은 수와 큰 수를 나누어 두가지 그룹으로 분리한다는 것이 핵심이다.

Quick Sort(퀵정렬) Java(자바) 코드

package src;

public class TestQuicksort {
    public static void main(String[] args) {
        int[] a1 = {1, 2, 3};
        int[] a2 = {3, 2, 1};
        int[] a3 = {};
        int[] a4 = {3, 1, -1, 0, 2, 5};
        int[] a5 = {2, 2, 1, 1, 0, 0, 4, 4, 2, 2, 2};
        int[] a6 = {0};
        int[] a7 = {3, -2, -1, 0, 2, 4, 1};
        int[] a8 = {1, 2, 3, 4, 5, 6, 7};
        int[] a9 = {2, 2, 2, 2, 2, 2, 2};

        int[] a1Sorted = {1, 2, 3};
        int[] a2Sorted = {1, 2, 3};
        int[] a3Sorted = {};
        int[] a4Sorted = {-1, 0, 1, 2, 3, 5};
        int[] a5Sorted = {0, 0, 1, 1, 2, 2, 2, 2, 2, 4, 4};
        int[] a6Sorted = {0};
        int[] a7Sorted = {-2, -1, 0, 1, 2, 3, 4};
        int[] a8Sorted = {1, 2, 3, 4, 5, 6, 7};
        int[] a9Sorted = {2, 2, 2, 2, 2, 2, 2};

        quicksort(a1);
        quicksort(a2);
        quicksort(a3);
        quicksort(a4);
        quicksort(a5);
        quicksort(a6);
        quicksort(a7);
        quicksort(a8);
        quicksort(a9);

        assert arrayEquals(a1, a1Sorted);
        assert arrayEquals(a2, a2Sorted);
        assert arrayEquals(a3, a3Sorted);
        assert arrayEquals(a4, a4Sorted);
        assert arrayEquals(a5, a5Sorted);
        assert arrayEquals(a6, a6Sorted);
        assert arrayEquals(a7, a7Sorted);
        assert arrayEquals(a8, a8Sorted);
        assert arrayEquals(a9, a9Sorted);
        System.out.println("If you didn't get an assertion error, this program has run successfully.");
    }

    static void quicksort(int[] arr) {
        qs(arr, 0, arr.length - 1);
    }

    static void qs(int[] arr, int l, int r) {
        if (l >= r) {
            return;
        }
        int p = partition(arr, l, r);

        qs(arr, l, p - 1);
        qs(arr, p + 1, r);
    }

    static int partition(int[] arr, int l, int r) {
        int pivot = arr[r];
        int i = l - 1;
        for (int j = l; j < r; j++) {
            if (arr[j] < pivot) {
                i += 1;
                int temp = arr[i];
                arr[i] = arr[j];
                arr[j] = temp;
            }
        }
        int temp = arr[i + 1];
        arr[i + 1] = arr[r];
        arr[r] = temp;
        return i + 1;
    }

    // 두 배열이 동일한 지 다른 지 검사
    static boolean arrayEquals(int[] a1, int[] a2) {
        if (a1.length != a2.length) {
            return false;
        }
        for(int i = 0; i < a1.length; i++) {
            if (a1[i] != a2[i]) {
                return false;
            }
        }
        return true;
    }
}

 

Time Complexity(시간 복잡도)

 

1. Worst Case

 

[1, 2, 3, 4, 5, 6, 7] - 매차례 돌아가면서 비교해야할 때

[2, 2, 2, 2, 2, 2, 2]

 

 qs(arr, 0, 6) -> qs(arr, 0, 5) -> qs(arr, 0, 4) -> ... -> qs(arr, 0, 1)

 

2. Best Case & Average Case

 

[-2, 3, -1, 5, 4, -3, 0] - pivot 보다 작은 수와 큰 수가 동일하게 있을 때

 

 

 


1. Pivot을 선택하는 방법

- 마지막 요소 선택

- 랜덤 요소 선택(위의 퀵소트 정렬을 하기 전에 배열을 셔플하여 실행)

- 3개의 요소를 랜덤으로 선택한 후 중간 요소를 선택

 

2. 중복을 해결하는 방법

- 3-Way Quicksort. (Less/Equal/High)

www.geeksforgeeks.org/3-way-quicksort-dutch-national-flag/

 

3-Way QuickSort (Dutch National Flag) - GeeksforGeeks

A Computer Science portal for geeks. It contains well written, well thought and well explained computer science and programming articles, quizzes and practice/competitive programming/company interview Questions.

www.geeksforgeeks.org

 

퀵 정렬의 루프는 컴퓨터 아키텍처에서 효율적으로 작동하도록 설계되어 있다.
메모리 참조가 지역화되어 있어 CPU 캐시의 히트율이 높아지기 때문에 worst case의 시간 복잡도에도 불구하고 많이 사용한다.