자료구조Medium#28
스택(Stack)과 큐(Queue)의 차이점과 실제 활용 사례를 설명해주세요.
#자료구조#스택#큐
힌트
LIFO vs FIFO 원칙과 실제 시스템에서의 활용을 생각해보세요.
정답 및 해설
스택(Stack)과 큐(Queue)의 차이점과 실제 활용 사례를 설명해주세요.
스택과 큐는 데이터 삽입·삭제의 순서가 다른 두 가지 선형 자료구조입니다. 스택은 LIFO(Last In First Out), 큐는 FIFO(First In First Out) 원칙을 따르며, 이 차이가 각각의 적합한 활용 사례를 결정합니다. 두 자료구조 모두 배열이나 연결 리스트로 구현할 수 있으며, 다양한 알고리즘과 시스템 설계의 핵심 구성 요소입니다.
스택 (Stack)
LIFO 원리
스택은 나중에 들어온 것이 먼저 나가는(Last In First Out) 자료구조입니다. 책을 쌓는 것처럼, 가장 위에 있는 항목만 꺼낼 수 있습니다.
push(1) → [1]
push(2) → [1, 2]
push(3) → [1, 2, 3]
pop() → 3 반환, [1, 2]
pop() → 2 반환, [1]
peek() → 1 (제거 없이 확인)
핵심 연산
| 연산 | 설명 | 시간 복잡도 |
|---|---|---|
| push(item) | 스택 맨 위에 추가 | O(1) |
| pop() | 맨 위 항목 제거 및 반환 | O(1) |
| peek() / top() | 맨 위 항목 확인 (제거 안 함) | O(1) |
| isEmpty() | 스택이 비어있는지 확인 | O(1) |
| size() | 스택의 크기 반환 | O(1) |
스택 구현
class Stack:
def __init__(self):
self._items = []
def push(self, item):
self._items.append(item)
def pop(self):
if self.is_empty():
raise IndexError("Stack is empty")
return self._items.pop()
def peek(self):
if self.is_empty():
raise IndexError("Stack is empty")
return self._items[-1]
def is_empty(self):
return len(self._items) == 0
def size(self):
return len(self._items)
// Java에서 스택 사용
import java.util.Deque;
import java.util.ArrayDeque;
// Java 공식 문서에서는 Stack 클래스 대신 Deque 사용 권장
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1); // 맨 위에 추가
stack.push(2);
stack.push(3);
stack.pop(); // 3 반환
stack.peek(); // 2 확인 (제거 안 함)
스택의 실제 활용 사례
1. 함수 호출 스택 (Call Stack)
프로그램의 함수 호출과 반환은 스택 구조로 관리됩니다.
def c():
print("c 실행")
# 호출 스택: [main, a, b, c]
return
def b():
c()
# 호출 스택: [main, a, b]
return
def a():
b()
# 호출 스택: [main, a]
return
# 호출 스택: [main]
a()
# a 반환 후: [main]
재귀 함수가 너무 깊어지면 Stack Overflow 발생합니다.
2. 브라우저 뒤로가기/앞으로가기
class Browser:
def __init__(self):
self.back_stack = [] # 방문 기록
self.forward_stack = [] # 앞으로 가기 기록
self.current = None
def visit(self, url):
if self.current:
self.back_stack.append(self.current)
self.current = url
self.forward_stack.clear() # 새 페이지 방문 시 앞으로가기 초기화
def back(self):
if self.back_stack:
self.forward_stack.append(self.current)
self.current = self.back_stack.pop()
def forward(self):
if self.forward_stack:
self.back_stack.append(self.current)
self.current = self.forward_stack.pop()
browser = Browser()
browser.visit("google.com")
browser.visit("github.com")
browser.visit("stackoverflow.com")
browser.back() # github.com으로
browser.back() # google.com으로
browser.forward() # github.com으로
3. 괄호 유효성 검사
def is_valid_brackets(s):
stack = []
pairs = {')': '(', '}': '{', ']': '['}
for char in s:
if char in '({[':
stack.append(char)
elif char in ')}]':
if not stack or stack[-1] != pairs[char]:
return False
stack.pop()
return len(stack) == 0
print(is_valid_brackets("({[]})")) # True
print(is_valid_brackets("({[})")) # False
print(is_valid_brackets("((())")) # False
4. DFS (깊이 우선 탐색)
def dfs_iterative(graph, start):
visited = set()
stack = [start]
while stack:
node = stack.pop() # 스택에서 꺼냄 (LIFO)
if node not in visited:
visited.add(node)
print(node, end=" ")
for neighbor in graph[node]:
if neighbor not in visited:
stack.append(neighbor)
graph = {1: [2, 3], 2: [4, 5], 3: [6], 4: [], 5: [], 6: []}
dfs_iterative(graph, 1) # 1 3 6 2 5 4 (LIFO 순서)
큐 (Queue)
FIFO 원리
큐는 먼저 들어온 것이 먼저 나가는(First In First Out) 자료구조입니다. 줄을 서는 것처럼, 가장 먼저 들어온 항목이 먼저 처리됩니다.
enqueue(1) → [1]
enqueue(2) → [1, 2]
enqueue(3) → [1, 2, 3]
dequeue() → 1 반환, [2, 3]
dequeue() → 2 반환, [3]
front() → 3 (제거 없이 확인)
핵심 연산
| 연산 | 설명 | 시간 복잡도 |
|---|---|---|
| enqueue(item) | 큐 뒤에 추가 | O(1) |
| dequeue() | 맨 앞 항목 제거 및 반환 | O(1) |
| front() / peek() | 맨 앞 항목 확인 | O(1) |
| isEmpty() | 큐가 비어있는지 확인 | O(1) |
| size() | 큐의 크기 반환 | O(1) |
큐 구현
from collections import deque # 파이썬에서 효율적인 큐 구현
class Queue:
def __init__(self):
self._items = deque() # O(1) 양끝 삽입/삭제
def enqueue(self, item):
self._items.append(item) # 뒤에 추가
def dequeue(self):
if self.is_empty():
raise IndexError("Queue is empty")
return self._items.popleft() # 앞에서 제거
def front(self):
if self.is_empty():
raise IndexError("Queue is empty")
return self._items[0]
def is_empty(self):
return len(self._items) == 0
def size(self):
return len(self._items)
배열로 큐를 구현하면 dequeue 시 O(n) 이동이 필요하므로, 파이썬에서는
collections.deque를 사용합니다.
큐의 실제 활용 사례
1. 작업 스케줄링 (Task Scheduling)
import threading
import time
from collections import deque
class TaskQueue:
def __init__(self):
self.queue = deque()
def add_task(self, task):
self.queue.append(task)
print(f"작업 추가: {task}")
def process(self):
while self.queue:
task = self.queue.popleft() # FIFO
print(f"처리 중: {task}")
time.sleep(0.1)
queue = TaskQueue()
queue.add_task("이메일 발송")
queue.add_task("보고서 생성")
queue.add_task("DB 백업")
queue.process()
# 이메일 발송 → 보고서 생성 → DB 백업 순서로 처리
2. BFS (너비 우선 탐색)
from collections import deque
def bfs(graph, start):
visited = set([start])
queue = deque([start])
while queue:
node = queue.popleft() # 큐에서 꺼냄 (FIFO)
print(node, end=" ")
for neighbor in graph[node]:
if neighbor not in visited:
visited.add(neighbor)
queue.append(neighbor)
graph = {1: [2, 3], 2: [4, 5], 3: [6], 4: [], 5: [], 6: []}
bfs(graph, 1) # 1 2 3 4 5 6 (레벨 순서)
3. 이벤트 처리 (Event Handling)
// 브라우저의 이벤트 루프 (개념적 표현)
const eventQueue = [];
// 이벤트 발생 시 큐에 추가
eventQueue.push({ type: 'click', handler: handleClick });
eventQueue.push({ type: 'keypress', handler: handleKeypress });
// 이벤트 루프: 큐에서 순서대로 처리
function eventLoop() {
while (eventQueue.length > 0) {
const event = eventQueue.shift(); // FIFO
event.handler();
}
requestAnimationFrame(eventLoop);
}
덱 (Deque, Double-Ended Queue)
덱은 앞뒤 양쪽에서 삽입과 삭제가 모두 가능한 자료구조입니다. 스택과 큐의 특성을 모두 가집니다.
앞 ←→ [a, b, c, d] ←→ 뒤
appendleft(x): 앞에 추가
append(x): 뒤에 추가
popleft(): 앞에서 제거
pop(): 뒤에서 제거
from collections import deque
dq = deque([1, 2, 3])
dq.appendleft(0) # [0, 1, 2, 3] - 앞에 추가
dq.append(4) # [0, 1, 2, 3, 4] - 뒤에 추가
dq.popleft() # 0 반환, [1, 2, 3, 4]
dq.pop() # 4 반환, [1, 2, 3]
덱의 활용: 슬라이딩 윈도우
from collections import deque
def max_sliding_window(nums, k):
"""크기 k인 슬라이딩 윈도우에서 최댓값 구하기"""
dq = deque() # 인덱스 저장
result = []
for i, num in enumerate(nums):
# 윈도우 범위 벗어난 인덱스 제거
while dq and dq[0] < i - k + 1:
dq.popleft()
# 현재 값보다 작은 값들 제거 (오른쪽에서)
while dq and nums[dq[-1]] < num:
dq.pop()
dq.append(i)
if i >= k - 1:
result.append(nums[dq[0]])
return result
print(max_sliding_window([1, 3, -1, -3, 5, 3, 6, 7], 3))
# [3, 3, 5, 5, 6, 7]
우선순위 큐 (Priority Queue)
일반 큐와 달리, 우선순위가 높은 항목이 먼저 나옵니다.
import heapq # 파이썬 힙 (우선순위 큐)
pq = []
heapq.heappush(pq, (3, "낮은 우선순위"))
heapq.heappush(pq, (1, "높은 우선순위"))
heapq.heappush(pq, (2, "중간 우선순위"))
print(heapq.heappop(pq)) # (1, "높은 우선순위")
print(heapq.heappop(pq)) # (2, "중간 우선순위")
print(heapq.heappop(pq)) # (3, "낮은 우선순위")
스택 vs 큐 비교 요약
| 특성 | 스택 (Stack) | 큐 (Queue) |
|---|---|---|
| 원리 | LIFO (Last In First Out) | FIFO (First In First Out) |
| 삽입 위치 | 맨 위 (top) | 맨 뒤 (rear) |
| 삭제 위치 | 맨 위 (top) | 맨 앞 (front) |
| 주요 연산 | push, pop, peek | enqueue, dequeue, front |
| 시간 복잡도 | O(1) | O(1) |
| 탐색 알고리즘 | DFS | BFS |
| 주요 활용 | 함수 호출, 뒤로가기, 괄호 검사 | 작업 스케줄링, 이벤트 처리 |
| 비유 | 책 쌓기, 접시 쌓기 | 줄 서기, 티켓 발권 |
| 파이썬 구현 | list 또는 deque | deque |
| Java 구현 | Deque (ArrayDeque) | Queue (LinkedList, ArrayDeque) |