힙
힙(Heap)은 완전 이진 트리 기반의 자료구조로, 부모 노드가 항상 자식 노드보다 크거나(Max-Heap) 작은(Min-Heap) 조건을 만족합니다. 최댓값 또는 최솟값을 O(1)에 조회하고 O(log n)에 삽입·삭제할 수 있어 우선순위 큐의 핵심 구현체로 사용됩니다.
핵심 개념
힙 조건 (Heap Property)
Max-Heap: 모든 부모 노드의 값 ≥ 자식 노드의 값. 루트가 전체 최댓값입니다. Min-Heap: 모든 부모 노드의 값 ≤ 자식 노드의 값. 루트가 최솟값입니다. BST와 달리 형제 노드 사이에는 대소 관계가 없습니다.
핵심 개념
완전 이진 트리와 배열 표현
힙은 완전 이진 트리(마지막 레벨을 제외하고 모두 채워진 트리, 마지막 레벨은 왼쪽부터 채움)입니다. 배열로 표현할 때 인덱스 i의 왼쪽 자식은 2i+1, 오른쪽 자식은 2i+2, 부모는 (i-1)/2입니다.
핵심 개념
삽입 — Heapify-Up
새 원소를 배열 끝(트리의 마지막 위치)에 추가한 뒤, 부모와 비교하며 힙 조건을 만족할 때까지 위로 올립니다. 최대 트리 높이(log n)번 스왑이 발생하므로 O(log n)입니다.
핵심 개념
삭제 — Heapify-Down
루트(최댓값)를 제거하고 배열 마지막 원소를 루트로 이동합니다. 이후 두 자식 중 더 큰 자식과 비교하며 힙 조건을 만족할 때까지 아래로 내립니다. 마찬가지로 O(log n)입니다.
핵심 개념
힙 정렬 (Heap Sort)
① 배열을 Max-Heap으로 만들고 ② 루트(최댓값)를 배열 끝으로 이동한 뒤 힙 크기를 줄이고 Heapify-Down을 반복합니다. n번의 Extract-Max로 오름차순 정렬이 완성됩니다. 시간복잡도 O(n log n), 추가 메모리 O(1)의 제자리 정렬입니다.
핵심 개념
우선순위 큐와 Java PriorityQueue
Java의 PriorityQueue는 Min-Heap 기반입니다. peek()으로 최솟값을 O(1)에 조회, offer()로 O(log n) 삽입, poll()로 O(log n) 삭제. Max-Heap이 필요하면 Collections.reverseOrder()나 람다 Comparator를 사용합니다.
힙이 필요한 이유
힙은 '가장 큰 값(또는 가장 작은 값)을 빠르게 꺼내는' 데 특화된 자료구조입니다. 전체를 정렬해 두는 것보다 약한 규칙(부모가 자식보다 크거나 같다)만 지켜서 삽입과 삭제를 O(log n)에 끝내고, 최댓값 확인은 O(1)로 합니다. 우선순위 큐의 표준 구현이 힙인 이유입니다.
숫자로 확인하기
자주 하는 오해
면접에서 이렇게 물어요
면접 질문
힙과 이진 탐색 트리의 차이는 무엇인가요?
힙은 부모와 자식의 대소만 보장해 최댓값 접근이 O(1)이고 배열로 표현됩니다. BST는 왼쪽 < 현재 < 오른쪽을 보장해 정렬된 순회와 임의 값 탐색이 가능합니다.
면접 질문
Build-Heap이 O(n)인 이유는 무엇인가요?
아래쪽 노드는 내려갈 거리가 짧습니다. 높이가 h인 노드는 최대 h번 내려가는데 그런 노드의 수가 n/2^(h+1) 정도라서 전체 합이 O(n)으로 수렴합니다.
면접 질문
스트림에서 중앙값을 실시간으로 구하려면 어떻게 하나요?
작은 쪽 절반을 최대 힙에, 큰 쪽 절반을 최소 힙에 넣고 두 힙의 크기를 맞춥니다. 두 힙의 루트로 중앙값을 O(1)에 읽고, 값을 추가하는 삽입은 O(log n)입니다.
면접 질문
힙 정렬이 퀵 정렬보다 느린 경우가 많은 이유는 무엇인가요?
힙 정렬은 부모와 자식 사이를 오가며 메모리를 건너뛰어 읽어서 캐시 효율이 떨어지는 편이라, 보통 잘 구현한 퀵 정렬보다 느리다고 알려져 있습니다. 최악의 경우 O(n log n)이 보장되는 점은 장점입니다.
실제 시스템에서는
PriorityQueue 문서는 우선순위 힙 기반의 큐이고 기본은 자연 순서(또는 지정한 Comparator)에서 가장 작은 원소를 꺼내며, offer와 poll은 O(log n), peek는 상수 시간이라고 설명합니다. remove(Object)와 contains는 선형 시간이고 스레드에 안전하지 않습니다.
heapq 모듈은 리스트를 최소 힙으로 다루는 함수를 제공하고(최대 힙 전용 함수는 3.14부터 추가되었습니다), heap[0]이 가장 작은 원소입니다. nlargest와 nsmallest는 상위 K개를 구하는 함수입니다.
함께 보면 좋은 토픽
참고 자료
Java — Max-Heap 직접 구현
/* ── Max-Heap 배열 기반 구현 ────────────────────────────── */ public class MaxHeap { private int[] data; private int size; public MaxHeap(int capacity) { data = new int[capacity]; } private int parent(int i) { return (i - 1) / 2; } private int left(int i) { return 2 * i + 1; } private int right(int i) { return 2 * i + 2; } private void swap(int i, int j) { int t = data[i]; data[i] = data[j]; data[j] = t; } /* 삽입 + Heapify-Up */ public void offer(int val) { data[size++] = val; int i = size - 1; // 부모보다 크면 올라간다 while (i > 0 && data[parent(i)] < data[i]) { swap(i, parent(i)); i = parent(i); } } /* 루트(최댓값) 조회 */ public int peek() { return data[0]; } // O(1) /* Extract-Max + Heapify-Down */ public int poll() { int max = data[0]; data[0] = data[--size]; // 마지막 원소를 루트로 heapifyDown(0); return max; } private void heapifyDown(int i) { while (left(i) < size) { int largest = left(i); if (right(i) < size && data[right(i)] > data[largest]) largest = right(i); if (data[i] >= data[largest]) break; // 힙 조건 만족 swap(i, largest); i = largest; } } }
배열 인덱스 i의 왼쪽 자식은 2i+1, 오른쪽 자식은 2i+2, 부모는 (i-1)/2입니다.
Heapify-Up은 삽입 후 부모보다 크면 계속 올라가고, Heapify-Down은 삭제 후 자식 중 더 큰 쪽으로 내려갑니다.
두 연산 모두 트리 높이(log n)만큼만 수행하므로 O(log n)입니다.
Java — 힙 정렬 (Heap Sort)
/* ── 힙 정렬: 제자리 정렬 O(n log n), 추가 메모리 O(1) ─── */ public static void heapSort(int[] arr) { int n = arr.length; // 1단계: Build Max-Heap (마지막 내부 노드부터 heapify-down) for (int i = n / 2 - 1; i >= 0; i--) heapifyDown(arr, i, n); // O(n) // 2단계: 루트(최댓값)를 끝으로 이동하며 정렬 for (int end = n - 1; end > 0; end--) { // 루트(최댓값) ↔ 마지막 원소 교환 int t = arr[0]; arr[0] = arr[end]; arr[end] = t; // 힙 크기를 줄이고 루트 재정렬 heapifyDown(arr, 0, end); // O(log n) } // 오름차순 정렬 완료 — O(n log n) 전체 } private static void heapifyDown(int[] arr, int i, int n) { while (2 * i + 1 < n) { int largest = 2 * i + 1; // 왼쪽 자식 if (2 * i + 2 < n && arr[2 * i + 2] > arr[largest]) largest = 2 * i + 2; // 오른쪽이 더 크면 if (arr[i] >= arr[largest]) break; int t = arr[i]; arr[i] = arr[largest]; arr[largest] = t; i = largest; } }
Build Max-Heap은 배열 절반 지점부터 역순으로 heapifyDown을 호출합니다.
각 호출이 O(log n)처럼 보이지만 전체는 O(n)으로 분석됩니다.
정렬 단계는 루트(최댓값)를 배열 끝으로 보내고 힙 크기를 줄이는 과정을 n-1번 반복합니다.
각 반복이 O(log n)이므로 전체 정렬은 O(n log n).
추가 배열이 필요 없는 제자리(in-place) 정렬이지만, 캐시 지역성이 낮아 실제 성능은 Quick Sort보다 느린 경우가 많습니다.
Java — PriorityQueue 활용
/* ── Java PriorityQueue (Min-Heap 기본) ─────────────────── */ PriorityQueue<Integer> minHeap = new PriorityQueue<>(); minHeap.offer(15); minHeap.offer(4); minHeap.offer(42); minHeap.peek(); // 4 — 최솟값 O(1) minHeap.poll(); // 4 — 최솟값 제거 O(log n) /* ── Max-Heap: reverseOrder 사용 ─────────────────────────── */ PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Collections.reverseOrder()); maxHeap.offer(15); maxHeap.offer(4); maxHeap.offer(42); maxHeap.peek(); // 42 — 최댓값 O(1) /* ── K번째 최솟값: Min-Heap 활용 패턴 ────────────────────── */ public int kthSmallest(int[] nums, int k) { PriorityQueue<Integer> pq = new PriorityQueue<>(); for (int n : nums) pq.offer(n); while (--k > 0) pq.poll(); return pq.peek(); } /* ── 다익스트라에서의 활용 (최소 비용 노드 추출) ─────────── */ PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[1] - b[1]); // 비용 기준 Min-Heap pq.offer(new int[]{0, 0}); // {노드, 비용} while (!pq.isEmpty()) { int[] cur = pq.poll(); // 현재 최소 비용 노드 // 인접 노드 탐색 및 거리 갱신... }
Java PriorityQueue는 기본이 Min-Heap입니다.
Max-Heap이 필요하면 Collections.reverseOrder() 또는 람다 Comparator를 사용합니다.
K번째 최솟값, 다익스트라 최단 경로, CPU 스케줄링처럼 "현재 가장 우선순위 높은 항목을 빠르게 꺼내야 하는" 문제에서 PriorityQueue(힙)가 핵심 도구입니다.
Q1
Max-Heap의 특징은?
Q2
힙에 새 원소를 삽입할 때 일반적인 방법은?
Q3
힙 정렬(Heap Sort)의 시간 복잡도는?