본문 바로가기
코딩테스트/이코테

[CH6 정렬] 기준에 따라 데이터를 정렬 (4)

by 의정부핵꿀밤 2021. 12. 31.
728x90

퀵 정렬

  • 퀵 정렬은 지금까지 배운 정렬 알고리즘 중 가장 많이 사용되는 알고리즘이다!
  • 이 책에서 다루지는 않지만 퀵 정렬만큼 빠른 알고리즘으로 "병합 정렬 알고리즘"이 있다
  • 두 알고리즘은 대부분의 프로그래밍 언어에서 정렬 라이브러리의 근간이 되는 알고리즘이기도 하다
  • 퀵 정렬은 기준을 설정한 다음 큰 수와 작은 수를 교환한 후 리스트를 반으로 나누는 방식으로 동작한다
  • 이 때 기준이 되는 수피벗(Pivot)이라고 표현한다
  • 피벗을 설정하고 리스트를 분할하는 방법에 따라서 여러가지 방식으로 퀵 정렬을 구분하는데, 책에서는 가장 대표적인 분할 방식인 호어 분할(Hoare Partition) 방식을 기준으로 퀵 정렬을 설명한다

 


퀵 정렬의 단계별 설명

호어 분할 방식에서는 아래의 규칙에 따라 피벗을 설정한다

  1. 리스트에서 첫 번째 데이터를 피벗으로 정한다
  2. 피벗을 설정한 후 왼쪽에서부터 피벗보다 큰 데이터를 찾고, 오른쪽에서부터 피벗보다 작은 데이터를 찾는다
  3. 그 다음 큰 데이터와 작은 데이터의 위치를 서로 교환한다

이러한 과정을 반복하면 피벗에 대한 정렬이 수행된다

 

이제 단계별로 하나씩 퀵 정렬 과정을 살펴보자!

다음과 같이 초기 데이터가 있다고 가정해보자

초기 데이터

퀵 정렬은 전체를 3개의 파트로 나눠서 보는게 편하기 때문에 편의상 파트 1, 2, 3으로 나눠서 보자!

 

Part 1

STEP 0

STEP 0) 리스트의 첫 번째 데이터를 피벗으로 설정하므로 피벗은 5이다. 이후에 왼쪽에서부터 5보다 큰 데이터를 선택하므로 7이 선택되고, 오른쪽에서부터 5보다 작은 데이터를 선택하므로 4가 선택된다. 이제 이 두 데이터의 위치를 바꾼다.

 

 

STEP 1

STEP 1) 그 다음 다시 피벗(5)보다 큰 데이터와 작은 데이터를 각각 찾는다. 찾은 뒤에는 두 값의 위치를 서로 변경하는데, 현재 9와 2가 선택되었으므로 이 두 데이터의 위치를 변경한다

 

 

STEP 2

STEP 2) 그 다음 다시 피벗보다 큰 데이터와 작은 데이터를 찾는다. 단 , 현재 왼쪽에서부터 찾는 값과 오른쪽에서부터 찾는 값의 위치가 서로 엇갈린 것을 알 수 있다. 이렇게 두 값이 엇갈린 경우에는 '작은 데이터'와 '피벗'의 위치를 서로 변경하여 분할을 수행한다 -> 1과 5의 위치 변경

 

 

STEP 3

STEP 3) 이와 같이 피벗이 이동한 상태에서 왼쪽 리스트와 오른쪽 리스트를 살펴보자. 이제 5의 왼쪽에 있는 데이터는 모두 5보다 작고, 오른쪽에 있는 데이터는 모두 5보다 크다는 특징이 있다! 이렇게 피벗의 왼쪽에는 피벗보다 작은 데이터가 위치하고, 피벗의 오른쪽에는 피벗보다 큰 데이터가 위치히다록 작업을 분할(Divide) 또는 파티션(Partition)이라고 한다

 

  • 이 상태에서 왼쪽 리스트와 오른쪽 리스트를 개별적으로 정렬시킬 것이다
  • 어차피 왼쪽 리스트는 어떻게 정렬되어도 모든 데이터가 5보다 작고, 오른쪽 리스트는 어떻게 정렬되어도 모든 데이터가 5보다 크다
  • 따라서 왼쪽 리스트와 오른쪽 리스트에서도 각각 피벗을 설정하여 동일한 방식으로 정렬을 수행하면 전체 리스트에 대하여 모두 정렬이 이루어질 것이다

 

Part 2

왼쪽 리스트

왼쪽 리스트에서는 위의 그림과 같이 정렬이 진행되며 구체적인 정렬 과정은 이전과 동일하다

 

 

Part 3

