[Daily morning study] 정렬 알고리즘 비교 (Quick Sort, Merge Sort, Heap Sort)

#daily morning study

Image


정렬 알고리즘 비교

실무에서 직접 정렬을 구현하는 경우는 드물지만, 각 알고리즘의 원리와 시간 복잡도를 이해하면 내장 정렬 함수의 동작을 예측하고 성능 문제를 진단하는 데 도움이 된다.


Quick Sort (퀵 정렬)

원리

피벗(pivot) 원소를 하나 선택한 뒤, 피벗보다 작은 원소는 왼쪽, 큰 원소는 오른쪽으로 분할하는 과정을 재귀적으로 반복한다. 분할 정복(Divide and Conquer) 전략이다.

[3, 6, 8, 10, 1, 2, 1]  → pivot = 3
 left: [1, 2, 1]   pivot: [3]   right: [6, 8, 10]
 → 각 부분을 재귀적으로 정렬

구현 (Python)

def quick_sort(arr):
    if len(arr) <= 1:
        return arr
    pivot = arr[len(arr) // 2]
    left  = [x for x in arr if x < pivot]
    mid   = [x for x in arr if x == pivot]
    right = [x for x in arr if x > pivot]
    return quick_sort(left) + mid + quick_sort(right)

시간 복잡도

경우복잡도
평균O(n log n)
최악 (이미 정렬된 배열 + 고정 피벗)O(n²)
공간 복잡도O(log n) ~ O(n) (재귀 스택)

특징

  • 제자리 정렬(in-place)이 가능해 메모리 효율이 좋다.
  • 캐시 지역성(cache locality)이 좋아 실제 성능이 빠른 편이다.
  • 피벗 선택이 성능의 핵심이다. 랜덤 피벗이나 Median-of-three 전략으로 최악 케이스를 피한다.
  • 불안정 정렬(unstable sort)이다.

Merge Sort (병합 정렬)

원리

배열을 절반으로 계속 분할한 뒤, 정렬된 두 부분 배열을 합치는(merge) 방식이다. 항상 O(n log n)을 보장한다.

[38, 27, 43, 3]
→ [38, 27] | [43, 3]
→ [38] [27] | [43] [3]
← [27, 38]  ← [3, 43]
← [3, 27, 38, 43]

구현 (Python)

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
    return result + left[i:] + right[j:]

시간 복잡도

경우복잡도
최선 / 평균 / 최악O(n log n)
공간 복잡도O(n) (임시 배열 필요)

특징

  • 안정 정렬(stable sort)이다. 동일한 값의 원소 순서가 유지된다.
  • 추가 메모리(O(n))가 필요하다. 메모리가 제한적인 환경에서는 단점이 된다.
  • 연결 리스트 정렬에 특히 유리하다 (포인터 연결만 바꾸면 돼서 추가 메모리 없이 가능).
  • Java의 Arrays.sort(Object[]) (TimSort 기반)가 Merge Sort를 활용한다.

Heap Sort (힙 정렬)

원리

배열을 최대 힙(Max-Heap)으로 구성한 뒤, 루트(최댓값)를 배열 끝으로 이동시키고 힙 크기를 줄이며 반복한다.

1단계: 배열 → Max-Heap 구성
2단계: 루트(최댓값)와 마지막 원소 교환 → 힙 크기 -1
3단계: Heapify(루트 복원) → 반복

구현 (Python)

def heapify(arr, n, i):
    largest = i
    left, right = 2 * i + 1, 2 * i + 2
    if left < n and arr[left] > arr[largest]:
        largest = left
    if right < n and arr[right] > arr[largest]:
        largest = right
    if largest != i:
        arr[i], arr[largest] = arr[largest], arr[i]
        heapify(arr, n, largest)

def heap_sort(arr):
    n = len(arr)
    for i in range(n // 2 - 1, -1, -1):  # 최대 힙 구성
        heapify(arr, n, i)
    for i in range(n - 1, 0, -1):         # 정렬
        arr[0], arr[i] = arr[i], arr[0]
        heapify(arr, i, 0)
    return arr

시간 복잡도

경우복잡도
최선 / 평균 / 최악O(n log n)
공간 복잡도O(1) (제자리 정렬)

특징

  • 최악의 경우에도 O(n log n)을 보장하면서 추가 메모리가 O(1)이다.
  • 불안정 정렬이다.
  • 캐시 지역성이 나빠서 Quick Sort보다 실제 성능이 느린 경우가 많다.
  • 메모리가 극도로 제한된 임베디드 환경에서 유용하다.

세 알고리즘 비교 요약

알고리즘평균최악공간안정성
Quick SortO(n log n)O(n²)O(log n)불안정
Merge SortO(n log n)O(n log n)O(n)안정
Heap SortO(n log n)O(n log n)O(1)불안정

실무에서 어떤 게 쓰이나?

  • Python sorted() / list.sort() — TimSort 사용 (Merge Sort + Insertion Sort 혼합). 안정 정렬.
  • Java Arrays.sort(int[]) — Dual-Pivot Quick Sort 사용.
  • Java Arrays.sort(Object[]) — TimSort 사용 (안정 정렬 보장).
  • C++ std::sort — IntroSort 사용 (Quick Sort + Heap Sort + Insertion Sort 혼합). 최악 O(n log n) 보장.

대부분의 언어 표준 라이브러리는 순수 Quick Sort나 Merge Sort를 단독으로 쓰지 않는다. 각각의 단점을 보완한 하이브리드 알고리즘을 채택한다.


핵심 정리

  • Quick Sort: 평균적으로 가장 빠르고 캐시 효율 좋음. 최악 O(n²) 주의.
  • Merge Sort: 항상 O(n log n). 안정 정렬 필요할 때 선택. 추가 메모리 필요.
  • Heap Sort: 항상 O(n log n)이면서 O(1) 공간. 하지만 캐시 비효율로 실제론 느림.
  • 면접에서는 각 알고리즘의 시간 복잡도와 안정성 여부, 언제 어떤 걸 선택할지를 설명할 수 있어야 한다.