콘텐츠로 이동

11.6   병합 정렬

병합 정렬(merge sort)은 분할 정복 전략에 기반한 정렬 알고리즘으로, 아래 그림(11-10)의 '분할' 단계와 '병합' 단계로 이루어진다.

  1. 분할 단계: 재귀적으로 배열을 계속 중간에서 나누어, 긴 배열을 정렬하는 문제를 짧은 배열을 정렬하는 문제로 바꾼다.
  2. 병합 단계: 부분 배열의 길이가 1이 되면 분할을 멈추고 병합을 시작한다. 정렬된 짧은 왼쪽 배열과 오른쪽 배열을 하나의 더 긴 정렬된 배열로 계속 합치며, 끝날 때까지 반복한다.

병합 정렬의 분할 단계와 병합 단계

그림 11-10   병합 정렬의 분할 단계와 병합 단계

11.6.1   알고리즘 과정

아래 그림(11-11)과 같이 '분할 단계'에서는 위에서 아래로 재귀하며 배열을 중간에서 두 부분 배열로 나눈다.

  1. 배열의 중간 인덱스 mid를 계산하고 왼쪽 부분 배열(구간 [left, mid])과 오른쪽 부분 배열(구간 [mid + 1, right])을 재귀적으로 나눈다.
  2. 부분 배열 구간의 길이가 1이 될 때까지 1. 단계를 재귀적으로 반복한다.

'병합 단계'에서는 아래에서 위로 왼쪽 부분 배열과 오른쪽 부분 배열을 하나의 정렬된 배열로 합친다. 길이가 1인 부분 배열부터 병합하므로, 병합 단계의 각 부분 배열은 이미 정렬되어 있다.

병합 정렬 단계

병합 정렬 2단계

병합 정렬 3단계

병합 정렬 4단계

병합 정렬 5단계

병합 정렬 6단계

병합 정렬 7단계

병합 정렬 8단계

병합 정렬 9단계

병합 정렬 10단계

그림 11-11   병합 정렬 단계

병합 정렬의 재귀 순서는 이진 트리의 후위 순회와 같다는 것을 알 수 있다.

  • 후위 순회: 왼쪽 서브트리에 재귀한 다음 오른쪽 서브트리에 재귀하고, 마지막으로 루트 노드를 처리한다.
  • 병합 정렬: 왼쪽 부분 배열에 재귀한 다음 오른쪽 부분 배열에 재귀하고, 마지막으로 병합한다.

병합 정렬의 구현은 다음 코드와 같다. nums에서 병합할 구간은 [left, right]이고, 이에 대응하는 tmp의 구간은 [0, right - left]라는 점에 주의하자.

merge_sort.py
def merge(nums: list[int], left: int, mid: int, right: int):
    """왼쪽 부분 배열과 오른쪽 부분 배열 병합"""
    # 왼쪽 부분 배열의 구간은 [left, mid]이고, 오른쪽 부분 배열의 구간은 [mid+1, right]이다
    # 병합 결과를 저장할 임시 배열 tmp를 생성한다
    tmp = [0] * (right - left + 1)
    # 왼쪽 부분 배열과 오른쪽 부분 배열의 시작 인덱스를 초기화한다
    i, j, k = left, mid + 1, 0
    # 양쪽 부분 배열 모두에 원소가 남아 있으면 비교하여 더 작은 원소를 임시 배열에 복사한다
    while i <= mid and j <= right:
        if nums[i] <= nums[j]:
            tmp[k] = nums[i]
            i += 1
        else:
            tmp[k] = nums[j]
            j += 1
        k += 1
    # 왼쪽 부분 배열과 오른쪽 부분 배열의 남은 원소를 임시 배열에 복사한다
    while i <= mid:
        tmp[k] = nums[i]
        i += 1
        k += 1
    while j <= right:
        tmp[k] = nums[j]
        j += 1
        k += 1
    # 임시 배열 tmp의 원소를 원래 배열 nums의 해당 구간에 다시 복사한다
    for k in range(0, len(tmp)):
        nums[left + k] = tmp[k]

