읽기 전에 알아두면 좋아요
아래 개념이 익숙하면 본문을 바로 읽으세요. 낯선 개념만 링크에서 복습하면 됩니다.
- 트리 자료구조 차이: 이진 트리 BST MST 구분하기트리는 노드가 부모와 자식으로 연결된 구조입니다. 연결을 따라 읽는 방법을 확인합니다.
- JavaScript 배열: 인덱스 조회와 삽입·삭제 비용배열은 여러 값을 순서대로 담습니다. 인덱스로 값을 읽고 반복문으로 하나씩 살펴봅니다.
개념과 예제는 실행하지 않아도 읽을 수 있습니다. 내 컴퓨터·터미널에서 직접 실행하는 과정은 선택 실습입니다.
과정 위치와 선택 복습 안내
과정 위치: 트리·그래프와 최단거리 · 자료구조 개념
과정 위치는 이 글이 속한 묶음입니다. 이 단계의 모든 개념을 먼저 읽을 필요는 없습니다. 위에 연결한 개념 중 낯선 내용만 복습하세요. 문제의 조건·상태·갱신 순서는 익숙한 언어로 설명하면 됩니다.
JavaScript로 문법·실행 복습하기 (선택)
개념 글의 코드는 JavaScript 예시입니다. 문제 화면에서는 JavaScript·Python·Java·C++·C# 중 익숙한 언어의 함수를 작성하세요. 본문의 Node.js 입출력 예시는 로컬 실행을 위한 별도 안내입니다.
JavaScript 최소 힙에서 push와 pop 뒤의 우선순위는 어떻게 복구하나요?
최소 힙은 가장 작은 우선순위를 먼저 꺼내는 구조입니다. 배열을 트리처럼 읽고, 추가한 값은 위로 올리며 꺼낸 뒤의 값은 아래로 내려 순서를 복구합니다.
적용 조건·범위: compare가 외부 상태에 따라 바뀌거나 저장한 객체의 priority를 나중에 수정하면 기존 부모-자식 조건이 깨질 수 있습니다. 코드가 모든 비교법의 수학적 성질을 검사하지는 않습니다. 저장 중인 우선순위를 바꾸려면 제거·재삽입이나 인덱스 추적을 갖춘 별도 갱신 API가 필요합니다.
최소 힙: 가장 작은 값이 위에 있도록 자리만 고칩니다
작업 우선순위 예제로 최소 힙을 배웁니다. 배열과 트리 위치를 대응시키고 값이 위아래로 이동하는 과정을 단계별로 확인합니다.

먼저 작은 예제로 원리를 익히고, 마지막에 전체 구현을 펼쳐 보세요. 앞쪽 예제는 각각 독립적으로 브라우저 개발자 도구 Console에서 실행합니다. 같은 이름을 다시 선언했다는 오류가 나오면 새로고침 후 해당 예제를 실행하세요. 출력 뒤에 콘솔이 별도로 보여주는 undefined는 마지막 명령의 반환값일 수 있습니다.

선수 지식: JavaScript 변수·배열·조건문을 먼저 익힙니다. 객체 참조나 함수가 낯설면 아래 단계별 예제부터 시작하고, class 전체 구현은 해당 동작을 손으로 추적한 뒤 읽습니다. 각각의 코드는 독립 실행합니다.
가장 급한 작업 하나만 계속 꺼내고 싶습니다
작업마다 작은 숫자일수록 급하다고 정해 보겠습니다. 작업이 들어올 때마다 전체를 정렬하는 대신, 다음에 꺼낼 가장 작은 값만 맨 앞에 유지할 수 있습니다. 최소 힙은 이 일을 하는 구조입니다.
![JavaScript 최소 힙 구현: 우선순위 큐의 push·pop과 비교 함수 3 배열 [2, 5, 7, 9]와 같은 값을 담은 최소 힙의 대응](/wp-content/themes/generatepress_child/assets/images/algorithm-figures/9595/9595-h2-02.webp)
힙은 트리 모양이지만 배열에 차례로 저장할 수 있습니다. 아래는 값 2, 5, 7, 9가 들어 있는 상태입니다.
| 배열 인덱스 | 0 | 1 | 2 | 3 |
|---|---|---|---|---|
| 값 | 2 | 5 | 7 | 9 |
| 트리에서의 자리 | 맨 위 | 2의 왼쪽 | 2의 오른쪽 | 5의 왼쪽 |
부모는 자식보다 작거나 같습니다. 다만 배열 전체가 정렬된 것은 아닙니다. 예를 들어 5와 7 중 어느 것이 먼저 배열에 놓이는지는 전체 정렬 규칙이 아닙니다. 확실한 것은 0번의 2가 가장 작다는 점입니다.
인덱스로 부모 위치를 찾습니다

