정렬(Sorting)이란 데이터를 특정 기준에 따라서 순서대로 나열하는 것이다. 예를들어 오름차순, 내림차순 등이 있다.
정렬 알고리즘 중에 유명한 것은 선택 정렬, 삽입 정렬, 퀵 정렬, 계수 정렬등이 있다.
1. 선택 정렬
7 | 5 | 9 | 0 | 3 | 1 | 6 | 2 | 4 | 8 |
위와 같은 상태의 데이터가 있다고 가정해보자. 선택정렬은 데이터들 중 가장 작은 데이터를 선택해 맨앞의 데이터와 바꾸고, 그다음 작은 데이터는 두번째 데이터와 바꾸고... 의 과정을 반복하는 것이다. 일단 가장 작은 데이터를 찾는다. 그리고 맨앞의 7과 바꾼다.
0 | 5 | 9 | 7 | 3 | 1 | 6 | 2 | 4 | 8 |
이제 0은 정렬이 된 상태이다. 이제 두번째로 작은 값인 1을 두번째 값과 바꾼다.
0 | 1 | 9 | 7 | 3 | 5 | 6 | 2 | 4 | 8 |
0과 1은 정렬이 된 상태이다. 이를 계속 반복하면 결국 0부터 9까지 정렬된 데이터를 얻을 수 있다. 코드는 다음과 같다.
#include <iostream>
using namespace std;
int main() {
int arr[10]={7,5,9,0,3,1,6,2,4,8};
int n=10;
int small_idx;
for(int i=0;i<n;i++){
small_idx=i;
for(int j=i+1;j<n;j++){
if(arr[small_idx]>arr[j])
small_idx=j;
}
swap(arr[small_idx],arr[i]);
}
for(int i=0;i<10;i++){
cout<<arr[i]<<" ";
}
}
선택정렬의 시간복잡도는 어떻게 될까?
정석적으로 접근하면, N-1번만큼 작은 수를 찾아서 맨앞으로 보내기 때문에 N+(N-1)+(N-2)+...+2 이므로 (N^2+N)/2 이다. 이는 O(N^2) 로 표현 할 수 있다. 간단하게 생각하면 2중 for문에서 각각 N회씩 계산을 하므로 O(N^2) 라고 이해 할 수 있다.
2. 삽입 정렬
위와 같은 상태의 데이터가 있다고 가정하자.
7 | 5 | 9 | 0 | 3 | 1 | 6 | 2 | 4 | 8 |
삽입 정렬 에서는 두번째 데이터 부터 시작한다. 첫번째 데이터인 7은 이미 그자체로 정렬 되어 있다고 가정하기 때문이다. 5는 앞의 수인 7보다 작으므로 7의 앞으로 이동한다.
5 | 7 | 9 | 0 | 3 | 1 | 6 | 2 | 4 | 8 |
다음 수 인 9는 5와 7보다 크므로 7의 뒤에 위치한다. 즉 그대로 둔다.
5 | 7 | 9 | 0 | 3 | 1 | 6 | 2 | 4 | 8 |
0은 5,7,9보다 작으므로 맨앞에 삽입한다.
0 | 5 | 7 | 9 | 3 | 1 | 6 | 2 | 4 | 8 |
이 과정을 반복하면 정렬이 되는 것이다. 삽입 정렬을 할 때에는 이동하고자 하는 데이터를 선택하고 왼쪽으로 비교를 진행하여 그 데이터보다 작은 데이터를 만나면 그 위치에서 삽입하면 된다.
#include <bits/stdc++.h>
using namespace std;
int n = 10;
int arr[10] = {7, 5, 9, 0, 3, 1, 6, 2, 4, 8};
int main(void) {
for (int i = 1; i < n; i++) {
// 인덱스 i부터 1까지 감소하며 반복하는 문법
for (int j = i; j > 0; j--) {
// 한 칸씩 왼쪽으로 이동
if (arr[j] < arr[j - 1]) {
swap(arr[j], arr[j - 1]);
}
// 자기보다 작은 데이터를 만나면 그 위치에서 멈춤
else break;
}
}
for(int i = 0; i < n; i++) {
cout << arr[i] << ' ';
}
}
삽입 정렬의 시간 복잡도는 O(N^2)이다. 하지만 정렬하고자하는 데이터가 거의 정렬되어 있는 경우에는 실행 속도가 많이 빨라진다. 보통의 상황에서는 삽입정렬은 비효율적인것은 맞지만 데이터가 거의 정렬되어 있는 경우는 퀵 정렬보다 빠르다.
3. 퀵 정렬
퀵 정렬의 핵심 알고리즘은 다음과 같다.
'기준 데이터(pivot)를 설정하고 그 기준보다 큰 데이터와 작은 데이터의 위치를 바꾼다.'
이때 기준데이터인 pivot은 배열에서 첫번째 데이터를 pivot으로 정한다. 피벗을 정한 이후에는 왼쪽에서부터 피벗보다 큰 데이터를 찾고, 오른쪽에서부터 피벗보다 작은 데이터를 찾는다. 그 다음에는 데이터의 위치를 서로 교환 해준다.
#include <bits/stdc++.h>
using namespace std;
int n = 10;
int arr[10] = {7, 5, 9, 0, 3, 1, 6, 2, 4, 8};
void quickSort(int* arr, int start, int end) {
if (start >= end) return; // 원소가 1개인 경우 종료
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[pivot], arr[right]);
// 엇갈리지 않았다면 작은 데이터와 큰 데이터를 교체
else swap(arr[left], arr[right]);
}
// 분할 이후 왼쪽 부분과 오른쪽 부분에서 각각 정렬 수행
quickSort(arr, start, right - 1);
quickSort(arr, right + 1, end);
}
int main(void) {
quickSort(arr, 0, n - 1);
for (int i = 0; i < n; i++) {
cout << arr[i] << ' ';
}
}
퀵 정렬은 시간복잡도가 평균적으로 O(NlogN)을 따른다. 하지만 최악의 경우에는 O(N^2) 을 가진다. 이미 데이터가 정렬이 되어 있는경우가 이러하다. 이럴때는 퀵 정렬 보다 삽입정렬을 사용할 때가 훨씬 빠르게 작동 한다.
C++ 에서는 퀵 정렬을 기반으로 제공하는 라이브러리가 있다. algorithm 헤더에 포함되어있는 sort()함수이다.
'Algorithm' 카테고리의 다른 글
Dijkstra (다익스트라) 2 (0) | 2022.06.06 |
---|---|
Dijkstra (다익스트라) (0) | 2022.06.01 |
Greedy (탐욕법) (0) | 2022.05.21 |
DP (Dynamic Programming) (0) | 2022.05.05 |
BFS의 특성 (최단경로) (0) | 2022.03.29 |