def merge_sort(nums: list[int], left: int, right: int):
    """병합 정렬"""
    # 종료 조건
    if left >= right:
        return  # 부분 배열의 길이가 1이면 재귀를 종료한다
    # 분할 단계
    mid = (left + right) // 2 # 중간 위치를 계산한다
    merge_sort(nums, left, mid)  # 왼쪽 부분 배열에 재귀 호출한다
    merge_sort(nums, mid + 1, right)  # 오른쪽 부분 배열에 재귀 호출한다
    # 병합 단계
    merge(nums, left, mid, right)
merge_sort.cpp
/* 왼쪽 부분 배열과 오른쪽 부분 배열 병합 */
void merge(vector<int> &nums, int left, int mid, int right) {
    // 왼쪽 부분 배열의 구간은 [left, mid]이고, 오른쪽 부분 배열의 구간은 [mid+1, right]이다
    // 병합 결과를 저장할 임시 배열 tmp를 생성한다
    vector<int> tmp(right - left + 1);
    // 왼쪽 부분 배열과 오른쪽 부분 배열의 시작 인덱스를 초기화한다
    int i = left, j = mid + 1, k = 0;
    // 양쪽 부분 배열 모두에 원소가 남아 있으면 비교하여 더 작은 원소를 임시 배열에 복사한다
    while (i <= mid && j <= right) {
        if (nums[i] <= nums[j])
            tmp[k++] = nums[i++];
        else
            tmp[k++] = nums[j++];
    }
    // 왼쪽 부분 배열과 오른쪽 부분 배열의 남은 원소를 임시 배열에 복사한다
    while (i <= mid) {
        tmp[k++] = nums[i++];
    }
    while (j <= right) {
        tmp[k++] = nums[j++];
    }
    // 임시 배열 tmp의 원소를 원래 배열 nums의 해당 구간에 다시 복사한다
    for (k = 0; k < tmp.size(); k++) {
        nums[left + k] = tmp[k];
    }
}

/* 병합 정렬 */
void mergeSort(vector<int> &nums, int left, int right) {
    // 종료 조건
    if (left >= right)
        return; // 부분 배열의 길이가 1이면 재귀를 종료한다
    // 분할 단계
    int mid = left + (right - left) / 2;    // 중간 위치를 계산한다
    mergeSort(nums, left, mid);      // 왼쪽 부분 배열에 재귀 호출한다
    mergeSort(nums, mid + 1, right); // 오른쪽 부분 배열에 재귀 호출한다
    // 병합 단계
    merge(nums, left, mid, right);
}
merge_sort.java
/* 왼쪽 부분 배열과 오른쪽 부분 배열 병합 */
void merge(int[] nums, int left, int mid, int right) {
    // 왼쪽 부분 배열의 구간은 [left, mid]이고, 오른쪽 부분 배열의 구간은 [mid+1, right]이다
    // 병합 결과를 저장할 임시 배열 tmp를 생성한다
    int[] tmp = new int[right - left + 1];
    // 왼쪽 부분 배열과 오른쪽 부분 배열의 시작 인덱스를 초기화한다
    int i = left, j = mid + 1, k = 0;
    // 양쪽 부분 배열 모두에 원소가 남아 있으면 비교하여 더 작은 원소를 임시 배열에 복사한다
    while (i <= mid && j <= right) {
        if (nums[i] <= nums[j])
            tmp[k++] = nums[i++];
        else
            tmp[k++] = nums[j++];
    }
    // 왼쪽 부분 배열과 오른쪽 부분 배열의 남은 원소를 임시 배열에 복사한다
    while (i <= mid) {
        tmp[k++] = nums[i++];
    }
    while (j <= right) {
        tmp[k++] = nums[j++];
    }
    // 임시 배열 tmp의 원소를 원래 배열 nums의 해당 구간에 다시 복사한다
    for (k = 0; k < tmp.length; k++) {
        nums[left + k] = tmp[k];
    }
}