| 자식 인덱스 | 부모 인덱스 |
|---|---|
| 1, 2 | 0 |
| 3, 4 | 1 |
| 5, 6 | 2 |
const childIndex = 3;
const parentIndex = Math.floor((childIndex - 1) / 2);
console.log(parentIndex);
1
3번 칸의 부모는 1번 칸입니다. Math.floor는 소수점 아래를 버립니다. 새 값은 배열 끝에 들어오므로 이 계산으로 부모를 찾아 비교할 수 있습니다. 부모 위치를 index / 2로만 계산하면 소수가 생겨 올바른 배열 칸을 읽지 못합니다.
새 값은 끝에서 시작해 위로 올라갑니다
[2, 5, 7, 9]에 새 값 1을 넣는 과정을 손으로 따라가겠습니다.

| 단계 | 배열 | 비교 |
|---|---|---|
| 끝에 1 추가 | [2, 5, 7, 9, 1] | 1과 부모 5 |
| 자리 교환 | [2, 1, 7, 9, 5] | 1과 부모 2 |
| 한 번 더 교환 | [1, 2, 7, 9, 5] | 1이 맨 위 |
const heap = [2, 5, 7, 9, 1];
[heap[1], heap[4]] = [heap[4], heap[1]];
[heap[0], heap[1]] = [heap[1], heap[0]];
console.log(heap.join(', '));
1, 2, 7, 9, 5
[heap[1], heap[4]] = [heap[4], heap[1]]은 두 칸의 값을 서로 바꾸는 JavaScript 문법입니다. 오른쪽에서 기존 두 값을 먼저 묶고, 왼쪽의 반대 칸에 다시 넣습니다. 이 짧은 코드는 방금 표의 두 교환만 재현합니다. 완성 구현은 새 값이 부모보다 앞설 동안 이 과정을 반복합니다. 첫 비교 뒤 바로 멈추면 1이 2 아래에 남아 맨 위가 최솟값이라는 규칙이 깨집니다.
맨 위를 꺼내면 마지막 값으로 빈칸을 채웁니다
[1, 2, 7, 9, 5]에서 1을 꺼낸 뒤 맨 위를 그냥 비워 둘 수는 없습니다. 마지막 값 5를 맨 위로 옮기고 아래의 두 자식과 비교합니다.

| 단계 | 배열 | 판단 |
|---|---|---|
| 1 보관, 마지막 5를 위로 | [5, 2, 7, 9] | 자식 2와 7 중 2 선택 |
| 5와 2 교환 | [2, 5, 7, 9] | 5는 자식 9보다 작음 |
| 완료 | [2, 5, 7, 9] | 다음 최솟값은 2 |
const afterPop = [5, 2, 7, 9];
[afterPop[0], afterPop[1]] = [afterPop[1], afterPop[0]];
console.log(afterPop.join(', '));
2, 5, 7, 9
아래로 내릴 때는 두 자식 중 더 작은 쪽을 먼저 골라야 합니다. 5를 오른쪽 자식 7과 먼저 바꾸면 맨 위가 7이 되고, 그 아래에 더 작은 2가 남아 규칙이 깨집니다.
확인 문제: [8, 3, 4]의 맨 위 8은 어느 자식과 먼저 바꿔야 할까요?
3과 먼저 바꿔야 합니다. 두 자식 3과 4 중 작은 값이 3이기 때문입니다. 결과는 [3, 8, 4]가 됩니다.
우선순위가 같을 때 순서는 저절로 정해지지 않습니다

