힙(Heap)과
우선순위 큐
이진 트리의 구조적 특성이 O(log N)을 보장하는 원리부터, 프로그래머스 '더 맵게' 문제 풀이까지 단계별로 해설합니다.
이진 트리 (Binary Tree)
이진 트리는 각 노드가 최대 두 개의 자식 노드를 가질 수 있는 계층적 자료구조입니다. 왼쪽 자식(left child)과 오른쪽 자식(right child)으로 구분됩니다.
힙의 근간이 되는 완전 이진 트리(Complete Binary Tree)는 추가 조건이 있습니다.
① 마지막 레벨을 제외한 모든 레벨이 완전히 채워져 있다.
② 마지막 레벨의 노드는 반드시 왼쪽부터 순서대로 채워진다.
N=10 → H=3 | N=100 → H=6 | N=1,000 → H=9 | N=1,000,000 → H=19
노드가 1,000배 늘어도 높이는 3배 증가에 불과합니다. 이것이 O(log N)의 근거입니다.
힙 (Heap)
힙은 완전 이진 트리에 힙 속성(Heap Property)이라는 추가 규칙을 부여한 자료구조입니다. 최댓값 또는 최솟값을 O(1)에 조회하기 위해 설계되었습니다.
루트 = 전체 최솟값
Java PriorityQueue 기본값
루트 = 전체 최댓값
Collections.reverseOrder() 사용
힙은 완전 정렬이 아닙니다. 부모가 자식보다 작다는 것만 보장하며, 형제 노드 사이에는 순서 보장이 없습니다. (예: 위 트리에서 9와 20의 위치는 바뀌어도 유효한 힙입니다)
시간복잡도 — 왜 O(log N)인가
힙의 모든 핵심 연산이 O(log N)인 이유는 하나입니다. 완전 이진 트리의 높이가 ⌊log₂N⌋이기 때문입니다. 삽입과 삭제 모두 트리를 위아래로 한 번만 이동하며, 최대 이동 횟수 = 높이 = log N입니다.
새 값을 배열 맨 끝(= 트리 마지막 리프)에 추가한 뒤, 부모보다 작으면 교환하며 위로 올라갑니다. 루트 도달 또는 힙 조건 만족 시 종료합니다.
루트(최솟값)를 꺼내고, 배열 마지막 노드를 루트 자리로 이동합니다. 두 자식 중 작은 쪽과 비교해서 부모가 더 크면 교환하며 아래로 내려갑니다.
| 연산 | 힙 (PriorityQueue) | 정렬 배열 | 이유 |
|---|---|---|---|
| 최솟값 조회 peek() | O(1) | O(1) | 루트 = 배열[0] 직접 접근 |
| 삽입 offer() | O(log N) | O(N) | 힙: 높이만큼만 / 배열: 자리 찾아 이동 |
| 최솟값 삭제 poll() | O(log N) | O(1) | 힙: 재정렬 필요 / 배열: 그냥 제거 |
| 임의 검색 | O(N) | O(log N) | 힙: 이진탐색 불가 / 배열: 이진탐색 가능 |
배열 인덱스 매핑
힙은 트리 구조이지만 실제 메모리는 1차원 배열에 저장됩니다. 완전 이진 트리를 레벨 순서대로 나열하면 인덱스 산술만으로 부모·자식 위치를 O(1)에 계산할 수 있기 때문입니다. 포인터가 전혀 필요 없고, 메모리가 연속 배치되어 캐시 효율도 우수합니다.
(i - 1) / 2
2 * i + 1
2 * i + 2
우선순위 큐 (Priority Queue)
우선순위 큐는 FIFO(선입선출) 방식의 일반 큐와 달리, 우선순위가 가장 높은(= 값이 가장 작은) 데이터가 항상 먼저 나오는 추상 자료형입니다. 내부 구현으로 힙을 사용합니다.
삽입 O(1), 제거 O(1)
삽입 O(log N), 제거 O(log N)
내부적으로 offer()는 배열 끝에 추가 후 Heapify Up을, poll()은 루트 제거 후 마지막 원소를 루트로 올리고 Heapify Down을 실행합니다. 앞서 확인한 O(log N) 과정과 완전히 동일합니다.
문제 해설 — 더 맵게
섞는 공식: 새 음식 = 가장 작은 값 + (두 번째로 작은 값 × 2)
최소 횟수를 반환. 불가능하면 -1 반환.
매 단계마다 가장 작은 값 2개를 찾아야 합니다. 이 작업을 반복적으로 효율적으로 수행하는 것이 핵심입니다.
배열 재정렬 방식: 매 단계 O(N log N) × 최대 N 단계 = O(N² log N) → 시간 초과
최소 힙 방식: 꺼내기 O(log N) × 2 + 삽입 O(log N) = 단계당 O(log N), 전체 O(N log N) → 통과
peek(): 루트 읽기 → O(1) | offer(): Heapify Up → O(log N) | poll(): Heapify Down → O(log N)
전체 시간복잡도: O(N log N) — 완전 이진 트리의 높이가 log N임을 이용한 결과