Idealim
article thumbnail
Published 2021. 9. 30. 13:06
[자료구조] 스택 CS/Algorithm

/* 본 게시물은 ' 뇌를 자극하는 알고리즘 | with 박상현 ' 의 내용과 참고자료를 토대로 작성되었습니다. */

/* 본 글은 개인적으로 공부한 내용을 정리한 글이므로 오류가 있을 수 있습니다. */

참고 자료

[백준] 10028번 - 스택 : https://www.acmicpc.net/problem/10828


1. 스택이란?

'스택'의 사전적 의미는 건초나 짚더미와 같은 뭔가를 쌓아놓은 더미를 뜻한다. 자료구조의 스택 또한 데이터를 아래에서부터 위로 쌓아 얹어 올리도록 하는 자료구조이다. 스택은 중간에 데이터를 삽입하거나 삭제하는 것을 허용하지 않는다. 데이터의 입/출력은 오로지 스택의 꼭대기에서만 이루어진다. 즉, 스택은 '선입후출' (First In - Last out) 의 특징을 가진다. 

[출처] : https://velog.io/@sbinha/%EC%8A%A4%ED%83%9D-%ED%81%90


스택 주요 기능

스택의 주요 기능은 '삽입(push)' 과 '제거(pop)' 두 가지이다. 삽입 연산은 스택 위에 새로운 노드를 '쌓는' 작업이고, 제거 연산은 제일 위에 있는 스택을 '제거' 하는 작업이다.


2. 스택 구현 (백준 10828번)

백준 10828번 - 스택에서 주어진 문제를 풀어보자. 문제는 다음과 같다.


문제

정수를 저장하는 스택을 구현한 다음, 입력으로 주어지는 명령을 처리하는 프로그램을 작성하시오.

명령은 총 다섯 가지이다.

  • push X: 정수 X를 스택에 넣는 연산이다.
  • pop: 스택에서 가장 위에 있는 정수를 빼고, 그 수를 출력한다. 만약 스택에 들어있는 정수가 없는 경우에는 -1을 출력한다.
  • size: 스택에 들어있는 정수의 개수를 출력한다.
  • empty: 스택이 비어있으면 1, 아니면 0을 출력한다.
  • top: 스택의 가장 위에 있는 정수를 출력한다. 만약 스택에 들어있는 정수가 없는 경우에는 -1을 출력한다.
 
입력

첫째 줄에 주어지는 명령의 수 N (1 ≤ N ≤ 10,000)이 주어진다. 둘째 줄부터 N개의 줄에는 명령이 하나씩 주어진다. 주어지는 정수는 1보다 크거나 같고, 100,000보다 작거나 같다. 문제에 나와있지 않은 명령이 주어지는 경우는 없다.


배열로 스택 구현

먼저 배열로 스택과 주요 기능인 push, pop 을 구현해보자. 배열로 구현할 경우 동적으로 스택의 용량을 조절하기 어렵다는 단점이 있지만, 구현은 간단하다는 장점이 있다. 배열을 이용한 스택 구현은 다음과 같다.

class Stack {
    var capacity = 10
    var size = 0
    var arr = IntArray(capacity)

    fun push(element: Int){
        if (size < capacity){
            arr[size] = element
            size++
        }
        else {
            // 데이터 복사
            val tmpArr = IntArray(capacity + 10)
            for (i in arr.indices){
                tmpArr[i] = arr[i]
            }
            arr = tmpArr

            capacity += 10
            arr[size] = element
            size++
        }
    }

    fun pop(){
        println(arr[size - 1])
        arr[size - 1] = 0
        size--
    }
}

배열은 정적이기 때문에 데이터를 추가하려면 새로운 배열을 만들어 데이터를 옮겨야한다. 하나 추가할 때 마다 새로운 배열을 만드는 것은 비용이 많이 들기 때문에 배열에 여유 공간(capacity)를 준다. 여기서는 기본 capacity로 10으로 설정하고 배열의 크기를 넘길 시 10개의 공간을 추가한다. (사실 ArrayList 를 사용하는 것이랑 거의 비슷하다.)