const a = { priority: 1, order: 0 };
const b = { priority: 1, order: 1 };
const compare = (x, y) => x.priority - y.priority || x.order - y.order;
console.log(compare(a, b) < 0);
true
두 작업의 priority는 같습니다. 이때 먼저 들어온 순서인 order를 두 번째 기준으로 비교합니다. priority만 비교하면 동률 작업의 처리 순서는 구현 중 교환 결과에 따라 달라질 수 있습니다. 먼저 온 동률 작업을 먼저 처리해야 한다면 이 규칙을 명시해야 합니다.
바로 실행해 보는 핵심 예제
독자 질문: 가장 작은 우선순위만 반복해서 꺼낼 때 왜 최소 힙을 쓰나요?

왜 쓰나요? 모든 항목을 정렬하지 않고 루트의 최소 조건을 유지합니다.
아래 바닐라 JavaScript 코드는 독립 실행 예제입니다. 본문에서 배운 개념을 새 데이터로 확인합니다. 다른 예제와 같은 콘솔에 이어 붙이지 말고 새 콘솔 또는 새 .cjs 파일에서 실행하세요.
class MinHeap {
values = [];
push(value) {
const a = this.values; a.push(value);
for (let i = a.length - 1; i > 0;) {
const p = Math.floor((i - 1) / 2);
if (a[p] <= a[i]) break;
[a[p], a[i]] = [a[i], a[p]]; i = p;
}
}
pop() {
const a = this.values;
if (!a.length) return undefined;
const top = a[0], last = a.pop();
if (a.length) {
a[0] = last;
for (let i = 0;;) {
let child = i, l = i * 2 + 1, r = l + 1;
if (l < a.length && a[l] < a[child]) child = l;
if (r < a.length && a[r] < a[child]) child = r;
if (child === i) break;
[a[i], a[child]] = [a[child], a[i]]; i = child;
}
}
return top;
}
}
const heap = new MinHeap();
[5, 1, 3].forEach((x) => heap.push(x));
console.log(heap.pop(), heap.pop(), heap.pop());
실행 결과와 비용
이번 검토에서는 Node.js v24.18.0에서 위 독립 예제를 실행해 다음 출력과 일치하는지 확인했습니다.
1 3 5
시간·공간 복잡도: 최솟값 확인은 O(1), push·pop은 O(log n), 전체 배열이 정렬되는 것은 아닙니다. comparator로 객체 우선순위를 다루려면 비교 규칙을 한곳에 모아야 합니다.
직접 풀 문제
처음 [5,1,3]을 넣은 뒤 2를 push하고 연속 pop하면?
풀이와 기대 결과
1,2,3,5입니다. 동점 우선순위에서 입력 순서까지 보장하려면 별도 순번이 필요합니다.
원자료의 다른 학습 순서 (선택)
자료구조 과정 원래 순서의 다음 글: 12 그래프의 이웃과 BFS 방문 순서
검증 범위: 위 독립 예제의 출력만 로컬에서 확인했습니다. 이 글은 독립 자료구조 실습입니다. 실제 브라우저 표시와 모든 입력·플랫폼의 실행은 검증하지 않았습니다.
이제 기존 전체 구현을 읽는 순서
먼저 배열 인덱스로 부모와 두 자식을 계산하는 부분을 찾습니다. push에서는 끝에 넣고 부모와 비교하며 위로 가는 반복을, pop에서는 루트를 보관하고 마지막 값을 위에 둔 뒤 더 작은 자식과 비교하는 반복을 위 표와 맞춰 보세요. 마지막으로 빈 힙과 원소 하나인 힙의 처리를 확인합니다.