/* 병합 정렬 */
void mergeSort(int[] nums, int left, int right) {
    // 종료 조건
    if (left >= right)
        return; // 부분 배열의 길이가 1이면 재귀를 종료한다
    // 분할 단계
    int mid = left + (right - left) / 2; // 중간 위치를 계산한다
    mergeSort(nums, left, mid); // 왼쪽 부분 배열에 재귀 호출한다
    mergeSort(nums, mid + 1, right); // 오른쪽 부분 배열에 재귀 호출한다
    // 병합 단계
    merge(nums, left, mid, right);
}
merge_sort.cs
/* 왼쪽 부분 배열과 오른쪽 부분 배열 병합 */
void Merge(int[] nums, int left, int mid, int right) {
    // 왼쪽 부분 배열의 구간은 [left, mid]이고, 오른쪽 부분 배열의 구간은 [mid+1, right]이다
    // 병합 결과를 저장할 임시 배열 tmp를 생성한다
    int[] tmp = new int[right - left + 1];
    // 왼쪽 부분 배열과 오른쪽 부분 배열의 시작 인덱스를 초기화한다
    int i = left, j = mid + 1, k = 0;
    // 양쪽 부분 배열 모두에 원소가 남아 있으면 비교하여 더 작은 원소를 임시 배열에 복사한다
    while (i <= mid && j <= right) {
        if (nums[i] <= nums[j])
            tmp[k++] = nums[i++];
        else
            tmp[k++] = nums[j++];
    }
    // 왼쪽 부분 배열과 오른쪽 부분 배열의 남은 원소를 임시 배열에 복사한다
    while (i <= mid) {
        tmp[k++] = nums[i++];
    }
    while (j <= right) {
        tmp[k++] = nums[j++];
    }
    // 임시 배열 tmp의 원소를 원래 배열 nums의 해당 구간에 다시 복사한다
    for (k = 0; k < tmp.Length; ++k) {
        nums[left + k] = tmp[k];
    }
}

/* 병합 정렬 */
void MergeSort(int[] nums, int left, int right) {
    // 종료 조건
    if (left >= right) return;       // 부분 배열의 길이가 1이면 재귀를 종료한다
    // 분할 단계
    int mid = left + (right - left) / 2;    // 중간 위치를 계산한다
    MergeSort(nums, left, mid);      // 왼쪽 부분 배열에 재귀 호출한다
    MergeSort(nums, mid + 1, right); // 오른쪽 부분 배열에 재귀 호출한다
    // 병합 단계
    Merge(nums, left, mid, right);
}
merge_sort.go
/* 왼쪽 부분 배열과 오른쪽 부분 배열 병합 */
func merge(nums []int, left, mid, right int) {
    // 왼쪽 부분 배열의 구간은 [left, mid]이고, 오른쪽 부분 배열의 구간은 [mid+1, right]이다
    // 병합 결과를 저장할 임시 배열 tmp를 생성한다
    tmp := make([]int, right-left+1)
    // 왼쪽 부분 배열과 오른쪽 부분 배열의 시작 인덱스를 초기화한다
    i, j, k := left, mid+1, 0
    // 양쪽 부분 배열 모두에 원소가 남아 있으면 비교하여 더 작은 원소를 임시 배열에 복사한다
    for i <= mid && j <= right {
        if nums[i] <= nums[j] {
            tmp[k] = nums[i]
            i++
        } else {
            tmp[k] = nums[j]
            j++
        }
        k++
    }
    // 왼쪽 부분 배열과 오른쪽 부분 배열의 남은 원소를 임시 배열에 복사한다
    for i <= mid {
        tmp[k] = nums[i]
        i++
        k++
    }
    for j <= right {
        tmp[k] = nums[j]
        j++
        k++
    }
    // 임시 배열 tmp의 원소를 원래 배열 nums의 해당 구간에 다시 복사한다
    for k := 0; k < len(tmp); k++ {
        nums[left+k] = tmp[k]
    }
}

/* 병합 정렬 */
func mergeSort(nums []int, left, right int) {
    // 종료 조건
    if left >= right {
        return
    }
    // 분할 단계
    mid := left + (right - left) / 2
    mergeSort(nums, left, mid)
    mergeSort(nums, mid+1, right)
    // 병합 단계
    merge(nums, left, mid, right)
}
merge_sort.swift
/* 왼쪽 부분 배열과 오른쪽 부분 배열 병합 */
func merge(nums: inout [Int], left: Int, mid: Int, right: Int) {
    // 왼쪽 부분 배열의 구간은 [left, mid]이고, 오른쪽 부분 배열의 구간은 [mid+1, right]이다
    // 병합 결과를 저장할 임시 배열 tmp를 생성한다
    var tmp = Array(repeating: 0, count: right - left + 1)
    // 왼쪽 부분 배열과 오른쪽 부분 배열의 시작 인덱스를 초기화한다
    var i = left, j = mid + 1, k = 0
    // 양쪽 부분 배열 모두에 원소가 남아 있으면 비교하여 더 작은 원소를 임시 배열에 복사한다
    while i <= mid, j <= right {
        if nums[i] <= nums[j] {
            tmp[k] = nums[i]
            i += 1
        } else {
            tmp[k] = nums[j]
            j += 1
        }
        k += 1
    }
    // 왼쪽 부분 배열과 오른쪽 부분 배열의 남은 원소를 임시 배열에 복사한다
    while i <= mid {
        tmp[k] = nums[i]
        i += 1
        k += 1
    }
    while j <= right {
        tmp[k] = nums[j]
        j += 1
        k += 1
    }
    // 임시 배열 tmp의 원소를 원래 배열 nums의 해당 구간에 다시 복사한다
    for k in tmp.indices {
        nums[left + k] = tmp[k]
    }
}

