Study
data-structure
04 Tree and Priority Queue

트리(Tree)

  • 가계도와 같이 계층적인 구조를 표현할 때 사용할 수 있는 자료구조

용어

  • 루트 노드(root node) : 부모가 없는 최상위 노드
  • 단말 노드(leaf node) : 자식이 없는 노드
  • 트리에서는 부모와 자식 관계가 성립한다
  • 깊이 : 루트 노드에서의 길이, 출발 노드에서 목적지 노드까지 거쳐야 하는 간선의 수
  • 트리의 높이 : 루트 노드에서 가장 깊은 노드까지의 길이

이진 트리(Binary Tree)

  • 최대 2개의 자식을 가질 수 있는 트리

포화 이진 트리(Full Binary Tree)

  • 리프 노드를 제외한 모든 노드가 두 자식을 가지고 있는 트리

완전 이진 트리(Complete Binary Tree)

  • 모든 노드가 왼쪽 자식부터 차근차근 채워진 트리

    완전 이진 트리

높이 균형 트리(Height Balanced Tree)

  • 왼쪽 자식 트리와 오른쪽 자식 트리의 높이가 1 이상 차이나지 않는 트리

우선순위 큐(Priority Queue)

  • 우선순위에 따라서 데이터를 추출하는 자료구조
  • 컴퓨터 운영체제, 온라인 게임 매칭 등에서 활용
  • 일반적으로 힙(heap)을 이용해 구현한다
자료구조추출되는 데이터
스택가장 나중에 삽입된 데이터
가장 먼저 삽입된 데이터
우선순위 큐가장 우선순위가 높은 데이터

우선순위 큐를 구현하는 방법

  • 데이터의 개수가 N개일 때, 구현 방식에 따른 시간 복잡도는 다음과 같다
우선순위 큐 구현 방식삽입 시간삭제 시간
리스트 자료형O(1)O(N)
O(logN)O(logN)
  • 일반적인 형태의 큐는 선형적인 구조를 가진다.

  • 반면에 우선순위 큐이진 트리 구조를 사용하는 것이 일반적이다

    우선순위 큐

힙(Heap)

  • 원소들 중에서 최댓값 혹은 최솟값을 빠르게 찾아내는 자료구조
  • 최대 힙(max heap) : 값이 큰 원소부터 추출
  • 최소 힙(min heap) : 값이 작은 원소부터 추출
  • 힙은 원소의 삽입과 삭제를 위해 O(logN)의 수행 시간을 요구
  • 단순한 N개의 데이터를 힙에 넣었다가 모두 꺼내는 작업은 정렬과 동일
  • 이 경우 시간 복잡도는 O(NlogN)

힙의 특징

  • 힙은 완전 이진 트리 자료구조를 따른다
  • 힙에서는 우선순위가 높은 노드가 루트(root)에 위치한다
    1. 최대 힙
      • 부모 노드의 키 값이 자식 노드의 키 값보다 항상 크다
      • 루트 노드가 가장 크며, 값이 큰 데이터가 우선순위를 가진다
    2. 최소 힙
      • 부모 노드의 키 값이 자식 노드의 키 값보다 항상 작다
      • 루트 노드가 가장 작으며, 값이 작은 데이터가 우선순위를 가진다
  • 직관적으로, 거슬러 갈 때마다 처리해야 하는 범위에 포함된 원소의 개수가 절반씩 줄어든다.
  • 따라서 삽입과 삭제에 대한 시간 복잡도는 O(logN)이다

최소 힙 구성 함수: Heapify

  • (상향식) 부모로 거슬러 올라가며, 부모보다 자신이 더 작은 경우에 위치를 교체한다

    힙-1

  • 새로운 원소가 삽입되었을 때

    • O(logN)의 시간 복잡도로 힙 성질을 유지하도록 할 수 있다

      힙-2

  • 원소를 제거할 때

    • O(logN)의 시간 복잡도로 힙 성질을 유지하도록 할 수 있다

    • 원소를 제거할 때는 가장 마지막 노드가 루트 노드의 위치에 오도록 한다

      힙-3

    • 이후에 루트 노드에서부터 하향식으로(더 작은 자식 노드로) heapify()를 진행한다

      힙-4

자바스크립트의 힙(Heap) 라이브러리

사용 예시

var PriorityQueue = require('priorityqueuejs');
 
var queue = new PriorityQueue(function(a, b) {
  return a.cash - b.cash;
});
 
queue.enq({ cash: 250, name: 'Valentina' });
queue.enq({ cash: 300, name: 'Jano' });
queue.enq({ cash: 150, name: 'Fran' });
queue.size(); // 3
queue.peek(); // { cash: 300, name: 'Jano' }
queue.deq(); // { cash: 300, name: 'Jano' }
queue.size(); // 2

PriorityQueue 내부 코드

/**
 * Expose `PriorityQueue`.
 */
module.exports = PriorityQueue;
 
/**
 * Initializes a new empty `PriorityQueue` with the given `comparator(a, b)`
 * function, uses `.DEFAULT_COMPARATOR()` when no function is provided.
 *
 * The comparator function must return a positive number when `a > b`, 0 when
 * `a == b` and a negative number when `a < b`.
 *
 * @param {Function}
 * @return {PriorityQueue}
 * @api public
 */
function PriorityQueue(comparator) {
  this._comparator = comparator || PriorityQueue.DEFAULT_COMPARATOR;
  this._elements = [];
}
 