나머지 문제 조건을 충족시키기 위한 코드를 추가한다.

fun main() = with(BufferedReader(InputStreamReader(System.`in`))){
    val stack = Stack()

    val testTime = readLine()!!.toInt()

    for (i in 1..testTime){
        val st = StringTokenizer(this.readLine())
        val order = st.nextToken()
        when (order){
            "push" -> {
                stack.push(st.nextToken().toInt())
            }
            "top" -> {
                if (stack.size == 0) println(-1) else println(stack.arr[stack.size - 1])
            }
            "empty" -> {
                if (stack.size == 0) println(1) else println(0)
            }
            "pop" ->{
                if(stack.size == 0) println(-1) else stack.pop()
            }
            "size" -> println(stack.size)
        }
    }
}

실행 결과 (416ms / 17636 KB)

 위 코드를 그대로 실행하면 시간초과 (0.5초) 가 뜬다. 문제에서는 명령어를 1~10000 사이로 주는데 만약 push 가 10000번 일 경우는 배열의 용량을 초과하는 경우가 많기 때문에 새로운 배열을 만드는 과정이 많을 것이다. 그래서 처음 capacity를 10 -> 10000으로 수정할 시 시간 초과가 안뜬다.

var capacity = 10000

이를 통해 배열로 만든 스택의 단점을 확인할 수 있다.


링크드 리스트로 스택 구현

리스트는 동적이기 때문에 배열로 스택을 만들 때 처럼 여유 공간을 주지 않아도 되는 장점이 있다. 저번에 구현한 LinkedList의 addLast / removeLast 를 활용하여 스택을 구현해보자.
class StackByList(){
    val list = LinkedList<Int>()
    class LinkedList<T> {
        data class Node<T>(
            var data: T?,
            var next: Node<T>?
        )

        private var headNode: Node<T>? = null
        var lastNode: Node<T>? = null

        var currentSize: Int = 0

        fun addLast(data: T){
            val newNode = Node(data, null)
            //headNode 가 비어있는 경우 즉, 요소가 없는 경우
            if (headNode == null){
                headNode = newNode
                lastNode = newNode
                currentSize++
                return
            }
            lastNode?.next = newNode
            lastNode = newNode
            currentSize++
        }
        private fun removeFirst(){
            // Empty List
            if (headNode == null){
                return
            }
            // headNode == lastNode -> 요소 1개
            if (headNode == lastNode){ // or headNode.next == null or currentSize == 1
                println(lastNode?.data)
                lastNode = null
                headNode = null
            }
            else {
                headNode = headNode?.next
            }
            currentSize--
        }

        fun removeLast(){
            //Empty List
            if (headNode == null){
                return
            }
            // 요소 1개
            if (headNode == lastNode){
                return removeFirst()
            }
            //임시 포인터
            var currentNode = headNode
            var prevNode: Node<T>? = null
            //lastNode 이전 노드 즉 마지막에서 두번째 node
            while (currentNode != lastNode){
                prevNode = currentNode
                currentNode = currentNode?.next
            }
            // currentNode == lastNode
            println(currentNode?.data)
            prevNode?.next = null
            lastNode = prevNode
            currentSize--
        }
    }
}

사실 코틀린의 linkedList를 이용하면 되지만 복습하는 차원에서 다시 구현해봤다. 

 

실행 결과 (444ms, 23472KB)

결과만 놓고 보면 배열의 성능이 우수하다고 생각할 수 있다. 하지만 배열은 Array 크기를 이미 10000(문제에서 주어진 최대 값)으로 설정해놓고 테스트를 진행했기 때문에 만약 문제에서 준 데이터 개수를 몰랐더라면 링크드리스트로 만든 Stack이 더 좋은 성능을 보였을 것이다.

 

 

 

스택 구현 끝. 

다음 해볼 것은 큐!

 

 

반응형
profile

Idealim

@Idealim

읽어주셔서 감사합니다. 잘못된 내용이 있으면 언제든 댓글로 피드백 부탁드립니다.