위 교환 코드는 특정 상태의 한 장면만 보여 주는 축소 예제입니다. 아래 전체 구현은 어떤 길이에서도 반복하도록 만들고, 비교 함수를 받아 숫자뿐 아니라 작업 객체의 우선순위와 동률 순서도 처리합니다.
관련 코딩테스트 문제로 이어서 연습합니다
이 자료구조의 블로그 연습 문제와 해설에서 배운 동작을 적용해 보세요. 먼저 작은 예시를 직접 처리한 뒤 입력 전체를 다루는 코드로 확장하면 됩니다. 아래 전체 구현은 연결된 문제의 기존 메서드와 반환 형식을 유지합니다.
전체 구현 · 상세 설명 · 예외와 성능 분석 펼치기
여기부터는 필요한 기능을 골라 읽는 참고 영역입니다. 새로운 파일로 전체 코드를 실행할 때는 아래 Node.js 실행 안내를 따르세요. 앞쪽의 작은 브라우저 실습과 전체 파일을 한 콘솔에 이어 붙이지 마세요. 기능별 입력 조건과 반환 형식은 아래 설명을 기준으로 합니다.
가장 작은 항목만 필요할 때 전체 정렬을 유지해야 할까요
우선순위 큐는 먼저 넣은 항목이 아니라 우선순위가 가장 높은 항목을 먼저 꺼냅니다. 여기서는 비교 값이 작은 항목을 우선하는 최소 힙을 구현합니다. 작업이 계속 들어오는 동안 다음 작업 하나만 선택한다면 매번 전체 배열을 정렬하는 것보다 갱신 범위를 좁힐 수 있습니다.

힙은 배열 전체를 정렬해 두지 않습니다. 부모가 자식보다 우선한다는 지역 조건만 유지하면 루트가 전체 최솟값이라는 결론을 얻습니다. 따라서 data[1]과 data[2]의 대소 순서나 형제 간 순서를 가정하면 안 됩니다. 임의 값 찾기와 범위 검색은 BST처럼 빠르지 않습니다.
완전 이진 트리를 배열 인덱스로 표현합니다
0번을 루트로 두면 i의 부모는 floor((i-1)/2), 왼쪽 자식은 2i+1, 오른쪽 자식은 2i+2입니다. 배열 마지막에만 추가·삭제하여 중간 빈자리가 없는 완전 이진 트리 모양을 유지합니다. 이 모양 덕분에 루트에서 마지막 층까지의 이동은 O(log n)단계에 그칩니다.

유일하게 순서를 정하는 것은 compare(a,b)입니다. 음수이면 a가 앞, 0이면 동률, 양수이면 b가 앞입니다. 모든 저장 값에 대해 일관된 순서를 만들고 NaN이 아닌 유한한 수를 반환해야 합니다. 기본 비교 함수 a-b는 유한한 숫자용이며 이 글의 예시는 작은 정수만 사용합니다.
compare가 외부 상태에 따라 바뀌거나 저장한 객체의 priority를 나중에 수정하면 기존 부모-자식 조건이 깨질 수 있습니다. 코드가 모든 비교법의 수학적 성질을 검사하지는 않습니다. 저장 중인 우선순위를 바꾸려면 제거·재삽입이나 인덱스 추적을 갖춘 별도 갱신 API가 필요합니다.
빈 peek/pop은 undefined를 반환하므로 undefined 저장은 금지합니다. push는 값을 추가하며 반환값을 사용하지 않고, size는 현재 항목 수입니다. 동률 항목의 삽입 순서는 일반 힙이 보장하지 않습니다. 안정적인 동률 처리가 필요하면 입력 순번을 비교의 두 번째 기준으로 포함해야 합니다.
전체 코드와 실행
heap.cjs에 아래 전체를 저장한 뒤 Node.js에서 node ./heap.cjs를 실행합니다. CommonJS 한 파일에 구현·export·직접 실행 예제가 모두 있습니다. 내부 배열의 모양이 아니라 pop을 반복한 결과가 정렬 순서인지 확인하는 예제입니다.

