
본 시리즈는 프로그래밍 알고리즘의 개념을 정리하고 실습을 진행해보는 시리즈입니다.


분할 정복 기반 정렬 알고리즘은 문제를 작은 부분으로 나누고, 각각을 정렬한 후 다시 합쳐 전체 문제를 해결하는 방식입니다.
저번 포스팅에서 퀵 정렬, 듀얼 피벗 퀵 정렬을 다루었고, 이번 포스팅에서는 합병 정렬과 팀 정렬을 다룹니다.

합병 정렬 (Merge Sort)은 분할 정복 (Divide and Conquer) 기법을 사용하여 배열을 정렬하는 안정적인 정렬 알고리즘입니다.
특징:
또한, 합병 정렬은 Tim Sort 알고리즘의 핵심적인 부분으로, 부분적으로 정렬된 데이터에서 효율적으로 동작하도록 최적화되어 있습니다. Python과 Java의 표준 정렬 알고리즘인 Tim Sort는 삽입정렬과 합병 정렬을 기반으로 만들어졌습니다.
Merge Sort는 다음과 같은 과정으로 동작합니다:
분할(Divide):
병합(Merge):

동작 예시:
주어진 배열 [9, 2, 1, 5, 4]을 Merge Sort로 정렬하는 과정:
분할:
[9, 2, 1]과 [5, 4][9, 2, 1]을 다시 나누면 [9]와 [2, 1][2, 1]을 다시 나누면 [2]와 [1][5, 4]를 나누면 [5]와 [4]병합:
[2]와 [1]을 병합하여 [1, 2]로 정렬[9]과 [1, 2]를 병합하여 [1, 2, 9]으로 정렬[5]와 [4]를 병합하여 [4, 5]로 정렬최종적으로 [1, 2, 9]과 [4, 5]를 병합하여 [1, 2, 4, 5, 9]으로 정렬
import java.util.Arrays;
public class MergeSort {
// Merge Sort 메서드
public static void mergeSort(int[] arr, int left, int right) {
if (left < right) {
// 중간 지점 계산
int middle = (left + right) / 2;
// 왼쪽 절반과 오른쪽 절반 재귀적으로 정렬
mergeSort(arr, left, middle);
mergeSort(arr, middle + 1, right);
// 두 부분 병합
merge(arr, left, middle, right);
}
}
// 두 부분을 병합하는 메서드
public static void merge(int[] arr, int left, int middle, int right) {
// 두 서브 배열의 크기 계산
int n1 = middle - left + 1;
int n2 = right - middle;
// 임시 배열 생성
int[] leftArr = new int[n1];
int[] rightArr = new int[n2];
// 데이터 복사
for (int i = 0; i < n1; i++) {
leftArr[i] = arr[left + i];
}
for (int j = 0; j < n2; j++) {
rightArr[j] = arr[middle + 1 + j];
}
// 병합 과정
int i = 0, j = 0;
int k = left;
while (i < n1 && j < n2) {
if (leftArr[i] <= rightArr[j]) {
arr[k] = leftArr[i];
i++;
} else {
arr[k] = rightArr[j];
j++;
}
k++;
}
// 남은 요소 복사
while (i < n1) {
arr[k] = leftArr[i];
i++;
k++;
}
while (j < n2) {
arr[k] = rightArr[j];
j++;
k++;
}
}
public static void main(String[] args) {
int[] arr = {9, 2, 1, 5, 4};
mergeSort(arr, 0, arr.length - 1);
System.out.println("Sorted array: " + Arrays.toString(arr));
}
}
Java 구현 설명:
mergeSort 메서드는 배열을 재귀적으로 분할하고, merge 메서드는 분할된 배열을 병합합니다.merge 메서드는 두 개의 서브 배열을 정렬하여 하나의 배열로 병합하는 역할을 합니다.출력 예시:
Sorted array: [1, 2, 4, 5, 9]
def merge_sort(arr):
if len(arr) > 1:
# 중간 지점 계산
middle = len(arr) // 2
# 배열을 두 부분으로 나누기
left_half = arr[:middle]
right_half = arr[middle:]
# 각 부분을 재귀적으로 정렬
merge_sort(left_half)
merge_sort(right_half)
# 병합 과정
i = j = k = 0
# 양쪽 배열을 병합
while i < len(left_half) and j < len(right_half):
if left_half[i] < right_half[j]:
arr[k] = left_half[i]
i += 1
else:
arr[k] = right_half[j]
j += 1
k += 1
# 남은 요소 복사
while i < len(left_half):
arr[k] = left_half[i]
i += 1
k += 1
while j < len(right_half):
arr[k] = right_half[j]
j += 1
k += 1
# 예시 배열
arr = [9, 2, 1, 5, 4]
merge_sort(arr)
print("Sorted array:", arr)
Python 구현 설명:
merge_sort 함수는 배열을 재귀적으로 두 부분으로 나누고, 이를 다시 병합하여 정렬된 배열을 얻습니다.출력 예시:
Sorted array: [1, 2, 4, 5, 9]

