컴퓨터 구조 운영체제 네트워크 자료구조 알고리즘 데이터베이스 내 학습
ds · 02

스택 / 큐

자료구조스택큐

스택(Stack)과 큐(Queue)는 데이터 삽입·삭제 순서를 제한하는 선형 자료구조입니다. 스택은 마지막에 넣은 데이터를 먼저 꺼내고(LIFO), 큐는 먼저 넣은 데이터를 먼저 꺼냅니다(FIFO).

핵심 개념

스택 — LIFO

Last In, First Out. 책을 쌓아 올린 구조를 떠올리면 됩니다. 가장 위에 올린 책을 먼저 꺼냅니다. Push로 쌓고 Pop으로 꺼내며, 항상 top 위치에서만 연산이 일어납니다. 삽입·삭제 모두 O(1)입니다.

핵심 개념

큐 — FIFO

First In, First Out. 줄 서기와 같습니다. 먼저 줄 선 사람이 먼저 나갑니다. Enqueue로 rear에 추가하고 Dequeue로 front에서 꺼냅니다. 삽입·삭제 모두 O(1)이며, front와 rear 두 포인터를 관리합니다.

핵심 개념

스택의 활용

함수 호출 스택(Call Stack), 브라우저 뒤로가기, 괄호 유효성 검사, DFS 탐색, 수식 계산(후위 표기법), 실행 취소(Undo) 기능이 스택 기반입니다. JVM의 스택 프레임도 스택 구조로 동작합니다.

핵심 개념

큐의 활용

OS 프로세스 스케줄링, 프린터 대기열, BFS 탐색, 메시지 큐(RabbitMQ 같은 메시징 시스템), 이벤트 루프(JavaScript), 네트워크 패킷 버퍼가 큐 기반입니다. 순서 보장이 필요한 모든 작업 대기열에 활용됩니다.

핵심 개념

원형 큐 (Circular Queue)

일반 배열 큐는 Dequeue 후 앞 공간이 낭비됩니다. 원형 큐는 인덱스를 (index + 1) % capacity로 순환시켜 공간을 재활용합니다. 한 칸을 비워 두는 방식에서는 front == rear이면 빈 큐, (rear + 1) % capacity == front이면 가득 찬 상태입니다.

핵심 개념

덱 (Deque)

Double-Ended Queue. 양쪽 끝에서 삽입·삭제가 모두 가능합니다. 스택과 큐를 동시에 구현할 수 있습니다. Java의 ArrayDeque가 대표적이며, Java 문서는 ArrayDeque가 스택으로 쓸 때 Stack보다, 큐로 쓸 때 LinkedList보다 빠를 가능성이 높다고 설명합니다.

Stack Overflow / Underflow — 스택이 가득 찬 상태에서 Push하면 Stack Overflow, 빈 상태에서 Pop하면 Underflow가 발생합니다. Java 문서는 Stack 대신 Deque 구현을 쓰라고 안내합니다.
Java에서의 선택 — Stack 클래스는 Vector를 상속하고 Vector의 메서드는 동기화되어 있습니다. 단일 스레드 환경에서는 ArrayDeque를 Deque 인터페이스로 사용하세요. 큐로 쓸 때도 ArrayDeque가 LinkedList보다 빠를 가능성이 높습니다.
우선순위 큐 (PriorityQueue) — 일반 큐와 달리 삽입 순서가 아닌 우선순위에 따라 꺼냅니다. 내부적으로 힙(Heap) 자료구조로 구현됩니다. Dequeue 시 O(log n), 최솟값(또는 최댓값) 조회는 O(1)입니다. 다익스트라, 프림 알고리즘에 사용됩니다.
모노토닉 스택 (Monotonic Stack) — 스택 내부 원소가 단조 증가 또는 단조 감소를 유지하는 스택입니다. 다음 큰 원소 찾기, 히스토그램 최대 직사각형 등 O(n) 풀이에 활용됩니다. 코딩 테스트에서 자주 등장하는 패턴입니다.

스택과 큐가 쓰이는 곳