'use strict';
class MinHeap {
constructor(compare = (a, b) => a - b) {
if (typeof compare !== 'function') throw new TypeError('compare must be a function');
this.compare = compare;
this.data = [];
}
get size() {
return this.data.length;
}
peek() {
return this.data[0];
}
push(value) {
if (value === undefined) throw new TypeError('undefined is reserved for an empty heap');
let hole = this.data.length;
this.data.push(value);
while (hole > 0) {
const parent = Math.floor((hole - 1) / 2);
if (this.compare(this.data[parent], value) <= 0) break;
this.data[hole] = this.data[parent];
hole = parent;
}
this.data[hole] = value;
}
pop() {
if (this.data.length === 0) return undefined;
const minimum = this.data[0];
const last = this.data.pop();
if (this.data.length === 0) return minimum;
let hole = 0;
while (hole * 2 + 1 < this.data.length) {
let child = hole * 2 + 1;
const right = child + 1;
if (right < this.data.length &&
this.compare(this.data[right], this.data[child]) < 0) {
child = right;
}
if (this.compare(last, this.data[child]) <= 0) break;
this.data[hole] = this.data[child];
hole = child;
}
this.data[hole] = last;
return minimum;
}
}
module.exports = { MinHeap };
if (require.main === module) {
const heap = new MinHeap();
for (const value of [8, 3, 5, 1, 3]) heap.push(value);
const smallest = heap.peek();
const sorted = [];
while (heap.size > 0) sorted.push(heap.pop());
console.log(JSON.stringify({ smallest, sorted, empty: heap.pop() === undefined }));
}
실행 출력
{"smallest":1,"sorted":[1,3,3,5,8],"empty":true}
push: 새 값 하나만 위로 올라갑니다
새 값을 배열 끝에 놓으면 완전 이진 트리 모양은 유지됩니다. 기존 노드끼리의 순서도 그대로여서 새 값과 조상 사이만 검사하면 됩니다. hole은 새 값이 들어갈 위치를 뜻하며, 부모가 새 값보다 크면 부모를 hole로 내려 보내고 hole을 부모 자리로 올립니다.

이 구현은 매 단계 두 값을 교환하지 않고 새 값을 지역 변수 value에 보관합니다. 마지막 위치가 결정된 뒤 한 번만 value를 씁니다. 이동 중 같은 부모 값이 잠시 두 칸에 보여도 메서드가 끝날 때 hole을 채우면 완전한 힙이 됩니다. 중간 상태를 외부에서 읽지 않는 동기 메서드 계약입니다.
pop: 두 자식 중 작은 쪽을 먼저 선택합니다
최솟값은 루트에 있으므로 먼저 따로 저장합니다. 배열의 마지막 원소를 pop하면 트리 모양이 유지되지만 루트 자리가 비었다고 볼 수 있습니다. 원소가 하나뿐이었다면 마지막 원소가 곧 최솟값이어서 더 복구하지 않고 반환합니다.

빈 루트에 마지막 값을 무조건 넣으면 자식보다 클 수 있습니다. 두 자식 중 작은 쪽을 고르고, 그 자식이 last보다 앞서면 자식을 hole로 올립니다. 큰 자식을 올리면 작은 자식이 부모보다 작아지는 위반이 남으므로 자식 선택 순서가 핵심입니다.
last가 선택한 자식보다 앞서거나 같으면 그 아래로 내려갈 필요가 없습니다. 해당 자식은 자기 자식보다 앞서므로 현재 자리의 last도 모든 아래 값보다 앞섭니다. 자식이 없는 곳까지 내려간 경우에도 hole에 last를 넣으면 복구가 끝납니다.
| 단계 | 배열 또는 빈칸 이동 | 이유 |
|---|---|---|
| [3,8,5]에 1 추가 | 마지막 위치 3에서 시작 | 모양은 완전 이진 트리 |
| push의 첫 이동 | 부모 8을 위치 3으로, hole=1 | 1이 8보다 우선 |
| push의 두 번째 이동 | 부모 3을 위치 1로, hole=0 | 1이 3보다 우선 |
| push 종료 | [1,3,5,8] | 루트에 1 기록 |
| pop 시작 | 최소 1 저장, last=8, 배열 길이 3 | 마지막 슬롯 제거 |
| pop 복구 | 자식 3을 루트로 올리고 위치 1에 8 | [3,8,5]로 복구 |
최소 힙은 안정 정렬 자료구조가 아닙니다
동률을 compare=0으로만 표현하면 나중에 들어온 항목이 먼저 나올 수 있습니다. 예를 들어 우선순위가 같은 작업 세 개는 pop할 때 마지막 작업이 루트로 올라가는 과정에서 순서가 바뀔 수 있습니다. 작업의 입력 순번 order를 저장하고 priority 차이 다음 order 차이를 비교하면 원하는 FIFO 동률 계약을 만들 수 있습니다.

