d-힙 · 좌향 힙
기본 이진 힙을 두 방향으로 확장한다. 하나는 자식 수를 늘려 트리를 얕게 만드는 d-힙이고, 다른 하나는 두 힙을 효율적으로 합칠 수 있게 하는 좌향 힙이다.
d-힙
이진 힙binary heap을 간단히 일반화한 것이
- 이진 힙은 곧
-힙이다. -힙도 결국 이진 힙의 성질을 따르므로, 배열array로 간단히 구현할 수 있다.
아래 그림은
배열로 구현할 때, 노드를
복잡도는 다음과 같이 정리된다.
-힙은 이진 힙보다 훨씬 얕으므로,insert연산을 으로 단축한다.- 반면
가 크면deleteMin은 더 비싸진다. 트리가 얕더라도 개의 자식 중 최솟값을 찾아야 하므로 번의 비교comparison가 필요하기 때문이다.- 즉 이 연산은
이다. 는 상수이므로,insert와deleteMin모두 결국 이다.
- 즉 이 연산은
좌향 힙
힙 자료구조의 약점 중 하나는 두 힙을 하나로 합치는 일이다. 이 추가 연산을 merge라 부른다. 힙을 배열로 구현하면, 같은 크기의 배열 두 개를 합칠 때 적어도 한 배열을 복사해 다른 배열에 붙여야 하므로
좌향 힙leftist heap은 이진 힙과 마찬가지로 두 가지 성질을 가진다.
- 구조 성질. 임의의 노드에 대해, 왼쪽 자식의 널 경로 길이null path length는 오른쪽 자식의 널 경로 길이보다 크거나 같아야 한다.
- 힙-순서 성질. 일반적인 힙의 순서 성질과 동일하다.
널 경로 길이
널 경로 길이란 그 노드로부터 자식이 없거나 하나뿐인 노드까지의 최소 경로 길이를 말한다.
- 자식이 없거나 하나인 노드의 널 경로 길이는
이다. - 임의의 노드의 널 경로 길이는, 그 왼쪽·오른쪽 자식의 널 경로 길이 중 최솟값보다
만큼 크다.
아래 두 트리에서 노드 안의 숫자는 각 노드의 널 경로 길이이며, 왼쪽 트리만 좌향 힙이다. (오른쪽 트리는 파랗게 칠한 노드에서 왼쪽 자식의 널 경로 길이가 오른쪽보다 작아 구조 성질을 위반한다.)
이 구조 성질 때문에 좌향 힙은 전체적으로 왼쪽 서브트리가 더 깊어지는 경향을 가지며, 이것이 '좌향(leftist)'이라는 이름의 유래이다.
병합(merge) 연산
좌향 힙의 모든 연산은 병합 위에서 정의된다. 힙에 원소 하나를 넣는 스며오르기 연산조차, 임의의 힙과 노드 하나짜리 힙을 병합하는 것으로 볼 수 있다.
병합 규칙은 다음과 같다.
- 임의의 힙과 비어 있는empty 힙을 병합하면, 그 힙을 그대로 반환하고 끝낸다.
- 그렇지 않으면 재귀적으로 처리한다.
아래 두 힙
먼저 두 힙의 루트root를 비교하여, 더 작은 루트를 가진 힙의 오른쪽 서브힙subheap을 선택한다.
다시 두 루트를 비교해, 더 작은 힙(
같은 과정을 반복해
이제 더 작은 루트를 가진
병합 후에는 널 경로 길이를 계산해 좌향 조건을 확인한다. 문제가 없으면 그대로 두고, 구조 성질을 위반하면 왼쪽과 오른쪽 서브트리의 위치를 바꾼다. 아래 단계에서는 위반이 생겨 위치를 교환한 뒤 병합을 이어 간다.
이 과정을 위로 올라가며 재귀적으로 반복한다.
최종 결과는 다음과 같다.
이렇게 병합만 정의하면 나머지 연산은 자연스럽게 따라온다.
insert: 노드 하나짜리 힙과의 병합.deleteMin: 루트를 제거한 뒤, 남은 왼쪽·오른쪽 서브힙을 서로 병합.
병합은 오른쪽 경로right path를 따라서만 진행되고, 좌향 성질 덕분에 오른쪽 경로의 길이가
실습: 좌향 힙 병합
위 예시의 두 힙을 그대로 만들어 병합한 뒤, deleteMin을 반복해 원소가 정렬 순으로 빠져나오는지 확인한다. 병합의 핵심(더 작은 루트 선택 → 오른쪽 서브힙 재귀 병합 → 널 경로 길이 위반 시 좌우 교환)이 merge 함수 한 곳에 담겨 있다.
#include <iostream>
#include <vector>
using namespace std;
struct Node {
int val;
int npl; // 널 경로 길이(null path length)
Node *left, *right;
Node(int v) : val(v), npl(0), left(nullptr), right(nullptr) {}
};
int npl(Node* h) { return h ? h->npl : -1; }
// 두 좌향 힙을 병합한다.
Node* merge(Node* h1, Node* h2) {
if (!h1) return h2;
if (!h2) return h1;
if (h1->val > h2->val) swap(h1, h2); // 더 작은 루트를 h1 으로
h1->right = merge(h1->right, h2); // 오른쪽 서브힙과 재귀 병합
if (npl(h1->left) < npl(h1->right)) // 구조 성질 위반 시
swap(h1->left, h1->right); // 좌우 교환
h1->npl = npl(h1->right) + 1;
return h1;
}
Node* insert(Node* h, int v) { return merge(h, new Node(v)); }
Node* deleteMin(Node* h, int& out) {
out = h->val;
Node* res = merge(h->left, h->right);
delete h;
return res;
}
int main() {
Node *H1 = nullptr, *H2 = nullptr;
for (int v : {3, 10, 21, 14, 23, 8, 17, 26}) H1 = insert(H1, v);
for (int v : {6, 12, 18, 24, 33, 7, 37, 18}) H2 = insert(H2, v);
Node* H = merge(H1, H2);
cout << "병합 후 루트(최솟값): " << H->val
<< ", 루트의 npl: " << H->npl << endl;
cout << "deleteMin 순서: ";
while (H) {
int x;
H = deleteMin(H, x);
cout << x << (H ? " " : "\n");
}
return 0;
}핵심
좌향 힙은 "합치기 어렵다"는 힙의 약점을 병합 한 연산으로 해결한다. insert와 deleteMin을 모두 병합으로 환원하고, 구조 성질로 오른쪽 경로를 짧게 유지하는 것이 요점이다.