프로그래머스 데브코스: 클라우드 기반 웹 프론트엔드 3기
Javascript로 Heap을 구현해보자
hanlabong
2024. 11. 29. 11:38
728x90
힙이란?
완전 이진 트리의 일종
정렬, 우선순위 큐, 스케쥴링 등 다양한 알고리즘에 사용된다.
용어 정리
부모 노드 | 자식 노드 | 루트 노드 | 리프 노드 | 레벨 | 높이 |
특정 노드의 상위 | 특정 노으의 하위 | 트리의 최상단 | 트리의 말단 | 루트 ~ 리프 트리의 깊이 0부터 시작 |
리프 ~ 루트 트리의 높이 1부터 시작 |
종류
최대 힙
부모 노드의 값 >= 자식 노드의 값
항상 부모 노드의 값이 자식 노드의 값보다 크거나 같음
최소 힙
부모 노드의 값 <= 자식 노드의 값
항상 부모 노드의 값이 자식 노드의 값보다 작거나 같음
활용 예시
삽입/삭제 정렬 시간복잡도
삽입 | 삭제 | |
순서 없는 배열 | O(1) | O(n) |
순서 없는 연결 리스트 | O(1) | O(n) |
정렬된 배열 | O(n) | O(1) |
정렬된 연결 리스트 | O(n) | O(1) |
힙 | O(logn) | O(logn) |
우선순위 큐
배열, 연결리스트, 힙으로 구현하능하지만 힙이 가장 효율적이다.
최단 경로 알고리즘
우선 순위가 높은 기준으로 노드를 선택해 목표 노드까지 최단 경로를 구한다.
힙정렬
최대값 또는 최소값을 구하는 데 효율적이다. 다만 특정 값의 위치를 구하기에는 모든 노드를 방문해야 할 수 있기 때문에 비효율적이다.
구현 방법
기본 아이디어
위의 그림을 통해 알 수 있는 정보
- 부모 인덱스 = 자식 인덱스 / 2
- 왼쪽 자식 인덱스 = 부모 인덱스 * 2
- 오른쪽 자식 인덱스 = 부모 인덱스 * 2 + 1
또한, 힙은 정렬하기 위해서 자신의 부모 또는 자식노드와 값을 비교한 다음 자리를 교환하는 방식을 사용한다.
따라서
- 배열 생성
- 이때 편의를 위해 인덱스 0은 비워두고 1부터 요소를 삽입한다.
- 새로운 요소가 생기면 가장 마지막 노드 뒤에 추가 (push)
- 해당 요소를 부모 노드와 비교하며 위치 바꾸기 (swap)
class MaxHeap {
constructor() {
// 힙을 저장할 배열
this.heap = [null];
}
// 힙의 크기를 반환하는 메서드
size() {
return this.heap.length - 1;
}
// 두 값을 바꿔주는 메서드
swap(idx1, idx2) {
[this.heap[idx1], this.heap[idx2]] = [this.heap[idx2], this.heap[idx1]];
}
}
삽입
- 노드 삽입
- 부모 노드와 비교 후 자리 교환
- 교환 중단
코드
// 새로운 노드를 추가하는 메서드
push(value) {
this.heap.push(value); // 힙의 끝에 새로운 노드 추가
this.bubbleUp(); // 새로운 값이 추가된 위치에서 bubbleUp() 수행
}
bubbleUp() {
let index = this.heap.length - 1; // 새로운 노드가 추가된 위치
let parentIdx = Math.floor(index / 2); // 부모 노드의 위치
while (
this.heap[parentIdx] && // 부모 노드가 존재하고
this.heap[index] > this.heap[parentIdx] // 새로운 노드가 부모 노드보다 큰 경우
) {
this.swap(index, parentIdx); // 두 노드의 값을 교체
index = parentIdx; // 인덱스를 부모 노드의 인덱스로 변경
parentIdx = Math.floor(index / 2); // 새로운 부모 노드의 인덱스 계산
}
}
삭제
- 루트 노드 제거
- 배열 마지막 노드 루트로 올리기
- 자식 노드와 비교하며 교환
- 교환 중단
코드
pop() {
if (this.heap.length === 2) {
return this.heap.pop(); // 힙의 크기가 1인 경우, 힙에서 값을 삭제하고 return
}
const value = this.heap[1]; // 힙의 최소값(루트 노드의 값)을 저장
this.heap[1] = this.heap.pop(); // 힙의 끝에 있는 값을 루트 노드로 이동
this.bubbleDown(); // 루트 노드에서 bubbleDown을 수행
return value; // 삭제한 최소값 return
}
bubbleDown() {
let index = 1; // 루트 노드의 index
let leftIdx = index * 2; // 왼쪽 자식 노드의 index
let rightIdx = index * 2 + 1; // 오른쪽 자식 노드의 index
while (
// 왼쪽 자식 노드가 존재 하면서 값이 루트 노드보다 작거나
(this.heap[leftIdx] && this.heap[leftIdx] > this.heap[index]) ||
// 오른쪽 자식 노드가 존재 하면서 값이 루트 노드보다 작은 경우
(this.heap[rightIdx] && this.heap[rightIdx] > this.heap[index])
) {
let smallerIdx = leftIdx; // 왼쪽 자식 노드가 더 작다고 가정
if (
this.heap[rightIdx] &&
this.heap[rightIdx] < this.heap[smallerIdx] // 오른쪽 자식 노드가 더 작다면
) {
smallerIdx = rightIdx; // 오른쪽 자식 노드의 index로 변경
}
this.swap(index, smallerIdx); // 두 노드의 값을 교체
index = smallerIdx; // index를 더 작은 값의 자식 노드의 index로 변경
leftIdx = index * 2; // 왼쪽 자식 노드의 index 계산
rightIdx = index * 2 + 1; // 오른쪽 자식 노드의 index 계산
}
}
전체 코드
class MaxHeap {
constructor() {
this.heap = [0];
}
size() {
return this.heap.length - 1;
}
swap(idx1, idx2) {
[this.heap[idx1], this.heap[idx2]] = [this.heap[idx2], this.heap[idx1]];
}
push(value) {
this.heap.push(value);
this.bubbleUp();
}
pop() {
if (this.heap.length === 2) {
return this.heap.pop();
}
const value = this.heap[1];
this.heap[1] = this.heap.pop();
this.bubbleDown();
return value;
}
bubbleUp() {
let index = this.heap.length - 1;
let parentIdx = Math.floor(index / 2);
while (
this.heap[parentIdx] &&
this.heap[index] > this.heap[parentIdx]
) {
this.swap(index, parentIdx);
index = parentIdx;
parentIdx = Math.floor(index / 2);
}
}
bubbleDown() {
let index = 1;
let leftIdx = index * 2;
let rightIdx = index * 2 + 1;
while (
(this.heap[leftIdx] && this.heap[leftIdx] > this.heap[index]) ||
(this.heap[rightIdx] && this.heap[rightIdx] > this.heap[index])
) {
let smallerIdx = leftIdx;
if (
this.heap[rightIdx] &&
this.heap[rightIdx] > this.heap[smallerIdx]
) {
smallerIdx = rightIdx;
}
this.swap(index, smallerIdx);
index = smallerIdx;
leftIdx = index * 2;
rightIdx = index * 2 + 1;
}
}
}
728x90