Idealim
article thumbnail
Published 2021. 10. 1. 10:11
[자료구조] 큐 CS/Algorithm

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

참고 자료

[백준] 18258번 - 큐2 : https://www.acmicpc.net/problem/18258


1. 큐(Queue)란?

큐는 스택과는 반대로 입력과 출력 창구가 따로 존재하고, 제일 먼저 들어간 데이터가 제일 먼저 나오는 자료구조(First In First Out / 선입 선출)이다. 이는 우리가 먼저 온 차례대로 줄을 세우는 것과 같다.

큐는 작업을 처리하는 요소에 부하를 주지 않으면서도 처리 능력을 넘어서는 작업들을 놓치지 않고 수용할 수 있게 한다. 이러한 특성으로 큐는 '완충장치(대기열)'로 사용되는 경우가 많다. 

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


큐의 기능

스택과 마찬가지로 큐의 주요 기능도 삽입(Enqueue), 제거(Dequeue)이다. 둘의 차이점은 스택의 제거와는 다르게 큐는 맨 앞(처음)에 있는 데이터를 삭제한다. 스택에서는 삽입(push) / 제거(pop) 이라고 칭하고, 큐에서는 삽입(dequeue) / 제거(enqueue)라 한다.


2. 큐(Queue) 구현 (백준 18258번)

이번에도 백준 18258번 - 큐2 문제를 풀어보면서 큐를 구현해보겠다. 문제는 다음과 같다. (스택 문제와 유사)

문제

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

명령은 총 여섯 가지이다.

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

 

입력

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


순환 큐 구현하기 (배열을 이용한 큐)

기본적으로 배열의 앞 부분에 데이터를 추가하는 작업은 모든 데이터를 한 칸씩 밀어내고 빈 앞 자리에 데이터를 추가하는 방식이다. 이는 O(N)의 시간복잡도를 가진다. 

이를 해결하기 위해 '순환 배열'을 만들어보자. 링크드 리스트를 headNode 와 lastNode 를 이용해 만든 것과 비슷한 원리이다. 첫 번째 데이터와 마지막 데이터에 바로 접근 할 수 있으므로 O(1)의 시간복잡도를 가진다. 

예시인 크기가 10인 배열을 통해 이해해보자.

  1. 데이터는 배열의 중간인 5에서부터 넣는다. 만약 데이터의 수가 배열의 크기를 초과할 경우 배열 맨 앞자리에 추가한다. 예를 들어 데이터 6개를 추가하는 경우 마지막 데이터는 index가 0인 자리에 추가한다.
  2. 전단(head)을 배열의 중간으로 설정한다.  
  3. 후단은 데이터가 들어있는 index + 1 (다음 데이터가 들어갈 위치)로 설정한다.
  4. 배열이 비어있는 경우 / 가득 차 있는 경우를 구분해보자. 두 경우 모두 전단 과 후단이 같다. 그래서 배열이 가득 차 있는 경우를 구분할 때는 후단이 전단 보다 1작은 값을 갖게 될 때로 정한다. (데이터 넣을 공간이 1자리 남았을 때) 이 때는 새 배열을 만들어 데이터를 옮겨주어야 한다.

위 과정을 코드로 구현해보자.

먼저, queue 클래스와 enqueue() 를 구현하면 다음과 같다.

class Queue(var capacity: Int){
    var size = 0

    var arr = IntArray(capacity)

    var front = capacity / 2
    var last = front

    fun enqueue(element: Int){

        //용량이 0 ,1 일 때
        when (capacity){
            0, 1 -> {
                newArrayFromArr()
                arr[last] = element
                last++
                size++
                return
            }
        }

        // 꽉찼는지 검사
        if(last - front == -1) { // 배열이 꽉 찬 경우 (데이터를 넣을 공간이 1개 남은 경우)
            newArrayFromArr()
            arr[last] = element
            last++
            size++
            return
        }

        // emptyArray
        if (last == front) {
            arr[last] = element
            last++
            size++
            return
        }

        // 배열에 여유공간 있을 때
        if (last < capacity){ // 배열 마지막 index 초과 x
            arr[last] = element
            last++
            size++
        }
        else { // 배열 마지막 index 초과
            last = 0
            arr[last] = element
            last++
            size++
        }
    }

    private fun newArrayFromArr(){
        // 데이터 옮기기
        capacity += 10
        val tmpArr = IntArray(capacity)
        front = capacity / 2
        last = front
        for (element in arr){
            if (last < capacity){
                if (element != 0){
                    tmpArr[last] = element
                    last++
                }
            }
            else {
                last = 0
                tmpArr[last] = element
                last++
            }
        }
        arr = tmpArr
    }
 }

경계조건을 처리하느라 코드가 길어졌다.

