본문 바로가기
Tutorial/Algorithm

병합 정렬(Merge Sort) 완벽하게 이해하기

by CLJ 2025. 3. 19.

정렬 알고리즘은 컴퓨터 과학에서 중요한 개념 중 하나이며, 데이터 정렬을 효율적으로 수행하기 위해 다양한 방법이 사용된다. 그중에서도 병합 정렬(Merge Sort)은 높은 안정성과 효율성을 갖춘 강력한 정렬 알고리즘으로 알려져 있다. 병합 정렬은 분할 정복(Divide and Conquer) 기법을 활용하여 데이터를 작은 단위로 나눈 후, 정렬하면서 다시 합치는 방식으로 동작한다. 최악의 경우에도 O(n log n)의 시간 복잡도를 유지하며, 안정적인 성능을 제공한다는 점에서 많은 개발자들이 선호하는 정렬 방식이다. 이번 글에서는 병합 정렬의 개념과 동작 원리, 시간 복잡도, 구현 방법, 장단점, 그리고 다른 정렬 알고리즘과의 비교를 다룬다. 

목차

1. 병합 정렬이란?
2. 동작 원리 및 예제
3. 시간 복잡도
4. 구현 예제
5. 장점
6. 단점
7. 다른 정렬 알고리즘과의 비교
정리
 

1. 병합 정렬이란?

 
병합 정렬은 데이터를 작은 단위로 나누고, 각각을 정렬한 후 다시 합치는 방식으로 동작하는 정렬 알고리즘이다. 이러한 과정은 재귀적으로 반복되며, 최종적으로 모든 요소가 정렬된 상태로 병합된다.
병합 정렬의 핵심은 데이터를 분할하고 정렬한 후 병합하는 과정에서 두 개의 정렬된 배열을 비교하며 병합하는 것이다. 이를 통해 정렬된 순서를 유지하며 데이터를 정렬할 수 있다.

 

2. 동작 원리 및 예제

 
병합 정렬은 데이터를 작은 단위로 나눈 후, 정렬된 상태로 합치는 방식으로 동작한다. 이 과정은 분할(Divide) → 정렬(Sort) → 병합(Merge)의 3단계로 이루어진다. 병합 정렬의 핵심 개념을 차근차근 쉽게 이해할 수 있도록 하나씩 설명해 보겠다.

 

(1) 분할(Divide) 단계

 
먼저 정렬할 배열을 절반으로 나눈다. 이 과정을 배열의 크기가 1이 될 때까지 반복한다.
예를 들어, 다음과 같은 배열이 있다고 가정하자.
[8, 3, 7, 4, 9, 1, 5, 2]
이 배열을 절반으로 나누면 다음과 같이 된다.
[8, 3, 7, 4] | [9, 1, 5, 2]
이제 각 부분도 계속해서 반으로 나눈다.
[8, 3] | [7, 4] | [9, 1] | [5, 2]
더 이상 나눌 수 없을 때까지 반복하면, 결국 모든 요소가 하나씩 분리된 상태가 된다.
[8] [3] [7] [4] [9] [1] [5] [2]
 

(2) 정렬(Sort) 및 병합(Merge) 단계

 
이제 병합을 시작할 차례다. 병합을 할 때는 두 개의 작은 배열을 비교하면서 정렬된 순서로 합친다.
먼저, 가장 작은 단위인 1개의 원소를 가진 배열들을 병합해 보자.
[8]과 [3]을 비교하면, 3이 더 작으므로 [3, 8]이 된다.
[7]과 [4]를 비교하면, 4가 더 작으므로 [4, 7]이 된다.
[9]과 [1]을 비교하면, 1이 더 작으므로 [1, 9]가 된다.
[5]과 [2]를 비교하면, 2가 더 작으므로 [2, 5]가 된다.
이제 병합된 결과는 다음과 같다.
[3, 8] | [4, 7] | [1, 9] | [2, 5]
다음으로, 두 개씩 합쳐서 정렬한다.
[3, 8]과 [4, 7]을 비교하며 병합하면 [3, 4, 7, 8]이 된다.
[1, 9]과 [2, 5]을 비교하며 병합하면 [1, 2, 5, 9]가 된다.
이제 마지막으로 두 개의 정렬된 배열을 병합한다.
[3, 4, 7, 8]과 [1, 2, 5, 9]를 비교하면서 정렬하면 최종적으로 [1, 2, 3, 4, 5, 7, 8, 9]가 된다.
 

(3)  과정 정리

 
정렬 과정 전체를 도식화하면 다음과 같다.
초기 배열: [8, 3, 7, 4, 9, 1, 5, 2]
분할 과정: [8, 3, 7, 4] | [9, 1, 5, 2] [8, 3] | [7, 4] | [9, 1] | [5, 2] [8] [3] [7] [4] [9] [1] [5] [2]
병합 과정: [3, 8] [4, 7] [1, 9] [2, 5] [3, 4, 7, 8] [1, 2, 5, 9] [1, 2, 3, 4, 5, 7, 8, 9]
 

