자바스크립트로 알고리즘 정리하기 #4 Stack, Queue, Deque 연습문제 풀이
위 문제의 경우에는 단어를 뒤집는 것(Stack
의 특성 이용) + tag
를 고려해주어야 한다.
단어를 뒤집을 때는 Stack
에 순서대로 push()
후에 순서대로 pop()
하면 LIFO(Last In First Out)
특성에 의해 단어가 반전되어 나온다.
공백(
)이 나오기 전까지 스택에 담았다가 공백이 나올 때 스택에 담겨있는 것을 모두 순서대로 pop()
한 뒤에 공백 한 칸을 추가해주는 방식으로 하면 단어가 반전된다.
그 외에 <
>
화살괄호가 나왔을 때는 반전하지 말고 문자열을 그대로 저장소에 넣어주면 된다.
class Stack {
constructor() {
this.data = [];
this.top = 0;
}
push(element) {
this.data[this.top] = element;
this.top += 1;
}
length() {
return this.top;
}
peek() {
return this.data[this.top - 1];
}
isEmpty() {
return this.top === 0;
}
pop() {
if (this.isEmpty() === false) {
this.top = this.top - 1;
return this.data.splice(-1)[0];
}
}
print() {
this.data.map((element) => {
console.log(element);
});
}
reverse() {
this._reverse(0);
}
// private method
_reverse(index) {
if (index < this.top - 1) {
this._reverse(index + 1);
}
console.log(this.data[index]);
}
}
let solution = (line) => {
let stack = new Stack();
let isTagOpen = false;
let answer = '';
line.split('').map((w) => {
if (w === '<') {
while (!stack.isEmpty()) {
answer += stack.pop();
}
isTagOpen = true;
}
if (isTagOpen) {
answer += w;
}
if (isTagOpen && w === '>') {
isTagOpen = false;
}
if (!isTagOpen && w !== '>' && w !== ' ') {
stack.push(w);
}
if (!isTagOpen && w === ' ') {
while (!stack.isEmpty()) {
answer += stack.pop();
}
answer += ' ';
}
});
while (!stack.isEmpty()) {
answer += stack.pop();
}
return answer;
};
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
rl.on('line', function (line) {
console.log(solution(line));
rl.close();
}).on('close', function () {
process.exit();
});
이 경우가 확실히 소스코드는 짧다. 근데 시간복잡도는 위의 Stack
을 이용할 때와 비슷하다.
이 경우에는 정규표현식으로 <``>
형태의 태그를 잡아내어 그 부분을 따로 처리하고 나머지는 반전시키는 방법으로 하였다.
이 방법보다는 위의 Stack
을 이용한 방법이 훨씬 직관적이고 쉽게 풀렸다.
let solution = (line) => {
// 하나의 태그는 <로 시작해서 >가 아닌 것들로 이어지다가 >로 끝나야 함
// <>로만 잡으면 <로시작해서 >로 끝나는 가장 긴 게 잡힘.
// 내부 내용에 닫히는 태그가 없어야 하나의 태그씩 잘 집을 수 있음
let re = /\<([^\>]*)\>/gi;
let regex = RegExp(re);
line.match(re) !== null
? line.match(re).map((m) => {
// match한 결과가 이상하게 스페이스가 줄어있음
// replace를 고차함수로 이용하면 원본을 활용할 수 있음
// m이 스페이스가 줄어있는데도 replace 매칭은 잘 됨
line = line.replace(m, (o) => o.replace(/ /g, '$'));
})
: '';
let words = line.split(' ');
return words
.map((word) => {
return word
.replace(re, (o) => ' ' + o + ' ')
.trim()
.split(' ')
.map((w) => (regex.test(w) ? w : w.split('').reverse().join('')))
.join('');
})
.join(' ')
.replace(/\$/g, ' ');
};
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
rl.on('line', function (line) {
console.log(solution(line));
rl.close();
}).on('close', function () {
process.exit();
});
스택을 이용해 풀었다.
케이스를 정리하면
()
(레이저)가 나왔을 때마다 스택에 들어있는 (
의 개수를 세어준다.)
가 나왔을 때 레이저인지 쇠막대기인지 구분을 해줘야 한다.(
의 갯수에 따라 쇠막대기의 수가 증가한다.()
와 같이 붙어서 나온다.(
의 인덱스를 넣어서 인덱스 차이가 1이 나는지 확인해야 한다.코드를 말로 정리하면
(
가 들어왔을 때(
를 추가(
가 들어올 때마다 쇠막대기가 생기므로 정답에 +1
(
가 아닌 다른 것이 들어왔을 때 ()
)(
를 뺌(
가 들어왔었다면, 레이저, pop()
이후의 Stack
의 길이만큼 더함 (쇠막대기 * 2)(
가 연속으로 들어오는지 카운트하던 변수 초기화class Stack {
constructor() {
this.data = [];
this.top = 0;
}
push(element) {
this.data[this.top] = element;
this.top += 1;
}
length() {
return this.top;
}
peek() {
return this.data[this.top - 1];
}
isEmpty() {
return this.top === 0;
}
pop() {
if (this.isEmpty() === false) {
this.top = this.top - 1;
return this.data.splice(-1)[0];
}
}
print() {
this.data.map((element) => {
console.log(element);
});
}
reverse() {
this._reverse(0);
}
// private method
_reverse(index) {
if (index < this.top - 1) {
this._reverse(index + 1);
}
console.log(this.data[index]);
}
}
let solution = (line) => {
let stack = new Stack();
let openCount = 0;
let answer = 0;
line.split('').map((e) => {
if (e === '(') {
openCount += 1;
stack.push(e);
// 2번 이상 연속으로 '('가 나올 때는 쇠막대기가 생김
if (openCount > 1) {
answer += 1;
}
} else {
stack.pop();
// 레이저 쏜 경우 / 이전에 생성된 막대 길이만큼 더해줌
// 이전에 생성된 막대 길이 = pop() 연산을 수행한 stack의 길이
if (openCount > 0) {
answer += stack.length();
}
openCount = 0;
}
});
return answer;
};
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
rl.on('line', function (line) {
console.log(solution(line));
rl.close();
}).on('close', function () {
process.exit();
});
시간/메모리 제한이 있기 때문에, 생각보다 어려운 문제이다.
이 문제의 핵심은 스택을 이용하면서 시간복잡도를 최대한 줄이는 것이다.
처음에 풀었던 답안은 아래와 같은데,
let solution = ([length, line]) => {
let stack = [];
let answer = [];
line
.split(' ')
.map((e) => +e)
.map((e, i, arr) => {
stack = stack.filter((j) => {
if (arr[j] < e) {
answer[j] = e;
return false;
}
return true;
});
if (e < arr[i + 1]) {
answer.push(arr[i + 1]);
} else {
answer.push(-1);
stack.push(i);
}
return null;
});
return answer.join(' ');
};
let input = [];
const readline = require('readline');
const {Console} = require('console');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
rl.on('line', function (line) {
input.push(line);
}).on('close', function () {
console.log(solution(input));
process.exit();
});
위 답안의 경우 메모리초과로 실패한다.
그 이유는 아마 filter
메소드에서 stack
의 모든 수를 순회하기 때문이다. 사실 stack
을 배열처럼 쓰면 stack
을 쓰는 의미가 전혀 없다.
stack
이란 것은
top()
으로 값을 보고 push()
pop()
으로 해야 한다.그래야 의미가 있는 자료구조이다.
오큰수에서 시간복잡도를 줄이는 핵심은 '스택으로 어떻게 시간복잡도를 줄이냐?'이다.
이를테면
5 4 6 9 8 41 3 2 1
이라는 숫자가 있다고 가정했을 때
오큰수를 구한다면, 내 기준 오른쪽 숫자 중에 나보다 큰 가장 왼쪽 숫자를 찾아야 한다.
가장 일반적인 방법은 5
일 때, 4 6 9 8 41 3 2 1
왼쪽부터 순차적으로 돌아가며 찾아보고 나보다 큰 숫자가 나오면 바로 그게 가장 왼쪽 수니까 더이상 찾을 필요 없이 return
할 것이다.
그런데 이렇게 하면 O(n^2)이 나오게 된다. 그러면 어떻게 할까? 스택을 이용해서 나보다 큰 수가 나왔을 때 한방에 처리하는 것이다.
5
로 시작했을 때, 그냥 스택에 5
의 인덱스 번호를 넣는다. 그러면 스택에 [0]
이 들어간다.
다음숫자는 4
이다. 4
가 스택 맨 위의 인덱스에 해당하는 값인 5
보다 큰지 확인한다. 4
는 5
보다 작다. 그러면 또 4
의 인덱스를 스택에 push()
한다. [0, 1]
이 스택에 차있다.
다음 숫자는 6
이다. 6
이 스택 맨 위의 인덱스에 해당하는 값인 4
보다 큰지 확인한다. 6
은 4
보다 크다. 그러면 스택을 pop()
연산하고 4
의 인덱스 자리에 6
을 넣는다. 또 6
이 스택의 다음 맨 위 인덱스에 해당하는 값인 5
보다 큰지 확인한다. 5
보다 6
이 더 크다. 그러면 5
의 인덱스 값을 기록하고 있는 스택의 맨 위 인덱스 값을 pop()
연산한다. 5
의 인덱스 자리에도 6
을 넣어준다.
여기서 알 수 있는 간단한 사실은, 스택에는 계속 숫자가 내림차순으로 들어갈 것이란 거다. 스택의 맨 위에는 가장 작은 값이 올 수 밖에 없다. 왜냐면 큰 값이 나오는 순간 pop()
되어버리기 때문이다.
이러한 특성 때문에 새로운 숫자가 들어왔을 때는 스택의 맨 위 인덱스에 해당하는 숫자만 비교하면 된다. 왜냐하면 내림차 순으로 정렬했을 때 가장 작은 값이니까, 그것보다도 큰 값이 아니라면 그 전 값보다 클 수가 없다.
이러한 특성을 이용해 시간복잡도를 줄인 코드가 아래 코드이다.
let solution = ([length, line]) => {
let stack = [];
let answer = new Array(length).fill(-1);
line
.split(' ')
.map((e) => +e)
.map((e, i, arr) => {
while (stack.length !== 0 && arr[stack[stack.length - 1]] < e) {
answer[stack.pop()] = e;
}
stack.push(i);
});
while (stack.length !== 0) {
answer[stack.pop()] = -1;
}
return answer.join(' ');
};
let input = [];
const readline = require('readline');
const {Console} = require('console');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
rl.on('line', function (line) {
input.push(line);
}).on('close', function () {
console.log(solution(input));
process.exit();
});
결과적으로 이 문제를 통해 우리가 배워야 할 것은,
수가 정렬되는 등의 경우로 배열의 맨 위만 바라봐도 되는 경우, 스택을 활용하여 시간복잡도를 줄일 수 있다는 것이다.
split()
메소드를 이용해서 띄어쓰기 기준으로 배열로 변경pop()
해서 지워줌 -> while()
을 이용하여 반복join()
하여 띄어쓰기 형태로 반환하면 끝let solution = ([n, line]) => {
let dict = {};
let answer = new Array(+n).fill(-1);
let stack = [];
line
.split(' ')
.map((e) => +e)
.map((e) => {
dict[e] = dict[e] ? dict[e] + 1 : 1;
return e;
})
.map((e, i, arr) => {
// 인덱스를 저장했으므로 arr의 배열에서 스택에 저장된 인덱스를 불러와야 함 그리고 그것을 다시 딕셔너리의 빈도수로 바꿈
while (stack.length > 0 && dict[e] > dict[arr[stack[stack.length - 1]]]) {
answer[stack.pop()] = e;
}
stack.push(i);
});
// 스택에 조건 안맞았던 인덱스들 쌓아놨다가 해결하기
// 스택은 끝까지 돌아보게 되므로 하나하나씩 해결해서 전부 해결 가능
return answer.join(' ');
};
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
let input = [];
rl.on('line', function (line) {
input.push(line);
}).on('close', function () {
console.log(solution(input));
process.exit();
});