/* 병합 정렬 */
func mergeSort(nums: inout [Int], left: Int, right: Int) {
    // 종료 조건
    if left >= right { // 부분 배열의 길이가 1이면 재귀를 종료한다
        return
    }
    // 분할 단계
    let mid = left + (right - left) / 2 // 중간 위치를 계산한다
    mergeSort(nums: &nums, left: left, right: mid) // 왼쪽 부분 배열에 재귀 호출한다
    mergeSort(nums: &nums, left: mid + 1, right: right) // 오른쪽 부분 배열에 재귀 호출한다
    // 병합 단계
    merge(nums: &nums, left: left, mid: mid, right: right)
}
merge_sort.js
/* 왼쪽 부분 배열과 오른쪽 부분 배열 병합 */
function merge(nums, left, mid, right) {
    // 왼쪽 부분 배열의 구간은 [left, mid]이고, 오른쪽 부분 배열의 구간은 [mid+1, right]이다
    // 병합 결과를 저장할 임시 배열 tmp를 생성한다
    const tmp = new Array(right - left + 1);
    // 왼쪽 부분 배열과 오른쪽 부분 배열의 시작 인덱스를 초기화한다
    let i = left,
        j = mid + 1,
        k = 0;
    // 양쪽 부분 배열 모두에 원소가 남아 있으면 비교하여 더 작은 원소를 임시 배열에 복사한다
    while (i <= mid && j <= right) {
        if (nums[i] <= nums[j]) {
            tmp[k++] = nums[i++];
        } else {
            tmp[k++] = nums[j++];
        }
    }
    // 왼쪽 부분 배열과 오른쪽 부분 배열의 남은 원소를 임시 배열에 복사한다
    while (i <= mid) {
        tmp[k++] = nums[i++];
    }
    while (j <= right) {
        tmp[k++] = nums[j++];
    }
    // 임시 배열 tmp의 원소를 원래 배열 nums의 해당 구간에 다시 복사한다
    for (k = 0; k < tmp.length; k++) {
        nums[left + k] = tmp[k];
    }
}

/* 병합 정렬 */
function mergeSort(nums, left, right) {
    // 종료 조건
    if (left >= right) return; // 부분 배열의 길이가 1이면 재귀를 종료한다
    // 분할 단계
    let mid = Math.floor(left + (right - left) / 2); // 중간 위치를 계산한다
    mergeSort(nums, left, mid); // 왼쪽 부분 배열에 재귀 호출한다
    mergeSort(nums, mid + 1, right); // 오른쪽 부분 배열에 재귀 호출한다
    // 병합 단계
    merge(nums, left, mid, right);
}
merge_sort.ts
/* 왼쪽 부분 배열과 오른쪽 부분 배열 병합 */
function merge(nums: number[], left: number, mid: number, right: number): void {
    // 왼쪽 부분 배열의 구간은 [left, mid]이고, 오른쪽 부분 배열의 구간은 [mid+1, right]이다
    // 병합 결과를 저장할 임시 배열 tmp를 생성한다
    const tmp = new Array(right - left + 1);
    // 왼쪽 부분 배열과 오른쪽 부분 배열의 시작 인덱스를 초기화한다
    let i = left,
        j = mid + 1,
        k = 0;
    // 양쪽 부분 배열 모두에 원소가 남아 있으면 비교하여 더 작은 원소를 임시 배열에 복사한다
    while (i <= mid && j <= right) {
        if (nums[i] <= nums[j]) {
            tmp[k++] = nums[i++];
        } else {
            tmp[k++] = nums[j++];
        }
    }
    // 왼쪽 부분 배열과 오른쪽 부분 배열의 남은 원소를 임시 배열에 복사한다
    while (i <= mid) {
        tmp[k++] = nums[i++];
    }
    while (j <= right) {
        tmp[k++] = nums[j++];
    }
    // 임시 배열 tmp의 원소를 원래 배열 nums의 해당 구간에 다시 복사한다
    for (k = 0; k < tmp.length; k++) {
        nums[left + k] = tmp[k];
    }
}

