본문 바로가기
컴퓨터공학/자료구조 및 알고리즘

Quick sort

by 유리병 2022. 7. 4.

퀵정렬은 최악의 경우 O(n^2)의 수행시간으로, 평균 O(n log n)의 수행시간으로 배열을 정렬하는 알고리즘이다. 

O(n log n) 내에 숨겨진 상수인자가 매우 작아 속도 면에서 매우 효율적이며, 내부 정렬로 작동하기 때문에 실제 문제에서 가장 유용한 정렬 방법 중 하나이다. 

(내부정렬이란, 정렬하고자 하는 배열 이외의 추가적인 배열공간을 필요로 하지 않는 것을 의미한다.)

 

퀵정렬은 사전에 배열이 보다 많이 정렬되어 있을수록, 배열 내의 겹치는 원소 수가 많아질 수록 수행시간이 O(n^2)에 가까워진다. 

이에 대한 자세한 내용은 후술한다. 

 

퀵정렬은 divide&conquer방식으로 작동한다. 

A[first~last]의 본래 배열을 A[first~mid-1] A[mid+1~ last]로 분할한다. 

분할 과정에서 A[first~mid-1]의 배열에는 A[mid]보다 작은 원소들이, A[mid+1,~ast]의 배열에는 A[mid]보다 큰 원소들이 배열된다. 

 

quick_sort(A,first,last)
    if first<last
        mid=partition(A,first,last)
        quick_sort(first,mid-1)
        quick_sort(mid+1,last)
partition(A,first,last)
    i=first-1
    x=A[last]
    for j=first to last-1
        if A[j]<x
            i++
            SWAP(A[i],A[j])
    i++
    SWAP(A[i],A[last])
    return i

 

퀵정렬의 성능

분할을 가장 나쁘게 하는 경우

퀵정렬에서 최악의 경우는 분할 과정에서 원래 문제를 n-1개의 원소와 0개의 원소를 갖는 부분문제로 분할하는 것이다. 

이 경우 O(n^2)의 수행시간이 걸린다. 

이 경우는 입력배열이 거의 다 정렬되어있을 경우 발생하게 되는데, 이런 경우는 insertion sort를 이용할 경우 오히려 O(n)시간에 정렬이 가능하다.

 

분할을 가장 잘 하는 경우

가장 고른 분할은 본래 문제를 거의 같은 크기의 두 개의 부분문제로 나누는 경우이다. 

이 경우 O(n log n)의 수행시간에 정렬이 가능하다.

 

평균적인 수행시간에 대하여

퀵 정렬의 평균 수행시간은 최적의 경우와 더 가깝다.

이를 이해하기 위해서는 분할의 균형 정도가 수행속도에 어느정도의 영향을 미치는지를 알아봐야 한다. 

 

가령, 분할 알고리즘이 문제를 항상 9:1로 분할하는 경우를 생각해보자. 

언뜻 보기에 매우 불균등하게 보이지만, 이 경우 퀵 정렬의 수행시간에 대한 재귀 관계식은 다음과 같다. 

T(n)<=T(9n/10)+T(n/10)+cn

이 경우, 퀵정렬의 수행시간은 O(n log n)이 된다. 

 

직관적으로 매우 불균등해 보이지만, 매 재귀 단계에서 9:1 비율로 분할된다면, 퀵 정렬은 점근적으로는 정확하게 반반씩 분할할 때 처럼 O(n logn)의 수행시간에 정렬을 완료할 수 있다.

심지어 99:1로 분할하더라도 O(n log n)의 수행시간이 나온다. 

즉, 분할이 100:0으로 분할되지만 않는다면 수행시간은 O(n log n)이 된다. 

가장 불균등하게 분할하는 단계를 몇 개 끼워넣는다 해도 총 수행시간은 여전히 O(n log n)이다. 

랜덤화된 퀵 정렬

상술한대로 거의 정렬된 배열의 경우 O(n^2)의 수행시간이 필요하다.

그러나 알고리즘에 랜덤화기법을 적용함으로써 모든 입력에 대해 성능의 기대값을 높일 수 있다.

 

부분배열 A[first~last]중 하나의 원소를 선택해서 이 원소를 A[last]와 교환하는 것이다.

이렇게 할 경우, 거의 정렬되어있는 배열일지라도 100:0이 아닌 보다 균형잡힌 크기의 부분배열들로 배열을 분할할 수 있고, 점근적으로 수행시간이 O(n logn)에 근접하게 된다. 

 

randomize_partition(A,first,last)
    i=random(first,last)
    SWAP(A[i],A[last])
    return partition(A,first,last)
randomize_quick_sort(A,first,last)
    if(first<last)
        mid=randomize_partition(A,first,last)
        randomize_quick_sort(A,first,mid-1)
        randomize_quick_sort(A,mid+1,last)

 

셋 중 중앙값 분할

랜덤화된 퀵정렬을 개선하기 위한 방법으로, 기준이 되는 원소를 임의로 하나를 선정하는 것이 아니라 보다 신중하게 선택하는 방법이 있다. 

바로 배열 내의 무작위의 3개의 원소를 뽑아 그 중앙값을 기준 원소로 삼는 방법이 있다. 

 

동일한 원소들에 대한 퀵 정렬

퀵정렬의 성능분석에서 보았듯, 100:0으로 분할되는 경우가 많아질수록 퀵정렬의 성능은 안좋아진다.

그렇다면 모든 원소들이 같다면 모든 분할이 100:0으로 분할될 것이고, 총 수행시간은 O(n^2)이 될 것이다.

 

이 문제를 해결하기 위해 partition단계에서 기준 원소와 중복되는 원소들의 위치를 파악한 후, A[first~ mid_first-1]까지는 기준원소보다 작은 원소들을, A[mid_last+1 ~ last]까지는 기준 원소보다 큰 원소들을 정렬시키는 방식으로 보다 수행시간을 단축시킬 수 있다. 

 

quick_sort_for_same_element(A,first,last)
    if first<last
        mid_first,mid_last = partition_for_same_element(A,first,last)
        quick_sort_for_same_element(A,first,mid_first-1)
        quick_sort_for_same_element(A,mid_last+1,last);
partition_for_same_element(A,first,last)
    i=first-1
    k=first;
    x=ary[last];
    
    for j=first to last-1
        if ary[j] == x
            if ary[j]==x
                i++
                SWAP(ary[i],ary[j])
            else
                i++
                SWAP(ary[i],ary[j])
                SWAP(ary[i],ary[k])
                k++
    i++
    SWAP(ary[i],ary[last]);
	mid_first=k
    mid_last=i
    
    return mid_first,mid_last

위와같은 방법을 통해 같은 원소들이 있을 경우, 해당 원소의 뭉텅이들을 완료한 이후, 그것들을 제외한 나머지 부분을 수행하는 형태로 보다 정렬을 효율적으로 수행할 수 있다. 

'컴퓨터공학 > 자료구조 및 알고리즘' 카테고리의 다른 글

Binary Search Tree  (0) 2022.07.08
Selection Problem, 중앙값과 순서통계량  (0) 2022.07.05
Counting sort  (0) 2022.07.05
HEAP  (0) 2022.07.01