힙(Heap)과 우선순위 큐
완전 이진 트리 기반 힙의 성질과 배열 구현, 삽입·삭제 과정, 힙 정렬, 파이썬 heapq 사용법을 정리한다.
시리즈 · 자료구조 · 알고리즘7 / 13
- 시간 복잡도와 공간 복잡도
- 재귀 함수와 꼬리 재귀 최적화
- 연결 리스트(Linked List)
- 스택과 큐: 원형 큐 구현과 덱
- 해시 테이블: 해시 함수와 충돌 처리
- 트리와 이진 탐색 트리
- 힙(Heap)과 우선순위 큐
- 균형 이진 탐색 트리: AVL, Red-Black, B-Tree
- 그래프: 인접 행렬과 인접 리스트
- 최단 경로 알고리즘: BFS, 다익스트라, 벨만-포드, 플로이드-워셜, A*
- 최소 신장 트리(MST): Kruskal, Prim, Union-Find
- 정렬 알고리즘: 퀵·병합·기수 정렬부터 Timsort까지
- Thread-Safe 자료구조: Python GIL과 Java ConcurrentHashMap
힙이란
최댓값 또는 최솟값을 빠르게 찾기 위해 고안된 완전 이진 트리 기반의 자료구조로, 우선순위 큐를 구현하는 데 쓰인다.
- 부모 노드와 자식 노드의 키 사이에 항상 대소 관계가 성립한다.
- 최소 힙(Min Heap): 각 노드의 키가 자식 노드의 키보다 크지 않다.
- 최대 힙(Max Heap): 각 노드의 키가 자식 노드의 키보다 작지 않다.
- 부모와 자식 사이의 관계만 정해져 있고 형제 사이의 순서는 없는 반정렬 상태이다.
- 이진 탐색 트리와 달리 중복된 값을 허용한다.

스택과 큐는 들어온 순서에 따라 나가는 순서가 정해지지만, 우선순위 큐는 내가 정한 우선순위에 따라 나가는 순서가 정해진다.
배열로 힙을 구현하는 방법
완전 이진 트리는 중간에 빈 자리가 없으므로 배열에 차례대로 담을 수 있다. 구현을 쉽게 하기 위해 0번 인덱스를 비워 두고 1번부터 사용하면 인덱스 관계가 다음과 같다.
| 노드 | 인덱스 |
|---|---|
| 왼쪽 자식 | 2i |
| 오른쪽 자식 | 2i + 1 |
| 부모 | i / 2 (몫) |
0번 인덱스부터 사용하는 경우에는 왼쪽 자식이 2i + 1, 오른쪽 자식이 2i + 2가 된다. 파이썬의 heapq가 이 방식이다.
삽입과 삭제
삽입과 삭제의 시간 복잡도는 O(log N)이다. 원소를 넣고 빼는 것 자체는 O(1)이지만, 힙의 성질을 다시 맞추는 과정(heapify)이 트리의 높이만큼, 즉 O(log N) 걸리기 때문이다.
삽입
- 새로운 노드를 힙의 마지막 위치에 넣는다.
- 부모 노드와 비교해 대소 관계가 어긋나면 자리를 바꾼다. 어긋나지 않을 때까지 위로 올라가며 반복한다.
삭제
- 힙의 삭제 연산은 루트 노드(최대 힙이면 최댓값)를 꺼내는 것이다.
- 가장 마지막 노드를 루트 자리로 옮긴다.
- 자식 노드와 비교하며 아래로 내려가 힙의 성질을 다시 맞춘다.
힙에서는 편향이 발생하지 않는 이유
힙은 항상 완전 이진 트리 형태를 유지한다. 새 노드는 언제나 마지막 위치에 채워지고, 자식은 부모와의 대소 관계만 만족하면 될 뿐 왼쪽과 오른쪽 중 어디에 있어야 한다는 제약이 없다. 그래서 이진 탐색 트리처럼 한쪽으로 치우칠 일이 없고 높이가 항상 log N으로 유지된다.
힙 정렬의 시간 복잡도
- 배열을 힙으로 만든다.
- 루트(최댓값)를 꺼내는 일을 N번 반복한다. 한 번 꺼낼 때마다 heapify에 O(log N)이 든다.
따라서 전체 시간 복잡도는 O(N log N)이다.
파이썬 heapq
파이썬의 heapq 모듈은 리스트를 최소 힙으로 다룬다. k번째 원소가 항상 자식 원소(2k+1, 2k+2번째)보다 작거나 같도록 유지된다.
heapq.heappush(heap, item): heap에 item을 추가한다.heapq.heappop(heap): 가장 작은 원소를 꺼내 반환한다. 비어 있으면IndexError가 발생한다.heapq.heapify(x): 리스트 x를 제자리에서 힙으로 변환한다. O(n)
최대 힙 만들기
heapq는 최소 힙만 지원한다. 최대 힙이 필요하면 부호를 뒤집은 값을 우선순위로 삼아 (-item, item) 튜플로 넣는다.
import heapq
max_heap = []
for item in [3, 1, 5]:
heapq.heappush(max_heap, (-item, item))
print(heapq.heappop(max_heap)[1]) # 5