/**
 * Compares `a` and `b`, when `a > b` it returns a positive number, when
 * it returns 0 and when `a < b` it returns a negative number.
 *
 * @param {String|Number} a
 * @param {String|Number} b
 * @return {Number}
 * @api public
 */
PriorityQueue.DEFAULT_COMPARATOR = function(a, b) {
  if (typeof a === 'number' && typeof b === 'number') {
    return a - b;
  } else {
    a = a.toString();
    b = b.toString();
 
    if (a == b) return 0;
 
    return (a > b) ? 1 : -1;
  }
};
 
/**
 * Returns whether the priority queue is empty or not.
 *
 * @return {Boolean}
 * @api public
 */
PriorityQueue.prototype.isEmpty = function() {
  return this.size() === 0;
};
 
/**
 * Peeks at the top element of the priority queue.
 *
 * @return {Object}
 * @throws {Error} when the queue is empty.
 * @api public
 */
PriorityQueue.prototype.peek = function() {
  if (this.isEmpty()) throw new Error('PriorityQueue is empty');
 
  return this._elements[0];
};
 
/**
 * Dequeues the top element of the priority queue.
 *
 * @return {Object}
 * @throws {Error} when the queue is empty.
 * @api public
 */
PriorityQueue.prototype.deq = function() {
  var first = this.peek();
  var last = this._elements.pop();
  var size = this.size();
 
  if (size === 0) return first;
 
  this._elements[0] = last;
  var current = 0;
 
  while (current < size) {
    var largest = current;
    var left = (2 * current) + 1;
    var right = (2 * current) + 2;
 
    if (left < size && this._compare(left, largest) >= 0) {
      largest = left;
    }
 
    if (right < size && this._compare(right, largest) >= 0) {
      largest = right;
    }
 
    if (largest === current) break;
 
    this._swap(largest, current);
    current = largest;
  }
 
  return first;
};
 
/**
 * Enqueues the `element` at the priority queue and returns its new size.
 *
 * @param {Object} element
 * @return {Number}
 * @api public
 */
PriorityQueue.prototype.enq = function(element) {
  var size = this._elements.push(element);
  var current = size - 1;
 
  while (current > 0) {
    var parent = Math.floor((current - 1) / 2);
 
    if (this._compare(current, parent) <= 0) break;
 
    this._swap(parent, current);
    current = parent;
  }
 
  return size;
};
 
/**
 * Returns the size of the priority queue.
 *
 * @return {Number}
 * @api public
 */
PriorityQueue.prototype.size = function() {
  return this._elements.length;
};
 
/**
 *  Iterates over queue elements
 *
 *  @param {Function} fn
 */
PriorityQueue.prototype.forEach = function(fn) {
  return this._elements.forEach(fn);
};
 
/**
 * Compares the values at position `a` and `b` in the priority queue using its
 * comparator function.
 *
 * @param {Number} a
 * @param {Number} b
 * @return {Number}
 * @api private
 */
PriorityQueue.prototype._compare = function(a, b) {
  return this._comparator(this._elements[a], this._elements[b]);
};
 
/**
 * Swaps the values at position `a` and `b` in the priority queue.
 *
 * @param {Number} a
 * @param {Number} b
 * @api private
 */
PriorityQueue.prototype._swap = function(a, b) {
  var aux = this._elements[a];
  this._elements[a] = this._elements[b];
  this._elements[b] = aux;
};

최소 힙 구현하기

  • enq시, 원소를 삽입한 후 heapify를 진행한다
    • curIdx 값이 0보다 크고, 부모 노드 값이 현재 노드의 값보다 클 경우, heapify를 진행한다
  • deq시, root 값을 제거한 후, root 자리에 마지막 원소를 삽입한다
    • curIdx * 2 + 1 값이 최소 힙의 사이즈보다 작을 경우, heapify를 진행한다
    • 이때, 현재 원소 값이 자식 원소 값보다 작은 경우, heapify 반복을 중단한다
class MinHeap {
  #arr;
 
  constructor(els) {
    this.#arr = els ?? [];
  }
 
  enq(el) {
    this.#arr.push(el);
 
    let curIdx = this.#arr.length - 1;
    let parIdx = Math.floor((curIdx - 1) / 2);
 
    while (curIdx > 0 && this.#arr[parIdx] > this.#arr[curIdx]) {
      this._swap(curIdx, parIdx);
      curIdx = parIdx;
      parIdx = Math.floor((curIdx - 1) / 2);
    }
  }
 
  deq() {
    if (this.#arr.length === 0) return null;
    if (this.#arr.length === 1) return this.#arr.pop();
 
    const originRoot = this.#arr[0];
    this.#arr[0] = this.#arr.pop();
    let curIdx = 0;
 
    while (curIdx * 2 + 1 < this.#arr.length) {
      let childIdx =
        curIdx * 2 + 2 < this.#arr.length &&
        this.#arr[curIdx * 2 + 2] < this.#arr[curIdx * 2 + 1]
          ? curIdx * 2 + 2
          : curIdx * 2 + 1;
 
      if (this.#arr[curIdx] < this.#arr[childIdx]) {
        break;
      }
 
      this._swap(curIdx, childIdx);
      curIdx = childIdx;
    }
    return originRoot;
  }
 
  _swap(a, b) {
    const tmp = this.#arr[a];
    this.#arr[a] = this.#arr[b];
    this.#arr[b] = tmp;
  }
 
  get size() {
    return this.#arr.length;
  }
 
  get peek() {
    return this.#arr[0];
  }
}