(4) 특징

 
병합 정렬은 항상 O(n log n)의 시간 복잡도를 유지하며, 데이터의 크기가 커질수록 일정한 속도를 보장하는 정렬 알고리즘이다. 하지만 추가적인 메모리 공간이 필요하기 때문에, 메모리 사용이 중요한 환경에서는 적절하지 않을 수 있다.
이제 병합 정렬의 시간 복잡도에 대해 살펴보자.

 

3. 시간 복잡도

 
병합 정렬의 시간 복잡도는 O(n log n)으로, 데이터 크기가 커져도 일정한 성능을 보장하는 정렬 알고리즘이다. 이를 더 쉽게 이해하기 위해, 병합 정렬이 데이터를 어떻게 처리하는지 단계별로 분석해 보겠다.

 

 

(1) 시간 복잡도 분석

 
병합 정렬의 연산 과정은 크게 두 가지로 나뉜다.
1) 분할(Divide): 데이터를 계속해서 반으로 나누는 과정
2) 병합(Merge): 정렬된 데이터를 다시 합치는 과정
각 과정에서 얼마나 많은 연산이 발생하는지 살펴보자.

① 분할 과정의 시간 복잡도

배열의 크기가 n일 때, 매번 절반으로 나누어 크기가 1이 될 때까지 나누게 된다. 즉, 한 번 나눌 때마다 배열의 크기가 n → n/2 → n/4 → n/8 ... 이렇게 줄어든다.
배열 크기가 1이 될 때까지 나누는 횟수를 k라고 하면, 다음과 같은 관계식을 얻을 수 있다.
n / 2^k = 1 → 2^k = n → k = log₂(n)
즉, 병합 정렬은 데이터를 log₂(n)번 나눈다.

② 병합 과정의 시간 복잡도

병합 과정에서는 각 단계를 거칠 때마다 모든 원소를 한 번씩 비교하면서 합친다. 배열의 크기가 1인 상태에서 병합을 시작할 때, 원소의 총개수는 n개이며, 병합 과정에서 각각의 원소를 비교해야 한다. 각 단계에서 비교 연산이 수행되므로, 병합 과정은 O(n)의 시간이 걸린다.
 

(2) 총 시간 복잡도

 
병합 정렬의 총 연산 횟수는 분할 횟수(log₂(n)) × 병합 연산 횟수(O(n))로 계산할 수 있다.
즉, O(n log n) 이 된다. 따라서 병합 정렬의 평균 및 최악의 경우에도 항상 O(n log n)의 성능을 보장한다.

 

(3) 공간 복잡도

 
병합 정렬은 정렬된 데이터를 저장하기 위한 추가적인 배열 공간이 필요하다. 즉, 정렬을 수행하면서 새로운 배열을 생성해야 하므로, O(n)의 공간이 추가로 필요하다. 이러한 이유로, 병합 정렬은 메모리 사용량이 중요한 환경에서는 비효율적일 수 있다.

 

(4) 시간 복잡도 비교

 

정렬 알고리즘 평균 시간 복잡도 최악 시간 복잡도 공간 복잡도
병합 정렬 O(n log n) O(n log n) O(n)
퀵 정렬 O(n log n) O(n²) O(log n)
삽입 정렬 O(n²) O(n²) O(1)

 

4. 구현 예제

 
 
병합 정렬을 직접 구현해 보자. 여기서는 Python을 사용하여 병합 정렬을 재귀적으로 구현하고, 반복문을 활용한 방식도 함께 살펴볼 것이다.
 

(1) 병합 정렬의 기본 구현 (재귀적 방식)

 
병합 정렬의 재귀적 구현은 데이터를 분할(Divide) → 정렬(Sort) → 병합(Merge)하는 과정으로 이루어진다.

def merge_sort(arr):
    if len(arr) <= 1:
        return arr

    mid = len(arr) // 2  # 배열을 두 부분으로 나누기
    left_half = merge_sort(arr[:mid])
    right_half = merge_sort(arr[mid:])

    return merge(left_half, right_half)

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

# 실행 예제
data = [8, 3, 7, 4, 9, 1, 5, 2]
sorted_data = merge_sort(data)
print("정렬된 배열:", sorted_data)

 
이 코드는 배열을 계속해서 반으로 나눈 후, 정렬된 두 부분을 병합하는 방식으로 동작한다. 분할 과정은 재귀적으로 실행되며, 더 이상 나눌 수 없을 때 병합을 수행한다.

 

(2) 반복문을 이용한 병합 정렬 (Iterative 방식)

 
재귀를 사용하지 않고 반복문을 이용하여 병합 정렬을 구현할 수도 있다. 이 방식은 스택 오버플로우 문제를 방지할 수 있지만, 구현이 다소 복잡해진다.