/* 병합 정렬 */
function mergeSort(nums: number[], left: number, right: number): void {
    // 종료 조건
    if (left >= right) return; // 부분 배열의 길이가 1이면 재귀를 종료한다
    // 분할 단계
    let mid = Math.floor(left + (right - left) / 2); // 중간 위치를 계산한다
    mergeSort(nums, left, mid); // 왼쪽 부분 배열에 재귀 호출한다
    mergeSort(nums, mid + 1, right); // 오른쪽 부분 배열에 재귀 호출한다
    // 병합 단계
    merge(nums, left, mid, right);
}
merge_sort.dart
/* 왼쪽 부분 배열과 오른쪽 부분 배열 병합 */
void merge(List<int> nums, int left, int mid, int right) {
  // 왼쪽 부분 배열의 구간은 [left, mid]이고, 오른쪽 부분 배열의 구간은 [mid+1, right]이다
  // 병합 결과를 저장할 임시 배열 tmp를 생성한다
  List<int> tmp = List.filled(right - left + 1, 0);
  // 왼쪽 부분 배열과 오른쪽 부분 배열의 시작 인덱스를 초기화한다
  int i = left, j = mid + 1, k = 0;
  // 양쪽 부분 배열 모두에 원소가 남아 있으면 비교하여 더 작은 원소를 임시 배열에 복사한다
  while (i <= mid && j <= right) {
    if (nums[i] <= nums[j])
      tmp[k++] = nums[i++];
    else
      tmp[k++] = nums[j++];
  }
  // 왼쪽 부분 배열과 오른쪽 부분 배열의 남은 원소를 임시 배열에 복사한다
  while (i <= mid) {
    tmp[k++] = nums[i++];
  }
  while (j <= right) {
    tmp[k++] = nums[j++];
  }
  // 임시 배열 tmp의 원소를 원래 배열 nums의 해당 구간에 다시 복사한다
  for (k = 0; k < tmp.length; k++) {
    nums[left + k] = tmp[k];
  }
}

/* 병합 정렬 */
void mergeSort(List<int> nums, int left, int right) {
  // 종료 조건
  if (left >= right) return; // 부분 배열의 길이가 1이면 재귀를 종료한다
  // 분할 단계
  int mid = left + (right - left) ~/ 2; // 중간 위치를 계산한다
  mergeSort(nums, left, mid); // 왼쪽 부분 배열에 재귀 호출한다
  mergeSort(nums, mid + 1, right); // 오른쪽 부분 배열에 재귀 호출한다
  // 병합 단계
  merge(nums, left, mid, right);
}
merge_sort.rs
/* 왼쪽 부분 배열과 오른쪽 부분 배열 병합 */
fn merge(nums: &mut [i32], left: usize, mid: usize, right: usize) {
    // 왼쪽 부분 배열의 구간은 [left, mid]이고, 오른쪽 부분 배열의 구간은 [mid+1, right]이다
    // 병합 결과를 저장할 임시 배열 tmp를 생성한다
    let tmp_size = right - left + 1;
    let mut tmp = vec![0; tmp_size];
    // 왼쪽 부분 배열과 오른쪽 부분 배열의 시작 인덱스를 초기화한다
    let (mut i, mut j, mut k) = (left, mid + 1, 0);
    // 양쪽 부분 배열 모두에 원소가 남아 있으면 비교하여 더 작은 원소를 임시 배열에 복사한다
    while i <= mid && j <= right {
        if nums[i] <= nums[j] {
            tmp[k] = nums[i];
            i += 1;
        } else {
            tmp[k] = nums[j];
            j += 1;
        }
        k += 1;
    }
    // 왼쪽 부분 배열과 오른쪽 부분 배열의 남은 원소를 임시 배열에 복사한다
    while i <= mid {
        tmp[k] = nums[i];
        k += 1;
        i += 1;
    }
    while j <= right {
        tmp[k] = nums[j];
        k += 1;
        j += 1;
    }
    // 임시 배열 tmp의 원소를 원래 배열 nums의 해당 구간에 다시 복사한다
    for k in 0..tmp_size {
        nums[left + k] = tmp[k];
    }
}

