티스토리 뷰
320x100
우선순위 큐(Priority Queue)[참고]
우선순위 큐는 큐의 FIFO 구조를 변형한 것으로 가장 우선순위가 높은 데이터가 가장 먼저 나옵니다.
배열이나 링크드 리스트, 힙으로 구현할 수 있고, 구현 방법에 따라 아래와 같은 시간 복잡도를 갖습니다.
구현 방식 | 삽입 | 제거 |
unordered array | O(1) | O(n) |
unordered linked list | O(1) | O(n) |
ordered array | O(n) | O(1) |
ordered linked list | O(n) | O(1) |
heap | O(log n) | O(log n) |
배열이나 링크드 리스트로 구현하는 방법은 매우 간단합니다.
정렬되어 있지 않는 경우, 삽입은 맨 뒤에 하고, 제거할 때 전체를 순회하면서 가장 우선순위가 높은 데이터를 찾아 제거합니다.
정렬되어 있는 경우, 삽입할 때 순회하면서 우선순위에 맞는 위치를 찾아 삽입하고, 제거할 때는 맨 앞에 있는 데이터를 제거합니다.
이 글에서는 Heap으로 구현한 코드를 소개합니다.
구현 내용
- 데이터를 삽입하는 push 구현
- 가장 우선순위가 높은 데이터를 반환하고 제거하는 pop 구현(default: max heap)
- capacity가 부족하거나 많이 남는 경우 resize 하는 기능 구현
- 우선순위 비교 함수를 사용자가 설정할 수 있는 기능 구현
- 가장 우선순위가 높은 데이터를 확인할 수 있는 top, 비어있는지 여부를 확인할 수 있는 empty, 우선순위 큐를 초기화하는 clear 구현
소스코드
PriorityQueue.ts
더보기
// PriorityQueue.ts
class PriorityQueue<T> {
static readonly defaultCapacity = 16;
static readonly EMPTY_SYMBOL = Symbol('EMPTY');
private _pq: Array<T>;
private _capacity: number = PriorityQueue.defaultCapacity;
private _size: number = 0;
// a가 b보다 우선순위가 높거나 같을 때 true 반환. default: max heap
private _compareFn: Function = (a: T, b: T): boolean =>
typeof a === 'number' ? a >= b : String(a) >= String(b);
constructor(_compareFn?: Function) {
_compareFn && (this._compareFn = _compareFn);
this._pq = new Array(this._capacity + 1);
}
get size(): number {
return this._size;
}
push(value: T) {
if (this._size === this._capacity) {
this.resize(this._capacity * 2);
}
this._pq[++this._size] = value;
// slide
let child: number = this._size;
let parent: number = Math.floor(child / 2);
while (parent > 0 && this._compareFn(this._pq[child], this._pq[parent])) {
this.swapByIndex(child, parent);
child = parent;
parent = Math.floor(child / 2);
}
}
pop(): T | Symbol {
if (this.empty()) {
return PriorityQueue.EMPTY_SYMBOL;
}
if (
this._size > PriorityQueue.defaultCapacity &&
this._size <= this._capacity / 2
) {
this.resize(Math.floor(this._capacity / 2));
}
const returnValue = this._pq[1];
// slide
let parent: number = 1;
this._pq[1] = this._pq[this._size];
while (true) {
let leftChild: number = parent * 2;
let rightChild: number = parent * 2 + 1;
if (
rightChild <= this._size &&
this._compareFn(this._pq[rightChild], this._pq[leftChild]) &&
this._compareFn(this._pq[rightChild], this._pq[parent])
) {
// 오른쪽 자식과 swap하는 경우
this.swapByIndex(rightChild, parent);
parent = rightChild;
} else if (
leftChild <= this._size &&
this._compareFn(this._pq[leftChild], this._pq[parent])
) {
// 왼쪽 자식과 swap하는 경우
this.swapByIndex(leftChild, parent);
parent = leftChild;
} else {
// swap을 모두 마친 경우
break;
}
}
this._size--;
return returnValue;
}
top(): T | Symbol {
return this.empty() ? PriorityQueue.EMPTY_SYMBOL : this._pq[1];
}
empty(): boolean {
return this._size === 0;
}
clear() {
this._size = 0;
this._capacity = PriorityQueue.defaultCapacity;
this._pq = new Array(this._capacity + 1);
}
private resize(newCapacity: number) {
const newPQ = new Array(newCapacity + 1);
this._capacity = newCapacity;
for (let i = 1; i <= this.size; ++i) {
newPQ[i] = this._pq[i];
}
this._pq = newPQ;
}
private swapByIndex(indexA: number, indexB: number) {
let tmp: T = this._pq[indexA];
this._pq[indexA] = this._pq[indexB];
this._pq[indexB] = tmp;
}
}
export default PriorityQueue;
PriorityQueue.test.ts
더보기
// PriorityQueue.test.ts
import PriorityQueue from './PriorityQueue';
describe('priority queue test', () => {
let pq: PriorityQueue<number>;
beforeEach(() => {
pq = new PriorityQueue<number>();
});
it('new 연산자로 PriorityQueue 인스턴스를 생성한다.', () => {
expect(Object.getPrototypeOf(pq)).toEqual(PriorityQueue.prototype);
});
it('push - 값을 추가한다.', () => {
pq.push(1);
expect(pq.size).toBe(1);
pq.push(2);
expect(pq.size).toBe(2);
});
it('push - 꽉 찬 상태에서 값을 추가하면 resize 후, 값을 추가한다.', () => {
for (let i = 0; i <= PriorityQueue.defaultCapacity; ++i) {
pq.push(i);
}
expect(pq.size).toBe(PriorityQueue.defaultCapacity + 1);
});
it('pop - 비어있으면 Symbol을 반환한다.', () => {
expect(pq.pop()).toBe(PriorityQueue.EMPTY_SYMBOL);
});
it('pop - 우선순위가 가장 높은 값을 제거하고 반환한다(default: max heap).', () => {
pq.push(1);
pq.push(1);
pq.push(2);
pq.push(3);
expect(pq.pop()).toBe(3);
expect(pq.pop()).toBe(2);
expect(pq.pop()).toBe(1);
expect(pq.pop()).toBe(1);
expect(pq.size).toBe(0);
});
it('pop - capacity와 size가 2배 이상 차이나면 resize한다.', () => {
for (let i = 0; i <= PriorityQueue.defaultCapacity * 2; ++i) {
pq.push(i);
}
for (let i = 0; i <= PriorityQueue.defaultCapacity; ++i) {
pq.pop();
}
});
it('top - 비어있으면 Symbol을 반환한다.', () => {
expect(pq.top()).toBe(PriorityQueue.EMPTY_SYMBOL);
});
it('top - 다음에 pop될 값(가장 우선순위가 높은 값)을 반환한다.', () => {
pq.push(1);
expect(pq.top()).toBe(1);
pq.push(3);
expect(pq.top()).toBe(3);
pq.push(2);
expect(pq.top()).toBe(3);
});
it('empty - 비어있으면 true, 차있으면 false를 반환한다.', () => {
expect(pq.empty()).toBe(true);
pq.push(1);
expect(pq.empty()).toBe(false);
});
it('clear - PriorityQueue를 초기화한다.', () => {
pq.push(1);
pq.push(2);
pq.clear();
expect(pq.empty()).toBe(true);
});
it('compareFn - 숫자가 아닌 값의 경우에는 String으로 변환하여 우선순위를 비교한다(string).', () => {
const strPQ = new PriorityQueue<string>();
strPQ.push('a');
strPQ.push('b');
strPQ.push('c');
strPQ.push('d');
expect(strPQ.pop()).toBe('d');
expect(strPQ.pop()).toBe('c');
expect(strPQ.pop()).toBe('b');
expect(strPQ.pop()).toBe('a');
});
it('compareFn - 숫자가 아닌 값의 경우에는 String으로 변환하여 우선순위를 비교한다(boolean).', () => {
const boolPQ = new PriorityQueue<boolean>();
boolPQ.push(false);
boolPQ.push(true);
expect(boolPQ.pop()).toBe(true);
expect(boolPQ.pop()).toBe(false);
});
it('compareFn - 우선순위 비교 함수를 설정할 수 있다(min heap).', () => {
const compareFn = (a: number, b: number): boolean => b > a;
const minHeap = new PriorityQueue<number>(compareFn);
minHeap.push(1);
minHeap.push(2);
minHeap.push(3);
minHeap.push(4);
expect(minHeap.pop()).toBe(1);
expect(minHeap.pop()).toBe(2);
expect(minHeap.pop()).toBe(3);
expect(minHeap.pop()).toBe(4);
});
it('compareFn - 우선순위 비교 함수를 설정할 수 있다(object).', () => {
type Person = {
name: string;
age: number;
};
const compareFn = (a: Person, b: Person): boolean => a.age > b.age;
const customPQ = new PriorityQueue<Person>(compareFn);
customPQ.push({ name: 'choi', age: 10 });
customPQ.push({ name: 'shim', age: 20 });
customPQ.push({ name: 'lee', age: 30 });
customPQ.push({ name: 'shin', age: 40 });
customPQ.push({ name: 'park', age: 50 });
expect(customPQ.pop()).toEqual({ name: 'park', age: 50 });
expect(customPQ.pop()).toEqual({ name: 'shin', age: 40 });
expect(customPQ.pop()).toEqual({ name: 'lee', age: 30 });
expect(customPQ.pop()).toEqual({ name: 'shim', age: 20 });
expect(customPQ.pop()).toEqual({ name: 'choi', age: 10 });
});
});
기타
코드 내 연산을 간단하게 만들기 위해 배열의 0번째 인덱스는 비워두고 1번째 인덱스부터 사용했습니다.
320x100
'개발 > 자료구조' 카테고리의 다른 글
JavaScript로 Stack과 Queue를 구현해보자(feat.Jest) (0) | 2021.06.14 |
---|
댓글
최근에 올라온 글
TAG
- mkdirp
- jest
- 웹팩 에러
- 스토리북 에러
- node cp -r
- 프로그래머스
- 인가
- fs-extra
- node mkdir -p
- sass
- ModuleParseError: Module parse failed: Unexpected token
- 스터디
- Storybook Error
- node file package
- node fs
- rimraf
- createAction
- 인증
- javascript event
- make-dir
- ELIFECYCLE
- external editor
- ECONNRESET
- file opener preference
- 페이지 특정 위치 link
- JavaScript
- errno 253
- 자바스크립트
- node rm -rf
- Webpack Error