오른쪽 리스트에서는 위의 그림과 같이 정렬이 진행되며 구체적인 정렬 과정은 이전과 동일하다

 

  • 퀵 정렬에서는 이처럼 특정한 리스트에서 피벗을 설정하여 정렬을 수행한 이후에, 피벗을 기준으로 왼쪽 리스트와 오른쪽 리스트에서 각각 다시 정렬을 수행한다
  • 재귀함수와 동작 원리가 같다고 생각하면 된다
  • 실제로 퀵 정렬은 재귀 함수 형태로 작성했을 때 구현이 매우 간결해진다
  • 퀵 정렬은 현재 리스트의 데이터 개수가 1개인 경우 동작을 멈춘다
  • 리스트 원소가 1개라면 이미 정렬이 된 것과 같으며, 분할이 불가능하다
  • 따라서 아래와 같이 정리할 수 있다

퀵 정렬 한눈에 보기


퀵 정렬의 소스코드

 

아래는 널리 사용되고 있는 가장 직관적인 형태의 퀵 정렬 파이썬 코드이다

array = [5, 7, 9, 0, 3, 1, 6, 2, 4, 8]

def quick_sort(array, start, end):
    if start>=end: #원소가 1개인 경우 정렬 종료
        return

    pivot = start
    left = start + 1
    right = end
    while left <= right:
        #피벗보다 큰 데이터를 찾을 때까지 반복
        while left <= end and array[left] <= array[pivot]:
            left += 1

        #피벗보다 작은 데이터를 찾을 때까지 반복
        while right > start and array[right] >= array[pivot]:
            right -= 1

        #엇갈린 경우, 작은 데이터와 피벗을 교체
        if left > right :
            array[right], array[pivot] = array[pivot], array[right]
        #엇갈리지 않은 경우, 작은 데이터와 큰 데이터 교체
        else :
            array[left], array[right] = array[right], array[left]

    #분할 이후 왼쪽 부분과 오른쪽 부분에서 각각 정렬 수행
    quick_sort(array, start, right-1) #왼쪽 리스트
    quick_sort(array, right+1, end) #오른쪽 리스트


quick_sort(array, 0, len(array)-1)
print(array)

재귀 함수를 사용하여 구현했다

흠 확실히 코드가 복잡한 것 같긴 하다

 

 

다음은 파이썬의 장점을 살려 짧게 작성한 퀵 정렬 소스코드를 보자

전통 퀵 정렬의 분할 방식과는 조금 다르다

피벗과 데이터를 비교하는 비교 연산 횟수가 증가하므로 시간 면에서 조금 비효율적이지만, 더 직관적이고 기억하기 쉽다!

array = [5, 7, 9, 0, 3, 1, 6, 2, 4, 8]

def quick_sort(array):
    #리스트가 하나 이하의 원소만을 갖는다면 종료
    if len(array) <= 1:
        return array

    pivot = array[0] #피벗은 첫번째 원소
    tail = array[1:] #피벗을 제외한 리스트

    left_side = [x for x in tail if x <= pivot] #분할된 왼쪽 리스트
    right_side = [x for x in tail if x > pivot] #분할된 오른쪽 리스트

    #분할 이후 왼쪽 부분과 오른쪽 부분에서 각각 정렬을 수행하고, 전체 리스트 반환
    return quick_sort(left_side) + [pivot] + quick_sort(right_side)

print(quick_sort(array))

흠 간단해 보이기는 하는데 난 그냥 직관적인 코드 사용해야할듯?

 

파이썬 퀵정렬 결과화면

 

 

다음은 C++ 퀵 정렬 소스코드이다

#include <algorithm>
#include <iostream>

using namespace std;

int n=10;
int arr[10]={5, 7, 9, 0, 3, 1, 6, 2, 4, 8};

void quickSort(int *arr, int start, int end)
{
    if(start>=end) return;
    int pivot = start;
    int left = start + 1;
    int right = end;
    
    while(left<=right)
    {
        while(left <= end && arr[left] <= arr[pivot])
        {
            left++;
        }
        while(right > start && arr[right] >= arr[pivot])
        {
            right--;
        }
        if(left>right)
        {
            swap(arr[right], arr[pivot]);
        }
        else
        {
            swap(arr[left], arr[right]);
        }
    }
    quickSort(arr, start, right-1);
    quickSort(arr, right+1, end);
}

int main()
{
    quickSort(arr, 0, n-1);
    
    for(int i=0;i<n;i++)
    {
        cout<<arr[i]<<" ";
    }

    return 0;
}

알고리즘 모두 동일합니다~

C++ 퀵정렬 결과화면


퀵 정렬의 시간 복잡도

  • 이전에서 다룬 선택 정렬과 삽입 정렬의 시간 복잡도는 O(N^2)이었다
  • 선택 정렬과 삽입 정렬은 최악의 경우에도 항상 시간 복잡도 O(N^2)을 보장한다
  • 이에 반해 퀵 정렬의 평균 시간 복잡도 O(NlogN)으로, 다른 알고리즘에 비해 빠른 편이다!
  • 하지만 퀵 정렬은 최악의 경우에는 시간 복잡도가 O(N^2)이다
  • 즉, 무작위로 입력이 들어오는 경우에는 퀵 정렬이 빠르게 동작할 확률이 높지만, 데이터가 이미 정렬되어 있는 경우에는 매우 느리게 동작한다!

 

728x90

댓글