7-1. 큐의 개념
큐는 먼저 들어간 데이터가 먼저 나오는 구조이다.
일상생활에서 줄 서는 것이 큐 구조이다. 먼저 선 사람이 먼저 들어간다.
이런 큐의 특징을 선입선출, FIFO(First in First out) 이라고 한다.
큐도 스택처럼 꺼내는 연산을 팝, 삽입하는 연산을 푸시라고 한다.
*큐의 특성을 활용하는 분야
큐의 동작 방식은 프로그래밍 언어에서 많이 활용된다. 대표적으로 여러 이벤트가 발생했을때 발생한 순서대로 처리가 필요할 때 큐가 활용된다. 작업 대기열이나 이벤트 처리의 경우 큐 구조로 처리된다.
큐의 ADT(abstract data type, 추상자료형)
*연산
정의 |
설명 |
boolean isFull() |
큐에 들어 있는 데이터의 개수가 maxsize인지 확인해서 boolean 값을 반환한다. (꽉 찼으면 True, 아니면 False) |
boolean isEmpty() |
큐에 들어있는 데이터가 하나도 없는지 확인해서 boolean 값을 반환한다. (비었으면 True, 아니면 False) |
void push(ItemType item) |
큐에 데이터를 푸시한다. |
ItemType pop() |
큐에서 마지막에 푸시한 데이터를 꺼내어 반환한다. |
*상태
정의 |
설명 |
int front |
큐에서 가장 마지막에 팝한 위치를 기록한다. (꽉 찼으면 True, 아니면 False) |
int rear |
큐에서 최근에 푸시한 데이터의 위치를 기록한다. |
ItemType data[maxsize] |
큐의 데이터를 관리하는 배열이다. 최대 maxsize개의 데이터를 관리한다. |
front는 큐의 앞, rear는 큐의 뒤를 의미한다.
큐는 앞에서 데이터를 꺼내고(팝), 뒤에서 데이터를 넣으므로(푸시) 이렇게 앞 뒤의 최종 데이터의 위치를 기억해야한다.
초기에는 front와 rear모두 초깃값을 -1로 설정한다.
*큐의 세부동작 이해하기
푸시 연산
1. isFull() 연산을 통해 현재 큐가 가득 찼는지 확인한다.
2. 큐가 가득 차지 않았다면, rear을 +1 하고 다음 rear가 가리키는 위치에 값을 푸시한다.
팝 연산
1. isEmpty() 연산을 통해 큐가 비었는지 확인한다. 해당 연산을 통해 front, rear의 값이 같은지 확인해서 큐에 원소가 없는데 팝하는 동작을 방지한다.
2. 만약 큐가 비어있지 않다면 front을 +1 한다. 이렇게 하면 front, rear가 0으로 같아지고,
3. isEmpty() 연산 시 큐가 빈것으로 처리되어 실제 배열의 데이터를 삭제하지 않고도 데이터를 삭제한 것처럼 관리할 수 있다.
front의 다음 부터 rear까지만 큐가 관리하는 데이터라고 생각해야 한다.
이런식으로 큐를 관리하다보면, 실제 data 공간에 비해 큐가 관리하는 데이터가 줄어들게 되면 메모리 공간을 낭비한 상황을 만날 수 있다. 이렇게 된 이유는 큐를 한 방향으로 관리하기 때문이다. 이렇게하면, fornt 이전의 부분을 활용할 수 없게된다.
이를 개선하기 위해 원형 큐를 사용한다.
코딩테스트에서 파이썬을 사용하면 그냥 리스트만 사용해도 충분하게 이런 형태들의 큐를 사용할 수 있다.
* 큐 구현하기
큐를 간단하게 구현하는 방식은 크게 2가지가 있다.
1. 리스트를 활용하는 방식
queue = []
# 큐에 데이터 추가
queue.append(1)
queue.append(2)
# 큐의 맨 앞 데이터 제거, 스택과의 차이는 pop()에 인수로 0을 넣어 맨 앞 데이터를 제거했다는 점이다
first_item = queue.pop(0)
print(first_item) # 출력: 1
2. 덱을 활용하는 방식
from collections import deque
queue = deque()
# 큐에 데이터 추가
queue.append(1)
queue.append(2)
# 큐의 맨 앞 데이터 제거
first_item = queue.popleft()
print(first_item) # 출력: 1
실제로 큐를 구현할 때 덱을 사용하는 이유는 속도 때문이다.
pop(0)을 10 만번하면 0.79초가 소요되고,
popleft()를 10 만번하면 0.007초로 매우 크게 차이가 난다.
[문제]
1. 큐를 이용하여 주어진 데이터를 순차적으로 처리하는 알고리즘을 설계하고 구현하세요. 이 알고리즘의 시간 복잡도는 어떻게 되나요? 큐를 사용하지 않고도 데이터를 순차적으로 처리할 수 있는 다른 방법을 설명하세요.
=>
from collections import deque
queue = deque()
# cnt를 인덱스 삼아 순회
cnt = 0
while queue:
print(queue[cnt])
# value = queue[cnt].popleft() # 팝 코드
# queue.append(value) # 푸시 코드
이 알고리즘의 시간 복잡도는 큐의 원소 개수가 n이라고 하면, O(N)이다.
큐를 사용하지 않고도 데이터를 순차적으로 처리할 수 있는 다른 방법은 배열을 순회하여 순차적으로 처리하는 방법이 있다.
2. 주어진 문자열에서 연속된 중복된 문자를 제거한 새로운 문자열을 반환하는 알고리즘을 설계하고 구현하세요. 단, 문자열 내에서 문자의 순서는 유지해야 합니다. 시간 복잡도를 분석하세요.
=>
from collections import deque
test = 'aaappllee'
test = deque(test)
newT=[]
for i in range(len(test)):
if not newT or newT[-1] != test[i]:
newT.append(test[i])
print(newT)
#=> 이렇게 시도했더니 문자열 순서가 안맞는 문제가 있음
# check = 0
# ok = 0
# cnt = 0
# while 1:
# if check == 0:
# value = test.popleft()
# rem = value
# test.append(value)
# check =1
# else:
# if test:
# value = test.popleft()
# if value == rem:
# ok = 1
# pass
# else:
# test.append(value)
# rem = value
# ok = 0
# # cnt+=1
# # if cnt == len(test):
# # break
# print(test)
# print(test)
큐의 크기가 n이라면 O(n) 인 시간복잡도를 가지는 알고리즘이다.
3. 큐와 스택의 차이점을 설명하고, 각각의 자료구조가 적합한 상황을 예시와 함께 설명해보세요. 두 자료구조의 시간 복잡도에 대한 일반적인 차이도 설명하세요.
=>
*큐와 스택의 차이점
큐: 앞에서도 pop할 수 있다. (먼저 들어간 것을 pop 할 수 있다.)
스택: 앞에서는 pop할 수 없다. (먼저 들어간 것을 pop 할 수 있다.) 뒤에서부터 pop할 수 있다.
*각각의 자료구조가 적합한 상황
큐가 적합한 상황 |
데이터를 순서대로 처리해야 하는 상황에 적합하다. 큐의 데이터를 앞에서 pop 하면서 순차적으로 처리할 수 있다. |
스택이 적합한 상황 |
마지막에 들어간 데이터 먼저 처리해야 하는 상황에 적합하다. 스택의 데이터를 뒤에서부터 pop하면서 마지막에 들어간 데이터 부터 처리할 수 있다. |
*각각의 구조가 적합한 상황의 예시
큐가 적합한 상황 예시 |
FIFO(First In Frist Out)이 적합한 상황,
1. 프린터 작업 대기열 : 프린터는 인쇄 요청을 받은 순서대로 작업을 처리함
2. 네트워크 패킷 처리 : 네트워크에서 데이터 패킷이 도착하는 순서대로 처리되어야 하는 경우 적합함 |
스택이 적합한 상황 예시 |
FILO(First In Last Out)이 적합한 상황,
1. 웹브라우저의 뒤로가기 기능 : 웹 페이지에 뒤로가기 버튼을 누르면 가장 최근에 방문한 페이지부터 순차적으로 돌아감
2. 후위 표기법 계산 : 후위 표기법 계산의 처리의 경우, 피연산자는 스택에 넣고 연산자가 나오면 스택에서 두 숫자를 꺼내 연산자로 연산을 하고 스택에 넣음 |
* 두자료구조의 시간 복잡도에 대한 일반적인 차이
두 자료구조의 시간 복잡도에 대한 일반적인 차이는 다음과 같은 경우에서 확인할 수 있다.
스택이 맨 앞에서 원소를 꺼내려고 할때 O(N)의 시간복잡도가 걸리지만, 큐의 경우 O(1)의 시간복잡도가 걸린다.
4. 큐를 이용하여 우선순위가 있는 작업을 처리하는 방법에 대해 설명하세요. 우선순위가 높은 작업을 먼저 처리할 수 있는 자료구조의 동작 원리와 그에 따른 시간 복잡도를 설명해보세요.
* 큐를 이용해 우선순위가 있는 작업을 처리하는 방법
이때, 우선순위 큐(Priority Queue)를 사용할 수 있다. 우선순위 큐는 각 작업에 우선순위를 부여하고, 우선순위에 따라 요소를 삽입하고 삭제 처리하는 자료구조이다.
* 우선순위 큐의 동작 원리
우선순위 큐는 요소를 큐에 추가하거나, 가장 높은 우선순위를 가진 요소를 큐에서 제거하는 동작이 있다.
이러한 동작을 수행하기 위해 완전 이진 트리 구조를 가진 자료 구조인 힙(Heap)을 사용한다.
(완전 이진 트리 구조란? 모든 레벨이 완전히 채워져 있으면서, 마지막 레벨만 왼쪽부터 차례대로 노드가 채워진 형태)
*힙의 종류
힙은 부모 노드의 값이 자식 노드의 값보다 항상 작거나 같은 최소 힙(Min Heap), 부모 노드의 값이 자식 노드의 값보다 항상 크거나 같은 최대 힙(Max Heap)이 있다.
우선 순위 큐에서 작업을 처리할 때는, 우선순위에 따라 요소가 정렬된다. 우선순위가 가장 높은 요소가 루트에 위치하게 된다.
이후에 우선순위가 높은 요소가 먼저 처리된다.
* 우선순위 큐의 시간 복잡도
삽입(Enqueue) |
요소를 추가할 때, 힙의 높이에 따라 위치를 정해야 하므로, O(log n) |
삭제(Dequeue) |
우선순위가 가장 높은 요소를 삭제하고, 힙을 재구성 해야하므로, O(log n) |
힙 (Heap)이란?
힙은 이진 트리 기반의 자료구조로, 최소 힙 (Min-Heap)과 최대 힙 (Max-Heap)으로 나뉜다.
- 최소 힙: 각 부모 노드가 자식 노드보다 작거나 같은 값을 가지는 구조.
- 최대 힙: 각 부모 노드가 자식 노드보다 크거나 같은 값을 가지는 구조.
파이썬의 heapq 모듈은 기본적으로 최소 힙을 사용하며, heapify() 함수는 주어진 리스트를 이러한 힙 구조로 정렬한다.
heapq.heapify()의 동작
heapify()는 주어진 리스트를 인덱스 0부터 힙의 조건에 맞게 재배치한다. 이때, 첫 번째 원소(인덱스 0)가 최소값을 가지도록 정렬된다. 힙은 완전 이진 트리의 성질을 가지기 때문에, heapify()의 시간 복잡도는 O(n)이다.
사용 예시
'''
우선순위 큐 구현
'''
import heapq
tasks = [(3, 'Task A'), (1, 'Task B'), (2, 'Task C'), (1, 'Task D')]
heapq.heapify(tasks)
result = [i[1] for i in tasks]
print(result)
* 정리된 개념 출처: 코딩 테스트 합격자 되기 - 박경록(파이썬 편)