스택과 큐는 데이터를 넣고 꺼내는 순서를 제한한 자료구조입니다. 이 제한 덕분에 연산이 단순해지고, 함수 호출, 작업 대기열, 그래프 탐색처럼 순서가 곧 의미인 문제에 바로 맞습니다.

숫자로 확인하기

원형 큐의 사용 가능 칸 — 한 칸을 비워 두는 방식(front == rear이면 빈 큐, (rear + 1) % capacity == front이면 가득 참)에서는 용량 8짜리 배열에 7개까지만 담깁니다. 가득 찬 것과 빈 것을 구별하려고 한 칸을 쓰지 않기 때문입니다.
괄호 검사 예 — 문자열 {[()]}를 왼쪽부터 읽으면 스택 깊이가 1 → 2 → 3으로 올라갔다가 닫는 괄호마다 하나씩 줄어 0이 되어 끝납니다. 끝날 때 스택이 비어 있으면 올바른 괄호입니다.
모노토닉 스택의 비용 — 각 원소가 한 번 들어가고 최대 한 번 나오므로 전체 시간이 O(n)입니다. 이중 반복문이면 O(n²)이어서 n = 100,000일 때 약 50억 번 대 10만 번 수준의 차이입니다.

자주 하는 오해

Java에서는 Stack 클래스를 쓰는 게 기본이다 — 문서가 Stack 대신 Deque 구현을 쓰라고 안내합니다. Stack은 Vector를 상속해 오래된 설계의 흔적이 남아 있습니다.
큐는 LinkedList로 만드는 게 정석이다 — Java 문서는 ArrayDeque가 큐로 쓸 때 LinkedList보다 빠를 가능성이 높다고 설명합니다.
원형 큐는 항상 한 칸을 비운다 — 비우는 방식은 한 가지 구현 방법입니다. 원소 개수를 따로 세는 변수를 두면 모든 칸을 쓸 수 있습니다.
우선순위 큐도 FIFO다 — 우선순위 큐는 넣은 순서가 아니라 우선순위가 높은 것부터 꺼냅니다. 같은 우선순위끼리의 순서는 구현이 보장하지 않을 수 있습니다.

면접에서 이렇게 물어요

면접 질문

스택 두 개로 큐를 만들려면 어떻게 하나요?

한 스택에 넣고, 꺼낼 때 두 번째 스택이 비어 있으면 첫 스택의 원소를 모두 옮긴 뒤 두 번째에서 꺼냅니다. 각 원소는 첫 스택에 한 번 넣어지고, 옮겨질 때 한 번 꺼내져 두 번째 스택에 넣어지며, 두 번째에서 한 번 꺼내집니다. n번 연산의 총비용이 O(n)이라 연산당 분할 상환 O(1)입니다.

면접 질문

괄호 유효성 검사를 어떻게 하나요?

여는 괄호는 스택에 넣고, 닫는 괄호를 만나면 꼭대기와 짝이 맞는지 확인하며 꺼냅니다. 끝에 스택이 비어 있어야 올바른 문자열입니다.

면접 질문

스택 오버플로는 언제 발생하나요?

스택 용량이 찼는데 push할 때입니다. 함수 호출 스택에서는 재귀가 너무 깊어질 때 발생하고, Java는 StackOverflowError를 던집니다.

면접 질문

우선순위 큐는 어떻게 구현하나요?

보통 힙으로 구현해 삽입과 최솟값 꺼내기가 O(log n)이고 최솟값 조회는 O(1)입니다. Java의 PriorityQueue도 힙 기반입니다.

실제 시스템에서는

Java Deque — Deque 문서는 LIFO 스택 연산을 더 완전하고 일관되게 제공한다며 Stack 클래스보다 Deque를 쓰라고 안내합니다. ArrayDeque는 스택으로 쓸 때 Stack보다, 큐로 쓸 때 LinkedList보다 빠를 가능성이 높다고 설명합니다.
함수 호출 스택 — 호출될 때마다 스택 프레임이 쌓이고 반환하면 사라집니다. 너무 깊은 재귀는 StackOverflowError를 일으킵니다.
메시징 시스템 — Kafka나 RabbitMQ 같은 시스템은 메시지를 큐처럼 순서대로 전달하는 구조를 제공합니다. 다만 각 시스템의 보장 범위(파티션 단위 순서 등)는 다르니 문서를 확인해야 합니다.

