본문 바로가기
알고리즘/정렬

[알고리즘-4] 퀵 정렬

by 정긔린 2022. 3. 15.
반응형

안녕하세요 기린입니다 :)

지난 시간까지 다루었던 선택 정렬, 버블 정렬, 삽입 정렬 알고리즘 모두 시간 복잡도 O(N^2)를 가지는 알고리즘 이었는데요.

이러한 알고리즘 들은 데이터의 갯수가 10만 개만 넘어가도 사용하기가 매우 어려운 알고리즘입니다.

그렇기 때문에 더욱 빠른 정렬 알고리즘이 필요하겠죠 ?

자 대표적으로 빠른 알고리즘인 퀵 정렬에 대해 알아보겠습니다.

퀵 정렬은 대표적인 '분할 정복' 알고리즘으로 평균 속도가 O(N * logN) 입니다. 예시를 통해 바로 확인해보시죠 :]

 

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

퀵 정렬은 하나의 큰 문제를 두 개의 작은 문제로 분할하는 방법으로 빠르게 정렬하는데요, 더 쉽게 말해 특정한 값을 기준으로 큰 숫자 작은 숫자를 서로 교환한 뒤에 배열을 반으로 나눕니다.

바로 한 번 살펴보도록 하죠 !

일반적으로 퀵 정렬에서는 기준 값이 있는데 이를 피벗(Pivot)이라 합니다. 보통 첫 번째 원소를 피벗 값으로 설정하고 사용하는데요. 다음과 같이 1이라는 값이 먼저 피벗 값으로 설정되었다 생각해봅시다.

(1) 10 5 8 7 6 4 3 2 9

-> 10

                      1 <-

이 경우 1보다 큰 숫자를 왼쪽부터 찾고, 1보다 작은 숫자를 오른쪽부터 찾습니다. 이 때 1보다 큰 숫자로 10, 작은 숫자는 찾지 못해 1 까지 도달하게 됩니다. 이 때 작은 값의 인덱스큰 값의 인덱스보다 작으므로 피벗 값과 작은 값의 위치를 변경합니다. 즉, 1과 1을 교환하므로 정렬에 변함이 없겠죠 !

위와 같은 방법으로 왼쪽과 오른쪽에서 퀵 정렬을 순환적으로 수행하면 되는데요

 

1 (10) 5 8 7 6 4 3 2 9

-> 10

                       9 <-

자 이제 피벗값 기준으로 왼쪽은 없으므로 무시하고, 오른쪽 피벗값으로 10이 채택됩니다. 그리고 10보다 큰 값을 왼쪽부터, 10보다 작은 값을 오른쪽부터 찾습니다. 큰 값은 찾지 못하게 되며 작은 값으로는 바로 9를 찾을 수 있습니다. 이 때 작은 값의 인덱스 큰 값의 인덱스보다 작으므로 9와 10을 교환합니다.

1 9 5 8 7 6 4 3 2 10

따라서 위와 같이 구성이 되며, 이 때 피벗 값이었던 10의 왼쪽에는 10보다 작은 값이 존재하고 오른쪽에는 10보다 큰 값이 존재하게 되는거죠. 이해 되시나요 ???

즉, 한번 수행할때마다 피벗값의 위치를 찾게되는거죠 

이렇게 퀵 정렬을 순환적으로 수행하면 반으로 쪼개 들어가면서 분할 정복식으로 정렬이 완료됩니다

이게 왜 빠른지 생각해보겠습니다

예를들어 10자리 수를 정렬할 때 선택 정렬을 기준으로 시간 복잡도가 N^2 = 10 * 10 = 100 이 나오는데요

10자리 수를 반으로 나누어 시간 복잡도를 계산해보면 5 * 5 = 25 와 5 * 5 = 25 로 50이 나온다는거죠

이해 되시나요 ? 이를 전부 1개씩 나누게 되면 1 * 1 를 10번 수행한다고 생각하시면 되겠죠 ??

자 이제 소스코드를 보실게요

#include <stdio.h>

int number = 10;
int data[] = {1, 10, 5, 8, 7, 6, 4, 3, 2, 9};

void show() {
  int i;
  for (i = 0; i < number; i++) {
    printf("%d ", data[i]);
  }
}

void quickSort(int* data, int start, int end) {
  if (start >= end) { // 원소가 1개인 경우 그대로 두기
    return;
  }
  
  int key = start // 키는 첫 번 째 원소
  int i = start + 1, j = end, temp;
  
  while (i <= j) { // 엇갈릴 때까지 반복
    while (i <= end && data[i] <= data[key]) { // 키 값보다 큰 값을 만날 경우
      i++;
    }
    while (j > start && data[j] >= data[key]) { // 키 값보다 작은 값을 만날 경우
      j--;
    }
    if (i > j) { // 현재 엇갈린 상태면 키 값과 교체
      temp = data[j];
      data[j] = data[key];
      data[key] = temp;
    } else { // 엇갈리지 않았다면 i와 j를 교체
      temp = data[i];
      data[i] = data[j];
      data[j] = temp;
    }
    
    quickSort(data, start, j - 1);
    quickSort(data, j + 1, end);
  }
  
  int main(void) {
    quickSort(data, 0, number - 1);
    show();
    return 0;
  }

위 소스코드를 보시면 '키 값보다 작은 값을 만날때까지' 반복하는 부분에서 j 가 start 보다 클 때에 한해서만 반복문이 수행되도록 처리되어 있습니다. 이는 항상 왼쪽에 있는 값과 피벗 값을 교환하기 때문인데요 오른쪽에 있는 값은 피벗 값과 교환되지 않으므로 처리해 줄 필요가 없습니다. 즉 퀵 정렬 알고리즘은 기본적으로 N번씩 탐색하되 반으로 쪼개 들어간다는 점에서 logN 을 곱한 복잡도를 가집니다.

하지만 퀵 정렬의 최악 시간 복잡도는 O(N^2) 인데요 

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

위와 같이 이미 정렬이 되어 있는 경우 퀵 정렬의 시간 복잡도 O(N^2)에 가깝습니다. 반면에 이전 시간에 다루었던 삽입 정렬의 경우 위 문제를 매우 빠르게 풀어낼 수 있습니다. 즉 정렬할 데이터의 특성에 따라서 적절한 정렬 알고리즘을 사용하는 것이 중요하다는 것입니다. 자 그럼 위의 문제를 내림차순으로 바꾸어보는 문제를 한번 풀어보길 권장드립니다

자 그럼 즐거운 코딩하세요 !

 

문의: ralla0405@gmail.com

반응형