본문으로 건너뛰기
김신건의 로그

Merge Sort

· 수정 · 📖 약 3분 · 1,139자/단어 #algorithm #sorting #divide-and-conquer
merge sort, 병합 정렬, 머지 정렬, Merge

정의

Merge Sort (병합 정렬) 는 분할 정복 (Divide & Conquer) 으로 동작하는 비교 정렬. 배열을 반으로 나눠 각각 정렬한 뒤, 두 정렬된 부분을 병합 (merge) 한다.

John von Neumann 이 1945 년에 고안한 알고리즘. 모든 입력에 대해 O(n log n) 을 보장 하고 안정 정렬 인 것이 특징. External Merge Sort 의 기초.

전체 비교는 정렬 알고리즘 참고. RDBMS 에서의 메모리 spill 메커니즘은 정렬·해시는 메모리가 부족하면 어디로 새는가, PGA, work_mem, Workspace Memory 참고.

시각화

알고리즘

세 단계.

  1. Divide: 배열을 중간에서 둘로 나눈다.
  2. Conquer: 각 부분을 재귀적으로 Merge Sort.
  3. Combine (Merge): 정렬된 두 부분을 비교하며 합친다.
mergeSort(arr):
  if length(arr) ≤ 1: return arr
  mid = length(arr) / 2
  left  = mergeSort(arr[0..mid])
  right = mergeSort(arr[mid..])
  return merge(left, right)

merge(left, right):
  result = []
  i = j = 0
  while i < len(left) and j < len(right):
    if left[i] ≤ right[j]:
      result.push(left[i]); i++
    else:
      result.push(right[j]); j++
  result.push(나머지 left, 나머지 right)
  return result

Merge 단계가 핵심

두 정렬된 배열을 합치는 데 선형 시간 이 든다. 각 배열의 맨 앞만 비교하면 되기 때문.

left:  [1, 4, 7]
right: [2, 5, 6]

1 vs 2 → 1 채택       result = [1]
4 vs 2 → 2 채택       result = [1, 2]
4 vs 5 → 4 채택       result = [1, 2, 4]
7 vs 5 → 5 채택       result = [1, 2, 4, 5]
7 vs 6 → 6 채택       result = [1, 2, 4, 5, 6]
7 (남음)               result = [1, 2, 4, 5, 6, 7]

복잡도

항목
시간 (최선)O(n log n)
시간 (평균)O(n log n)
시간 (최악)O(n log n)
공간O(n) (보조 배열)
안정성✓ Stable
In-place✗ (보조 배열 필요)

왜 항상 O(n log n) 인가

재귀 트리의 깊이가 log₂ n. 각 깊이에서 모든 원소가 정확히 한 번씩 merge 됨 (총 n 비교 작업). 따라서 log n × n = n log n 회 작업.

[8, 3, 1, 7, 0, 10, 2, 6]
       ↓ divide
[8, 3, 1, 7]  [0, 10, 2, 6]
       ↓ divide
[8,3] [1,7]  [0,10] [2,6]
       ↓ divide
[8][3] [1][7]  [0][10] [2][6]
       ↓ merge (level log n)
[3,8] [1,7]  [0,10] [2,6]
       ↓ merge
[1,3,7,8]  [0,2,6,10]
       ↓ merge
[0,1,2,3,6,7,8,10]

IMPORTANT

Quicksort 의 O(n²) 최악 케이스가 없다. 입력이 어떻게 생겼든 정확히 같은 시간이 든다. 응답 시간 보장이 중요한 시스템 (실시간 / DB / 게임 서버) 에서 유리.

In-place 변형

표준 Merge Sort 는 O(n) 보조 메모리를 쓰지만, In-place Merge Sort 도 가능하다. 다만 구현이 복잡하고 상수항이 크다. 실무에서 흔히 쓰는 변형은 다음.

Bottom-up Merge Sort

재귀 대신 반복 으로 구현. 크기 1 → 2 → 4 → … 의 부분 배열을 차례로 머지.

function mergeSortBottomUp(arr) {
  const n = arr.length;
  const aux = new Array(n);
  for (let width = 1; width < n; width *= 2) {
    for (let lo = 0; lo < n - width; lo += 2 * width) {
      const mid = lo + width - 1;
      const hi = Math.min(lo + 2 * width - 1, n - 1);
      merge(arr, aux, lo, mid, hi);
    }
  }
  return arr;
}

재귀 호출 스택이 없어 공간 O(log n) → O(1) (보조 배열 외). DB 시스템이 자주 쓰는 패턴.

