nokia's blog

Heap, Priority Queue

2022-06-06

힙(Heap)

힙(heap)은 최댓값 및 최솟값을 빠르게 찾아내기 위해 만들어진 자료구조입니다.
완전이진트리를 기본으로 다음과 같은 조건을 만족합니다.

  • 최대 힙: 트리의 부모 노드의 값이 자식 노드의 값보다 더 크다.
  • 최소 힙: 트리의 부모 노드의 값이 자식 노드의 값보다 더 작다.
  • 대소관계는 부모 노드와 자식 노드 간에만 성립하지만, sibling node 사이에는 대소관계가 성립되지 않는다.
  • 최대 힙에서는 루트 노드의 값이 가장 큰 값을 갖고 최소 힙에서는 루트 노드의 값이 가장 작은 값을 갖는다. 이를 응용하여 우선순위 큐와 같은 추상적 자료형을 구현할 수 있다.

힙의 삽입

  1. 트리의 가장 끝에 노드를 추가합니다.
  2. 추가한 노드와 부모 노드의 크기를 비교하여 크기를 바꿔가며 위치를 찾습니다.(heapify)
// heapify 과정에서 노드의 자리를 바꾸는 함수
function swap(index1, index2) {
  [heap[index1], heap[index2]] = [heap[index2], heap[index1]];
}
function push(number) {
  heap.push(number);
  heapCount++;
 
  let child = heapCount;
  let parent = Math.floor(child / 2);
 
  while (child > 1 && heap[child] > heap[parent]) {
    swap(child, parent);
    child = parent;
    parent = Math.floor(child / 2);
  }
}

시간복잡도

트리의 가장 끝에 추가할 때 O(1), heapify를 할 때 O(logn)의 시간 복잡도가 필요합니다.

힙의 삭제

  1. 트리의 루트 노드를 반환합니다.
  2. 루트 노드와 가장 마지막 노드를 swap합니다.
  3. 가장 마지막 노드가 루트 노드가 되었습니다.
  4. 트리의 크기를 1 만큼 감소시킵니다.
  5. 루트 노드와 부모 노드의 크기를 비교하며 크기를 바꿔가며 위치를 찾습니다.(heapify)
function pop() {
  const value = heap[1];
 
  swap(1, heapCount);
  heapCount = heapCount - 1;
 
  let parent = 1;
  let child = parent * 2;
 
  // 두 자식 노드 중 값이 더 큰 자식의 index를 child로 한다.
  if (child + 1 <= heapCount) {
    child = heap[child] > heap[child + 1] ? child : child + 1;
  }
 
  while (child <= heapCount && heap[parent] < heap[child]) {
    swap(child, parent);
 
    parent = child;
    child = parent * 2;
 
    if (child + 1 <= heapCount) {
      child = heap[child] > heap[child + 1] ? child : child + 1;
    }
  }
 
  return value;
}

시간복잡도

루트 노드에서 heapify 할 때 O(logn) 만큼의 시간복잡도가 필요합니다.

힙 정렬

  1. 정렬하고자 하는 값들을 힙에 모두 삽입합니다.
  2. 힙의 크기만큼 삭제를 합니다.
  3. 삭제하면서 리턴된 값들을 출력합니다.
const heap = [null];
let heapCount = 0;
 
function heapSort() {
  // 5 6 3 7 9 8 1 2 4 10
  push(5);
  push(6);
  push(3);
  push(7);
  push(9);
  push(8);
  push(1);
  push(2);
  push(4);
  push(10);
 
  // 10 9 8 7 6 5 4 3 2 1
  while (heapCount >= 1) {
    console.log(pop());
  }
}
 
heapSort();

시간복잡도

모든 값들을 힙에 삽입할 때 O(n _ logn),
모든 노드를 힙에서 삭제할 때 O(n _ logn),
따라서 정렬을 할 때 시간복잡도는 O(nlogn) 만큼이 필요합니다.

worst: O(nlogn)
average: O(nlogn)
best: O(nlogn)

우선순위 큐(Priority Queue)

들어가는 순서에 관계없이 나올 때는 우선순위대로 힙에서 나오게 되는 자료구조입니다.
최대 힙 또한 값이 우선순위가 되는 우선순위 큐의 한 종류라고 볼 수 있습니다.
이를 응용한다면 단순한 number 값만이 아니라 다양한 종류의 객체에 대해 어느 property에든 우선순위를 두어 정렬할 수 있습니다.
앞서 구현한 heapify의 while문의 조건을 수정하여 우선순위 큐를 구현할 수 있습니다.

우선순위 큐 활용

const maxHeap = [null];
let heapCount = 0;
 
function swap(index1, index2) {
  [maxHeap[index1], maxHeap[index2]] = [maxHeap[index2], maxHeap[index1]];
}
 
function push(value) {
  maxHeap.push(value);
  heapCount++;
 
  let child = heapCount;
  let parent = Math.floor(child / 2);
 
  while (child > 1 && maxHeap[child].age > maxHeap[parent].age) {
    swap(child, parent);
    child = parent;
    parent = Math.floor(child / 2);
  }
}
 
function pop() {
  const value = maxHeap[1];
 
  swap(1, heapCount);
  heapCount = heapCount - 1;
 
  let parent = 1;
  let child = parent * 2;
 
  if (child + 1 <= heapCount) {
    child = maxHeap[child].age > maxHeap[child + 1].age ? child : child + 1;
  }
 
  while (child <= heapCount && maxHeap[parent].age < maxHeap[child].age) {
    swap(child, parent);
    parent = child;
    child = parent * 2;
 
    if (child + 1 <= heapCount) {
      child = maxHeap[child].age > maxHeap[child + 1].age ? child : child + 1;
    }
  }
 
  return value;
}
 
push({ name: "nokia", age: 27 });
push({ name: "heehee", age: 26 });
push({ name: "judy", age: 14 });
push({ name: "sophia", age: 16 });
push({ name: "bjork", age: 32 });
 
for (let i = 0; i < 5; i++) {
  console.log(pop());
}
// age를 기준으로 객체들이 정렬된 것을 확인할 있었습니다.
(2) {name: "bjork", age: 32}
(2) {name: "nokia", age: 27}
(2) {name: "heehee", age: 26}
(2) {name: "sophia", age: 16}
(2) {name: "judy", age: 14}

참고자료

위키피디아 - 힙
안경잡이개발자 - 힙 정렬