지금까지는 자료 구조에 따라 연산의 성능이 어떻게 달라지는가에 초점을 맞췄다.
스택과 큐는 제약을 갖는 배열이다. 제약 덕분에 두 자료 구조가 매우 간결해진다.
스택과 큐는 임시 데이터를 처리할 수 있는 간결한 도구로, 다양한 분야에서 사용해 뛰어난 알고리즘을 만들 수 있다.
임시 데이터는 처리 후에는 의미 없는 정보이므로 다 쓴 후에는 버려도 된다.
스택에는 세 가지 제약이 있다.
스택의 끝을 위(top), 스택의 시작을 밑(bottom)이라고 부른다.
스택에 새 값을 삽입하는 것을 스택에 푸시한다고 하고, 스택의 위에서 원소를 제거하는 것을 스택으로부터 팝한다고 한다.
스택 연산은 LIFO(Last In, First Out)이다.
즉, 스택에 푸시된 마지막 항목이 스택에서 팝될 첫 번째 항목이라는 의미다.
대부분의 프로그래밍 언어에는 스택이 내장 데이터 타입이나 클래스로 딸려 있지 않다. 직접 구현해야 한다.
class Stack {
constructor() {
this._data = [];
}
push(item) {
this._data.push(item);
}
pop() {
return this._data.pop();
}
read() {
return this._data[this._data.length - 1];
}
}
이 스택 구현은 _data
라는 배열에 데이터를 저장한다.
배열을 기반으로 Stack
클래스를 만들다 보니 사용자가 배열과 상호작용하는 인터페이스가 제한적이다. 원래 배열은 어떤 인덱스든 정상적으로 읽을 수 있으나, 스택 인터페이스로 배열을 사용하면 마지막 항목만 읽고, 삽입하고, 삭제할 수 있다.
사실 스택은 꼭 배열로만 만들어야 하는 것은 아니다. LIFO 방식으로 동작하는 데이터 원소들의 리스트면 된다. 그래서 스택은 추상 데이터 타입에 속한다.
임시 데이터를 다뤄야 하는 다양한 알고리즘에서는 스택이 유용한 도구다.
자바스크립트 린터의 시작 부분을 여는 괄호와 닫는 괄호에만 초점을 맞춰서 만들어보자.
문법 오류는 세 가지 상황에서 발생한다.
(var x = 2;
var x = 2);
(var x = [1, 2, 3)]
빈 스택을 준비해서 규칙에 따라 각 문자를 왼쪽부터 오른쪽 방향으로 읽는다.
(var x = {y: [1, 2, 3]})
을 왼쪽부터 오른쪽으로 읽기 시작한다.
(
이다.var x =
는 괄호가 아니므로 무시한다.{
다.y:
는 무시한다.[
다.1, 2, 3
은 무시한다.]
가 나왔다.[
가 나왔다. 닫는 괄호와 스택에서 팝한 원소의 괄호 종류가 같으니 오류 없이 알고리즘을 이어갈 수 있다.}
가 나왔다.{
가 나왔다. 닫는 괄호와 스택에서 팝한 원소의 괄호 종류가 같다.)
가 나왔다.(
가 나왔다. 닫는 괄호와 스택에서 팝한 원소의 괄호 종류가 같다.class Stack {
constructor() {
this._data = [];
}
push(item) {
this._data.push(item);
}
pop() {
return this._data.pop();
}
read() {
return this._data[this._data.length - 1];
}
}
class Linter extends Stack {
lint(text) {
for (let i = 0; i < text.length; i++) {
if (this.isOpeningBrace(text[i])) {
super.push(text[i]);
} else if (this.isClosingBrace(text[i])) {
let poppedOpeningBrace = this.pop();
if (!poppedOpeningBrace) {
return `${text[i]} doesn't have opening brace`;
}
if (this.isNotAMatch(poppedOpeningBrace, text[i])) {
return `${text[i]} has mismatched opening brace`;
}
}
}
if (super.read()) {
return `${super.read()} does not have closing brace`;
}
return true;
}
isOpeningBrace(char) {
return ["(", "{", "["].includes(char);
}
isClosingBrace(char) {
return [")", "}", "]"].includes(char);
}
isNotAMatch(openingBrace, closingBrace) {
return closingBrace !== { "(": ")", "{": "}", "[": "]" }[openingBrace];
}
}
스택이 제약을 갖는 배열일 분이라면 스택이 하는 일은 배열도 할 수 있다.
스택, 큐처럼 제약을 갖는 데이터 구조는 다음과 같은 이유로 중요하다.
스택은 마지막에 들어온 데이터부터 먼저 처리해야 할 때 이상적이다. Ctrl + Z
동작이 스택의 훌륭한 활용 사례다.
큐 역시 임시 데이터를 처리하기 위해 디자인된 데이터 구조로, 데이터를 처리하는 순서만 제외하면 많은 면에서 스택과 비슷하다.
큐는 First In First Out의 약자인 FIFO로 표현한다. 주로 가로로 묘사 되며, 큐의 시작을 앞(front), 큐의 끝을 뒤(back)이라고 부른다.
큐에는 세 가지 제약이 있다.
class Queue {
constructor() {
this._data = [];
}
enqueue(element) {
this._data.push(element);
}
dequeue() {
this._data.shift();
}
read() {
return this._data[0]
}
}
프린터가 네트워크 상에 있는 여러 컴퓨터로부터 출력 잡(job)을 받아들일 수 있도록 프로그래밍 한다. 요청 받은 순서대로 각 문서를 출력하도록 한다.
class Queue {
constructor() {
this._data = [];
}
enqueue(element) {
this._data.push(element);
}
dequeue() {
return this._data.shift();
}
read() {
return this._data[0]
}
}
class PrintManager extends Queue {
queuePrintJob(document) {
super.enqueue(document)
}
run() {
while (super.read()) {
this.print(super.dequeue())
}
}
print(document) {
console.log(document)
}
}
이 예제는 단순화됐고 실제 출력 시스템이 다뤄야 할 핵심 기능을 추상화하기도 했지만 이러한 애플리케이션은 필수적으로 큐를 사용한다.
큐는 요청받은 순서대로 요청을 처리하므로 비동기식 요청을 처리하는 완벽한 도구이기도 하다.
스택과 큐는 실용적인 알고리즘을 간결하게 처리할 수 있는 프로그래머의 도구다.