자료구조와 알고리즘/자료구조

[자료구조] 스택(Stack)

프린이8549 2024. 10. 2. 19:46

프로그래밍에서 Stack은 두 가지 의미를 가지고 있다:

  1. 프로그램 실행 시 사용되는 메모리 영역
    • 프로그램 실행 중 함수 호출이나 변수 저장을 위해 사용되는 메모리 스택 영역.
  2. 자료구조로서의 스택
    • 후입선출(Last-In, First-Out) 방식으로 데이터를 관리하는 자료구조.

본 글에서는 두 번째 의미인 자료구조로서의 스택에 대해 다루고자 한다.

 

 

1. Stack의 정의

1.1. 스택의 정의

스택은 마치 접시더미와 같다.

 

스택은 배열이나 연결 리스트를 사용해서 구현할 수 있는 선형 자료구조로,

 

삽입과 삭제가 한쪽 끝(top)에서만 이루어진 후입선출(Last-in First-out; LIFO)의 자료구조를 의미한다.

 

즉, 마지막에 삽입된 데이터가 가장 먼저 삭제되는 형태이다.

Do it! 알고리즘 코딩 테스트 - 자바 편

 

 

1.2. 관련 용어

 

위치

  • top : 삽입과 삭제가 일어나는 위치(또는 top pointer)

주요 연산

  • push : 입력 연산. top 위치에 새로운 데이터 삽입
  • pop : 출력 연산. top 위치에 현 데이터를 삭제하고 확인
  • peek : 조회 연산. 탑 포인터가 가리키는 데이터를 조회할 뿐, top의 인덱스를 변화시키지는 않음(단순 확인)
  • empty : 스택이 비어 있는지 확인
  • size : 스택의 크기 확인

 

 

2. Stack의 구현

 

 

스택은 배열 또는 연결 리스트를 이용하여 쉽게 구현할 수 있다.

 

배열을 사용하는 경우 크기가 고정되며, 크기를 초과하는 데이터를 삽입하면 오버플로우가 발생할 수 있다(하단 참고).

 

반면, 연결 리스트를 사용하면 크기 제한이 없지만 메모리 사용량이 더 많아질 수 있다.

 

배열을 이용해서 구현한다면,

  • 스택을 위한 배열을 생성한다.
  • 초기 index(top)값을 0으로 잡는다.
  • push 시 배열에 요소를 삽입하고 index값을 하나 올린다.
  • pop 시 index 값을 하나 없애고 감소된 인덱스에 있는 요소를 반환한다(동시에 배열에서는 삭제).
  • peek 시 (index - 1) 에 위치한 값을 조회한다.
  • empty 여부는 index == 0 을 통해 알 수 있다.

 

위 내용을 자바로 구현한 내용은 아래와 같다.(백준 28278번 문항).

import java.io.*;

public class Main {
    /*
    BOJ NO.28278 : 스택
    */
    static int[] stack;  // 스택을 저장할 배열
    static int top;  // 스택의 현재 위치(크기)

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

        // 명령의 수 N 입력 받기
        int N = Integer.parseInt(br.readLine());
        stack = new int[N];  // 명령의 수에 맞는 스택 크기

        top = 0;  // 스택의 초기 크기 0

        StringBuilder sb = new StringBuilder();

        while (N-- > 0) {
            String[] input = br.readLine().split(" ");
            int num = Integer.parseInt(input[0]);

            switch (num) {
                case 1: // 정수 X를 스택에 넣는다
                    int X = Integer.parseInt(input[1]);
                    push(X);
                    break;
                case 2: // 스택에서 맨 위의 정수를 빼고 출력한다. 없다면 -1 출력
                    sb.append(pop()).append("\n");
                    break;
                case 3: // 스택에 들어있는 정수의 개수를 출력
                    sb.append(size()).append("\n");
                    break;
                case 4: // 스택이 비어있으면 1, 아니면 0을 출력
                    sb.append(empty() ? 1 : 0).append("\n");
                    break;
                case 5: // 스택의 맨 위 정수를 출력. 없다면 -1 출력
                    sb.append(peek()).append("\n");
                    break;
            }
        }

        bw.write(sb.toString());
        bw.flush();
        bw.close();
        br.close();
    }

    // 정수 X를 스택에 넣는 메소드
    public static void push(int item) {
        stack[top++] = item;
    }

    // 스택의 맨 위 정수를 꺼내는 메소드 (꺼낸 후 해당 자리를 0으로 초기화)
    public static int pop() {
        if (empty()) {
            return -1;
        }
        int item = stack[--top];  // idx 감소 후 값을 가져옴
        stack[top] = 0;  // 해당 자리를 0으로 초기화
        return item;
    }

    // 스택의 크기 반환
    public static int size() {
        return top;
    }

    // 스택이 비어있는지 여부 확인
    public static boolean empty() {
        return top == 0;
    }

    // 스택의 맨 위 정수를 반환(꺼내지 않고)
    public static int peek() {
        if (empty()) {
            return -1;
        }
        return stack[top - 1];
    }
}

 

 

