자료 구조는 프로그래밍 분야에서 핵심적인 역할을 합니다. 데이터를 효율적으로 관리하고 활용할 수 있는 방식으로 조직하는 데 필수적입니다. 그중에서도 큐(Queue)는 비교적 단순하면서도 매우 유용한 자료 구조입니다.
본 글에서는 큐의 개념과 파이썬(Python)에서의 구현 방법을 상세히 알아보겠습니다.
큐란 무엇인가?
큐는 선입선출(FIFO, First-In-First-Out) 원칙을 따르는 선형 자료 구조입니다. 이는 스택(Stack) 자료 구조와 반대되는 특징입니다.
영화관 매표소 줄을 큐에 비유해 생각해 볼 수 있습니다. 아래 그림을 통해 큐를 좀 더 자세히 살펴보겠습니다.
매표소에서 줄을 서는 상황을 보면, 두 번째 사람은 첫 번째 사람이 업무를 완료해야만 매표소에 갈 수 있습니다. 즉, 먼저 줄을 선 사람이 먼저 업무를 처리하게 됩니다. 이러한 방식이 바로 FIFO(First-In/First-Out) 원칙입니다. 우리는 일상생활에서 이와 유사한 큐 형태를 자주 볼 수 있습니다.
큐 자료 구조도 동일한 원칙을 따릅니다. 이제 큐가 무엇이고, 어떤 원리로 작동하는지 이해하셨을 것입니다. 다음으로는 큐 자료 구조에서 수행할 수 있는 주요 연산에 대해 알아보겠습니다.
큐의 기본 연산
스택과 마찬가지로 큐 자료 구조에서도 두 가지 핵심적인 연산을 수행할 수 있습니다.
인큐(Enqueue, 데이터 삽입)
큐의 가장 뒤쪽에 새로운 데이터를 추가하는 연산입니다. 이 위치를 ‘후단(rear)’이라고 합니다.
디큐(Dequeue, 데이터 삭제)
큐의 가장 앞쪽에서 데이터를 제거하는 연산입니다. 이 위치를 ‘전단(front)’이라고 합니다.
큐 연산에 대한 이해를 돕기 위해 아래 그림을 참조하십시오. 한 장의 그림이 천 마디 말보다 낫다는 말이 있죠.
큐에 대한 좀 더 자세한 정보를 얻기 위해 몇 가지 보조 함수를 추가할 수 있습니다. 보조 함수의 수에는 제한이 없지만, 지금은 가장 중요한 기능에 집중해 보겠습니다.
후단(rear) 확인
큐의 가장 뒤쪽에 있는 첫 번째 요소를 반환합니다.
전단(front) 확인
큐의 가장 앞에 있는 첫 번째 요소를 반환합니다.
비어있음(empty) 확인
큐가 비어 있는지 여부를 반환합니다.
지금까지 큐에 대한 개념적인 설명을 다루었습니다. 지루하셨나요? 개념적인 내용은 때로는 지루하게 느껴질 수 있습니다.
그러나 개념을 제대로 이해하지 못하면 코드를 작성할 수 없습니다. 구현하기 전에 개념을 완전히 파악해야 합니다. 이제 코드를 직접 작성해 볼 시간입니다. 너무 걱정하지 마세요.
온라인 컴파일러를 이용하시거나, PC에 파이썬이 설치되어 있다고 가정하고 진행하겠습니다.
큐 구현
큐 구현은 프로그래머에게 15분 이상 걸리지 않습니다. 이 튜토리얼을 끝까지 보시면 몇 분 안에 큐를 구현할 수 있을 것입니다.
파이썬에서는 다양한 방식으로 큐를 구현할 수 있습니다. 여러 가지 큐 구현 방법을 단계별로 살펴보겠습니다.
#1. 리스트를 이용한 큐 구현
파이썬의 내장 데이터 유형인 리스트(list)를 사용하여 클래스 내에서 큐를 구현해 보겠습니다. 모듈 없이 처음부터 큐 자료 구조를 구현하는 다양한 단계를 안내할 것입니다.
1단계: Queue
라는 클래스를 정의합니다.
class Queue: pass
2단계: 큐의 데이터를 저장할 변수가 필요합니다. elements
라는 이름의 리스트를 사용합니다.
class Queue: def __init__(self): self.elements = []
3단계: 큐에 데이터를 삽입하려면 클래스에 메서드가 필요합니다. 이 메서드는 이전 섹션에서 이미 언급했듯이 enqueue
라고 합니다.
이 메서드는 일부 데이터를 인자로 받아 큐(elements
)에 추가합니다. 리스트의 append
메서드를 사용하여 큐의 끝에 데이터를 추가할 수 있습니다.
class Queue: # ... def enqueue(self, data): self.elements.append(data) return data
4단계: 큐에서 데이터를 제거하는 dequeue
메서드가 필요합니다. 리스트의 pop
메서드를 사용할 것입니다. 눈치채셨을 수도 있겠죠?
리스트의 pop
메서드는 주어진 인덱스의 요소를 삭제합니다. 인덱스를 제공하지 않으면 리스트의 마지막 요소를 삭제합니다.
여기서는 리스트의 첫 번째 요소를 삭제해야 하므로, 인덱스 0을 pop
메서드에 전달해야 합니다.
class Queue: # ... def dequeue(self): return self.elements.pop(0)
이제 큐의 기본 연산을 구현했습니다. 그러나 큐 연산이 제대로 작동하는지 확인하려면 몇 가지 보조 함수가 필요합니다. 보조 함수도 함께 작성해 보겠습니다.
5단계: rear()
메서드는 큐의 마지막 요소를 가져오는 데 사용됩니다. 리스트의 음수 인덱싱을 활용하여 큐의 마지막 요소를 쉽게 가져올 수 있습니다.
class Queue: # ... def rear(self): return self.elements[-1]
6단계: front()
메서드는 큐의 첫 번째 요소를 가져오는 데 사용됩니다. 리스트의 인덱싱을 사용하여 큐의 첫 번째 요소를 가져올 수 있습니다.
class Queue: # ... def front(self): return self.elements[0]
7단계: is_empty()
메서드는 큐가 비어 있는지 확인하는 데 사용됩니다. 리스트의 길이를 확인하여 큐가 비어있는지 확인할 수 있습니다.
class Queue: # ... def is_empty(self): return len(self.elements) == 0
큐 자료 구조 구현이 완료되었습니다! 이제 Queue
클래스를 사용하여 객체를 만들어 보겠습니다.
class Queue: # ... if __name__ == '__main__': queue = Queue()
이제 Queue
객체가 생성되었습니다. 일련의 연산을 통해 이 객체를 테스트해 보겠습니다.
is_empty
메서드를 사용하여 큐가 비어 있는지 확인합니다.True
를 반환해야 합니다.enqueue
메서드를 사용하여 숫자 1, 2, 3, 4, 5를 큐에 추가합니다.is_empty
메서드는False
를 반환해야 합니다. 확인해 보세요.front
및rear
메서드를 각각 사용하여 전단 및 후단 요소를 출력합니다.dequeue
메서드를 사용하여 큐에서 요소를 제거합니다.front
요소를 확인합니다. 요소 2를 반환해야 합니다.- 이제 큐에서 모든 요소를 제거합니다.
is_empty
메서드는True
를 반환해야 합니다. 확인해 보세요.
이 정도면 큐 구현을 테스트하기에 충분할 것입니다. 위에서 언급한 각 단계를 코드로 작성하여 큐를 테스트해 보겠습니다.
코드를 직접 작성해보셨나요?
아니라면, 걱정하지 마세요.
아래 코드를 참조하세요.
class Queue: def __init__(self): self.elements = [] def enqueue(self, data): self.elements.append(data) return data def dequeue(self): return self.elements.pop(0) def rear(self): return self.elements[-1] def front(self): return self.elements[0] def is_empty(self): return len(self.elements) == 0 if __name__ == '__main__': queue = Queue() ## checking is_empty method -> True print(queue.is_empty()) ## adding the elements queue.enqueue(1) queue.enqueue(2) queue.enqueue(3) queue.enqueue(4) queue.enqueue(5) ## again checking is_empty method -> False print(queue.is_empty()) ## printing the front and rear elements using front and rear methods respectively -> 1, 5 print(queue.front(), end=' ') print(queue.rear()) ## removing the element -> 1 queue.dequeue() ## checking the front and rear elements using front and rear methods respectively -> 2 5 print(queue.front(), end=' ') print(queue.rear()) ## removing all the elements queue.dequeue() queue.dequeue() queue.dequeue() queue.dequeue() ## checking the is_empty method for the last time -> True print(queue.is_empty())
위 코드를 실행하면 다음과 유사한 결과를 얻을 수 있습니다.
True False 1 5 2 5 True
리스트 데이터 유형을 사용하여 큐 자료 구조를 직접 구현할 수 있다는 것을 알았습니다. 위에서 구현한 큐는 다른 프로그래밍 언어에서 큐를 구현하는 방법을 이해하는 데 도움이 될 것입니다.
이제 이전에 했던 것처럼 객체를 생성하여 프로젝트의 다른 프로그램에서 위에서 구현한 큐 클래스를 사용할 수 있습니다.
리스트 데이터 유형을 사용하여 큐를 처음부터 구현했습니다. 큐를 위한 내장 모듈이 있을까요? 네! 내장 큐 구현이 있습니다. 함께 살펴보겠습니다.
#2. collections
모듈의 deque
를 이용한 큐 구현
deque
는 양방향 큐(double-ended queue)로 구현되어 있습니다. 양쪽 끝에서 요소를 추가하고 제거하는 것을 지원하므로 스택과 큐로 모두 사용할 수 있습니다. deque
를 사용하여 큐를 구현하는 방법을 살펴보겠습니다.
deque
는 이중 연결 리스트(doubly linked list)라는 다른 자료 구조를 사용하여 구현되었습니다. 따라서 요소 삽입 및 삭제 성능이 일정합니다. 연결 리스트의 중간에 있는 요소에 접근하는 데 O(n) 시간이 걸리지만, 큐에서는 중간 요소에 접근할 필요가 없으므로 큐로 사용하는 데 문제가 없습니다.
deque
에서 제공하는 다양한 메서드를 살펴보겠습니다.
append(data)
– 큐에 데이터를 추가하는 데 사용합니다.popleft()
– 큐에서 첫 번째 요소를 제거하는 데 사용합니다.
전단, 후단, is_empty
메서드에 대한 대체 메서드는 없습니다. 전단 및 후단 메서드 대신 전체 큐를 출력할 수 있습니다. 또한, len
메서드를 사용하여 큐가 비어 있는지 여부를 확인할 수 있습니다.
이전에 큐 구현을 테스트하기 위해 일련의 단계를 거쳤습니다. 여기에서도 동일한 단계를 따르겠습니다.
from collections import deque ## creating deque object queue = deque() ## checking whether queue is empty or not -> True print(len(queue) == 0) ## pushing the elements queue.append(1) queue.append(2) queue.append(3) queue.append(4) queue.append(5) ## again checking whether queue is empty or not -> False print(len(queue) == 0) ## printing the queue print(queue) ## removing the element -> 1 queue.popleft() ## printing the queue print(queue) ## removing all the elements queue.popleft() queue.popleft() queue.popleft() queue.popleft() ## checking the whether queue is empty or not for the last time -> True print(len(queue) == 0)
위 코드를 실행하면 다음과 유사한 결과를 얻을 수 있습니다.
True False deque([1, 2, 3, 4, 5]) deque([2, 3, 4, 5]) True
#3. queue
모듈의 Queue
를 이용한 큐 구현
파이썬에는 큐 구현을 위해 Queue
라는 클래스를 제공하는 queue
라는 내장 모듈이 있습니다. 이 모듈은 앞서 직접 구현한 것과 유사한 기능을 제공합니다.
먼저 Queue
클래스의 다양한 메서드를 살펴보겠습니다.
put(data)
– 데이터를 큐에 추가합니다.get()
– 큐에서 첫 번째 요소를 제거하고 반환합니다.empty()
– 큐가 비어 있는지 여부를 반환합니다.qsize()
– 큐의 길이를 반환합니다.
위 메서드를 사용하여 큐를 구현해 보겠습니다.
from queue import Queue ## creating Queue object queue_object = Queue() ## checking whether queue is empty or not -> True print(queue_object.empty()) ## adding the elements queue_object.put(1) queue_object.put(2) queue_object.put(3) queue_object.put(4) queue_object.put(5) ## again checking whether queue is empty or not -> False print(queue_object.empty()) ## removing all the elements print(queue_object.get()) print(queue_object.get()) print(queue_object.get()) ## checking the queue size print("Size", queue_object.qsize()) print(queue_object.get()) print(queue_object.get()) ## checking the whether queue_object is empty or not for the last time -> True print(queue_object.empty())
위 코드의 실행 결과를 확인해 보세요.
True False 1 2 3 Size 2 4 5 True
Queue
클래스에는 이 외에도 여러 메서드가 있습니다. dir
내장 함수를 사용하여 탐색해 볼 수 있습니다.
결론
큐 자료 구조와 그 구현에 대해 배우셨기를 바랍니다. 큐는 FIFO(First-In-First-Out) 순서로 처리해야 하는 여러 상황에서 유용하게 활용될 수 있습니다. 큐가 필요한 상황을 맞이했을 때, 이 글에서 배운 내용을 활용하여 문제를 해결해 보세요.
파이썬 마스터에 관심이 있으신가요? 다음 학습 자료들을 확인해 보세요.
즐거운 코딩하세요! 🙂 👨💻