트리(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)에 위치한다
- 최대 힙
- 부모 노드의 키 값이 자식 노드의 키 값보다 항상 크다
- 루트 노드가 가장 크며, 값이 큰 데이터가 우선순위를 가진다
- 최소 힙
- 부모 노드의 키 값이 자식 노드의 키 값보다 항상 작다
- 루트 노드가 가장 작으며, 값이 작은 데이터가 우선순위를 가진다
- 최대 힙
- 직관적으로, 거슬러 갈 때마다 처리해야 하는 범위에 포함된 원소의 개수가 절반씩 줄어든다.
- 따라서 삽입과 삭제에 대한 시간 복잡도는 O(logN)이다
최소 힙 구성 함수: Heapify
-
(상향식) 부모로 거슬러 올라가며, 부모보다 자신이 더 작은 경우에 위치를 교체한다
-
새로운 원소가 삽입되었을 때
-
O(logN)의 시간 복잡도로 힙 성질을 유지하도록 할 수 있다
-
-
원소를 제거할 때
-
O(logN)의 시간 복잡도로 힙 성질을 유지하도록 할 수 있다
-
원소를 제거할 때는 가장 마지막 노드가 루트 노드의 위치에 오도록 한다
-
이후에 루트 노드에서부터 하향식으로(더 작은 자식 노드로) heapify()를 진행한다
-
자바스크립트의 힙(Heap) 라이브러리
- JavaScript는 기본적으로 우선순위 큐를 라이브러리로 제공하지 않는다
- 최단 경로 알고리즘 등에서 힙(heap)이 필요한 경우 별도의 라이브러리를 사용해야 한다
- https://github.com/janogonzalez/priorityqueuejs (opens in a new tab)
사용 예시
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];
}
}