모든 데이터를 한꺼번에 받아 전체 정렬 결과만 반환한다면 내장 sort가 더 간단할 수 있습니다. 힙의 장점은 입력과 추출이 섞이거나 상위 K개만 유지하는 상황입니다. 이 구현에는 임의 항목 삭제, decrease-key, 선형 시간 heapify가 없으므로 그런 연산이 필요한 알고리즘에 그대로 있다고 가정하면 안 됩니다.
복잡도와 메모리

| 작업 | 시간 | 공간 |
|---|---|---|
| size / peek | O(1) | O(1) |
| push / pop의 힙 복구 | 최악 O(log n)번 비교 | 지역 변수 O(1) |
| 배열 저장 | n개 항목 참조 | O(n) |
| n개를 push 후 모두 pop | O(n log n) | 출력 포함 O(n) |
표는 compare 한 번과 배열 인덱스 접근을 O(1)로 보는 모델입니다. 비교 함수가 긴 문자열을 비교하면 그 비용을 곱해야 합니다. JavaScript 동적 배열의 저장 공간 확장까지 포함하면 특정 push에서 재할당 비용이 생길 수 있어 “상수 시간 배열 append”는 통상 상환 모델로 해석합니다. 힙의 위아래 이동 자체는 로그 단계로 제한됩니다.
경계 확인과 연습
빈 힙, 한 항목, 같은 우선순위 여러 개, 음수 값, 오른쪽 자식이 더 작은 경우를 확인하세요. 각 child에 대해 compare(parent,child) ≤ 0을 검사하는 불변식 테스트와, 정렬 배열에서 하나씩 꺼내는 독립 기준을 함께 쓰면 눈에 보이지 않는 내부 위반도 잡을 수 있습니다.
![JavaScript 최소 힙 구현: 우선순위 큐의 push·pop과 비교 함수 17 빈 힙 [], 한 원소 [2], 동률 작업의 경계 사례와 부모·자식 불변식](/wp-content/themes/generatepress_child/assets/images/algorithm-figures/9595/9595-h2-18.webp)
함께 연습하고 확인할 자료
Kth Largest Element in a Stream: 스트림에서 K번째 큰 값을 유지하는 외부 연습입니다. 이 글의 모든 작업 출력 문제와는 목표가 다릅니다.
관련 개념 참고: Princeton Algorithms — Priority Queues 공식 자료와 문제 페이지는 2026년 9월 10일 확인했습니다. 외부 문제의 지문·예시를 복제하지 않고 이 글의 예제와 창작 문제를 별도로 구성했습니다.
작은 값 우선과 큰 값 우선은 비교 기준으로 정합니다
우선순위가 높다는 말이 숫자가 크다는 뜻인지 작다는 뜻인지는 문제에서 정합니다. 마감까지 남은 시간이 2와 7이라면 작은 2를 먼저 처리할 수 있고, 위험 점수라면 큰 7을 먼저 처리할 수 있습니다. 현재 힙의 비교 함수는 앞 원소가 먼저 나와야 할 때 음수를 반환하는 규칙입니다. 수에 대해 a−b면 작은 값 우선, b−a면 큰 값 우선이 됩니다. 객체라면 비교할 priority 필드를 먼저 정하고, 동률의 순서가 필요하면 순번 같은 두 번째 기준도 명시합니다.