3. Stack의 활용

 

스택은 후입선출이 필요한 상황에서 유용하게 활용되고 있다.

 

3.1. 웹 브라우저의 뒤로 가기

웹 브라우저에서 뒤로 가기 버튼을 누를 때, 가장 최근에 방문한 페이지가 먼저 나오는 구조는 스택의 후입선출 원리와 같다.
방문한 페이지들을 스택에 저장하고, 뒤로 가기를 누르면 스택의 top에 있는 페이지를 반환하는 방식이다.

 

3.2 실행 취소(Undo)

텍스트 에디터에서 실행 취소(Undo) 기능은 스택의 전형적인 활용 예시이다. 사용자가 수행한 작업을 스택에 저장해 두고, 실행 취소 시 가장 최근 작업을 되돌리는 방식으로 구현된다.

 

3.3. 재귀 함수의 호출

함수 호출은 스택 프레임(Stack Frame)이라는 매커니즘을 사용한다.

 

함수가 호출될 때 메모리의 스택 영역이 할당되어 함수의 매개변수, 호출이 끝나고 돌아갈 반환 주소값, 함수에서 선언된 지역 변수 등이 저장되는데,

출처 https://www.tcpschool.com/c/c_memory_stackframe

 

이렇게 스택 영역에 차례로 저장되는 함수의 호출 정보를 스택 프레임이라고 한다.

 

스택 영역은 함수의 호출과 함께 할당되며, 함수 호출이 완료되면 소멸된다.

 

스택 프레임 덕분에 우리는 함수가 끝난 후에도 호출한 함수로 복귀할 수 있게 된다.

 

출처 https://www.tcpschool.com/c/c_memory_stackframe

 

3.4. 스택 버퍼 오버플로

 

앞서 스택 프레임에서 본 바와 같이 함수 호출 시 스택이 사용됨을 알 수 있었다.

 

만약 재귀 함수에서 탈출 조건 없이 무한히 반복된다면 스택 영역에서는 어떤 일이 발생할까?

위 그림에서 보듯 호출이 완료되지 않을 시 스택 영역에는 스택 프레임이 계속해서 쌓여만 갈 것이다.

 

이렇게 스택의 모든 공간을 다 차지한 뒤 여유 공간이 없을 때 또 다시 스택 프레임을 저장하게 되면, 해당 데이터는 스택 영역을 넘어가서 다른 메모리 영역을 침범하여 저장되게 된다.

 

이런 현상을 우리는 스택 오버플로우라고 한다.

 

그러나 위와 같은 상황은 프로그램을 오동작하게 하거나 보안상 큰 취약점을 야기하는 것이기 때문에

 

스택 영역의 여유 공간이 없을 경우 보통 해당 프로그램은 에러를 발생하고 강제 종료시킨다.

 

cf) 버퍼 오버플로우

사용자가 버퍼를 초과하는 값을 입력 시 버퍼 이후의 공간에 값을 덮어씀에 따라 해킹 등 공격이 가능해진다.

 

만약 특정 함수가 있고 해당 함수의 주솟값을 알고 어떤 입력이 버퍼 오버플로 공격이 가능하다면 입력값에 버퍼를 넘어서는 값과 주솟값을 입력한다면 입력 함수가 끝나고 자신이 원하는 함수로 접근할 수 있게 된다.

 

3.4. 코딩 테스트 시 주요 사례

 

스택은 코딩 테스트에서도 자주 등장하는 자료구조이다.

 

대표적으로 역순 문자열 만들기, 후위 표기법 계산, 재귀 함수 관련 문제에서 주로 사용된다.

 

또한, 깊이 우선 탐색(DFS)이나 백트래킹 문제에서도 스택을 활용하면 효과적으로 문제를 해결할 수 있다.

 

 

4. 참고자료

 

1. <Do it! 알고리즘 코딩 테스트 자바 편>, 2022,  김종관, 이지스퍼블리싱.

 

2.  https://www.tcpschool.com/c/c_memory_stackframe

 

3. https://namu.wiki/w/%EC%8A%A4%ED%83%9D(%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0)

 

4. https://ko.wikipedia.org/wiki/%EC%8A%A4%ED%83%9D