이럴 때 써요
- 가장 큰(작은) K개, 또는 K번째로 큰 수를 구할 때
- 빈도 상위 K개처럼 '많이 나온 순서'가 필요할 때
- 전체를 정렬하기엔 데이터가 크고, K는 작을 때
- 값이 하나씩 흘러 들어오는 스트림에서 실시간으로 상위/중앙값을 유지할 때
개념
상위 K개만 필요한데 전체를 정렬하면
O(n log n) 이라 아까워요. 크기 K짜리 최소 힙을 유지하면 O(n log k) 로 줄어요. 힙에 하나 넣고, 크기가 K를 넘으면 가장 작은 걸 하나 버려요. 그렇게 끝까지 훑으면 힙엔 가장 큰 K개만 남아요.왜 하필 최소 힙일까요? 최소 힙은 루트가 가장 작은 값이에요. 즉 힙에 든 K개 중 최솟값이 루트에 있어요. 새 값이 이 루트보다 크면 K개 안에 들 자격이 있으니 루트를 버리고 넣어요. 그래서 'K번째로 큰 수'는 다 훑은 뒤의 루트가 바로 답이에요."[5, 1, 8, 3, 9] 에서 가장 큰 3개" — 크기 3 최소 힙k = 3 , 힙은 항상 크기 3 이하로 유지해요 (루트 = 최솟값)5 : 넣어요 힙 {5}1 : 넣어요 힙 {1, 5}8 : 넣어요 힙 {1, 5, 8}3 : 넣으면 크기 4 → 최소 1 버림 힙 {3, 5, 8}9 : 넣으면 크기 4 → 최소 3 버림 힙 {5, 8, 9}가장 큰 3개 → {5, 8, 9} , 루트 5 는 3번째로 큰 수예요
스트림 중앙값도 힙 두 개로 풀어요. 작은 절반은 최대 힙, 큰 절반은 최소 힙에 담고 두 힙 크기를 비슷하게 맞추면, 두 루트만 보고 언제든 중앙값을 바로 알 수 있어요.
패턴 코드
크기 K 최소 힙으로 가장 큰 K개 유지파이썬은 내장
heapq 를, JS는 직접 만든 최소 힙을 써요.javascriptCopy codeclass MinHeap {constructor() { this.data = []; }get size() { return this.data.length; }peek() { return this.data[0]; }push(v) {this.data.push(v);let i = this.data.length - 1;while (i > 0) {const p = (i - 1) >> 1;if (this.data[p] <= this.data[i]) break;[this.data[p], this.data[i]] = [this.data[i], this.data[p]];i = p;}}pop() {const top = this.data[0];const last = this.data.pop();if (this.data.length > 0) {this.data[0] = last;let i = 0;const n = this.data.length;while (true) {let s = i;const l = 2 * i + 1, r = 2 * i + 2;if (l < n && this.data[l] < this.data[s]) s = l;if (r < n && this.data[r] < this.data[s]) s = r;if (s === i) break;[this.data[s], this.data[i]] = [this.data[i], this.data[s]];i = s;}}return top;}}function kLargest(nums, k) {const heap = new MinHeap();for (const x of nums) {heap.push(x);if (heap.size > k) heap.pop(); // 가장 작은 걸 버려요}// heap.peek() 은 K번째로 큰 수예요return heap.data.slice().sort((a, b) => a - b);}