본문 바로가기
C++ Programming/알고리즘 정리

[C++] 퀵 정렬 (Quick Sort)

by 쵸빙 2020. 5. 6.

이번 시간에는 퀵 정렬에 대해 알아보도록 하겠다.

 

 

 

 

저번 시간까지 배웠던 선택 정렬, 버블 정렬, 삽입 정렬 모두 시간 복잡도가 O(n ^ 2)였다.

이러한 복잡도를 가지는 알고리즘은 데이터의 갯수가 10만개만 넘어가도 일반적인 상황에서 사용하기가 매우 어려운

알고리즘이다. 그렇기 때문에 더욱 빠른 정렬 알고리즘이 필요하고, 대표적인 빠른 알고리즘이 퀵 정렬로,

O(N * logN)의 시간 복잡도를 가진다.

 

 

 

 

 

 

퀵 정렬(Quick Sort)는 하나의 큰 문제를 두 개의 작은 문제로 분할하는 식으로 빠르게 정렬한다.

특정한 값을 기준으로 큰 숫자와 작은 숫자를 서로 교환한 뒤에 배열을 반으로 나눈다.

 

 

 

 

일반적으로 퀵 정렬에서는 기준값이 있고, 이를 피벗(pivot)이라고 하는데, 보통 첫 번째 원소를 피벗 값으로 설정하고 사용한다.

 

 

 

 

3 7 8 1 5 9 6 10 2 4

예를 들어 다음과 같은 배열이 있다고 하자.

 

 

 

3 7 8 1 5 9 6 10 2 4

피봇을 맨 앞의 3으로 잡고, 그 뒤로 쭉 왼쪽부터는 pivot보다 큰 것, 맨 오른쪽부터는 pivot보다 작은 것을 고른다.

이 경우에는 왼쪽에서는 7, 오른쪽에서는 2가 골라질 것이고, 그 둘을 바꾼다.

 

 

3 2 8 1 5 9 6 10 7 4

 

 

그 뒤로부터 다시 진행하면 왼쪽에서는 8, 오른쪽에서는 1이 선택될 것이고, 이제 이 둘을 바꾼다.

 

3 2 1 8 5 9 6 10 7 4

 

다시 진행해보면 3보다 큰 값은 8, 3보다 작은 값은 1인데 이제 이 둘은 서로 엇갈린 것을 볼 수 있다.

8이 1보다 왼쪽에 있지 않다.

 

 

이렇게 인덱스가 엇갈린 경우에는 가장 작은 값인 1과 pivot을 바꾼다.

 

1 2 3 8 5 9 6 10 7 4

 

 

이렇게 한 뒤에는 기준값이었던 3보다 왼쪽에 있는 것들은 다 3보다 작고, 3보다 오른쪽에 있는 것들은 다 3보다 크다.

즉, 3을 기준으로 왼쪽, 오른쪽 부분 집합으로 분할했다고 볼 수 있다.

 

 

이제는 3을 기준으로 왼쪽 집합인 1 2 와 오른쪽 집합인 8 5 9 6 10 7 4에 대해 다시 퀵정렬을 수행한다.

 

 

1 2 3 8 5 9 6 10 7 4

먼저, 왼쪽 집합의 경우에서 보면 역시나 맨 앞의 원소인 1을 피봇으로 두고,

이제 기준인 1부터 오른쪽을 보면서 자기보다 큰 값인 2를 선택하고, 자기보다 작은 것이 없으므로 자기 자신을

선택하게 된다. 이 상태에서는 또 엇갈린 것이기 때문에 1과 자기 자신을 바꾸므로 원래와 같다.

그 후 큰 값인 2를 피봇으로 또 수행하는데 그 결과도 1 2로 같다.

 

 

이제 3보다 작은 왼쪽 1 2는 정렬되었고, 피봇인 3까지 포함해서 1 2 3은 정렬이 완성된 것이고, 정렬이 완성된 부분

보라색으로 표시하겠다.

 

 

1 2 3 8 5 9 6 10 7 4

다음으로, 오른쪽 집합의 경우에는 맨 앞의 원소인 8이 피봇이 된다.

왼쪽부터 8보다 큰 값인 9, 오른쪽부터 8보다 작은 값인 4를 선택하고, 바꾼다.

 

 

 

