Queue는 FIFO 구조라 먼저 들어온 것이 먼저 나간다. 근데 현실에서는 순서보다 중요도가 우선인 경우가 많다. 응급실을 생각해보면 — 먼저 온 경미한 환자보다 나중에 온 위급한 환자를 먼저 봐야 한다. 이럴 때 필요한 게 Priority Queue다.
1. Priority Queue (PQ)
각 entry가 (key, value) 쌍으로 저장된다. key가 우선순위를 결정하고, value는 실제 데이터다.
응급실 예시:
- key = 병증의 심각도
- value = 환자 정보
Methods
| Method | 설명 |
|---|---|
P.add(k, v) |
key k, value v인 entry 추가 |
P.remove_min() |
가장 작은 key의 entry를 꺼내서 반환 |
P.min() |
가장 작은 key의 entry를 확인만 (제거 안 함) |
P.is_empty() |
비어있는지 확인 |
len(P) |
entry 수 반환 |
List로 구현할 때의 trade-off
| Unsorted List | Sorted List | |
|---|---|---|
| add | O(1) — 그냥 끝에 추가 | O(n) — 자리 찾아야 함 |
| remove_min | O(n) — 전체 탐색 필요 | O(1) — 맨 앞에 있음 |
둘 다 한쪽이 느리다. 이 trade-off를 해결하는 게 Heap이다.
2. Heap
Heap은 두 가지 조건을 만족하는 binary tree다.
Heap-Order Property
- Min-heap: 부모 key ≤ 자식 key → 최솟값이 항상 root
- Max-heap: 부모 key ≥ 자식 key → 최댓값이 항상 root
Complete Binary Tree
- level 0 ~ h-1까지는 전부 꽉 차있고
- 마지막 level h는 왼쪽부터 채워져 있음
Complete binary tree여야 하는 이유는 height를 작게 유지하기 위해서다. 이 조건 덕분에 n개의 노드가 있을 때 height가 항상 ⌊log n⌋이 보장된다. Heap의 operations가 O(h)이기 때문에 결국 O(log n)이 된다.
(4,C) ← root = 최솟값
/ \
(5,A) (6,Z)
/ \ / \
(15,K)(9,F)(7,Q)(20,B)
3. Heap Operations
add(k, v) — Up-heap bubbling
Complete binary tree를 유지하려면 새 노드는 무조건 마지막 위치에 추가해야 한다. 근데 이러면 heap-order property가 깨질 수 있다.
해결책: 추가한 노드를 부모와 비교하면서 위로 swap — up-heap bubbling
1. 마지막 위치에 추가
2. 부모보다 key가 작으면 swap
3. heap-order property 만족할 때까지 반복
최악의 경우 root까지 올라가야 하므로 O(h) = O(log n)
remove_min() — Down-heap bubbling
Root를 그냥 삭제하면 트리가 두 개로 쪼개진다. 그래서 root를 바로 삭제하지 않는다.
해결책:
1. root와 last node를 swap
2. last node(원래 root) 삭제
3. 새 root를 더 작은 자식과 swap하면서 아래로 내려감
4. heap-order property 만족할 때까지 반복
내릴 때 더 작은 자식이랑 swap해야 한다. 큰 자식이랑 swap하면 그 자식이 sibling보다 작아져서 heap-order property가 또 깨지기 때문이다.
마찬가지로 O(h) = O(log n)
Time Complexity 비교
| Operation | Unsorted List | Sorted List | Heap |
|---|---|---|---|
add |
O(1) | O(n) | O(log n) |
remove_min |
O(n) | O(1) | O(log n) |
min |
O(n) | O(1) | O(1) |
Heap은 두 list 방식의 trade-off를 O(log n)으로 균형있게 해결한다. min은 항상 root에 있으니까 O(1).
4. Bottom-Up Heap Construction
n개의 원소로 heap을 만드는 두 가지 방법:
방법 1: add를 n번 → O(n log n)
방법 2: Bottom-up heap construction → O(n)
아이디어는 이렇다. n개를 그냥 트리에 배치한 뒤, 아래에서 위로 올라가면서 각 subtree를 down-heap으로 고쳐나간다.
아래 레벨부터:
[subtree 1] → down-heap
[subtree 2] → down-heap
...
점점 위로 올라가면서 전체를 heap으로 만듦
왜 O(n)이냐면, 아래 노드들은 down-heap을 거의 안 해도 되기 때문이다. 노드 수는 아래로 갈수록 많고, 위로 갈수록 적다. 많은 노드들이 조금만 이동하면 되니까 전체 합산하면 O(n)이 나온다.
add n번은 모든 노드가 최대 h번씩 이동하지만, bottom-up은 대부분의 노드가 조금만 이동하면 된다는 차이다.
5. Heap-Sort
Heap을 이용한 정렬이다. PQ로 정렬하는 방법(add n번 + remove_min n번)을 Heap으로 구현하면:
1. 주어진 리스트로 heap 만들기 (bottom-up construction: O(n))
2. remove_min()을 n번 반복 → 정렬된 순서로 출력 (O(n log n))
전체 시간복잡도: O(n log n)
Selection-sort, Insertion-sort가 O(n²)인 것과 비교하면 훨씬 빠르다.
용어 정리
- Heapify: 배열로부터 heap을 만드는 것
- Bottom-up heap construction: O(n)의 효율적인 heapify 알고리즘
- Up-heap / Down-heap: heap-order property를 위반한 노드를 고치는 것
6. 정리
Queue는 순서만 고려하지만, PQ는 우선순위를 고려한다. List로 PQ를 구현하면 add나 remove_min 중 하나가 반드시 느려지는 trade-off가 생긴다. Heap은 complete binary tree의 구조 덕분에 height가 항상 O(log n)으로 유지되어 이 trade-off를 균형있게 해결한다. 여기에 정렬까지 적용한 게 Heap-Sort로, O(n log n)의 시간복잡도를 가진다.
'공부 기록 > 자료구조' 카테고리의 다른 글
| Search Trees (1) (0) | 2026.06.19 |
|---|---|
| Map and Hash (0) | 2026.06.16 |
| Trees (0) | 2026.04.01 |
| Queues and Stacks (0) | 2026.03.23 |
| Recursion (0) | 2026.03.23 |