Tim Sort는 삽입 정렬(Insertion Sort)과 병합 정렬(Merge Sort)의 장점을 결합한 하이브리드 안정적 정렬 알고리즘으로, 실제 데이터에서 높은 성능을 발휘하도록 설계되었습니다.
시간 복잡도:
특징:
Tim Sort는 실제 데이터에서 최적의 성능을 발휘할 수 있도록 설계된 하이브리드 정렬 알고리즘입니다. 실제 동작 방식은 매우 효율적이고 복잡한 알고리즘이지만, 여기선 이해하기 쉽게 간소화해서 설명하도록 하겠습니다.
Run 생성:
이진 삽입 정렬(Binary Insertion Sort):
병합(Merge):

동작 예시 (간소화):
주어진 배열 [10, 9, 2, 5, 0, 7, 8, 3, 6, 4, 1]을 Tim Sort로 정렬하는 과정은 다음과 같습니다:
Run 생성:
[10, 9, 2, 5, 0, 7, 8, 3, 6, 4, 1]은 [10, 9, 2], [5, 0, 7], [8, 3, 6], [4, 1]로 나눕니다.이진 삽입 정렬(Binary Insertion Sort):
[2, 9, 10]이 됩니다.[0, 5, 7]이 됩니다.[3, 6, 8]이 됩니다.[1, 4]가 됩니다.병합(Merge):
[2, 9, 10]과 [0, 5, 7]을 병합하면 [0, 2, 5, 7, 9, 10]이 됩니다.[3, 6, 8]과 [1, 4]을 병합하면 [1, 3, 4, 6, 8]이 됩니다.[0, 2, 5, 7, 9, 10]과 [1, 3, 4, 6, 8]을 병합하여 최종적으로 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]이 완성됩니다.구현된 코드는 간소화된 Tim Sort 구현으로, RUN을 3으로 설정하여 각 작은 구간을 이진 삽입 정렬(Binary Insertion Sort)로 정렬한 후, 병합 정렬(Merge Sort)을 통해 전체 배열을 병합하는 방식으로 동작합니다.
import java.util.Arrays;
public class TimSort {
static final int RUN = 3; // 간소화를 위해 3으로 설정
// 이진 삽입 정렬 함수 (이진 탐색 사용)
public static void binaryInsertionSort(int[] arr, int left, int right) {
for (int i = left + 1; i <= right; i++) {
int key = arr[i];
int low = left;
int high = i - 1;
// 이진 탐색을 통해 삽입 위치를 찾음
while (low <= high) {
int mid = (low + high) / 2;
if (arr[mid] > key) {
high = mid - 1;
} else {
low = mid + 1;
}
}
// 찾은 위치에 삽입
for (int j = i - 1; j >= low; j--) {
arr[j + 1] = arr[j];
}
arr[low] = key;
}
}
// 배열을 병합하는 함수 (병합 정렬)
public static void merge(int[] arr, int l, int m, int r) {
int len1 = m - l + 1, len2 = r - m;
int[] left = new int[len1];
int[] right = new int[len2];
System.arraycopy(arr, l, left, 0, len1);
System.arraycopy(arr, m + 1, right, 0, len2);
int i = 0, j = 0, k = l;
while (i < len1 && j < len2) {
if (left[i] <= right[j]) {
arr[k++] = left[i++];
} else {
arr[k++] = right[j++];
}
}
while (i < len1) arr[k++] = left[i++];
while (j < len2) arr[k++] = right[j++];
}
// TimSort 구현 함수
public static void timSort(int[] arr, int n) {
// 작은 구간을 이진 삽입 정렬로 정렬합니다.
for (int i = 0; i < n; i += RUN) {
binaryInsertionSort(arr, i, Math.min(i + RUN - 1, n - 1));
}
// 구간을 병합하여 정렬합니다.
for (int size = RUN; size < n; size = 2 * size) {
for (int left = 0; left < n; left += 2 * size) {
int mid = Math.min(left + size - 1, n - 1);
int right = Math.min(left + 2 * size - 1, n - 1);
if (mid < right) {
merge(arr, left, mid, right);
}
}
}
}
public static void main(String[] args) {
int[] arr = {10, 9, 2, 5, 0, 7, 8, 3, 6, 4, 1};
timSort(arr, arr.length);
System.out.println("Sorted array: " + Arrays.toString(arr));
}
}
코드 설명:
binaryInsertionSort 함수:
merge 함수:
timSort 함수:
main 함수:
timSort 함수를 호출하여 정렬을 완료한 후, 정렬된 배열을 출력합니다.출력 예시:
Sorted array: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
위 구현은 실제 Tim Sort의 복잡한 부분을 간소화한 것입니다. 실제 Tim Sort는 메모리 최적화와 다양한 추가 최적화가 포함되어 있지만, 여기서는 기본적인 이진 삽입 정렬과 병합 정렬 방식을 결합한 핵심 개념에 집중했습니다.
Python에서도 비슷한 방식으로 간소화된 Tim Sort를 구현할 수 있습니다. 이 코드 역시 RUN을 3으로 설정하여 이진 삽입 정렬과 병합 정렬을 결합한 구조로 동작합니다.
# 이진 삽입 정렬 함수 (이진 탐색 사용)
def binary_insertion_sort(arr, left, right):
for i in range(left + 1, right + 1):
key = arr[i]
low = left
high = i - 1
# 이진 탐색으로 삽입할 위치 찾기
while low <= high:
mid = (low + high) // 2
if arr[mid] > key:
high = mid - 1
else:
low = mid + 1
# 찾은 위치로 요소 삽입
for j in range(i - 1, low - 1, -1):
arr[j + 1] = arr[j]
arr[low] = key
# 병합 함수 (병합 정렬)
def merge(arr, l, m, r):
len1, len2 = m - l + 1, r - m
left, right = arr[l:m + 1], arr[m + 1:r + 1]
i, j, k = 0, 0, l
while i < len1 and j < len2:
if left[i] <= right[j]:
arr[k] = left[i]
i += 1
else:
arr[k] = right[j]
j += 1
k += 1
while i < len1:
arr[k] = left[i]
i += 1
k += 1
while j < len2:
arr[k] = right[j]
j += 1
k += 1
# TimSort 구현 함수
def tim_sort(arr):
RUN = 3 # 간소화를 위해 3으로 설정
n = len(arr)
# 작은 구간을 이진 삽입 정렬로 정렬
for i in range(0, n, RUN):
binary_insertion_sort(arr, i, min(i + RUN - 1, n - 1))
# 병합 정렬을 통해 구간을 병합
size = RUN
while size < n:
for left in range(0, n, 2 * size):
mid = min(left + size - 1, n - 1)
right = min(left + 2 * size - 1, n - 1)
if mid < right:
merge(arr, left, mid, right)
size *= 2
# 예시 배열
arr = [10, 9, 2, 5, 0, 7, 8, 3, 6, 4, 1]
tim_sort(arr)
print("Sorted array:", arr)
코드 설명:
binary_insertion_sort 함수:
merge 함수:
tim_sort 함수:
출력 예시:
Sorted array: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
위 구현은 실제 Tim Sort의 복잡한 부분을 간소화한 것입니다. 실제 Tim Sort는 메모리 최적화와 다양한 추가 최적화가 포함되어 있지만, 여기서는 기본적인 이진 삽입 정렬과 병합 정렬 방식을 결합한 핵심 개념에 집중했습니다.
분할 정복 기반 정렬 알고리즘들은 문제를 작은 단위로 분할한 후 각각을 정렬하고 다시 합쳐 전체 문제를 해결하는 방식입니다.
다음은 각 정렬 알고리즘의 시간 복잡도와 특징을 비교하여 정리한 표입니다.
| 정렬 알고리즘 | 평균 시간 복잡도 | 최악 시간 복잡도 | 공간 복잡도 | 안정성 | 특징 |
|---|---|---|---|---|---|
| Quick Sort | 불안정 | 피벗 선택에 따라 최악의 성능을 가짐 (불균형 분할의 경우) | |||
| Dual-Pivot Quick Sort | 불안정 | 두 피벗을 사용하여 성능을 개선한 퀵 정렬 | |||
| Merge Sort | 안정적 | 재귀 호출로 인해 추가 메모리 사용, 안정적 정렬 보장 | |||
| Tim Sort | 안정적 | 실무에서 자주 사용되며, Java와 Python의 기본 정렬 알고리즘 |
Quick Sort:
Dual-Pivot Quick Sort:
Merge Sort:
Tim Sort:
분할 정복 기반 정렬 알고리즘은 대부분 의 시간 복잡도를 가지며, 데이터 특성에 따라 선택해야 합니다.
저번 포스팅을 포함해 퀵 정렬, 듀얼 피벗 퀵 정렬, 합병 정렬, 그리고 팀 정렬 등 분할 정복 기반 정렬 알고리즘에 대해 살펴보았습니다. 각 알고리즘이 가진 장단점, 시간 복잡도, 그리고 구현 방법을 이해하고 적재적소에 사용하는 것이 중요합니다.
정렬 알고리즘은 컴퓨터 과학에서 매우 중요한 주제입니다. 데이터의 크기가 커질수록, 그리고 성능이 중요할수록 올바른 정렬 알고리즘 선택이 문제 해결의 핵심이 됩니다.
다음 포스팅부터는 탐색 알고리즘이나 그래프 알고리즘과 같은 또 다른 중요한 알고리즘 주제들을 다룰 예정입니다.