Natural Merge Sort

입력에서 이미 정렬된 부분 (run) 을 찾아 그것을 단위로 merge. 거의 정렬된 입력에 O(n) 가까운 성능. Timsort 의 기초.

Quicksort 와의 비교

항목Merge SortQuick Sort
평균 시간O(n log n)O(n log n)
최악 시간O(n log n) 보장O(n²) (드물지만 가능)
공간O(n)O(log n)
안정성✓ Stable✗ Unstable
In-place✓ (거의)
캐시 친화△ (별도 메모리 접근)✓ (in-place)
실제 속도Quick 보다 조금 느림평균 더 빠름

TIP

선택 기준: 안정성이 필요하거나 응답 시간 보장이 필요하면 Merge. 평균 성능과 메모리 효율이 우선이면 Quick. 둘 다 필요하면 Timsort (Merge + Insertion 하이브리드, Python / Java / Rust 표준).

실무 활용

Timsort (Python / Java / Rust)

Tim Peters 가 2002 년 Python 용으로 만든 알고리즘. 현실 데이터에서 자주 보이는 정렬된 부분 (run) 을 활용한 Merge Sort 변형.

# Python
sorted([3, 1, 4, 1, 5, 9, 2, 6])  # Timsort
# Java
Arrays.sort(arr);  // 객체 배열은 Timsort

핵심 아이디어:

  1. 입력에서 자연스러운 run (이미 정렬된 부분) 식별
  2. run 이 너무 짧으면 Insertion Sort 로 길이 맞춤
  3. run 들을 Merge Sort 방식으로 합침
  4. 정렬된 입력에 O(n), 무작위 입력에 O(n log n)

외부 정렬의 기초

External Merge Sort 는 Merge Sort 를 디스크 단위로 확장 한 것:

  • “정렬된 부분 배열” → “정렬된 Run 파일”
  • “merge 단계” → “K-way Merge” (한 번에 여러 Run 합침)

RDBMS 의 ORDER BY 처리에서 데이터 > 메모리 일 때 사용.

연결 리스트 정렬에 최적

배열은 quicksort 가 빠르지만, 연결 리스트 는 임의 접근이 비싸 quicksort 가 비효율. Merge Sort 는 순차 접근만 하므로 연결 리스트 정렬의 표준.

// 연결 리스트 merge sort (LeetCode 148번)
function sortList(head) {
  if (!head || !head.next) return head;
  const mid = getMiddle(head);
  const right = mid.next;
  mid.next = null;
  return merge(sortList(head), sortList(right));
}

함정

1. 공간 비용

n=10⁹ 정렬에 추가 메모리 n=10⁹ 필요 → 메모리 부족하면 External Merge Sort 필요.

2. 캐시 미스

In-place 가 아니라 메모리 두 영역 (원본 + 보조) 을 번갈아 접근. 작은 n 에서는 quicksort 보다 캐시 효율이 떨어진다.

3. 작은 부분 배열에서의 비효율

Merge 의 상수항이 크다. 작은 부분 (예: n ≤ 16) 에서는 Insertion Sort 가 훨씬 빠르다. Timsort 가 이 점을 활용한다.

참고

이 글의 용어 (5개)
정렬 알고리즘algorithm
정의 정렬 (sort) 은 원소들의 컬렉션을 어떤 전순서 (total order) 기준으로 재배열하는 것. 알고리즘 입문의 정석 주제이자, 데이터베이스·검색·통계 등 모든 시스템…
External Merge Sortalgorithm
정의 External Merge Sort (외부 머지 정렬) 는 데이터가 메모리에 들어가지 않을 때 사용하는 변형. 두 단계로 동작한다. 1. Run 생성: 메모리 크기만큼 청크…
Heap Sortalgorithm
정의 Heap Sort (힙 정렬) 는 힙 (heap) 자료구조의 최댓값 / 최솟값을 O(log n) 에 추출 하는 성질을 이용한 정렬. In-place 이고 최악 O(n log…
Insertion Sortalgorithm
정의 Insertion Sort (삽입 정렬) 는 한 번에 한 원소씩 이미 정렬된 부분에 올바른 위치를 찾아 삽입 하는 정렬. 카드 정리하듯이 한 장씩 적절한 자리에 끼워넣는 동…
Quick Sortalgorithm
정의 Quick Sort (퀵 정렬) 는 분할 정복 (Divide & Conquer) 기반 비교 정렬. 1959 년 Tony Hoare 가 고안. 배열에서 pivot 을 하나 골…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기