/* 병합 정렬 */
fn merge_sort(nums: &mut [i32], left: usize, right: usize) {
    // 종료 조건
    if left >= right {
        return; // 부분 배열의 길이가 1이면 재귀를 종료한다
    }

    // 분할 단계
    let mid = left + (right - left) / 2; // 중간 위치를 계산한다
    merge_sort(nums, left, mid); // 왼쪽 부분 배열에 재귀 호출한다
    merge_sort(nums, mid + 1, right); // 오른쪽 부분 배열에 재귀 호출한다

    // 병합 단계
    merge(nums, left, mid, right);
}
merge_sort.c
/* 왼쪽 부분 배열과 오른쪽 부분 배열 병합 */
void merge(int *nums, int left, int mid, int right) {
    // 왼쪽 부분 배열의 구간은 [left, mid]이고, 오른쪽 부분 배열의 구간은 [mid+1, right]이다
    // 병합 결과를 저장할 임시 배열 tmp를 생성한다
    int tmpSize = right - left + 1;
    int *tmp = (int *)malloc(tmpSize * sizeof(int));
    // 왼쪽 부분 배열과 오른쪽 부분 배열의 시작 인덱스를 초기화한다
    int i = left, j = mid + 1, k = 0;
    // 양쪽 부분 배열 모두에 원소가 남아 있으면 비교하여 더 작은 원소를 임시 배열에 복사한다
    while (i <= mid && j <= right) {
        if (nums[i] <= nums[j]) {
            tmp[k++] = nums[i++];
        } else {
            tmp[k++] = nums[j++];
        }
    }
    // 왼쪽 부분 배열과 오른쪽 부분 배열의 남은 원소를 임시 배열에 복사한다
    while (i <= mid) {
        tmp[k++] = nums[i++];
    }
    while (j <= right) {
        tmp[k++] = nums[j++];
    }
    // 임시 배열 tmp의 원소를 원래 배열 nums의 해당 구간에 다시 복사한다
    for (k = 0; k < tmpSize; ++k) {
        nums[left + k] = tmp[k];
    }
    // 메모리 해제
    free(tmp);
}

/* 병합 정렬 */
void mergeSort(int *nums, int left, int right) {
    // 종료 조건
    if (left >= right)
        return; // 부분 배열의 길이가 1이면 재귀를 종료한다
    // 분할 단계
    int mid = left + (right - left) / 2;    // 중간 위치를 계산한다
    mergeSort(nums, left, mid);      // 왼쪽 부분 배열에 재귀 호출한다
    mergeSort(nums, mid + 1, right); // 오른쪽 부분 배열에 재귀 호출한다
    // 병합 단계
    merge(nums, left, mid, right);
}
merge_sort.kt
/* 왼쪽 부분 배열과 오른쪽 부분 배열 병합 */
fun merge(nums: IntArray, left: Int, mid: Int, right: Int) {
    // 왼쪽 부분 배열의 구간은 [left, mid]이고, 오른쪽 부분 배열의 구간은 [mid+1, right]이다
    // 병합 결과를 저장할 임시 배열 tmp를 생성한다
    val tmp = IntArray(right - left + 1)
    // 왼쪽 부분 배열과 오른쪽 부분 배열의 시작 인덱스를 초기화한다
    var i = left
    var j = mid + 1
    var k = 0
    // 양쪽 부분 배열 모두에 원소가 남아 있으면 비교하여 더 작은 원소를 임시 배열에 복사한다
    while (i <= mid && j <= right) {
        if (nums[i] <= nums[j])
            tmp[k++] = nums[i++]
        else
            tmp[k++] = nums[j++]
    }
    // 왼쪽 부분 배열과 오른쪽 부분 배열의 남은 원소를 임시 배열에 복사한다
    while (i <= mid) {
        tmp[k++] = nums[i++]
    }
    while (j <= right) {
        tmp[k++] = nums[j++]
    }
    // 임시 배열 tmp의 원소를 원래 배열 nums의 해당 구간에 다시 복사한다
    for (l in tmp.indices) {
        nums[left + l] = tmp[l]
    }
}