def merge_sort_iterative(arr):
    width = 1
    n = len(arr)

    while width < n:
        left = 0
        while left < n:
            mid = min(left + width, n)
            right = min(left + 2 * width, n)
            arr[left:right] = merge(arr[left:mid], arr[mid:right])
            left += 2 * width

        width *= 2

    return arr

# 실행 예제
data = [8, 3, 7, 4, 9, 1, 5, 2]
sorted_data = merge_sort_iterative(data)
print("정렬된 배열:", sorted_data)

 
이 방식은 반복문을 이용하여 부분 리스트를 점진적으로 정렬하는 방식으로 동작한다. 배열 크기를 2배씩 늘려가며 정렬된 리스트를 병합하는 방식이다.
 

5. 장단점

 

(1)  장점

 
병합 정렬은 여러 가지 장점을 가지고 있으며, 특정한 경우에 매우 효과적으로 사용할 수 있다. 아래에서 병합 정렬이 가지는 장점과 함께, 어떤 경우에 적합한지를 알아보자.
 
1) 안정 정렬(Stable Sort)
병합 정렬은 동일한 값을 가진 원소들의 상대적인 순서를 유지하는 안정 정렬이다. 즉, 정렬 전의 데이터에서 같은 값을 가진 요소들이 있었다면, 정렬 후에도 그 순서는 유지된다. 이런 특성 때문에, 데이터베이스 정렬이나 다중 기준 정렬(multi-key sort) 같은 작업에서 유용하게 활용된다.
 
2) 항상 O(n log n) 성능을 유지
병합 정렬은 최악의 경우에도 O(n log n)의 성능을 유지한다. 퀵 정렬(Quick Sort)의 경우, 특정한 입력(예: 이미 정렬된 배열)에서는 O(n²)의 성능이 나오지만, 병합 정렬은 입력 데이터에 상관없이 일관된 성능을 유지한다.
 
3) 대량의 데이터에서도 일정한 성능 유지
병합 정렬은 데이터 크기가 커질수록 일정한 성능을 유지한다. 따라서 **대용량 데이터 정렬, 금융 데이터 분석, 외부 정렬(external sorting)** 등에 자주 사용된다.
 
4) 링크드 리스트(Linked List) 정렬에 적합
배열에서는 병합 정렬이 추가적인 메모리를 사용해야 하지만, 연결 리스트(Linked List) 정렬에서는 매우 효과적이다. 연결 리스트의 특성상 빠른 요소 삽입이 가능하므로, 퀵 정렬보다 병합 정렬이 더 좋은 성능을 낼 수 있다.
 
그렇다면 병합 정렬은 어떤 경우에 적합할까? 적합한 경우는 다음과 같다. 
 
1) 정렬된 데이터를 유지해야 하는 경우
병합 정렬은 안정 정렬이므로, 데이터의 원래 순서를 유지해야 하는 경우 유용하다. 예를 들어, 이름순으로 정렬된 데이터를 나이순으로 다시 정렬할 때 기존의 이름순 정렬을 유지하면서 나이에 따라 정렬할 수 있다.
 
2) 데이터 크기가 매우 큰 경우
데이터 크기가 커질수록 병합 정렬이 다른 정렬 알고리즘보다 일정한 성능을 유지한다. 특히, 외부 정렬(external sorting)을 사용할 때 병합 정렬이 많이 활용된다.
 
3) 연결 리스트를 정렬할 때
연결 리스트는 임의 접근이 불가능하기 때문에 퀵 정렬보다 병합 정렬이 유리하다. 병합 정렬은 연결 리스트에서 효율적으로 동작하며, 추가적인 메모리도 많이 사용하지 않는다.
 

(2) 단점

 
병합 정렬이 항상 최선의 선택은 아니다. 특정한 경우에는 다른 정렬 알고리즘이 더 좋은 성능을 낼 수 있다.
 
1) 추가적인 메모리 공간이 필요하다
병합 정렬은 데이터를 병합할 때 새로운 배열을 사용하므로 **O(n)의 추가적인 메모리 공간**을 필요로 한다. 따라서 메모리 사용이 중요한 환경에서는 비효율적일 수 있다.
 
2) 작은 배열에서는 비효율적
데이터 크기가 작을 경우, 삽입 정렬(Insertion Sort)이나 버블 정렬(Bubble Sort)이 더 적은 연산으로 빠르게 정렬할 수 있다.
 
3) 제자리 정렬(In-place Sort)이 아니다
퀵 정렬(Quick Sort)이나 힙 정렬(Heap Sort)은 추가 메모리를 거의 사용하지 않고 정렬할 수 있지만, 병합 정렬은 **추가적인 배열 공간을 필요로 하기 때문에 제자리 정렬이 아니다.**
 
