📚 /algorithm

1. 병합 정렬 (Merge Sort)


병합 정렬은 분할 정복(Divide and Conquer) 알고리즘의 한 종류로, 데이터를 작은 부분으로 분할하고, 정렬된 부분을 병합하여 전체 데이터를 정렬하는 방식으로 동작한다. 이 알고리즘은 리스트를 반으로 나누어 재귀적으로 각 부분 리스트를 정렬한 후, 정렬된 부분 리스트들을 합쳐서 전체 리스트를 정렬한다.

병합 정렬은 다음과 같은 단계로 이루어진다:

  1. 분할(Divide): 리스트를 중간 지점에서 두 부분으로 분할한다.
  2. 정복(Conquer): 각 부분 리스트를 재귀적으로 병합 정렬을 이용하여 정렬한다.
  3. 병합(Combine): 두 개의 정렬된 부분 리스트를 하나의 정렬된 리스트로 병합한다.


2. 구현 분석


2-1. 구현

MergeSort Code
void mergeSort(int *arr, int left, int right) {
	if (left < right) {
		int mid = left + (right - left) / 2;

		mergeSort(arr, left, mid);
		mergeSort(arr, mid + 1, right);

		merge(arr, left, mid, right);
	}
}

void merge(int *arr, int left, int mid, int right) {
	int i, j, k;
	int left_len = mid - left + 1;
	int right_len = right - mid;

	int L[left_len], R[right_len];
	for (i = 0; i < left_len; i++)
		L[i] = arr[left + i];
	for (j = 0; j < right_len; j++)
		R[j] = arr[m + 1 + j];

	i = 0;
	j = 0;
	k = left;
	while (i < left_len && j < right_len) {
		if (L[i] <= R[j]) {
			arr[k] = L[i];
			i++;
		} else {
			arr[k] = R[j];
			j++;
		}
		k++;
	}

	while (i < left_len) {
		arr[k] = L[i];
		i++;
		k++;
	}
	while (j < right_len) {
		arr[k] = R[j];
		j++;
		k++;
	}
}

2-2. mergeSort

void mergeSort(int *arr, int left, int right) {
	/* 재귀 종료 조건 */
	if (left < right) {
		/* mid index 계산 */
		int mid = left + (right - left) / 2;

		/* 분할 재귀 호출 */
		mergeSort(arr, left, mid);
		mergeSort(arr, mid + 1, right);

		/* 정렬, 병합 */
		merge(arr, left, mid, right);
	}
}

주 함수 void mergeSort(int *arr, int left, int right)는 정렬 대상 배열, 좌측 끝 인덱스, 우측 끝 인덱스를 받는다. 이때 좌측, 우측 끝 인덱스를 받는 이유는 한 재귀 호출 안에서 다루는 배열의 범위가 어디서 부터 어디인지 알기 위함이다.



2-3. merge

void merge(int *arr, int left, int mid, int right) {
	/* 좌측, 우측 배열 길이 계산 */
	int left_len = mid - left + 1;
	int right_len = right - mid;
	
	/* 복사 */
	int i, j, k;
	int L[left_len], R[right_len];
	for (i = 0; i < left_len; i++)
		L[i] = arr[left + i];
	for (j = 0; j < right_len; j++)
		R[j] = arr[m + 1 + j];

	i = 0;
	j = 0;
	k = left;
	
	/* 비교 */
	while (i < left_len && j < right_len) {
		if (L[i] <= R[j]) {
			arr[k] = L[i];
			i++;
		} else {
			arr[k] = R[j];
			j++;
		}
		k++;
	}

	/* 잔여 처리 */
	while (i < left_len) {
		arr[k] = L[i];
		i++;
		k++;
	}
	while (j < right_len) {
		arr[k] = R[j];
		j++;
		k++;
	}
}

merge 함수의 구조는 생각보다 간단하다. 우선 이번 재귀 호출로 merge 함수가 받아온 좌측 배열 범위와 우측 배열 범위를 임시 배열에 각각 복사한다. 그 다음, 좌측 임시 배열과 우측 임시 배열에서 값을 하나씩 꺼내어 더 비교하며 원래 배열을 정렬한다. 비교가 모두 끝난 이후 좌측 또는 우측에 남아있는 값들이 있다면 그냥 뒤에 이어 붙여준다. 즉, 이 함수는 정렬된 두 부분 리스트를 병합하여 하나의 정렬된 리스트로 만드는 역할을 한다.



3. 정확성 분석


병합 정렬은 리스트를 재귀적으로 분할하고, 병합하는 과정에서 정렬을 수행한다. 이제 수학적 귀납법을 통해 이 알고리즘의 정확성을 증명해보자.


3-1. 명제 설정

명제 P(n): 길이 n인 배열을 mergeSort 함수로 정렬하면, 배열은 항상 오름차순으로 정렬된다.


3-2. 귀납적 증명의 구성 요소

  1. 기저 사례(Base Case): n = 1일 때, 명제가 참임을 보인다.
  2. 귀납 가정(Inductive Hypothesis): 길이가 k 이하인 배열에 대해 명제가 참이라고 가정한다.
  3. 귀납 단계(Inductive Step): 길이가 k + 1인 배열에 대해서도 명제가 참임을 보인다.
  4. 종료성(Termination): 알고리즘이 유한한 단계 내에 종료함을 보인다.

3-3. 귀납적 증명

기저 사례


귀납 가정


귀납 단계


종료성


결론



4. 효율성 분석


4-1. 시간 복잡도

병합 정렬의 시간 복잡도는 분할 단계와 병합 단계로 나누어 분석할 수 있다.

따라서 전체 시간 복잡도는 다음과 같다.

$$ T(n) = 2T\left(\frac{n}{2}\right) + O(n) $$

이를 마스터 정리를 통해 해석하면:

$$ T(n) = O(n \log n) $$

master method 더 알아보기 »


4-2. 공간 복잡도

병합 정렬은 추가적인 임시 배열을 사용하여 데이터를 저장하므로, 공간 복잡도는 O(n)이다. 이는 입력 배열의 크기에 비례하는 추가 메모리가 필요함을 의미한다.


4-3. Best, Average, Worst Case

병합 정렬은 입력 배열의 초기 상태와 관계없이 항상 동일한 분할과 병합 과정을 수행하므로, Best Case, Average Case, Worst Case 모두 시간 복잡도가 O(nlog n)이다.



5. 결론


병합 정렬은 분할 정복 알고리즘을 활용하여 안정적이고 효율적인 정렬을 수행한다. 시간 복잡도가 O(nlog n)으로 효율적이며, 입력 데이터의 분포에 영향을 받지 않는다. 그러나 추가적인 메모리 공간을 필요로 하기 때문에 메모리 사용이 중요한 환경에서는 고려가 필요하다. 대량의 데이터를 안정적으로 정렬해야 하는 경우에 적합한 알고리즘이다.



6. Reference