지식
Algorithm
큐 (Queue)
FIFO 원칙으로 동작하는 큐의 개념, 배열·연결 리스트 구현, 선형 배열 큐의 함정과 BFS 응용까지 알아봅니다.
지난 글에서 LIFO 원칙의 스택을 다뤘습니다. 이번에는 반대 원칙인 FIFO(First In, First Out)로 동작하는 큐(Queue)를 살펴봅니다. 큐는 줄 서기와 같은 원리로, BFS 탐색이나 운영체제의 프로세스 스케줄링처럼 순서를 보장해야 할 때 핵심적으로 사용됩니다.
큐란
큐는 먼저 들어온 원소가 먼저 나가는 선형 자료구조입니다. 삽입은 rear(뒤)에서, 제거는 front(앞)에서 이루어집니다.
| 연산 | 설명 | 복잡도 |
|---|---|---|
enqueue(x) | rear에 x 삽입 | O(1) |
dequeue() | front 원소 제거 후 반환 | O(1) |
peek() | front 원소 조회 (제거 없음) | O(1) |
is_empty() | 비어있는지 확인 | O(1) |
선형 배열 큐의 함정
단순 배열로 큐를 구현하면 dequeue() 시 나머지 원소들을 앞으로 당겨야 하므로 O(n) 이 됩니다.
# 나쁜 방법 — O(n) dequeue
class BadQueue:
def __init__(self):
self._data = []
def enqueue(self, x):
self._data.append(x) # O(1)
def dequeue(self):
return self._data.pop(0) # O(n) — 비효율!
해결책은 front 인덱스를 증가시켜 “논리적으로” 제거하거나, 원형 큐(Circular Queue), 또는 collections.deque를 활용하는 것입니다.
올바른 구현 — collections.deque 활용
Python에서 권장되는 방식입니다. deque는 양끝 O(1) 연산을 지원하는 이중 연결 리스트 기반 자료구조입니다.
from collections import deque
class Queue:
def __init__(self):
self._q = deque()
def enqueue(self, x):
self._q.append(x) # rear에 삽입
def dequeue(self):
if not self._q:
raise IndexError("큐가 비어 있습니다")
return self._q.popleft() # front에서 제거 — O(1)
def peek(self):
if not self._q:
raise IndexError("큐가 비어 있습니다")
return self._q[0]
def is_empty(self):
return len(self._q) == 0
def size(self):
return len(self._q)
연결 리스트로 구현하기
직접 구현할 경우 tail 포인터를 유지하면 enqueue가 O(1)이 됩니다.
class Node:
def __init__(self, val):
self.val = val
self.next = None
class LinkedQueue:
def __init__(self):
self._head = None # front
self._tail = None # rear
def enqueue(self, val):
node = Node(val)
if self._tail:
self._tail.next = node
self._tail = node
if not self._head:
self._head = node
def dequeue(self):
if not self._head:
raise IndexError("큐가 비어 있습니다")
val = self._head.val
self._head = self._head.next
if not self._head:
self._tail = None # 마지막 원소 제거 시 tail도 None
return val
응용 — BFS (너비 우선 탐색)
큐의 가장 중요한 응용은 BFS입니다. 시작 노드에서 가장 가까운(거리 순) 노드부터 탐색합니다.
from collections import deque
def bfs(graph, start):
visited = set()
queue = deque([start])
visited.add(start)
result = []
while queue:
node = queue.popleft() # FIFO: front에서 꺼냄
result.append(node)
for neighbor in graph[node]:
if neighbor not in visited:
visited.add(neighbor)
queue.append(neighbor) # rear에 넣음
return result
graph = {1: [2, 3], 2: [4, 5], 3: [6], 4: [], 5: [], 6: []}
print(bfs(graph, 1)) # [1, 2, 3, 4, 5, 6]
BFS를 DFS와 비교하면:
| 특성 | BFS (큐) | DFS (스택) |
|---|---|---|
| 탐색 순서 | 거리 순(레벨별) | 깊이 우선 |
| 최단 경로 | 보장 (가중치 없는 그래프) | 미보장 |
| 메모리 | 큐 최대 크기 = 너비 | 스택 깊이 = 트리 깊이 |
운영체제에서의 큐
- 프로세스 스케줄링: 준비 큐(Ready Queue)에 대기 중인 프로세스를 순서대로 CPU에 할당
- I/O 요청 큐: 디스크 읽기/쓰기 요청을 FIFO로 처리
- 네트워크 패킷 버퍼: 패킷 도착 순서대로 처리
정리
- 큐는 FIFO 구조로 enqueue/dequeue/peek이 모두 O(1) 입니다.
- Python에서는
list.pop(0)대신collections.deque를 사용하세요. - BFS의 핵심 도구이며, 프로세스 스케줄링·이벤트 처리 등에 폭넓게 활용됩니다.
지난 글: 스택
다음 글: 원형 큐
읽어주셔서 감사합니다. 😊