힙이란?
완전 이진 트리 형태의 자료구조이다.
우선순위가 있는 데이터를 저장하고 관리하는 특별한 트리다.
완전 이진 트리란?
- 모든 레벨이 왼쪽부터 차례대로 채워진다.
- 마지막 레벨을 제외한 모든 레벨이 완전히 채워져 있어야 한다.
- 앞 줄부터 왼쪽에서 오른쪽에서 차례대로 앉은 것처럼, 노드들이 순서대로 채워진다.
힙의 종류
- 최대 힙: 부모 노드가 자식 노드보다 항상 큰 힙 자료구조 (부모 > 자식)
- 최소 힙: 부모 노드가 자식 노드보다 항상 작은 힙 자료구조 (부모 < 자식)
힙의 특징
- 항상 완전 이진 트리 형태를 유지
- 부모 - 자식 간의 대소 관계만 중요(형제 노드 간의 관계는 상관 없음)
- 배열로 구현하면 쉽게 관리 가능
최소 힙 예시
// 배열
[0, 1, 2, 3, 4, 5, 6]
// 배열 -> 힙 변환 그림
0
/ \
1 2
/ \ / \
3 4 5 6
최소 힙 구현 예시
class MinHeap {
constructor() {
this.heap = [];
}
// 부모 인덱스 구하기
// (현재 인덱스 -1) / 2 => 소수점 버리기
getParentIndex(index) {
return Math.floor((index - 1) / 2);
}
// 값 추가하기
// 맨 뒤에 넣고 -> 부모 노드와 계속 비교하며 변경
insert(value) {
this.heap.push(value);
this.bubbleUp(this.heap.length - 1);
}
// 위로 올리기 (새로운 값 추가 시)
bubbleUp(index) {
while(index > 0){
const parentIndex = this.getParentIndex(index);
if (this.heap[parentIndex] <= this.heap[index]) break;
// 부모와 위치 교환
[this.heap[index], this.heap[parentIndex]] =
[this.heap[parentIndex], this.heap[index]];
index = parentIndex;
}
}
// 최소값 꺼내기
remove() {
if(this.heap.length === 0) return null;
const min = this.heap[0];
const last = this.heap.pop();
if(this.heap.length > 0) {
this.heap[0] = last;
this.bubbleDown(0);
}
return min;
}
}
// 사용 예시
const heap = new MinHeap();
heap.insert(5);
heap.insert(3);
heap.insert(7);
heap.insert(1);
console.log(heap.remove()); // 1
console.log(heap.remove()); // 3
1. getParentIndex 설명
getParentIndex(index) {
return Math.floor((index -1) / 2);
}
0
/ \
1 2
/ \ / \
3 4 5 6
// 위 트리 예시를 배열로 표현할 경우 [0, 1, 2, 3, 4, 5, 6]
규칙
- 노드 1의 부모는 0 : (1-1)/2 = 0
- 노드 2의 부모는 0 : (2-1)/2 = 0
- 노드 3의 부모는 1 : (3-1)/2 = 1
- 노드 4의 부모는 1 : (4-1)/2 = 1
- 노드 5의 부모는 2 : (5-1)/2 = 2
- 노드 6의 부모는 2 : (6-1)/2 = 2
수식 설명
1. 자식 노드의 인덱스와 부모 노드의 인덱스 사이에는 다음과 같은 관계가 성립한다.
- 왼쪽 자식 = 부모 * 2 + 1
- 오른쪽 자식 = 부모 * 2 + 2
2. 이 관계를 역으로 풀면
- 부모 = (자식 - 1) / 2
3. 소수점을 버리기 위해 Math.floor 사용
2. insert 설명
insert(value) {
// 1. 새로운 값을 배열의 마지막에 추가
this.heap.push(value);
// 2. 추가된 값을 올바른 위치로 이동 (bubbleUP 호출)
this.bubbleUp(this.heap.length - 1);
}
bubbleUp(index) {
// index가 0이 되면 중단
while (index > 0) {
// 1. 현재 노드의 부모 인덱스 계산
const parentIndex = this.getParentIndex(index);
// 2. 부모 값이 현재 값보다 작거나 같으면 종료
// (최소 힙의 경우 부모가 자식보다 작아야 함)
if(this.heap[parentIndex] <= this.heap[index]) break;
// 3. 부모 값이 더 크면 위치 교환
[this.heap[index], this.heap[index]] =
[this.heap[parentIndex], this.heap[index]];
// 4. 교환 후 인덱스를 부모 위치로 업데이트
index = parentIndex;
}
}
예시) 최소 힙에 [5, 8, 10]이 있는 상태에서 3을 삽입하는 경우
초기 상태 : // [5, 8, 10]
5
/ \
8 10
1. 마지막에 3 추가 : // [5, 8, 10, 3]
5
/ \
8 10
/
3
2. bubbleUp 시작 (3과 부모인 8 비교): // [5, 3, 10, 8]
5
/ \
3 10
/
8
3. 계속해서 bubbleUp (3과 부모인 5 비교): // [3, 5, 10, 8]
3
/ \
5 10
/
8
4. 완료! (부모가 더 작은 값이 없음)
3. remove 설명
힙에서 최소값(루트 노드) 반환 및 제거 & 재정렬
remove() {
// 1. 힙이 비어있으면 null 반환
if(this.heap.length === 0) return null;
// 2. 루트 노드(최솟값) 저장
const min = this.heap[0];
const last = this.heap.pop();
// 4. 힙에 노드가 남아있다면
if(this.heap.length > 0) {
// 마지막 노드를 루트로 이동
this.heap[0] = last;
// 루트에서부터 아래로 내려가며 재정렬
this.bubbleDown(0);
}
// 5. 최솟값 반환
return min;
}
예시) 최소 힙 [1, 3, 5, 4]에서 remove를 실행하는 경우:
1. 초기 상태:
1
/ \
3 5
/
4
2. 루트 노드(1) 제거 후, 마지막 노드(4)를 루트로 이동:
4
/ \
3 5
3. bubbleDown 시작 (4와 자식들 중 작은 값인 3과 비교 후 교환):
3
/ \
4 5
4. 완료! (더 이상 자식이 더 작지 않음)
4. bubbleDown 함수
힙의 특성을 유지하기 위해 노드를 아래로 내려가며 재정렬하는 과정
bubbleDown(index){
while(true) {
// 1. 현재 노드를 가장 작은 값으로 가정
let smallest = index;
// 2. 왼쪽, 오른쪽 자식의 인덱스 계산
const leftChild = 2 * index + 1; // 왼쪽 자식 인덱스
const rightChild = 2 * index + 2; // 오른쪽 자식 인덱스
// 3. 왼쪽 자식이 있고, 현재 노드보다 작은지 확인
if(leftChild < this.heap.length && this.heap[leftChild] < this.heap[index]) {
smalles = leftChild;
}
// 4. 오른쪽 자식이 있고, 현재까지의 최소값보다 작은지 확인
if(rightChild < this.heap.length && this.heap[rightChild] < this.heap[smallest]) {
smallest = rightChild;
}
// 5. 교환이 필요없으면 (현재 노드가 자식들보다 작으면) 종료
if (smallest === index) break;
// 6. 값 교환
[this.heap[index], this.heap[smallest]] =
[this.heap[smallest], this.heap[index]];
// 7. 교환된 위치로 이동하여 계속 진행
index = smallest;
}
}
예시) 다음과 같은 최소 힙이 있다고 가정해보자
초기 상태 (7이 루트에 있는 상태):
7
/ \
1 3
/ \ / \
2 5 4
1회차 반복:
- 왼쪽 자식(1)과 오른쪽 자식(3) 중 1이 더 작음
- 7과 1을 교환
1
/ \
7 4
/ \ /
2 5 3
2회차 반복:
- 왼쪽 자식(2)과 오른쪽 자식(5) 중 2가 더 작음
- 7과 2를 교환
1
/ \
2 3
/ \ /
7 5 4
3회차 반복:
- 더 이상 자식이 없거나 자식들이 더 크므로 종료
최종 상태:
1
/ \
2 3
/ \ /
7 5 4
핵심 정리
1. 자식 인덱스 계산
const leftChild = 2 * index + 1;
const rightChild = 2 * index + 2;
2. 자식 노드 존재 확인
leftChild < this.heap.length
3. 자식 노드와 비교
if (this.heap[leftChild] < this.heap[smallest])
4. 교환 필요성 확인
if (smallest === indext) break;
문제 풀어보기 (프로그래머스)
https://school.programmers.co.kr/learn/courses/30/lessons/42626
프로그래머스
SW개발자를 위한 평가, 교육, 채용까지 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프
programmers.co.kr
class MinHeap {
constructor() {
this.heap = [];
}
// 최소힙 배열 중 가장 마지막에 두고, bubbleUp 함수를 통해 최소힙 구조로 정렬
push(value) {
this.heap.push(value);
this.bubbleUp();
}
// 최소힙 배열 중 최소값(root)를 반환하고, 마지막 요소를 root로 올린 후 bubbleDown 함수를 통해 최소힙 구조로 정렬
pop() {
if (this.heap.length === 0) return null;
if (this.heap.length === 1) return this.heap.pop();
const min = this.heap[0];
this.heap[0] = this.heap.pop();
this.bubbleDown();
return min;
}
// 마지막 요소를 부모 요소와 비교하며 더 작은 경우 부모와 자리를 바꾼다.
bubbleUp() {
let index = this.heap.length - 1;
while (index > 0) {
const parentIndex = Math.floor((index - 1) / 2);
if (this.heap[parentIndex] <= this.heap[index]) break;
[this.heap[index], this.heap[parentIndex]] = [
this.heap[parentIndex],
this.heap[index],
];
index = parentIndex;
}
}
bubbleDown() {
let index = 0;
while (true) {
// 현재 루트에 있는 요소를 최소 인덱스라고 가정
let smallest = index;
// 왼쪽 자식 인덱스
const leftChild = 2 * index + 1;
// 오른쪽 자식 인덱스
const rightChild = 2 * index + 2;
// 왼쪽 자식 인덱스가 배열 길이보다 작고 (존재하고),
// 왼쪽 자식 요소가 현재 요소보다 작으면 최소 인덱스를 왼쪽 자식 인덱스로 변경
if (
leftChild < this.heap.length &&
this.heap[leftChild] < this.heap[smallest]
) {
smallest = leftChild;
}
// 오른쪽 자식 인덱스가 배열 길이보다 작고 (존재하고),
// 오른쪽 자식 요소가 현재 요소보다 작으면 최소 인덱스를 오른쪽 자식 인덱스로 변경
if (
rightChild < this.heap.length &&
this.heap[rightChild] < this.heap[smallest]
) {
smallest = rightChild;
}
// 최소 인덱스가 현재 인덱스와 같으면 더 이상 정렬할 필요가 없음
if (smallest === index) break;
// 현재 요소와 최소 인덱스의 요소를 자리를 바꾼다.
[this.heap[index], this.heap[smallest]] = [
this.heap[smallest],
this.heap[index],
];
// 현재 인덱스를 최소 인덱스로 변경
index = smallest;
}
}
size() {
return this.heap.length;
}
}
function solution(scoville, K) {
const minHeap = new MinHeap();
let count = 0;
// 모든 스코빌 지수를 힙에 넣기
scoville.forEach((s) => minHeap.push(s));
// 가장 작은 값이 K보다 작은 동안 반복
while (minHeap.size() >= 2 && minHeap.heap[0] < K) {
// 가장 작은 값 두 개를 꺼내서 섞은 후 힙에 넣기
const first = minHeap.pop();
const second = minHeap.pop();
const newScoville = first + second * 2;
minHeap.push(newScoville);
// 섞었기 때문에 count + 1
count++;
}
// 최소 힙 배열 중 최소값(root)이 K 이상인지 확인
// 여전히 없다면 -1 반환
return minHeap.heap[0] >= K ? count : -1;
}
너무 어려워서 튜터님이 푸신거 그대로 .... 복습 반드시 필필필 !
'JavaScript' 카테고리의 다른 글
JavaScript 실행컨텍스트(Execution Context) 이해하기 (1) | 2025.02.28 |
---|---|
(퀵/병합) 정렬 개념 정리 (0) | 2025.01.08 |
해시 개념 이해하기 (0) | 2025.01.06 |
[프로그래머스 Lv.1] 문자열 나누기 (0) | 2025.01.04 |
[프로그래머스][스택/큐] 올바른 괄호, 프로세스 javascript (1) | 2024.12.14 |