배열 용량을 초과 시 자동으로 용량을 추가한다. (default 값 10) 

 
이제 dequeue() 를 구현해보자.
    fun dequeue(): Int{
        var returnValue: Int
        if(front < capacity){
            returnValue = arr[front]
            arr[front] = 0
            front++
            size--
        }
        else {
            front = 0
            returnValue = arr[front]
            arr[front] = 0
            front++
            size--
        }
        return returnValue
    }

문제에서 뺀 값을 요구하므로 returnValue 라는 변수에 담아서 반환한다.

이제 문제 조건에 맞게 main() 함수를 짜고 실행해보자.

fun main() {
    val br = BufferedReader(InputStreamReader(System.`in`))
    val bw = BufferedWriter(OutputStreamWriter(System.out))

    val testTime = br.readLine()!!.toInt()
    val queue = Queue(testTime)
    repeat(testTime){
        val st = StringTokenizer(br.readLine())
        val order = st.nextToken()
        when (order){
            "push" -> {
                queue.enqueue(st.nextToken().toInt())
            }
            "pop" ->{
                if(queue.size == 0) bw.write("-1\n") else bw.write("${queue.dequeue()}\n")
            }
            "front" -> {
                if(queue.size == 0) bw.write("-1\n") else bw.write("${queue.arr[queue.front]}\n")
            }
            "back" -> {
                if (queue.size == 0) bw.write("-1\n") else {
                    if (queue.last != 0) bw.write("${queue.arr[queue.last - 1]}\n") else bw.write("${queue.arr.last()}\n")
                }
            }
            "empty" -> {
                if(queue.size == 0) bw.write("1\n") else bw.write("0\n")
            }
            "size" -> bw.write("${queue.size}\n")
        }
    }
    bw.flush()
    bw.close()
}

실행 결과 (1656ms / 337336KB)

공부하는 차원에서 배열을 이용해 직접 queue를 만들었지만 코틀린에서는 ArrayDeque<T> (덱)를 지원한다. 이를 이용하면 간단히 작성가능하다.

import java.lang.StringBuilder
import java.util.*
fun main() {
    var number = readLine()!!.toInt()
    var sb = StringBuilder()
    var q = ArrayDeque<Int>()

    repeat(number) {
        var str = StringTokenizer(readLine()!!, " ")

        when(str.nextToken()) {
            "push" -> q.offer(str.nextToken().toInt())
            "pop" -> sb.append("${q.poll() ?: -1}\n")
            "size" -> sb.append("${q.size}\n")
            "empty" -> sb.append("${if(q.isEmpty()) 1 else 0}\n")
            "front" -> sb.append("${q.peek() ?: -1}\n")
            "back" -> sb.append("${q.peekLast() ?: -1}\n")
        }
    }
    print(sb)
}

링크드 리스트 큐 구현하기

이번에는 링크드 리스트의 addLast() 기능과 removeFirst() 를 이용하여 큐를 구현해보자.

class QueueByLinkedList {
    val list = LinkedList<Int>()
    class LinkedList<T> {
        data class Node<T>(
            var data: T?,
            var next: Node<T>?
        )

        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++
        }

        fun removeFirst() : T?{
            var returnValue: T?
            // Empty List
            if (headNode == null) {
                return null
            }
            // headNode == lastNode -> 요소 1개
            if (headNode == lastNode) { // or headNode.next == null or currentSize == 1
                returnValue = headNode?.data
                lastNode = null
                headNode = null
            } else {
                returnValue = headNode?.data
                headNode = headNode?.next
            }
            currentSize--
            return returnValue
        }
    }
}

실행 결과(2664ms / 354128KB)

capacity를 고려하지 않아도 돼서 배열을 이용한 것보다 구현이 간단하다. 


정리

큐(queue)

  • 자료의 입력과 출력을 한 쪽 끝으로 제한한 자료구조이다. (FIFO(First In First Out)구조)
  • 컴퓨터 버퍼에서 주로 사용, 마구 입력이 되었으나 처리를 하지 못할 때, 버퍼(큐)를 만들어 대기 시킨다.
  • 일반적인 큐의 단점 : last(rear)가 배열의 끝에 도달했을 경우. 큐에 빈 메모리가 남아 있어도 꽉 차있는것으로 판단할 수 있음 -> 개선된 순환(원형) 큐 사용
  • 순환 큐의 단점 : 메모리 공간은 잘 활용하나 배열로 구현되어 있기 때문에 큐의 크기가 제한되는 단점이 존재한다. ->링크드리스트로 큐 사용
  • 링크드 리스트로 구현한 큐는 큐의 크기가 제한이 없고 삽입, 삭제가 편리하다.

 

 

 

스택 / 큐 구현완료

이제 트리 가보자!

 

 

반응형
profile

Idealim

@Idealim

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