함께 보면 좋은 토픽

배열 vs 연결 리스트 — 스택과 큐의 바탕이 되는 구현 방식을 봅니다.
BFS / DFS — 큐와 스택이 탐색 순서를 가르는 모습을 봅니다.
힙 — 우선순위 큐의 구현을 봅니다.
CPU 스케줄링 — 운영체제가 큐로 프로세스를 다루는 방식을 봅니다.
시스템 콜 — 호출 시 스택이 어떻게 쓰이는지 봅니다.
모노토닉 스택 — 스택 안의 값을 한 방향 순서로 유지하며 쓰는 방법을 봅니다.
원형 버퍼 — 배열로 큐를 만들 때 원소를 당기지 않고 앞뒤 칸을 돌려 쓰는 방법을 봅니다.

참고 자료

Java Deque — Deque — Java SE 21 API
Java ArrayDeque — ArrayDeque — Java SE 21 API
Java Stack — Stack — Java SE 21 API
Java PriorityQueue — PriorityQueue — Java SE 21 API
교재 — Cormen, Leiserson, Rivest, Stein, "Introduction to Algorithms" 4판, "Elementary Data Structures" 장(스택과 큐)

Java — 스택 직접 구현 vs Deque 활용

/* ── 배열 기반 스택 직접 구현 ───────────────────────────── */
public class ArrayStack<T> {
    private Object[] data;
    private int top = -1;

    public ArrayStack(int capacity) {
        data = new Object[capacity];
    }

    public void push(T item) {
        if (top == data.length - 1) throw new RuntimeException("Stack Overflow");
        data[++top] = item; // top 먼저 올리고 저장
    }

    @SuppressWarnings("unchecked")
    public T pop() {
        if (top == -1) throw new RuntimeException("Stack Underflow");
        T item = (T) data[top];
        data[top--] = null; // GC 참조 해제 후 top 낮춤
        return item;
    }

    @SuppressWarnings("unchecked")
    public T peek() {
        if (top == -1) throw new RuntimeException("Empty Stack");
        return (T) data[top]; // top만 반환, 제거 안 함
    }

    public boolean isEmpty() { return top == -1; }
    public int size()       { return top + 1; }
}

/* ── Java 표준 — ArrayDeque를 Stack으로 사용 (권장) ─────── */
Deque<Integer> stack = new ArrayDeque<>();
stack.push(10);       // = addFirst(), 앞에 추가
stack.push(20);
stack.pop();          // = removeFirst(), 20 반환 — LIFO
stack.peek();         // = peekFirst(), 제거 없이 조회

// Stack 클래스보다 ArrayDeque를 권장하는 이유:
// Stack은 Vector 상속 → 모든 메서드 synchronized → 불필요한 동기화 비용
// ArrayDeque는 동기화 없음 → 단일 스레드에서 훨씬 빠름

/* ── 활용 예: 괄호 유효성 검사 ─────────────────────────── */
public boolean isValid(String s) {
    Deque<Character> stack = new ArrayDeque<>();
    for (char c : s.toCharArray()) {
        if (c == '(' || c == '{' || c == '[') {
            stack.push(c);
        } else {
            if (stack.isEmpty()) return false;
            char top = stack.pop();
            if (c == ')' && top != '(') return false;
            if (c == '}' && top != '{') return false;
            if (c == ']' && top != '[') return false;
        }
    }
    return stack.isEmpty();
}