위의 단점들 때문에 적합하지 않은 경우는 다음과 같다. 
 
1) 메모리 사용이 제한된 환경
병합 정렬은 추가적인 배열을 필요로 하기 때문에, 메모리 사용량이 제한된 시스템에서는 적합하지 않다.
 
2) 작은 크기의 배열
배열 크기가 작을 경우, 퀵 정렬 또는 삽입 정렬이 더 빠르게 동작할 수 있다.
이제 병합 정렬을 다른 정렬 알고리즘과 비교해 보자.

 

7. 다른 정렬 알고리즘과의 비교

 
 
병합 정렬은 다른 정렬 알고리즘과 비교했을 때, 일정한 성능을 유지하는 장점이 있다. 하지만 특정한 경우에는 퀵 정렬(Quick Sort)이나 힙 정렬(Heap Sort)이 더 나은 선택이 될 수 있다. 아래에서 병합 정렬을 다른 주요 정렬 알고리즘과 비교해 보자.
 

(1) 병합 정렬 vs 퀵 정렬

 
성능: 합 정렬과 퀵 정렬은 모두 평균적으로 O(n log n)의 성능을 보인다. 하지만 퀵 정렬은 최악의 경우 O(n²)까지 성능이 떨어질 수 있다(예: 이미 정렬된 배열을 피벗으로 선택한 경우).
메모리 사용: 병합 정렬은 추가적인 배열 공간을 사용하므로 O(n)의 메모리가 필요하다. 반면 퀵 정렬은 제자리 정렬(In-place Sort)이므로 추가적인 메모리가 거의 필요하지 않는다(O(log n)).
안정 정렬 여부: 병합 정렬은 안정 정렬(Stable Sort)로, 같은 값을 가진 요소들의 상대적인 순서를 유지한다. 반면, 퀵 정렬은 기본적으로 안정 정렬이 아니다.
 

(2) 병합 정렬 vs 힙 정렬

 
성능: 병합 정렬과 힙 정렬(Heap Sort) 모두 평균 및 최악의 경우 O(n log n)의 성능을 보장한다. 하지만 힙 정렬은 비교적 정렬 속도가 일정하지 않고, 데이터의 정렬된 정도에 따라 성능이 변할 수 있다.
메모리 사용: 병합 정렬은 추가적인 메모리를 사용해야 하지만, 힙 정렬은 O(1) 또는 O(n)의 공간만 사용한다.
안정 정렬 여부: 병합 정렬은 안정 정렬이지만, 힙 정렬은 불안정 정렬(Unstable Sort)이다.
 

(3) 병합 정렬 vs 삽입 정렬

 
성능: 병합 정렬의 시간 복잡도는 O(n log n)으로, 데이터 크기가 클수록 유리하다. 반면 삽입 정렬(Insertion Sort)은 평균적으로 O(n²)의 성능을 보이며, 작은 배열에서는 더 효율적일 수 있다.
메모리 사용: 병합 정렬은 추가적인 배열 공간이 필요하지만, 삽입 정렬은 제자리 정렬이므로 추가적인 메모리가 필요하지 않다.
안정 정렬 여부: 두 정렬 알고리즘 모두 안정 정렬이다.
 

(4) 정렬 알고리즘 비교

 

정렬 알고리즘 평균 시간 복잡도 최악 시간 복잡도 공간 복잡도 안정성 제자리 정렬 여부
병합 정렬 O(n log n) O(n log n) O(n) ✅ (안정 정렬) ❌ (추가 메모리 필요)
퀵 정렬 O(n log n) O(n²) (최악의 경우) O(log n) ❌ (불안정 정렬) ✅ (제자리 정렬)
힙 정렬 O(n log n) O(n log n) O(1) ~ O(n) ❌ (불안정 정렬) ✅ (제자리 정렬)
삽입 정렬 O(n²) O(n²) O(1) ✅ (안정 정렬) ✅ (제자리 정렬)

 

정리

 
병합 정렬항상 일정한 O(n log n)의 성능을 유지하며, 안정 정렬이라는 장점이 있다. 그러나 추가적인 메모리 공간을 필요로 하기 때문에 메모리 사용이 중요한 환경에서는 다른 정렬 알고리즘이 더 적합할 수 있다. 대량의 데이터를 정렬하거나, 링크드 리스트를 정렬할 때는 병합 정렬이 유용하게 사용될 수 있다. 하지만 작은 배열을 정렬하거나 메모리 사용이 제한적인 환경에서는 퀵 정렬이나 삽입 정렬이 더 효율적일 수 있다. 이제 병합 정렬의 개념과 구현 방법을 이해했을 것이다. 실제 프로젝트에서 정렬 알고리즘을 선택할 때, 병합 정렬이 적합한 상황을 고려하여 활용하면 좋다.