본 시리즈는 프로그래밍 자료구조의 개념을 정리하고 실습을 진행해보는 시리즈입니다.
큐(Queue)는 FIFO(First In, First Out) 구조로 동작하는 선형 자료구조입니다.
큐에서 데이터를 삽입하는 연산은 enqueue(삽입), 데이터를 제거하는 연산은 dequeue(삭제)라고 부릅니다.
큐에서 주로 사용하는 연산들은 다음과 같습니다:
enqueue(item)
: 큐의 맨 뒤에 데이터를 삽입하는 연산.dequeue()
: 큐의 맨 앞에 있는 데이터를 제거하는 연산.peek()
: 큐의 맨 앞에 있는 데이터를 제거하지 않고 확인하는 연산.isEmpty()
: 큐가 비어 있는지 확인하는 연산.큐는 순서대로 처리해야 하는 작업을 관리하는데 유용한 자료구조입니다.
대표적인 활용 사례는 다음과 같습니다:
Queue는 스택과는 반대로, 먼저 들어간 데이터가 먼저 나오는 구조를 가지므로, 대기열이나 작업 처리의 순서를 보장해야 하는 상황에서 필수적으로 사용됩니다.
Queue
인터페이스가 제공되며, 이를 구현하는 다양한 클래스들이 있습니다.ArrayDeque
, LinkedList
, PriorityQueue가 있으며, 각각의 구현 방식에 따라 특성이 다릅니다.Queue
인터페이스는 데이터를 추가하는 offer()
메서드와 데이터를 제거하는 poll()
메서드, 맨 앞 데이터를 확인하는 peek()
메서드 등을 제공합니다.Queue
인터페이스에서 offer()
, poll()
, peek()
메서드와 같은 큐의 주요 연산을 정의하고, 구체적인 동작은 각 클래스(ArrayDeque
, LinkedList
)에서 구현됩니다.LinkedList
는 Queue
인터페이스를 구현하여 기본적인 큐 연산을 지원합니다.ArrayDeque
는 배열을 기반으로 한 큐의 구현체로, Deque
인터페이스도 함께 구현하고 있습니다.Deque
는 양방향 큐를 지원하지만, ArrayDeque
를 큐로 사용하는 경우에는 단방향으로만 사용하게 됩니다.ArrayDeque
는 연결 리스트 기반의 LinkedList
보다 메모리 사용 효율이 뛰어나고, 속도도 더 빠르기 때문에 Queue 구현으로 더 많이 사용됩니다.ArrayDeque
와 LinkedList
는 모두 Queue
를 구현할 수 있지만, 성능 면에서는 ArrayDeque
가 더 효율적입니다.ArrayDeque
는 연속된 배열 메모리를 사용해 삽입 및 삭제를 처리하며, LinkedList
는 각 요소에 대한 포인터를 사용하여 노드를 연결하기 때문에 메모리 오버헤드가 발생할 수 있습니다.ArrayDeque
는 추가적인 동기화 비용 없이 멀티스레드 환경에서 사용될 수 있는 반면, LinkedList는
멀티스레드 환경에서 별도의 동기화가 필요할 수 있습니다.import java.util.LinkedList;
import java.util.Queue;
public class QueueExample {
public static void main(String[] args) {
// Queue 생성
Queue<Integer> queue = new LinkedList<>();
// 데이터 삽입 (offer)
queue.offer(10);
queue.offer(20);
queue.offer(30);
// 데이터 제거 (poll)
System.out.println(queue.poll()); // 10 출력
// 맨 앞 데이터 확인 (peek)
System.out.println(queue.peek()); // 20 출력
// Queue 상태 출력
System.out.println(queue); // [20, 30] 출력
}
}
위 코드에서 Queue
인터페이스를 사용하여 데이터를 삽입할 때는 offer()
를, 데이터를 제거할 때는 poll()
을 사용하며, 현재 가장 앞에 있는 데이터를 확인하려면 peek()
을 사용합니다.
Deque
인터페이스를 사용해도 됩니다.Deque
는 양방향 큐를 제공하며, ArrayDeque
나 LinkedList
를 사용하여 큐를 구현할 수 있습니다.Queue
인터페이스만으로도 기본적인 큐 연산이 충분히 가능하기 때문에, 양방향 큐의 기능이 필요하지 않다면 Queue
인터페이스를 사용하는 것이 권장됩니다.Python에서는 기본적으로 Queue
자료구조를 위한 표준 라이브러리를 제공합니다.
주요한 구현 방식으로는 다음과 같은 두 가지가 있습니다:
1. collections.deque
: 빠르고 효율적인 양방향 큐를 제공하는 deque 객체.
2. queue.Queue
: 멀티스레드 환경에서 안전한 FIFO Queue를 구현하기 위한 클래스.
Python의 collections.deque는 양방향 큐를 지원하는 매우 효율적인 자료구조입니다.
deque
가 많이 사용됩니다.from collections import deque
# deque를 이용한 Queue 구현
queue = deque()
# 데이터 삽입 (append)
queue.append(10)
queue.append(20)
queue.append(30)
# 데이터 제거 (popleft)
print(queue.popleft()) # 10 출력
# 맨 앞 데이터 확인 (peek)
print(queue[0]) # 20 출력
# Queue 상태 출력
print(queue) # deque([20, 30]) 출력
위 코드에서 deque
객체를 사용하여 큐처럼 동작하도록 구현했습니다.
Python의 queue.Queue는 멀티스레드 환경에서 스레드 안전성을 보장하는 큐이고, FIFO(First In, First Out) 큐로 구현되어 있습니다.
import queue
# 멀티스레드 환경에서 안전한 Queue 생성
q = queue.Queue()
# 데이터 삽입 (put)
q.put(10)
q.put(20)
q.put(30)
# 데이터 제거 (get)
print(q.get()) # 10 출력
# 맨 앞 데이터 확인 (peek) - 큐에 peek 메서드는 없지만, 큐를 먼저 비우지 않고 처리 가능
print(q.queue[0]) # 20 출력
# Queue 상태 출력
print(list(q.queue)) # [20, 30] 출력
위 코드는 스레드 안전한 Queue를 사용하여 데이터를 삽입(put())하고 제거(get())하는 기본적인 큐 연산을 보여줍니다. queue.Queue는 내장된 잠금 메커니즘을 사용하여 멀티스레드 환경에서 안전하게 동작합니다.
Python에서 큐를 구현할 때는 용도에 따라 다음과 같은 기준을 사용할 수 있습니다:
구현 방식 | 장점 | 단점 | 사용 사례 |
---|---|---|---|
collections.deque | - 양방향 큐 지원 - 빠른 삽입/삭제 (O(1)) - 스레드 안전성이 필요 없을 때 적합 | - 크기 제한이 없음 - 멀티스레드 환경에서는 사용 주의 | 단일 스레드 환경에서 빠르고 효율적인 큐가 필요할 때 |
queue.Queue | - 멀티스레드 환경에서 스레드 안전성을 보장 - 큐의 최대 크기 지정 가능 | - 스레드 간의 데이터 통신을 위한 구조라 성능이 조금 느림 | 멀티스레드 환경에서의 데이터 전송 및 동기화 |
collections.deque
를 사용하는 것이 적합합니다. queue.Queue
를 사용하여 스레드 안전성을 확보할 수 있습니다.큐는 순서대로 작업을 처리하는 많은 문제에서 매우 유용하게 사용됩니다. 대표적인 큐의 활용 사례는 다음과 같습니다.
from collections import deque
# BFS를 이용한 그래프 탐색
def bfs(graph, start):
visited = []
queue = deque([start])
while queue:
node = queue.popleft()
if node not in visited:
visited.append(node)
queue.extend(graph[node]) # 현재 노드와 연결된 노드들을 큐에 추가
return visited
graph = {
'A': ['B', 'C'],
'B': ['A', 'D', 'E'],
'C': ['A', 'F'],
'D': ['B'],
'E': ['B', 'F'],
'F': ['C', 'E']
}
print(bfs(graph, 'A')) # ['A', 'B', 'C', 'D', 'E', 'F']
큐에서 이루어지는 연산들은 대부분 상수 시간으로 이루어지기 때문에 매우 효율적입니다.
enqueue(item)
: 큐의 맨 뒤에 데이터를 추가하는 연산으로, O(1) 시간 복잡도를 가집니다.dequeue()
: 큐의 맨 앞에서 데이터를 제거하는 연산으로, O(1) 시간 복잡도를 가집니다.peek()
: 큐의 맨 앞 데이터를 확인하는 연산으로, O(1) 시간 복잡도를 가집니다.isEmpty()
: 큐가 비어 있는지 확인하는 연산으로, O(1) 시간 복잡도를 가집니다.큐의 모든 기본 연산들은 큐의 크기와 무관하게 O(1)로 동작하기 때문에 매우 빠릅니다.
큐에서의 전체 탐색은 큐의 모든 요소를 순차적으로 살펴보는 작업이므로, 큐의 크기에 비례하여 시간이 소요됩니다.
n
은 큐에 들어 있는 데이터의 개수입니다.큐는 데이터를 저장하는 자료구조이기 때문에, 큐의 크기가 커질수록 메모리 사용량도 증가합니다.
또한, 배열 기반 큐의 경우 크기가 고정되어 있을 수 있으며, 크기 초과 시 재할당이 발생할 수 있지만, 동적 배열을 사용하는 구현체에서는 자동으로 배열의 크기를 확장하여 데이터를 추가합니다.
이번 포스팅에서는 큐(Queue) 자료구조의 개념과 구현 방법을 Java와 Python에서 각각 살펴보았습니다.
Queue
인터페이스를 구현한 다양한 자료구조들이 있으며, 성능 면에서는 ArrayDeque
가 가장 효율적이므로 일반적으로 가장 많이 사용됩니다.deque
를 사용하면 빠르고 간단하게 큐를 구현할 수 있으며, 멀티스레드 환경에서는 queue.Queue
를 사용하여 스레드 안전한 큐를 구현할 수 있습니다. 각 상황에 맞는 구현체를 선택하는 것이 중요합니다.다음 포스팅에서는 Deque 자료구조를 다룰 예정입니다.