스택은 push 시 top을 먼저 올리고 저장하며, pop 시 값을 꺼낸 뒤 top을 낮춥니다.
pop 후 배열 참조를 null로 지워야 GC가 메모리를 회수할 수 있습니다.
Java의 Stack 클래스는 Vector를 상속해 synchronized 오버헤드가 있으므로, 단일 스레드에서는 ArrayDeque를 Deque 인터페이스로 사용하는 것이 표준 권장 방법입니다.
괄호 유효성 검사는 스택의 대표적인 활용 예로, 여는 괄호는 push하고 닫는 괄호가 오면 pop해서 쌍을 맞춥니다.

Java — 큐 직접 구현 vs ArrayDeque 활용

/* ── 원형 배열 큐 직접 구현 ─────────────────────────────── */
public class CircularQueue<T> {
    private Object[] data;
    private int front = 0, rear = 0, size = 0;
    private final int capacity;

    public CircularQueue(int capacity) {
        this.capacity = capacity;
        data = new Object[capacity];
    }

    public void enqueue(T item) {
        if (size == capacity) throw new RuntimeException("Queue Full");
        data[rear] = item;
        rear = (rear + 1) % capacity; // 원형 인덱스 — 끝에 도달하면 0으로
        size++;
    }

    @SuppressWarnings("unchecked")
    public T dequeue() {
        if (size == 0) throw new RuntimeException("Queue Empty");
        T item = (T) data[front];
        data[front] = null;
        front = (front + 1) % capacity; // front도 원형으로 이동
        size--;
        return item;
    }

    public boolean isEmpty() { return size == 0; }
    public boolean isFull()  { return size == capacity; }
}

/* ── Java 표준 — ArrayDeque를 Queue로 사용 (권장) ──────── */
Queue<Integer> queue = new ArrayDeque<>();
queue.offer(10);    // enqueue — 실패 시 false 반환 (add는 예외)
queue.offer(20);
queue.poll();     // dequeue — 10 반환, 비어있으면 null (remove는 예외)
queue.peek();     // front 조회 — 제거 없음, 비어있으면 null

/* ── 활용 예: BFS (너비 우선 탐색) ──────────────────────── */
public void bfs(int start, List<List<Integer>> graph) {
    Queue<Integer> queue = new ArrayDeque<>();
    boolean[] visited = new boolean[graph.size()];
    queue.offer(start);
    visited[start] = true;

    while (!queue.isEmpty()) {
        int node = queue.poll(); // FIFO — 가장 먼저 넣은 노드부터 처리
        System.out.print(node + " ");
        for (int next : graph.get(node)) {
            if (!visited[next]) {
                visited[next] = true;
                queue.offer(next); // 인접 노드를 rear에 추가
            }
        }
    }
}

원형 큐에서 인덱스 이동은 (index + 1) % capacity로 계산해 배열 끝에서 0으로 순환합니다.
front와 rear가 같으면 빈 큐이고, size로 가득 찼는지 별도 추적합니다.
Java 표준에서 큐는 offer/poll/peek를 사용합니다. add/remove는 실패 시 예외를 던지므로 용량 제한이 있는 큐가 아니면 offer/poll이 안전합니다.
BFS는 큐의 대표적인 활용입니다. 시작 노드를 큐에 넣고, 꺼낸 노드의 인접 노드를 순서대로 큐에 넣어 레벨별(거리별) 탐색을 보장합니다.

Q1

스택(Stack)의 동작 원리는?

마지막에 넣은 것을 가장 먼저 꺼냅니다.
스택은 LIFO(Last In First Out) 구조로, 가장 나중에 push된 데이터가 가장 먼저 pop됩니다.

Q2

큐(Queue)의 동작 원리는?

줄을 서는 것처럼 먼저 온 사람이 먼저 나갑니다.
큐는 FIFO(First In First Out) 구조로, 가장 먼저 enqueue된 데이터가 가장 먼저 dequeue됩니다.

Q3

함수 호출 스택(call stack)이 '스택' 구조를 쓰는 이유는?

A가 B를 호출하고 B가 C를 호출하면, C가 가장 먼저 끝나야 합니다.
함수 호출은 가장 최근에 호출된 함수가 가장 먼저 실행을 마치고 반환되는 LIFO 순서를 따르므로 스택 구조가 자연스럽게 맞습니다.