저장한 데이터와 노드 포장도 구분해야 합니다. 연결 트리 기반 힙은 remove가 트리 노드를 반환할 수 있고, 그 노드의 data가 다시 priority를 가진 객체일 수 있습니다. 현재 배열 힙의 pop은 push로 넣은 값 자체를 반환하므로 무조건 .data를 한 번 더 읽으면 안 됩니다. 빈 반환값인 undefined를 데이터로 넣지 않는 현재 계약도 유지합니다. 별도 힙에 반복 push·pop하여 결과 배열을 만드는 정렬은 입력 배열 내부에서 경계를 줄이는 제자리 힙 정렬과 추가 공간의 조건이 다릅니다.
이어서 공부할 개념
첨부 힙의 BinaryTree: 값과 연결을 나누어 읽기
첨부 자료의 heap/binaryTree.mjs는 연결형 힙의 노드입니다. 같은 압축 파일의 heap.mjs 첫 줄이 이 클래스를 가져오고, new BinaryTree(data)로 실제 노드를 만듭니다. 본문의 배열형 MinHeap에서 배운 “부모와 비교해 위로, 작은 자식과 비교해 아래로”라는 원리는 같지만, 원본은 배열 인덱스 대신 객체 연결을 따라갑니다.

원본 파일에서 먼저 찾을 세 묶음
| 원본 멤버 | 맡은 일 | 읽을 때 확인할 점 |
|---|---|---|
data와 getData/setData |
노드에 담긴 우선순위 값 | 삽입·제거의 복구 과정은 값을 교환합니다. |
leftSubTree/rightSubTree |
부모에서 자식으로 가는 연결 | 자식 설정만으로 자식의 부모까지 바뀌지 않습니다. |
parentTree와 getParent/setParent |
자식에서 부모로 돌아가는 연결 | 원본 Heap은 새 자식을 붙인 뒤 newNode.setParent(insertingParent)를 따로 호출합니다. |
이 파일은 일반 이진 트리와 BST에서 쓰는 노드와 이름이 같아도 내용이 완전히 같지는 않습니다. 힙판은 부모 연결을 추가했고, BST 제거판에는 없는 getParent/setParent를 제공합니다. AVL판은 높이, 레드-블랙 트리판은 색도 저장하므로 파일 이름만 보고 서로 바꾸면 안 됩니다.
삽입에서 움직이는 것은 값입니다
루트에 4가 있고 새 왼쪽 자식에 2를 넣었다고 해 보세요. 원본 Heap.insert는 두 객체의 위치를 바꾸는 대신 getData/setData로 4와 2를 교환합니다. 교환 후 같은 루트 객체에는 2, 같은 자식 객체에는 4가 들어 있습니다. newNode = newNode.getParent()는 다음 비교 위치를 위로 옮기는 지역 변수 변경입니다.
| 단계 | 루트 값 / 왼쪽 값 | 연결 |
|---|---|---|
| 자식 연결 | 4 / 2 | root.left는 child, child.parent는 아직 null |
| 부모도 설정 | 4 / 2 | child.parent는 root |
| 값 교환 | 2 / 4 | 두 객체의 연결은 그대로 |
| 자식 연결만 해제 | 루트 2, 분리한 객체 4 | child.parent는 아직 root이므로 따로 해제해야 함 |
보조 파일에 있다고 모든 메서드가 힙 연산에 쓰이지는 않습니다
preOrderTraversal/inOrderTraversal/postOrderTraversal은 값을 콘솔에 출력하는 순회입니다. 원본 heap.mjs의 마지막 시연은 중위 순회를 호출하지만, 힙의 중위 순회는 정렬 결과가 아닙니다. 원본의 삽입값 4, 2, 5, 7, 1을 넣으면 층별 값은 [1,2,5,7,4], 중위 출력은 7,2,4,1,5입니다. 우선순위 순서를 확인하려면 제거를 반복한 결과를 봅니다.
removeLeftSubTree/removeRightSubTree도 노드 클래스에 있지만 원본 Heap.remove의 연결 해제는 자식 setter와 setParent(null)를 직접 사용합니다. 힙의 마지막 자리 찾기와 완전 이진 트리 모양 유지는 Heap의 책임입니다. 이 보조 클래스 자체는 순서·중복·사이클·완전성을 검사하지 않습니다.
반환 계약도 구분합니다. 원본 Heap.remove()는 꺼낸 값을 담은 노드 객체를 돌려주고 빈 힙에서는 null입니다. 본문의 MinHeap.pop()은 값 자체를 돌려주고 빈 힙에서는 undefined입니다. 아래 작은 예제는 부모·자식 연결과 값 교환만 분리한 관찰용 코드이며, 기존 push/pop 계약을 바꾸지 않습니다.
추가 예제 코드와 실행 방법 펼치기
직접 실행할 때만 펼쳐 보세요. Node.js 시작 안내를 확인하고 linked-heap-node.mjs로 저장합니다.
// 힙 전체가 아니라 원본 BinaryTree의 연결과 값 교환만 분리한 예제입니다.
class BinaryTree {
constructor(data) {
this.data = data;
this.leftSubTree = null;
this.rightSubTree = null;
this.parentTree = null;
}
getData() { return this.data; }
setData(data) { this.data = data; }
getLeftSubTree() { return this.leftSubTree; }
getRightSubTree() { return this.rightSubTree; }
setLeftSubTree(tree) { this.leftSubTree = tree; }
setRightSubTree(tree) { this.rightSubTree = tree; }
getParent() { return this.parentTree; }
setParent(tree) { this.parentTree = tree; }
}
const root = new BinaryTree(4);
const child = new BinaryTree(2);
root.setLeftSubTree(child);
console.log(child.getParent() === null);
child.setParent(root);
// 노드 자체를 옮기지 않고 두 노드에 담긴 값만 바꿉니다.
const saved = root.getData();
root.setData(child.getData());
child.setData(saved);
console.log(JSON.stringify({
root: root.getData(), left: child.getData(),
sameChild: root.getLeftSubTree() === child,
sameParent: child.getParent() === root
}));
// 부모의 자식 연결과 자식의 부모 연결은 따로 해제합니다.
root.setLeftSubTree(null);
console.log(child.getParent() === root);
child.setParent(null);
console.log(root.getLeftSubTree() === null, child.getParent() === null);
파일이 있는 폴더의 터미널에서 실행합니다.
node linked-heap-node.mjs
기대 출력
true
{"root":2,"left":4,"sameChild":true,"sameParent":true}
true
true true
직접 실습: 연산 비용으로 구조를 설명합니다
실습 주제: JavaScript 최소 힙 구현: 우선순위 큐의 push·pop과 비교 함수