1 2 3 8 5 4 6 10 7 9

이제 다시 왼쪽부터 8보다 큰 값인 10, 오른쪽부터 8보다 작은 값인 7을 선택하고, 바꾼다.

 

 

 

1 2 3 8 5 4 6 7 10 9

이제 8보다 큰 값인 10과 8보다 작은 값인 7이 엇갈렸으므로 작은 값과 피봇인 8을 바꾼다.

 

 

 

1 2 3 7 5 4 6 8 10 9

이제 8을 기준으로 작은 값들은 왼쪽에, 큰 값들은 오른쪽에 위치하게 되었다.

 

 

 

 

1 2 3 7 5 4 6 8 10 9

다시 피봇을 맨 앞의 숫자인 7로 두고 수행해나가자. 이제부터는 앞에서 진행했던 것과 똑같은 과정이므로 별다른 설명을 쓰지 않겠다. 

 

 

 

 

1 2 3 7 5 4 6 8 10 9 → 8의 왼쪽 집합인 7 5 4 6에서 피봇인 7보다 큰 건 없고, 작은 건 6

1 2 3 6 5 4 7 8 10 9  → 7 왼쪽으로 7보다 작은 값들, 오른쪽으로 7보다 큰 값들

 

이런 식으로 계속 진행하다보면 1 2 3 4 5 6 7 8 9 10으로 제대로 정렬된 배열을 볼 수 있을 것이다.

 

 

 

 

 

 

 

퀵 정렬은 이렇게 왼쪽과 오른쪽으로 나뉜다는 성질 때문에 기존의 정렬들보다 속도가 O(N * logN)으로 빠른 것이다.

직관적으로 보자면 1 2 3 4 5 6 7 8 9 10을 선택, 삽입, 버블 정렬로 보자면 N * N이었으니까 10 * 10 = 100이 걸리는데,

퀵 정렬은 1 2 3 4 5와 6 7 8 9 10으로 두 개로 나누므로 5 * 5 = 25와 5 * 5 = 25가 더해져서 50이 걸리는 것이다.

 

 

 

 

 

 

 

코드로 구현해보자면 아래와 같다.

#include <stdio.h>

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

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

void QuickSort(int* array, int start, int end) {
	if (start >= end) // 원소가 1개인 경우 그대로 두기
		return;

	int pivot = start; // 피봇은 첫번째 원소
	int i = start + 1, j = end, temp;

	while (i <= j) { // 엇갈릴 때까지 반복
		while (i <= end && (array[i] <= array[pivot])) { // 피봇보다 큰 값을 찾을 때까지
			i++;
		}
		while (j > start && (array[j] >= array[pivot])) { // 피봇보다 작은 값을 찾을 때까지
			j--;
		}

		if (i > j) { // 엇갈린 상태. 피봇보다 작은 값과 피봇과 교체
			temp = array[j];
			array[j] = array[pivot];
			array[pivot] = temp;
		}
		else { // 엇갈리지 않았다면 i와 j를 교체
			temp = array[i];
			array[i] = array[j];
			array[j] = temp;
		}
	}

	QuickSort(array, start, j - 1);
	QuickSort(array, j + 1, end);
}

int main() {
	QuickSort(array, 0, number - 1);
	show();

	return 0;
}

 

 

 

 

 

퀵 정렬의 치명적인 단점은 피봇을 어떻게 설정하느냐에 따라 성능이 확 달라진다는 것이다.

최악 시간 복잡도는 O(N * N)이 되버리고, 그 예는 이미 정렬이 되어있는 경우가 있다.

 

 

그래서 알고리즘 대회에서 시간 복잡도를 O(N * logN)을 요구하는 경우 퀵 정렬을 사용하면 틀리는 경우가 있다고 한다.

 

 

 

'C++ Programming > 알고리즘 정리' 카테고리의 다른 글

[C++] 큐(Queue)  (0) 2020.05.07
[C++] 스택(Stack)  (0) 2020.05.07
[C++] 삽입 정렬(Insertion Sort)  (0) 2020.05.06
[C++] 버블 정렬(Bubble Sort)  (0) 2020.05.06
[C++] 선택 정렬 (Selection Sort)  (0) 2020.05.06