우선순위 큐
우선순위 큐priority queue는 insert 연산과 함께, 최소값을 가진 원소를 찾아 반환하고 제거하는 removeMin(흔히 deleteMin이라고도 함) 연산을 포함하는 자료구조이다.
insert연산은 큐 자료구조의push연산과 비슷하다.removeMin은 큐의pop과 비슷한 일을 수행하는 우선순위 큐의 연산이다. 다만 "가장 먼저 들어온 것"이 아니라 "가장 우선순위가 높은(가장 작은) 것"이 나온다.
insert로 원소를 넣고 removeMin으로 최소값을 꺼낸다.왜 이진 힙인가
우선순위 큐를 구현할 수 있는 방법은 다양하지만, 이진 힙binary heap 구조를 사용하는 것이 가장 합리적이다.
- 리스트를 사용할 경우, 경우에 따라 삽입 혹은 삭제에서
이 소모된다. - 이진 탐색 트리binary search tree를 사용할 경우, 삽입과 삭제에서 평균적으로
이 소모된다.
이진 힙은 우선순위 큐를 구현하기 위한 사실상 표준de facto standard이다.
이진 힙의 두 가지 성질
이진 힙에는 2가지 성질이 존재한다.
1. 구조 성질
이진 힙은 완전히 차 있는 이진 트리binary tree이며, 가장 마지막 레벨에서는 왼쪽에서 오른쪽으로만 차도록 예외를 허용한다(완전 이진 트리complete binary tree).
- 이러한 성질은 힙을 단순한 배열로 구현할 수 있게 만들며, 보통
1번째 인덱스부터 시작한다. - 배열의
번째 원소에 대해:- 왼쪽 자식left child의 위치는
- 오른쪽 자식right child의 위치는
- 부모parent의 위치는
- 왼쪽 자식left child의 위치는
부모/자식 인덱스 공식 (1-based)
- 왼쪽 자식:
- 오른쪽 자식:
- 부모:
인덱스 0을 비워두고 1부터 쓰는 이유가 바로 이 공식을 깔끔하게 만들기 위해서이다.
2. 힙-순서 성질
힙 안의 임의의 노드node
- 이러한 성질을 만족하면 최소 힙minimum heap이다.
- 반대로 최대값이 루트root에 있고, 임의의 노드
의 모든 자식 노드가 보다 작거나 같으면 최대 힙maximum heap이다.
이 노트에서는 별다른 표기가 없을 경우 최소 힙을 기준으로 서술한다.
insert — 스며올리기(percolate up)
원소 insert하기 위해, 우선 가능한 자리(마지막 레벨의 다음 빈 칸)에 공간hole을 만든다. 그렇지 않으면 구조 성질을 위반한다.
- 만약
가 힙 순서를 위반하지 않는다면 그대로 그 자리에 넣는다. - 그렇지 않을 경우, 이 공간을 부모 노드와 자리를 바꾸며,
가 그 공간에 들어갈 수 있을 때까지 이 공간을 루트 방향으로 스며올린다percolate up.
removeMin — 스며내리기(percolate down)
removeMin도 비슷한 방식으로 이루어진다. 루트 원소가 제거되고, 가장 마지막 원소
가 빈 공간을 채울 수 있다면(양쪽 자식보다 작거나 같다면) 연산을 종료한다.- 그렇지 않을 경우, 빈 공간의 더 작은 자식 노드와 값을 교환하여,
가 빈 공간에 들어갈 수 있을 때까지 스며내린다percolate down.
buildHeap — 개 원소로 힙 만들기
이미 배열에 담긴 insert를 부르면 percolateDown을 적용하면, 전체 비용은
이다. 낮은 레벨의 노드일수록 개수는 많지만 스며내릴 높이가 작다는 사실을 합산하면(높이별 노드 수 × 높이의 합이 유한한 등비급수로 수렴) 선형 시간이 됨을 보일 수 있다.
실습 — 이진 최소 힙 직접 구현하기
배열(1-based)로 최소 힙을 구현하여 insert와 deleteMin을 수행한다. 순서 없이 넣은 원소들을 deleteMin으로 계속 꺼내면 오름차순으로 나온다(힙 정렬의 핵심).
#include <iostream>
#include <vector>
#include <stdexcept>
using namespace std;
class BinaryHeap {
public:
BinaryHeap() : array(1) {} // 인덱스 0은 사용하지 않음
bool empty() const { return array.size() == 1; }
void insert(int x) {
array.push_back(x); // 마지막에 새 공간(hole)
int hole = array.size() - 1; // percolate up 시작
while (hole > 1 && x < array[hole / 2]) {
array[hole] = array[hole / 2]; // 부모를 끌어내림
hole /= 2; // 루트 방향으로 이동
}
array[hole] = x;
}
int deleteMin() {
if (empty()) throw runtime_error("heap is empty");
int minItem = array[1]; // 루트가 최소값
int last = array.back();
array.pop_back();
if (!empty()) {
int hole = 1; // 루트에 빈 공간
int n = array.size() - 1;
while (2 * hole <= n) { // percolate down
int child = 2 * hole; // 왼쪽 자식
if (child != n && array[child + 1] < array[child])
child++; // 더 작은 자식 선택
if (array[child] < last)
array[hole] = array[child];
else
break;
hole = child;
}
array[hole] = last;
}
return minItem;
}
private:
vector<int> array; // 1-based 배열
};
int main() {
BinaryHeap h;
int data[] = {13, 21, 16, 24, 31, 19, 68, 65, 26, 32};
for (int x : data) h.insert(x);
cout << "deleteMin sequence: ";
while (!h.empty()) cout << h.deleteMin() << " ";
cout << endl;
return 0;
}deleteMin을 반복하면 정렬된 수열이 출력된다. 13 16 19 21 24 26 31 32 65 68
실습 — STL priority_queue
C++ 표준 라이브러리는 우선순위 큐를 직접 제공한다. 다만 기본은 최대 힙이라 top()이 가장 큰 값을 준다(최소 힙이 필요하면 priority_queue<int, vector<int>, greater<int>>).
#include <iostream>
#include <queue>
using namespace std;
int main() {
priority_queue<int> pq;
pq.push(4); pq.push(1); pq.push(10);
while (!pq.empty()) {
cout << pq.top() << endl; // 최대값부터
pq.pop();
}
return 0;
}연산별 복잡도
| 연산 | 이진 힙 | 정렬 안 된 리스트 | 정렬된 리스트 | 이진 탐색 트리(평균) |
|---|---|---|---|---|
insert | ||||
findMin | ||||
removeMin / deleteMin | ||||
buildHeap( | — |
핵심 정리
- 이진 힙 = 완전 이진 트리(구조 성질) + 부모 ≤ 자식(힙-순서 성질).
- 배열로 구현, 1-based에서 자식은
, 부모는 . insert는 percolate up,removeMin은 percolate down, 둘 다 트리 높이만큼 = . 개를 한꺼번에 힙으로 만드는buildHeap은 .