병합 정렬 (Merge Sort)
한눈에 보기
배열을 절반씩 나눠 정렬한 뒤 합치는 분할 정복 정렬인 병합 정렬을 다룬다. 항상 O(n log n)을 보장하는 이유, 안정성이 결정되는 지점, 구현 코드와 활용을 정리한다.
최종 업데이트 2026.09.17
병합 정렬(Merge Sort)은 배열을 절반씩 쪼개 더 이상 쪼갤 수 없을 때까지 내려간 뒤, 정렬된 두 조각을 합치며 올라오는 정렬 알고리즘이다. 어떤 입력이 들어와도 항상 O(n log n)을 보장하고, 같은 값의 순서가 뒤바뀌지 않는 안정 정렬이라는 두 가지 성질이 핵심이다.
코딩테스트에서 병합 정렬을 직접 구현할 일은 많지 않다. 언어의 기본 정렬을 쓰면 되기 때문이다. 대신 병합하는 과정 자체를 응용하는 문제가 나온다. 두 정렬된 배열 합치기, 역순 쌍(inversion) 개수 세기 같은 유형이 전부 이 글의 "병합" 파트를 그대로 쓴다.
개념과 원리
병합 정렬은 분할 정복(Divide and Conquer)의 교과서적인 예다. 세 단계로 나뉜다.
- 분할: 배열을 가운데서 두 조각으로 나눈다.
- 정복: 각 조각을 재귀적으로 정렬한다. 원소가 하나면 이미 정렬된 것이므로 멈춘다.
- 병합: 정렬된 두 조각을 하나의 정렬된 배열로 합친다.
여기서 실제로 일이 일어나는 곳은 3번 병합이다. 분할은 그냥 인덱스를 반으로 자르는 것뿐이고, 정렬은 병합의 결과로 따라온다.
병합은 어떻게 하는가
정렬된 두 배열을 합치는 것이 병합 정렬의 전부다. 두 배열의 맨 앞을 가리키는 포인터를 하나씩 두고, 더 작은 쪽을 결과에 넣은 뒤 그 포인터만 한 칸 옮긴다. 한쪽이 바닥나면 남은 쪽을 통째로 뒤에 붙인다.
시간 복잡도
| 구분 | 복잡도 | 설명 |
|---|---|---|
| 최선 / 평균 / 최악 | 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)인 이유와 이를 줄일 방법이 있는가.
- 연결 리스트를 정렬한다면 병합 정렬과 퀵 정렬 중 무엇이 유리한가.
- 병합 과정을 응용해 배열의 역순 쌍 개수를 세려면 어떻게 하겠는가.
알고리즘에서 같이 읽기
- 이분 탐색 (Binary Search)정렬된 배열에서 탐색 범위를 절반씩 줄여 O(log n)에 값을 찾는 이분 탐색의 원리와 동작 과정을 그림으로 정리했다. lower bound와 upper bound, 코딩테스트 단골인 파라메트릭 서치와 회전 정렬 배열, 구현 코드와 자주 하는 실수까지 다룬다.
- 비트마스크 (Bitmask)정수의 각 비트로 집합의 포함 여부를 표현하는 비트마스크 기법을 다룬다. 기본 비트 연산 정리, 부분집합 열거, 외판원 순회 동적 계획법 구현과 주의점을 정리한다.
- DFS와 BFS (Depth First Search, Breadth First Search)그래프의 모든 정점을 방문하는 DFS와 BFS의 원리와 방문 순서 차이를 스택과 큐로 설명하고, O(V + E) 복잡도, 구현 코드, 문제 유형별 선택 기준을 정리한다.
- 다익스트라 알고리즘 (Dijkstra's Algorithm)음수 간선이 없는 그래프에서 한 정점으로부터의 최단 거리를 구하는 다익스트라 알고리즘의 그리디 원리, 우선순위 큐 구현, 복잡도 비교와 한계를 정리한다.
이 개념, 내 코드에서는 어떻게 물어볼까요
데브핏(DevFit)에 채용공고 URL을 붙여 넣고 레포지토리를 연결하면, 내가 쓴 커밋에서 예상 질문과 어필 포인트를 만듭니다. 하루 10회까지 무료입니다. 합불 판정이나 지원자 랭킹은 만들지 않고, 최종 판단은 사람이 합니다.
공고 넣고 시작하기