스택과 큐는 선형 자료구조이다. (Linear)
스택
스택은 말 그대로 쌓다는 의미를 가지고 있다.
스택은 Last In First Out이다. 즉 마지막에 들어간 값이 제일 처음 나온다.
1,2,3,4를 순서대로 넣는다고 생각해보자.
위와 같이 1,2,3,4 순서대로 담기게 된다. 뺄 때는 위에서부터 한 개씩 빼게 된다.
스택을 구현할 때 우리가 생각할 부분은 스택 오버플로우와 스택 언더플로우이다.
오버플로우는 스택이 가득 찼는데, 우리가 원소를 더 넣으려 할 때 발생한다.
언더플로우는 스택이 비었을 때, 우리가 원소를 더 빼려고 할 때 발생한다.
스택 구현
구현하기에 앞서 우리가 어떤 기능을 구현할지 생각해보자.
S를 스택이라 하자
S.create(x) : S의 크기를 x로 지정한다.
S.push(x) : S에 x를 삽입한다.
S.pop : S에서 원소 하나를 제거한다.
S.peek : S의 가장 위에 있는 원소를 반환한다.
S.size : S내에 존재하는 원소의 개수를 반환한다.
package main;
import java.util.Arrays;
import java.util.Scanner;
import static main.Main.*;
public class Main {
static int size;
static int top=-1;
static int[] stack;
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
size = sc.nextInt();
stack s = new stack();
s.create(size);
s.push(2);
s.push(4);
s.push(6);
s.push(8);
s.push(10);
System.out.println(Arrays.toString(stack));
s.pop();
System.out.println(Arrays.toString(stack));
System.out.println(s.peek());
System.out.println(s.size());
}
}
class stack {
void create(int size){
stack = new int[size];
}
boolean isStackFull(){
if (top>=size-1)return true;
else return false;
}
boolean isStackEmpty(){
if (top==-1)return true;
else return false;
}
void push(int data){
if (isStackFull()){
System.out.println("Stack is full");
return;
}
top++;
stack[top]=data;
}
void pop(){
if (isStackEmpty()){
System.out.println("Stack is empty");
return;
}
stack[top] = 0;
top--;
}
int peek(){
return stack[top];
}
int size(){
return top+1;
}
}
top을 -1로 두어서 스택이 비었다면 top의 위치는 -1이 된다.
값이 0일 때를 빈 값으로 두고 구현하였다.
아래는 스택을 사용하였을 때, 결과이다.
만약 20번째 줄 다음에 한 번 더 push를 한다면 stack is full이라는 말이 나오고 저장을 실패한다.
peek은 맨뒤의 값을 반환해주고, size의 경우 원소의 개수를 반환한다.(0은 빈 값을 의미)
큐
큐는 First In Firsh Out이다. 즉 처음 들어간 값이 처음 나오게 된다.
값 1,2,3,4를 순서대로 넣는다고 생각해보자.
큐는 넣은 순서 그대로 1,2,3,4 차례대로 나오게 된다.
큐를 구현할 때 우리가 생각할 부분은 스택 오버플로우이다.
오버플로우는 큐가 가득 찼는데, 우리가 원소를 더 넣으려 할 때 발생한다.
언더플로우는 큐가 비었을 때, 우리가 원소를 더 빼려고 할 때 발생한다.
큐 구현
큐를 구현하기 전에 어떤 기능을 구현할지 생각해보자.
Q를 큐라 하자.
Q.create(x) : Q의 크기를 x로 지정한다.
Q.offer(x) : Q에 x를 삽입한다.
Q.poll() :Q에서 원소 하나를 제거한다.
Q.peek() : Q의 가장 앞에 있는 원소를 반환한다.
Q.size() : Q내에 존재하는 원소의 개수를 반환한다.
package main;
import java.util.Arrays;
import java.util.Scanner;
import static main.Main.*;
public class Main {
static int size;
static int front=-1;
static int rear =-1;
static int[] queue;
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
size = sc.nextInt();
main.queue q = new queue();
q.create(size);
q.offer(2);
q.offer(4);
q.offer(6);
q.offer(8);
System.out.println(Arrays.toString(queue));
q.poll();
System.out.println(Arrays.toString(queue));
System.out.println(q.peek());
System.out.println(q.size());
}
}
class queue {
void create(int size){
queue = new int[size];
}
boolean isStackFull(){
if (rear==size-1)return true;
else return false;
}
boolean isStackEmpty(){
if (front==rear)return true;
else return false;
}
void offer(int data){
if (isStackFull()){
System.out.println("Queue is full");
return;
}
rear++;
queue[rear]=data;
}
void poll(){
if (isStackEmpty()){
System.out.println("Queue is empty");
return;
}
front++;
queue[front]=0;
}
int peek(){
return queue[front+1];
}
int size(){
return rear-front;
}
}
큐는 front와 rear가 있다. 둘 다 -1로 시작하는데, front는 말 그대로 처음 값을 가리키고 rear는 말 그대로 마지막 값을 가리킨다.
그래서 값을 추가할 때마다 rear의 값이 증가된다. 반대로 front는 뺄 때마다 값이 증가한다.
아래는 큐를 사용했을 때, 결과이다.
peek의 할 경우 맨 앞의 값을 반환한다. size의 경우 원소의 개수를 반환한다.(0은 빈 값을 의미)
우리가 큐를 구현하면서, 이상한 점을 불편한 점을 느꼈을 것이다.
바로 제거한 앞부분을 더 이상 사용하지 못하게 된다는 것이다.
이러한 문제점은 원형 큐를 사용하여 해결할 수 있다.
원형 큐
원형 큐는 큐의 rear값이나 front값이 사이즈를 넘기면 다시 앞으로 가는 것이다.
만약 rear == front의 경우 큐가 가득 찬 것이다.
기본 큐에서 코드가 크게 달라진 것은 없다.
package main;
import java.util.Arrays;
import java.util.Scanner;
import static main.Main.*;
public class Main {
static int size;
static int front=-1;
static int rear =-1;
static int[] queue;
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
size = sc.nextInt();
main.queue q = new queue();
q.create(size);
q.offer(2);
q.offer(4);
q.offer(6);
q.offer(8);
q.offer(8);
System.out.println(Arrays.toString(queue));
q.poll();
q.poll();
q.poll();
q.offer(10);
q.offer(10);
System.out.println(Arrays.toString(queue));
System.out.println(q.peek());
System.out.println(q.size());
}
}
class queue {
void create(int size){
queue = new int[size];
}
boolean isStackFull(){
if ((rear+1)%size==front)return true;
else return false;
}
boolean isStackEmpty(){
if (front==rear)return true;
else return false;
}
void offer(int data){
if (isStackFull()){
System.out.println("Stack is full");
return;
}
rear = (rear+1)%size;
queue[rear]=data;
}
void poll(){
if (isStackEmpty()){
System.out.println("Stack is empty");
return;
}
front = (front+1)%size;
queue[front]=0;
}
int peek(){
return queue[(front+1)%size];
}
int size(){
if (front<rear)return rear-front;
return (size-front)+rear;
}
}
rear나 front값을 +1 하는 과정에서 % size가 추가되었다. 즉 범위를 넘어서면 앞으로 가는 것이다.
아래는 원형 큐를 사용할 때, 결과이다.
0은 빈 값을 의미한다. 큐가 가득 찬 상태에서 세 개를 제거하고 10을 두 번 추가하니 빈 구간인 앞에서부터 채워진다.
사용 예시 문제를 보자
대표적인 괄호 문제이다.
괄호
스택의 대표적인 문제이다.
중요한 점은 괄호가 )이면 pop을 해야 하는데, 스택이 비어있다면 오류가 뜨게 된다.
import java.util.Scanner;
import java.util.Stack;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
String str = sc.nextLine();
if (stack(str)) System.out.println("YES");
else System.out.println("NO");
}
static boolean stack(String string) {
Stack s = new Stack<>();
for (int i = 0; i < string.length(); i++) {
if (string.charAt(i) == '(') s.push('(');
else if (string.charAt(i) == ')'&&!s.empty()) s.pop();
else if (s.empty()&&string.charAt(i) == ')') return false;
}
if (s.empty())return true;
else return false;
}
}
괄호가 남아있다면 false이다. 만약 스택이 비어있는데 )이 나온다면 false이다.
탑
이문제는 스택을 사용한다.
아래는 처음 시도했던 코드이다. 런타임 에러가 뜨지만, 접근방법은 괜찮았다고 생각했었다. 지금 생각해보면 오히려 어렵게 풀은 것 같다.
import java.util.Scanner;
import java.util.Stack;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
Stack<Integer>s = new Stack<>();
int n = sc.nextInt();
int[] arr= new int[n+1];
int[] result = new int[n+1];
for (int i=1;i<=n;i++){
arr[i] =sc.nextInt();
}
for (int i=n;i>0;i--){
while (!s.empty()&&arr[s.peek()]<arr[i]){
result[s.pop()] = i;
}
s.push(i);
}
for (int i =1; i<=n;i++){
System.out.print(result[i]+" ");
}
}
}
중간에 for문이 가장 중요한 코드이다.
맨뒤의 index부터 시작하게 된다.
처음 시작은 당연하게 while문을 들어가지 않는다. 스택에 마지막 인덱스를 넣는다.
그리고 앞의 값과 비교한 뒤, 앞의 값이 더 크면 pop 시키고, 인덱스를 result에 저장한다.
생각보다 이해하기 어려울 수 있다.
아래 그림을 보면서 이해해보자
arr ={6,9,5,7,4}라고 가정한다. 또한 i는 1부터 n까지 저장된다. 즉 배열의 크기는 n+1이라 생각하자.
처음 i는 5이다. while문은 당연하게 실행되지 않고(스택이 비어있으므로), 스택에 5가 저장된다.
이후 i가 4일 때, 지금 인덱스의 배열의 값(7)이 스택에 저장된 인덱스의 값이(4) 보다 크기 때문에,
스택의 맨 위 값을 지우고, 스택에 저장된 값을 인덱스로 한 result에 저장한다.
이후 스택에 4를 추가한다.(실제 스택에 index값을 저장하는 것이 맞다. 이해를 돕기 위해 arr [index]를 저장해둔 값을 적었다.)
위의 말을 수식으로 나타내면
i =5 이므로 while문은 실행되지 않고, s.push(5), i=4일 때, arr [4]>arr [5]이므로 result [5] = 4가 된다. 이후 s.push(4)를 한다.
다음은 i가 3일 때이다. 스택에는 4가 추가되어있고, arr [3]이 arr [4]보다 작으므로, 스택에 3을 추가하고 끝난다.
i=2일 때를 보자. arr [2]가 arr [3], arr [4]보다 크므로, result [4]=2, result [3]=2를 해주고, s.push(2)를한다.
이후 스택에는 2,1이 남아있는 채로 for문이 종료된다. (이해를 돕기 위해 스택에 arr [index]를 적어두었다.)
따라서 result의 첫 번째와 두 번째는 초기값인 0으로 종료된다.
다음은 정답 코드이다.
package main;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Stack;
import java.util.StringTokenizer;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int num = Integer.parseInt(br.readLine());
Stack<int[]> stack = new Stack<>();
StringTokenizer st = new StringTokenizer(br.readLine());
for(int i=0; i<num; i++) {
int n = Integer.parseInt(st.nextToken());
while(!stack.isEmpty()) {
if(stack.peek()[0] < n)
stack.pop();
else {
System.out.print(stack.peek()[1] + " ");
stack.push(new int[] {n, i+1});
break;
}
}
if(stack.empty())
System.out.print("0 ");
stack.push(new int[] {n, i+1});
}
}
}
스택 자체를 int형 배열 값을 저장하게 만들었다.
반복문이 중요하다.
가장 앞의 탑이 n보다 작으면, 앞의 값들은 의미가 없으므로 pop 한다.
만약 n보다 크다면 해당 위치를 출력한다.
만약 스택을 전부 비워도 큰 값을 못 찾는다면 -> 스택이 비었다면 0을 출력한다.
그리고 반복문이 끝날 때마다 스택에 탑의 높이와 위치를 저장시킨다.
배운 점
스택과 큐를 기존에 알고 있었지만, 직접 구현은 이번이 처음이었다. 직접 구현해보니 생각보다 신경 써야 할 부분이 많았다.
스택을 이용한 문제들을 풀어봤는데, Stack에 int형 배열을 저장할 수 있다는 사실을 처음 알았다.
또한 스택을 잘 이용해서 문제를 풀면 시간 복잡도에서 큰 이점이 있다는 사실을 알았다.
자료구조에 더욱 익숙해지자
'PS > 알고리즘' 카테고리의 다른 글
자료구조(Graph) (2) | 2022.11.02 |
---|---|
자료구조(Tree) (0) | 2022.10.31 |
이진탐색 (0) | 2022.10.21 |
재귀함수 (1) | 2022.10.18 |
정렬-2 (3) | 2022.10.01 |