힙 정렬 (Heap Sort)
힙 정렬은 비교 기반 정렬 알고리즘으로, 이진 힙 자료구조를 활용하여 효율적인 정렬을 수행한다.
시간 복잡도가 안정적이고 추가 메모리를 거의 사용하지 않는 특징을 가지고 있어 많은 시스템에서 널리 사용된다.
힙 정렬은 비교 기반 정렬 알고리즘 중에서 시간 복잡도가 보장되고 추가 메모리를 거의 사용하지 않는 효율적인 알고리즘이다.
최선, 평균, 최악의 경우 모두 O(n log n)의 시간 복잡도를 가지며, 특히 메모리 제약이 있는 환경에서 유용하다.
불안정 정렬이라는 단점이 있지만, 안정성이 중요하지 않은 많은 응용 분야에서 여전히 강력한 선택지이다. 힙 자료구조의 이해는 우선순위 큐, 그래프 알고리즘 등 컴퓨터 과학의 다른 영역에도 도움이 된다.
힙(Heap) 자료구조의 이해
힙 정렬을 이해하기 위해서는 먼저 힙 자료구조에 대한 이해가 필요하다.
힙의 정의
힙은 다음 두 가지 특성을 만족하는 완전 이진 트리이다:
- 완전 이진 트리(Complete Binary Tree): 마지막 레벨을 제외한 모든 레벨이 완전히 채워져 있으며, 마지막 레벨의 노드들은 왼쪽부터 채워져 있다.
- 힙 속성(Heap Property):
- 최대 힙(Max Heap): 부모 노드의 키 값이 자식 노드의 키 값보다 크거나 같음
- 최소 힙(Min Heap): 부모 노드의 키 값이 자식 노드의 키 값보다 작거나 같음
힙의 표현
힙은 일반적으로 배열을 사용하여 표현한다.
이진 트리의 레벨 순서 탐색(level-order traversal)과 같은 방식으로 배열에 저장된다.
배열에서 인덱스 i
의 노드에 대해:
- 부모 노드 인덱스:
(i-1) // 2
(파이썬 기준) - 왼쪽 자식 노드 인덱스:
2*i + 1
- 오른쪽 자식 노드 인덱스:
2*i + 2
이러한 규칙을 통해 포인터 없이도 트리 구조를 효율적으로 표현할 수 있다.
힙 정렬 알고리즘의 원리
힙 정렬은 힙의 특성을 활용하여 정렬을 수행한다.
주요 단계는 다음과 같다:
- 힙 구성(Heapify): 주어진 배열을 힙으로 변환한다.
- 주어진 배열을 힙(Heap) 구조로 변환
- Heapify 연산을 수행하여 최대 힙(또는 최소 힙) 구성
- 정렬: 힙에서 원소를 하나씩 제거하여 정렬된 배열을 구성한다.
- 루트(최댓값 또는 최솟값)를 제거하고, 힙을 재정렬
- 반복적으로 수행하여 정렬된 배열 생성
힙 정렬 과정 예제
정렬할 배열:
|
|
최대 힙(Max Heap) 구성
루트(최대값 10)를 제거하고 배열 끝으로 이동
→ 정렬된 배열:
[10]
Heapify 수행 후 최대 힙 유지
다시 루트(최대값 5)를 제거하고 배열 끝으로 이동
→ 정렬된 배열:
[5, 10]
반복 수행하여 전체 정렬 완료
1
[1, 3, 4, 5, 10]
결과적으로 오름차순 정렬이 수행됨
최대 힙 구성 과정
- 초기 배열:
[4, 10, 3, 5, 1]
- 마지막 비단말 노드(인덱스 1, 값 10)부터 시작:
- 인덱스 1은 이미 최대 힙 속성을 만족
- 인덱스 0(값 4)에 대한 heapify:
- 자식 노드들과 비교하여 최댓값(10)을 찾음
- 4와 10을 교환:
[10, 4, 3, 5, 1]
- 인덱스 1에 대해 heapify 재귀 호출:
- 자식 노드들과 비교하여 최댓값(5)을 찾음
- 4와 5를 교환:
[10, 5, 3, 4, 1]
최종 최대 힙:[10, 5, 3, 4, 1]
정렬 과정
- 루트(10)와 마지막 요소(1) 교환:
[1, 5, 3, 4, 10]
- 힙 크기를 4로 줄이고 heapify 수행
- 결과:
[5, 4, 3, 1 | 10]
(막대는 힙과 정렬된 부분의 경계)
- 루트(5)와 마지막 요소(1) 교환:
[1, 4, 3, 5 | 10]
- 힙 크기를 3으로 줄이고 heapify 수행
- 결과:
[4, 1, 3 | 5, 10]
- 루트(4)와 마지막 요소(3) 교환:
[3, 1, 4 | 5, 10]
- 힙 크기를 2로 줄이고 heapify 수행
- 결과:
[3, 1 | 4, 5, 10]
- 루트(3)와 마지막 요소(1) 교환:
[1, 3 | 4, 5, 10]
- 힙 크기를 1로 줄이면 정렬 완료
최대 힙을 이용한 오름차순 정렬
- 배열을 최대 힙으로 구성한다.
- 루트(최댓값)를 배열의 마지막 요소와 교환한다.
- 힙의 크기를 1 감소시키고 루트에서 힙 속성을 복구한다(heapify).
- 힙의 크기가 1이 될 때까지 2~3 단계를 반복한다.
최소 힙을 이용한 내림차순 정렬
- 배열을 최소 힙으로 구성한다.
- 루트(최솟값)를 배열의 마지막 요소와 교환한다.
- 힙의 크기를 1 감소시키고 루트에서 힙 속성을 복구한다.
- 힙의 크기가 1이 될 때까지 2~3 단계를 반복한다.
힙 정렬 알고리즘의 구현
파이썬으로 구현한 힙 정렬
|
|
자바스크립트로 구현한 힙 정렬
|
|
힙 정렬의 성능 분석
시간 복잡도
- 힙 구성(Build Heap): O(n) (일반적으로 O(n log n)으로 생각할 수 있지만, 더 정확한 상한은 O(n)입니다)
- 정렬 과정: O(n log n)
- 전체 시간 복잡도: O(n log n)
힙 정렬의 시간 복잡도는 입력 배열의 초기 상태와 관계없이 항상 O(n log n)이다.
이는 최선, 평균, 최악의 경우 모두 동일하다.
공간 복잡도
힙 정렬은 추가 배열 없이 입력 배열 내에서 정렬을 수행하므로 공간 복잡도는 O(1)이다.
재귀 호출로 인한 스택 공간을 고려하면 최악의 경우 O(log n)이 될 수 있다.안정성
힙 정렬은 불안정(unstable) 정렬 알고리즘이다.
동일한 값을 가진 요소들의 상대적 순서가 정렬 과정에서 바뀔 수 있다. 이는 힙을 구성하는 과정과 요소를 추출하는 과정에서 발생할 수 있다.
힙 정렬의 최적화 기법
반복적 Heapify
재귀 대신 반복문을 사용하여 heapify를 구현하면 스택 오버플로우를 방지하고 성능을 향상시킬 수 있다.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
def heapify_iterative(arr, n, i): while True: largest = i left = 2 * i + 1 right = 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: break arr[i], arr[largest] = arr[largest], arr[i] i = largest
병렬 처리
힙 정렬의 일부 단계(특히 초기 힙 구성)는 병렬화가 가능하여 다중 코어 환경에서 성능을 향상시킬 수 있다.캐시 최적화
메모리 액세스 패턴을 개선하여 캐시 효율성을 높이는 방법도 있다. 배열 요소 간의 간격을 줄이거나 지역성(locality)을 높이는 기법이 활용된다.
힙 정렬의 응용 분야
K 개의 가장 큰/작은 요소 찾기
힙 자료구조를 활용하면 대용량 데이터에서 상위 K개 요소를 효율적으로 찾을 수 있다.
이는 빅데이터 처리, 데이터 마이닝 등에서 유용하게 활용된다.우선순위 큐 구현
힙은 우선순위 큐를 구현하는 데 가장 적합한 자료구조이다. 이를 활용한 응용 프로그램으로는 다음과 같은 것들이 있다:- 다익스트라 알고리즘: 최단 경로 찾기
- 프림 알고리즘: 최소 신장 트리 구성
- 허프만 코딩: 데이터 압축
- 작업 스케줄링: 우선순위에 따른 작업 할당
외부 정렬
대용량 데이터를 메모리에 모두 로드할 수 없을 때 사용하는 외부 정렬에서도 힙 정렬이 활용된다.
여러 파일에서 데이터를 병합할 때 우선순위 큐로 힙을 사용하는 방식이다.
힙 정렬의 변형
스무시 정렬(Smoothsort)
힙 정렬의 변형으로, 이미 정렬된 데이터에 대해 O(n) 시간 복잡도를 가지도록 최적화된 알고리즘이다.
레오나르도 수를 기반으로 한 특수한 힙 구조를 사용한다.인트로 정렬(Introsort)
퀵 정렬, 힙 정렬, 삽입 정렬을 혼합한 하이브리드 정렬 알고리즘이다.
퀵 정렬의 파티션 깊이가 너무 깊어지면 힙 정렬로 전환하여 최악의 경우 성능을 보장한다.제자리 병합 정렬(In-place Merge Sort)
힙을 사용하여 병합 정렬을 제자리(in-place)에서 수행하는 방식으로, 추가 메모리 사용을 최소화한다.