병합 정렬 (Merge Sort)

한눈에 보기

배열을 절반씩 나눠 정렬한 뒤 합치는 분할 정복 정렬인 병합 정렬을 다룬다. 항상 O(n log n)을 보장하는 이유, 안정성이 결정되는 지점, 구현 코드와 활용을 정리한다.

최종 업데이트 2026.09.17

병합 정렬(Merge Sort)은 배열을 절반씩 쪼개 더 이상 쪼갤 수 없을 때까지 내려간 뒤, 정렬된 두 조각을 합치며 올라오는 정렬 알고리즘이다. 어떤 입력이 들어와도 항상 O(n log n)을 보장하고, 같은 값의 순서가 뒤바뀌지 않는 안정 정렬이라는 두 가지 성질이 핵심이다.

코딩테스트에서 병합 정렬을 직접 구현할 일은 많지 않다. 언어의 기본 정렬을 쓰면 되기 때문이다. 대신 병합하는 과정 자체를 응용하는 문제가 나온다. 두 정렬된 배열 합치기, 역순 쌍(inversion) 개수 세기 같은 유형이 전부 이 글의 "병합" 파트를 그대로 쓴다.

개념과 원리

병합 정렬은 분할 정복(Divide and Conquer)의 교과서적인 예다. 세 단계로 나뉜다.

  1. 분할: 배열을 가운데서 두 조각으로 나눈다.
  2. 정복: 각 조각을 재귀적으로 정렬한다. 원소가 하나면 이미 정렬된 것이므로 멈춘다.
  3. 병합: 정렬된 두 조각을 하나의 정렬된 배열로 합친다.

여기서 실제로 일이 일어나는 곳은 3번 병합이다. 분할은 그냥 인덱스를 반으로 자르는 것뿐이고, 정렬은 병합의 결과로 따라온다.

병합 정렬: 여덟 개 원소가 네 단계로 쪼개졌다가 다시 병합되며 정렬되는 트리 구조 그림
원소가 8개면 깊이는 3단계다. 각 단계에서 모든 원소를 한 번씩 훑으며 병합하므로 단계마다 O(n), 단계 수가 log n이라 전체가 O(n log n)이 된다.

병합은 어떻게 하는가

정렬된 두 배열을 합치는 것이 병합 정렬의 전부다. 두 배열의 맨 앞을 가리키는 포인터를 하나씩 두고, 더 작은 쪽을 결과에 넣은 뒤 그 포인터만 한 칸 옮긴다. 한쪽이 바닥나면 남은 쪽을 통째로 뒤에 붙인다.

병합 정렬: 정렬된 두 배열 A와 B를 포인터로 비교하며 결과 배열에 작은 값부터 채워 넣는 과정
비교 한 번에 결과 칸 하나가 확정된다. 두 배열의 길이 합만큼만 비교하므로 병합 한 번은 O(n)이다.

시간 복잡도

구분복잡도설명
최선 / 평균 / 최악O(n log n)입력 상태와 무관하게 항상 동일
공간O(n)병합 결과를 담을 임시 배열이 필요
안정성안정 정렬같은 값의 원래 순서가 유지됨

퀵 정렬이 평균적으로 더 빠른데도 병합 정렬이 살아남은 이유는 최악의 경우에도 O(n log n)이라는 보장 때문이다. 퀵 정렬은 최악에 O(n²)까지 떨어진다. 대신 병합 정렬은 원본과 같은 크기의 임시 공간이 필요하다는 대가를 치른다.

구현 코드 예시

def merge_sort(arr):
    if len(arr) <= 1:               # 원소가 하나면 이미 정렬된 상태
        return arr

    mid = len(arr) // 2
    left = merge_sort(arr[:mid])
    right = merge_sort(arr[mid:])
    return merge(left, right)


def merge(left, right):
    result = []
    i = j = 0

    while i < len(left) and j < len(right):
        if left[i] <= right[j]:      # 이 <= 가 안정 정렬을 만든다
            result.append(left[i])
            i += 1
        else:
            result.append(right[j])
            j += 1

    result.extend(left[i:])          # 한쪽이 바닥나면 나머지를 통째로
    result.extend(right[j:])
    return result

left[i] <= right[j]에서 등호를 빼고 <로만 쓰면 값이 같을 때 오른쪽을 먼저 집는다. 그러면 원래 왼쪽에 있던 원소가 뒤로 밀려 안정성이 깨진다. 안정 정렬이 필요한 상황(예: 이미 이름순으로 정렬된 데이터를 점수순으로 다시 정렬하면서 동점자는 이름순을 유지하고 싶을 때)에서 이 등호 하나가 결과를 바꾼다.

안정 정렬이란

값이 같은 원소들의 원래 순서가 정렬 후에도 유지되는 성질이다. 예를 들어 [("김", 90), ("이", 80), ("박", 90)]을 점수 기준으로 정렬할 때, 안정 정렬은 90점인 김과 박의 순서를 그대로 둔다. 불안정 정렬은 뒤바꿀 수 있다.

주요 정렬의 안정성은 이렇게 갈린다.

안정 정렬불안정 정렬
병합 정렬, 삽입 정렬, 버블 정렬, 계수 정렬퀵 정렬, 힙 정렬, 선택 정렬

파이썬의 sorted()와 list.sort()는 병합 정렬을 개량한 Timsort를 쓰기 때문에 안정 정렬이다. 자바도 객체 배열 정렬에는 Timsort를, 기본형 배열에는 불안정한 듀얼 피벗 퀵 정렬을 쓴다.

자주 하는 실수

  • 재귀 종료 조건을 len(arr) == 0으로 쓴다. 원소가 1개일 때 멈추지 않으면 계속 자기 자신을 쪼개며 무한 재귀에 빠진다.
  • 병합 마지막에 남은 원소를 안 붙인다. while은 한쪽이 바닥나면 끝나므로, 남은 쪽을 extend로 붙이는 두 줄이 반드시 필요하다.
  • 부등호에서 등호를 뺀다. 안정성이 깨진다. 위 설명 참고.
  • 재귀 깊이를 넘긴다. 파이썬 기본 재귀 한도는 1000이라 n이 크면 위험하다. 코딩테스트에서는 sys.setrecursionlimit(10**6)을 걸어두는 편이 안전하다.
  • 슬라이싱 비용을 무시한다. arr[:mid]는 복사를 만들어 매 단계 O(n)을 추가로 쓴다. 성능이 중요하면 인덱스 범위만 넘기는 방식으로 바꾼다.

면접에서 이렇게 물어본다

  • 병합 정렬이 항상 O(n log n)인 이유를 설명해 보라.
  • 퀵 정렬이 평균적으로 더 빠른데 병합 정렬을 쓰는 이유는 무엇인가.
  • 안정 정렬이 무엇이고 병합 정렬이 안정적인 이유는 무엇인가.
  • 병합 정렬의 공간 복잡도가 O(n)인 이유와 이를 줄일 방법이 있는가.
  • 연결 리스트를 정렬한다면 병합 정렬과 퀵 정렬 중 무엇이 유리한가.
  • 병합 과정을 응용해 배열의 역순 쌍 개수를 세려면 어떻게 하겠는가.
알고리즘 문서 전체 보기

이 개념, 내 코드에서는 어떻게 물어볼까요

데브핏(DevFit)에 채용공고 URL을 붙여 넣고 레포지토리를 연결하면, 내가 쓴 커밋에서 예상 질문과 어필 포인트를 만듭니다. 하루 10회까지 무료입니다. 합불 판정이나 지원자 랭킹은 만들지 않고, 최종 판단은 사람이 합니다.

공고 넣고 시작하기