[자료구조/알고리즘] 병합 정렬 (Merge Sort)
1. 병합 정렬
병합 정렬은 정렬 알고리즘 중 하나로 분할 정복을 통해 정렬하는 알고리즘이다. 병합 정렬 또는 합병 정렬이라고 불린다.
2. 병합 정렬 성능
| 평균 시간복잡도 | $O(N\log{N})$ |
|---|---|
| 최선 시간복잡도 | $O(N\log{N})$ |
| 최악 시간복잡도 | $O(N\log{N})$ |
| 공간복잡도 | $O(N)$ |
$N$ 개의 원소에 대한 병합 정렬은 최선, 평균, 최악 시간복잡도 모두 $O(N\log{N})$ 으로 일관된 성능을 보여준다. 이 때문에 거의 정렬된 상태로 원소들이 주어져도 성능이 더 빨라지지 않지만 최소한의 성능은 보장한다는 장점이 있다.
병합 정렬은 주어진 원소들을 분할 후 정복(병합)하는 과정에서 임시 공간을 사용하며 이는 병합하는 두 원소들의 크기만큼이 된다. 최종 병합 과정에서는 전체 원소 크기만큼의 임시 공간이 필요하게 되고 이 때문에 병합 정렬의 공간복잡도는 $O(N)$ 이다.
병합 정렬은 병합 과정에서 동일한 가중치의 수에 대해 먼저 등장한 수를 먼저 병합할 수 있어 순서가 유지되는 안정 정렬(Stable Sort)이다.
3. 병합 정렬 진행 과정
병합 정렬은 주어진 원소들을 두 구간으로 분할하는 과정을 더 이상 분할할 수 없을 때까지 반복한 후 이를 임시 공간에 정렬하고 원본에 반영하는 방식으로 동작한다. 반복문을 통한 Bottom-Up 방식의 병합 정렬도 가능하지만 재귀를 통한 Top-Down 방식의 병합 정렬이 보다 일반적이라 재귀를 통한 병합 정렬로 진행하겠다.
주어진 수들이 $[4, 1, 8, 3, 6, 10, 8, 4]$ 일 경우 오름차순 병합 정렬 과정은 아래와 같이 진행된다.
먼저 주어진 원소들을 두 부분으로 분할한다. 분할은 중간 위치를 기준으로 분할하며 주어진 원소가 하나일 때까지 반복한다. 현재 1번부터 4번까지가 분할한 앞 부분이 된다.
아직 분할할 수 있으므로 두 부분으로 분할한다. 현재 1번부터 2번까지가 분할한 앞 부분이 된다.
아직 분할할 수 있으므로 두 부분으로 분할한다. 현재 1번부터 1번까지가 분할한 앞 부분이 된다.
더 이상 분할할 수 없으므로 이전으로 돌아가 이전에 분할했던 뒷 부분에 대해 똑같이 시도한다.
1번부터 2번 구간에 대한 분할이 끝났으므로 병합한다. 병합은 2칸짜리 임시 공간을 만들고 병합할 앞 부분과 뒷 부분의 첫 번째 원소부터 작은 것들을 순서대로 넣어준다.
1번부터 4번 구간에 대한 앞 부분의 병합이 완료됐다. 이제 뒷 부분에 대해 분할을 시작한다.
더 이상 분할할 수 없을 때까지 분할한다.
더 이상 분할할 수 없을 때까지 분할한다.
3번부터 4번 구간에 대한 분할이 끝났으므로 병합한다. 병합은 2칸짜리 임시 공간을 만들고 병합할 앞 부분과 뒷 부분의 첫 번째 원소부터 작은 것들을 순서대로 넣어준다.
1번부터 4번 구간에 대해 앞 부분의 병합과 뒷 부분의 병합이 각각 완료됐다. 이제 앞 부분과 뒷 부분도 병합해준다.
1번부터 8번 구간에 대해 앞 부분의 병합이 완료됐다. 이제 5번부터 8번 구간에 대해 똑같이 진행한다.
5번부터 6번 구간으로 분할해준다.
5번부터 5번 구간으로 분할해준다. 더 이상 분할할 수 없으므로 종료한다.
6번부터 6번 구간으로 분할해준다. 더 이상 분할할 수 없으므로 종료한다.
5번부터 6번 구간에 대한 분할이 끝났으므로 병합해준다.
5번부터 8번 구간의 뒷 부분에 대해 분할을 시작한다.
7번부터 7번 구간으로 분할해준다. 더 이상 분할할 수 없으므로 종료한다.
8번부터 8번 구간으로 분할해준다. 더 이상 분할할 수 없으므로 종료한다.
7번부터 8번 구간에 대한 분할이 끝났으므로 병합해준다.
5번부터 8번 구간에 대한 앞 부분 병합과 뒷 부분 병합이 각각 완료됐다. 이제 앞 부분과 뒷 부분을 병합해준다.
1번부터 8번 구간에 대한 앞 부분 병합과 뒷 부분 병합이 각각 완료됐다. 이제 앞 부분과 뒷 부분을 병합해준다. 이때 전체 구간의 길이만큼의 임시 배열이 필요해서 병합 정렬의 공간 복잡도는 $O(N)$ 이 되며 각 구간에서 동일한 가중치를 갖는 $4$, $8$ 에 대해 앞 구간의 원소를 먼저 병합하면 돼서 병합 정렬은 안정 정렬이다. 또한 분할이 $\log{N}$ 번 깊이로 발생하며 각 단계에서 $N$ 번의 병합이 이루어지므로 병합 정렬의 시간복잡도는 $O(N\log{N})$ 이다.
전체 구간에 대한 병합을 완료했으므로 병합 정렬을 종료한다. 이렇게 병합 정렬을 통해 주어진 수들을 오름차순으로 정렬하는데 성공했다.
4. 병합 정렬 코드
1. 병합 정렬 [Java]
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
35
36
37
38
39
40
41
42
43
44
45
static void mergeSort(int[] arr) {
int[] tmp = new int[arr.length];
mergeSort(arr, tmp, 0, arr.length - 1);
}
static void mergeSort(int[] arr, int[] tmp, int left, int right) {
if (left >= right) return; // 더 이상 분할할 수 없으면 분할 종료
int mid = (left + right) / 2; // 분할점 선정
mergeSort(arr, tmp, left, mid); // 앞 부분 재귀적으로 분할
mergeSort(arr, tmp, mid + 1, right); // 뒷 부분 재귀적으로 분할
merge(arr, tmp, left, mid, right); // 병합
}
static void merge(int[] arr, int[] tmp, int left, int mid, int right) {
int i = left; // 앞 부분의 포인터
int j = mid + 1; // 뒷 부분의 포인터
int k = left; // 임시 공간의 포인터
while (i <= mid && j <= right) {
// 앞 부분의 포인터가 가리키는 원소가 뒷 부분의 포인터가 가리키는 원소보다 작거나 같으면 임시 공간에 넣고 포인터 이동
// 이때 같아도 앞 부분의 원소를 먼저 넣어서 안정 정렬이 됨
if (arr[i] <= arr[j]) {
tmp[k++] = arr[i++];
}
// 뒷 부분의 포인터가 가리키는 원소가 앞 부분의 포인터가 가리키는 원소보다 작으면 임시 공간에 넣고 포인터 이동
else {
tmp[k++] = arr[j++];
}
}
// 앞 부분의 남은 자투리 넣기
while (i <= mid) {
tmp[k++] = arr[i++];
}
// 뒷 부분의 남은 자투리 넣기
while (j <= right) {
tmp[k++] = arr[j++];
}
// 병합 정렬 결과 원본에 반영
System.arraycopy(tmp, left, arr, left, right - left + 1);
}
2. 병합 정렬 [C++]
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
35
36
37
void merge(vector<int>& a, vector<int>& tmp, int l, int m, int r) {
int i = l; // 앞 부분의 포인터
int j = m + 1; // 뒷 부분의 포인터
int k = l; // 임시 공간의 포인터
while (i <= m && j <= r) {
// 앞 부분의 포인터가 가리키는 원소가 뒷 부분의 포인터가 가리키는 원소보다 작거나 같으면 임시 공간에 넣고 포인터
// 이동 이때 같아도 앞 부분의 원소를 먼저 넣어서 안정 정렬이 됨
if (a[i] <= a[j]) {
tmp[k++] = a[i++];
}
// 뒷 부분의 포인터가 가리키는 원소가 앞 부분의 포인터가 가리키는 원소보다 작으면 임시 공간에 넣고 포인터 이동
else {
tmp[k++] = a[j++];
}
}
while (i <= m) tmp[k++] = a[i++]; // 앞 부분의 남은 자투리 넣기
while (j <= r) tmp[k++] = a[j++]; // 뒷 부분의 남은 자투리 넣기
for (int idx = l; idx <= r; idx++) a[idx] = tmp[idx]; // 병합 정렬 결과 원본에 반영
}
void mergeSort(vector<int>& a, vector<int>& tmp, int l, int r) {
if (l >= r) return; // 더 이상 분할할 수 없으면 분할 종료
int m = (l + r) / 2; // 분할점 선정
mergeSort(a, tmp, l, m); // 앞 부분 재귀적으로 분할
mergeSort(a, tmp, m + 1, r); // 뒷 부분 재귀적으로 분할
merge(a, tmp, l, m, r); // 병합
}
void mergeSort(vector<int>& a) {
vector<int> tmp(a.size());
mergeSort(a, tmp, 0, a.size() - 1);
}