정렬 알고리즘(Sort Algorithm)
정렬 알고리즘이란?
데이터를 특정 기준에 따라 정렬하는 방법
크게 비교 기반 정렬과 비교하지 않는 정렬로 나뉘며, 각 알고리즘은 데이터의 크기, 데이터의 특성, 메모리 제약 등에 따라 다른 성능을 보임
- 비교 기반 정렬 (Comparison-based Sort): 각 원소를 비교하여 정렬 순서를 결정하는 방법
- 비교하지 않는 정렬 (Non-Comparison-based Sort): 데이터를 직접 비교하지 않고 정렬하는 방법으로, 특정 상황에서 더 효율적인 경우가 있음
정렬 알고리즘의 시간 복잡도
- O(N²): 작은 데이터에 적합. 비효율적이지만 구현이 간단.
- 버블 정렬, 선택 정렬, 삽입 정렬
- O(N log N): 대부분의 경우 효율적. 대규모 데이터에서 많이 사용.
- 병합 정렬, 퀵 정렬, 힙 정렬
- O(N): 특정 조건이나 특수한 경우에만 사용.
- 계수 정렬, 기수 정렬, 버킷 정렬
O(N²) 정렬 알고리즘
버블 정렬 (Bubble Sort)
인접한 두 원소를 반복적으로 비교하여 정렬
1
2
3
4
5
6
7
8
9
void bubbleSort(int arr[], int n) {
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
swap(arr[j], arr[j + 1]); // 두 원소를 교환
}
}
}
}
선택 정렬 (Selection Sort)
매번 최소값을 찾아 첫 번째 원소와 교환하는 방식으로 정렬
1
2
3
4
5
6
7
8
9
10
11
void selectionSort(int arr[], int n) {
for (int i = 0; i < n - 1; i++) {
int min_idx = i;
for (int j = i + 1; j < n; j++) {
if (arr[j] < arr[min_idx]) {
min_idx = j; // 최소값의 인덱스 업데이트
}
}
swap(arr[min_idx], arr[i]); // 최소값과 현재 위치의 원소 교환
}
}
삽입 정렬 (Insertion Sort)
정렬된 배열의 끝에서부터 새로운 값을 삽입할 위치를 찾아 정렬
작은 데이터에 대해서는 효율적이며, 거의 정렬된 데이터에는 O(N) 성능을 보이기도 함
1
2
3
4
5
6
7
8
9
10
11
void insertionSort(int arr[], int n) {
for (int i = 1; i < n; i++) {
int key = arr[i];
int j = i - 1;
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j]; // 값을 오른쪽으로 이동
j--;
}
arr[j + 1] = key; // 삽입할 위치에 key 값 넣기
}
}
O(N log N) 정렬 알고리즘
퀵 정렬 (Quick Sort)
분할 정복 알고리즘으로, 기준값(pivot)을 설정하고 좌우로 분할하여 정렬
평균 시간 복잡도는 O(N log N)이며, 최악의 경우 O(N²)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int partition(int arr[], int low, int high) {
int pivot = arr[high]; // 기준값 설정
int i = (low - 1); // 작은 원소의 인덱스
for (int j = low; j <= high - 1; j++) {
if (arr[j] < pivot) {
i++;
swap(arr[i], arr[j]); // 기준값보다 작은 값을 앞으로 이동
}
}
swap(arr[i + 1], arr[high]); // 기준값을 중앙으로 이동
return (i + 1);
}
void quickSort(int arr[], int low, int high) {
if (low < high) {
int pi = partition(arr, low, high);
quickSort(arr, low, pi - 1); // 왼쪽 부분 배열 정렬
quickSort(arr, pi + 1, high); // 오른쪽 부분 배열 정렬
}
}
병합 정렬 (Merge Sort)
배열을 반으로 나누고, 정렬된 두 배열을 병합하여 하나의 정렬된 배열로 만드는 방식
안정적인 정렬 방법으로, 최악의 경우에도 O(N log N)을 유지함
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void merge(int arr[], int l, int m, int r) {
int n1 = m - l + 1;
int n2 = r - m;
int L[n1], R[n2]; // 두 개의 임시 배열 생성
for (int i = 0; i < n1; i++) L[i] = arr[l + i];
for (int j = 0; j < n2; j++) R[j] = arr[m + 1 + j];
int i = 0, j = 0, k = l;
while (i < n1 && j < n2) {
if (L[i] <= R[j]) arr[k++] = L[i++];
else arr[k++] = R[j++];
}
while (i < n1) arr[k++] = L[i++]; // 남은 값 복사
while (j < n2) arr[k++] = R[j++]; // 남은 값 복사
}
void mergeSort(int arr[], int l, int r) {
if (l < r) {
int m = l + (r - l) / 2;
mergeSort(arr, l, m); // 왼쪽 부분 배열 정렬
mergeSort(arr, m + 1, r); // 오른쪽 부분 배열 정렬
merge(arr, l, m, r); // 두 부분 배열을 병합
}
}
힙 정렬 (Heap Sort)
힙 정렬은 최대/최소 힙을 사용하여 정렬
우선순위 큐를 사용하여 O(N log N) 시간에 정렬이 가능
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void heapify(int arr[], int n, int i) {
int largest = i; // 루트
int left = 2 * i + 1;
int right = 2 * i + 2;
if (left < n && arr[left] > arr[largest]) largest = left;
if (right < n && arr[right] > arr[largest]) largest = right;
if (largest != i) {
swap(arr[i], arr[largest]); // 루트와 교환
heapify(arr, n, largest); // 하위 서브트리 재귀적으로 힙 정렬
}
}
void heapSort(int arr[], int n) {
for (int i = n / 2 - 1; i >= 0; i--) heapify(arr, n, i); // 힙 구성
for (int i = n - 1; i > 0; i--) {
swap(arr[0], arr[i]); // 루트와 마지막 원소 교환
heapify(arr, i, 0); // 남은 원소 재정렬
}
}
O(N) 정렬 알고리즘
계수 정렬 (Counting Sort)
정수 배열의 값들을 기준으로 빈도수를 계산하여 정렬
데이터가 정수이고, 값의 범위가 제한된 경우 매우 효율적
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void countingSort(int arr[], int n, int max_val) {
int* count = new int[max_val + 1]{0}; // 빈도수 배열 초기화
int* output = new int[n];
// 1. 각 값의 빈도수 카운트
for (int i = 0; i < n; i++) {
count[arr[i]]++;
}
// 2. 빈도수 누적합 계산
for (int i = 1; i <= max_val; i++) {
count[i] += count[i - 1];
}
// 3. 빈도수를 기반으로 각 값을 올바른 위치에 삽입
for (int i = n - 1; i >= 0; i--) {
output[--count[arr[i]]] = arr[i];
}
// 4. 정렬된 배열을 원본 배열에 복사
for (int i = 0; i < n; i++) {
arr[i] = output[i];
}
delete[] count;
delete[] output;
}
기수 정렬 (Radix Sort)
가장 낮은 자릿수부터 높은 자릿수까지 정렬하여 전체를 정렬
각 자릿수의 정렬에는 계수 정렬을 사용하므로 시간 복잡도는 O(d * N)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
void countingSortForRadix(int arr[], int n, int exp) {
int output[n];
int count[10] = {0}; // 자릿수는 0~9까지이므로 크기 10
// 1. 현재 자릿수를 기준으로 각 값의 빈도수 계산
for (int i = 0; i < n; i++) {
count[(arr[i] / exp) % 10]++;
}
// 2. 빈도수 누적합 계산
for (int i = 1; i < 10; i++) {
count[i] += count[i - 1];
}
// 3. 현재 자릿수를 기준으로 정렬된 배열 생성
for (int i = n - 1; i >= 0; i--) {
output[--count[(arr[i] / exp) % 10]] = arr[i];
}
// 4. 정렬된 배열을 원본 배열에 복사
for (int i = 0; i < n; i++) {
arr[i] = output[i];
}
}
void radixSort(int arr[], int n) {
int max_val = *max_element(arr, arr + n); // 최대값 탐색
// 각 자릿수를 기준으로 계수 정렬 수행
for (int exp = 1; max_val / exp > 0; exp *= 10) {
countingSortForRadix(arr, n, exp);
}
}
버킷 정렬 (Bucket Sort)
데이터를 여러 개의 버킷으로 나누고, 각 버킷을 개별적으로 정렬하여 최종적으로 합치는 방식
정렬할 데이터가 균등하게 분포된 경우 효율적으로 동작
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void bucketSort(float arr[], int n) {
vector<float> bucket[n]; // n개의 버킷 생성
// 1. 각 원소를 버킷에 분배
for (int i = 0; i < n; i++) {
int idx = n * arr[i]; // 값에 따라 버킷의 인덱스 결정
bucket[idx].push_back(arr[i]);
}
// 2. 각 버킷을 개별적으로 정렬
for (int i = 0; i < n; i++) {
sort(bucket[i].begin(), bucket[i].end());
}
// 3. 정렬된 버킷을 합침
int index = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < bucket[i].size(); j++) {
arr[index++] = bucket[i][j];
}
}
}
특수한 정렬 알고리즘
트리 정렬 (Tree Sort)
이진 탐색 트리를 이용하여 정렬하는 방법
트리에 데이터를 삽입하고, 중위 순회를 통해 정렬
시간 복잡도는 O(N log N)이며, 트리의 높이에 따라 성능이 달라진다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
struct Node {
int data;
Node* left, * right;
Node(int val) : data(val), left(NULL), right(NULL) {}
};
Node* insert(Node* root, int data) {
if (!root) return new Node(data);
if (data < root->data) root->left = insert(root->left, data);
else root->right = insert(root->right, data);
return root;
}
void inorder(Node* root, vector<int>& sorted) {
if (root) {
inorder(root->left, sorted);
sorted.push_back(root->data);
inorder(root->right, sorted);
}
}
void treeSort(int arr[], int n) {
Node* root = NULL;
for (int i = 0; i < n; i++) {
root = insert(root, arr[i]); // 트리에 삽입
}
vector<int> sorted;
inorder(root, sorted); // 중위 순회로 정렬된 데이터 추출
for (int i = 0; i < n; i++) {
arr[i] = sorted[i]; // 정렬된 데이터를 배열에 복사
}
}
위상 정렬 (Topological Sort)
위상 정렬은 DAG(Directed Acyclic Graph)에서 각 정점의 순서를 정렬하는 방법
보통 그래프 알고리즘에서 작업 순서 결정 등에 사용
진입 차수가 0인 정점을 찾아 순서대로 처리하며, 시간 복잡도는 O(V + E)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void topologicalSort(vector<int> adj[], int V) {
vector<int> indegree(V, 0); // 모든 정점의 진입 차수 저장
// 각 정점의 진입 차수 계산
for (int i = 0; i < V; i++) {
for (int u : adj[i]) {
indegree[u]++;
}
}
queue<int> q;
for (int i = 0; i < V; i++) {
if (indegree[i] == 0) q.push(i); // 진입 차수가 0인 정점 삽입
}
// 위상 정렬 수행
while (!q.empty()) {
int node = q.front();
q.pop();
cout << node << " "; // 출력
// 인접한 정점의 진입 차수 감소
for (int u : adj[node]) {
if (--indegree[u] == 0) {
q.push(u); // 진입 차수가 0이 된 정점 삽입
}
}
}
}