O(n log n)
O(log n)
이다. 즉, 노드가 많아질수록 높이는 로그 형태로 증가한다.i
의 부모 노드 인덱스는 (i-1)/2
.i
의 왼쪽 자식 노드 인덱스는 2*i + 1
.i
의 오른쪽 자식 노드 인덱스는 2*i + 2
.class MinHeap {
constructor() {
this.heap = [];
}
private getParentIndex(index: number): number {
return Math.floor((index - 1) / 2);
}
private getLeftChildIndex(index: number): number {
return 2 * index + 1;
}
private getRightChildIndex(index: number): number {
return 2 * index + 2;
}
// 힙의 루트값(최소값)반환
public peek(): number | undefined {
return this.heap[0];
}
// 힙에서 루트 값을 제거하고 반환
// 루트에 힙의 마지막 요소를 이동시키고 heapifyDown 호출해 힙의 구조를 유지
public pop(): number | undefined {
if (this.heap.length === 1) return this.heap.pop();
const root = this.heap[0];
this.heap[0] = this.heap.pop()!;
this.heapifyDown(0);
return root;
}
public push(value: number): void {
this.heap.push(value);
this.heapifyUp(this.heap.length - 1);
}
public size(): number {
return this.heap.length;
}
// 힙에 새 요소가 추가 된 후, 힙의 구조를 유지하기 위해 부모 노드와 비교하며 위로 올라가는 작업 수행
// 루트에 도달하거나 부모 노드가 더 작을때까지 반복
private heapifyUp(index: number): void {
// 현재 위치 추적
let currentIndex = index;
while (currentIndex > 0) {
const parentIndex = this.getParentIndex(currentIndex);
if (this.heap[currentIndex] < this.heap[parentIndex]) {
[this.heap[currentIndex], this.heap[parentIndex]] = [
this.heap[parentIndex],
this.heap[currentIndex],
];
currentIndex = parentIndex;
} else break;
}
}
private heapifyDown(index: number): void {
// 현재 위치 추적
let currentIndex = index;
// 왼쪽 노드에 자식이 존재할 때 반복
while (this.getLeftChildIndex(currentIndex) < this.size()) {
const leftChildIndex = this.getLeftChildIndex(currentIndex);
const rightChildIndex = this.getRightChildIndex(currentIndex);
// 왼쪽 인덱스로 초기화
let smallerChildIndex = leftChildIndex;
// 오른쪽이 더 작다면 오른쪽으로 업데이트
if (
rightChildIndex < this.size() &&
this.heap[rightChildIndex] < this.heap[leftChildIndex]
) {
smallerChildIndex = rightChildIndex;
}
// 자식 노드 중 더 작은 값을 가진 노드와 현재 노드를 비교하여
// 현재 노드가 더 크다면 두 노드를 교환
if (this.heap[currentIndex] > this.heap[smallerChildIndex]) {
[this.heap[currentIndex], this.heap[smallerChildIndex]] = [
this.heap[smallerChildIndex],
this.heap[currentIndex],
];
currentIndex = smallerChildIndex;
} else break;
}
}
}
O(n log n)
(힙 생성 O(n)
+ 힙 제거 O(log n)
)프로그래머스 문제 중 "더 맵게" 라는 문제에 적용할 수 있다.
https://school.programmers.co.kr/learn/courses/30/lessons/42626
1 차 풀이
function solution(scoville: number[], k: number) {
let count = 0;
scoville.sort((a, b) => a - b);
while (scoville[0] < k) {
if (scoville.length < 2) return -1;
const first = scoville.shift();
const second = scoville.shift();
if (!first || !second) return;
const newScrovile = first + second * 2;
scoville.push(newScrovile);
scoville.sort((a, b) => a - b);
count++;
}
}
코드 비효율 분석
sort()
함수의 평균 시간복잡도는 O(n log n)
O(n)
이지만 최악일땐 O(n log n)
while
) 에서shift()
는 O(n)
sort()
는 O(n log n)
O(n log n)
* O(n)
= O(n^2 log n)
while
)에서 매 루프마다 shift()
와 sort()
하는 것이 비효율적이다.최소힙으로 개선
function solution(scoville, k) {
let count = 0;
// 최소 힙 구성
const minHeap = new MinHeap();
for (const sco of scoville) minHeap.push(sco);
while (minHeap.peek() < k) {
if (minHeap.size() < 2) return -1;
const first = minHeap.pop();
const second = minHeap.pop();
const newScoville = first + second * 2;
minHeap.push(newScoville);
count++;
}
return count;
}
힙으로 어떤 점을 개선?
매 루프마다 shift()
와 sort()
했던 것을 heap 을 이용하여 불필요한 정렬을 계산하지 않는다.
heap은 항상 최솟값이 루트에 위치하도록 유지(O(1)
)하고 깊이 만큼만 루프를 반복하게 되는데, heap 이 완전 이진트리이기 때문에 heap의 깊이(높이)는 log2 n
에 비례하여 시간복잡도가 O(log n)
이 된다.