- 본문 구현에서 저장되는 값과 연결 관계를 그림으로 적습니다.
- 조회·삽입·삭제 중 이 구조가 가장 자주 수행할 연산을 고릅니다.
- 연산 전후에도 유지되어야 하는 규칙을 한 문장으로 적습니다.
- 배열이나 Map 같은 다른 구조로 바꿨을 때 시간·공간 비용을 비교합니다.
풀이 기준과 확인 결과
메서드 이름만 외우지 말고 한 번의 연산에서 어떤 값과 연결이 바뀌는지 추적하세요. 빈 구조, 원소 한 개, 중복값, 연속 삽입·삭제를 실행했을 때 본문이 설명한 불변식이 유지되면 성공입니다.
테스트 체크리스트

- 빈 구조에 대한 조회·삭제 처리
- 첫 원소와 마지막 원소 변경
- 중복값 또는 동일 우선순위 처리
- 입력 크기가 커졌을 때 예상 복잡도 유지
이 글이 도움이 되었나요?
이어서 공부하기
체크포인트: 예제의 상태 변화를 손으로 추적하고 아래 문제에 적용하세요. 값이 없는 경우와 원소 하나인 경우도 설명해 보세요. 층별 방문 순서와 최솟값 추출을 추적하고, 다익스트라의 비음수 조건과 오래된 후보를 설명하세요.
다음 추천 · 알고리즘 개념: 제자리 힙 정렬: 배열 heapify와 확정 구간의 경계
권장 순서는 학습 가이드와 같습니다. 다른 순서는 접힌 참고 목록에서 확인할 수 있습니다. 읽음 표시는 숙달이나 채점 결과가 아닙니다.
원래 과정 목록 순서 (선택)
읽음 기록
읽음 — / 전체 23개 · 필수 23개
새 글 받아보기
RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.