/* 병합 정렬 */
fun mergeSort(nums: IntArray, left: Int, right: Int) {
    // 종료 조건
    if (left >= right) return  // 부분 배열의 길이가 1이면 재귀를 종료한다
    // 분할 단계
    val mid = left + (right - left) / 2 // 중간 위치를 계산한다
    mergeSort(nums, left, mid) // 왼쪽 부분 배열에 재귀 호출한다
    mergeSort(nums, mid + 1, right) // 오른쪽 부분 배열에 재귀 호출한다
    // 병합 단계
    merge(nums, left, mid, right)
}
merge_sort.rb
### 왼쪽 부분 배열과 오른쪽 부분 배열 병합 ###
def merge(nums, left, mid, right)
  # 왼쪽 부분 배열의 구간은 [left, mid]이고, 오른쪽 부분 배열의 구간은 [mid+1, right]이다
  # 병합 결과를 저장할 임시 배열 tmp를 생성한다
  tmp = Array.new(right - left + 1, 0)
  # 왼쪽 부분 배열과 오른쪽 부분 배열의 시작 인덱스를 초기화한다
  i, j, k = left, mid + 1, 0
  # 양쪽 부분 배열 모두에 원소가 남아 있으면 비교하여 더 작은 원소를 임시 배열에 복사한다
  while i <= mid && j <= right
    if nums[i] <= nums[j]
      tmp[k] = nums[i]
      i += 1
    else
      tmp[k] = nums[j]
      j += 1
    end
    k += 1
  end
  # 왼쪽 부분 배열과 오른쪽 부분 배열의 남은 원소를 임시 배열에 복사한다
  while i <= mid
    tmp[k] = nums[i]
    i += 1
    k += 1
  end
  while j <= right
    tmp[k] = nums[j]
    j += 1
    k += 1
  end
  # 임시 배열 tmp의 원소를 원래 배열 nums의 해당 구간에 다시 복사한다
  (0...tmp.length).each do |k|
    nums[left + k] = tmp[k]
  end
end

### 병합 정렬 ###
def merge_sort(nums, left, right)
  # 종료 조건
  # 부분 배열의 길이가 1이면 재귀를 종료한다
  return if left >= right
  # 분할 단계
  mid = left + (right - left) / 2 # 중간 위치를 계산한다
  merge_sort(nums, left, mid) # 왼쪽 부분 배열에 재귀 호출한다
  merge_sort(nums, mid + 1, right) # 오른쪽 부분 배열에 재귀 호출한다
  # 병합 단계
  merge(nums, left, mid, right)
end
코드 시각화

11.6.2   알고리즘 특성

  • 시간 복잡도 \(O(n \log n)\), 비적응형 정렬: 분할 과정에서 높이 \(\log n\)의 재귀 트리가 만들어지고, 각 레벨에서 병합에 필요한 총연산 수는 \(n\)이므로 전체 시간 복잡도는 \(O(n \log n)\)이다.
  • 공간 복잡도 \(O(n)\), 제자리 정렬이 아님: 재귀 깊이가 \(\log n\)이므로 \(O(\log n)\) 크기의 스택 프레임 공간을 사용한다. 병합에는 보조 배열이 필요하여 \(O(n)\)의 추가 공간을 사용한다.
  • 안정 정렬: 병합 과정에서 값이 같은 원소들의 순서가 유지된다.

11.6.3   연결 리스트 정렬

연결 리스트에서는 병합 정렬이 다른 정렬 알고리즘보다 뚜렷한 장점을 가진다. 연결 리스트 정렬의 공간 복잡도를 \(O(1)\)까지 최적화할 수 있다.

  • 분할 단계: 연결 리스트를 나눌 때 '재귀' 대신 '반복'을 사용하면 재귀에 필요한 스택 프레임 공간을 없앨 수 있다.
  • 병합 단계: 연결 리스트에서는 참조(포인터)만 바꾸어 노드를 추가하거나 삭제할 수 있다. 따라서 정렬된 짧은 연결 리스트 두 개를 하나의 긴 연결 리스트로 합칠 때 추가 연결 리스트를 만들 필요가 없다.

구체적인 구현은 다소 복잡하므로, 관심 있는 독자는 관련 자료를 찾아 학습해 보자.

댓글로 의견이나 질문, 제안을 남겨 주세요