CS 및 알고리즘 공부/알고리즘

[알고리즘🧩] 큐 (Queue)

Gogumiing 2024. 8. 13. 11:21

목차

💬 큐(Queue)란?

💬 선형 큐

💬 연결 큐

💬 우선순위 큐(Priority Queue)

💬 큐의 활용: 버퍼(Buffer)

💬 BFS (Breadth First Search)

💬 데크 (deque)

📑 연습문제


 

💬 큐 (Queue)란?

 

[자료구조🧬] 큐 (Queue)

큐 (Queue)스택(stack)과 마찬가지로 삽입과 삭제의 위치가 제한적인 자료구조FIFO(First In First Out) 구조이다. ✔ 큐 사용을 위해 필요한 주요 연산```createQueue() : 공백 큐 생성enQueue(A) : 원소 A를 삽입en

dionysaurus.tistory.com

 

💬 선형 큐

더보기

1차원 배열을 이용한 큐(Queue)

  • 큐의 크기 = 배열의 크기
  • front: 저장된 첫 번째 원소의 인덱스
  • rear: 저장된 마지막 원소의 인덱스
  • 초기 공백 큐를 생성하려면 크기가 n인 1차원 배열을 생성하고 front = rear = -1 로 초기화하면 됨

 

상태 표현

  • 초기 상태: front = rear = -1
  • 공백 상태: front == rear
  • 포화 상태: rear == n - 1       (n: 배열의 크기)

 

선형 큐의 연산

 

1. 삽입: enQueue(item) 

마지막 원소 뒤에 새로운 원소 삽입
def enQueue(item):
	global rear
    
    if isFull():
    	print("Queue_Full")
    else:
    	rear += 1
        Q[rear] = item

 

2. 삭제 deQueue()

가장 앞에 있는 원소 삭제
deQueue():
	if isEmpty() then Queue_Empty()
    else:
    	front += 1
        return Q[front]​



 

💡 공백상태 및 포화 상태 검사: isEmpty(), isFull()

def isEmpty():
	return front == rear


def isFull():
	return rear == len(Q) - 1

 

3. 검색 Qpeek()

가장 앞에 있는 원소를 검색하여 반환
(현재 front의 한자리 뒤에 있는 원소, 즉 큐의 첫 번째에 있는 원소를 반환함)
def Qpeek():
	if isEmpty():
    	print("Queue_Empty")
    else:
    	return Q[front + 1]​

 

💬 원형 큐

더보기
💡 선형 큐의 문제점을 해결하기 위해 등장한 자료구조

선형 큐를 이용하여 원소의 삽입/삭제를 반복하는 경우, 배열의 앞부분에 활용할 수 있는 공간이 있음에도 rear = n-1인 상태 즉, 포화상태로 인식하여 더이상의 데이터 삽입을 수행하지 않게 된다.



✔ 해결방법 1
매 연산(삽입/삭제)이 이루어질 때마다 저장된 원소들을 배열의 앞부분으로 모두 이동시키는 방법
But, 원소 이동에 많은 시간이 소요되어 큐의 효율성이 매우 떨어짐

✔ 해결방법 2
1차원 배열에 대해 논리적으로는 배열의 처음과 끝이 연결된 원형 형태의 큐를 이룬다고 가정하여 사용하는 방법

원형 큐의 논리적 구조

 

 

원형 큐의 특징

  • 초기 공백 상태 : front = rear = 0
  • Index의 순환
    • front와 rear의 위치가 배열의 마지막 인덱스(n - 1)를 가리킨 후, 논리적 순환을 이루어 배열의 처음 인덱스인 0으로 이동 -> 이를 구현하기 위해 나머지 연산자(mod)를 사용
  • 공백 상태와 포화 상태 구분을 위해 front가 있는 자리는 항상 사용하지 않고 빈자리로 둠

 

✅ 선형 큐와 원형 큐의 삽입/삭제 위치

 

 

✅ 원형 큐의 연산

 

1. 생성: create Queue

  • front = rear = 0 초기화

2. 삽입: enQueue(A)

def enQueue(item):
	global rear
    if isFull():
    	print("Queue_Full")
    else:
    	# rear 값을 조정하여 새로운 원소를 삽입할 자리를 마련
    	rear = (rear + 1) % len(cQ)
        # 그 인덱스에 해당하는 배열원소 cQ[rear]에 item 저장
        cQ[rear] = item


# 포화상태: 삽입할 rear의 다음 위치 == 현재 front
def isFull():
	# (rear + 1) mod n == front
	return (rear + 1) % len(cQ) == front

 

3. 삭제: deQueue()

def deQueue():
	global front
    if isEmpty():
    	print("Queue_Empty")
    else:
    	# front 값을 조정하여 삭제할 자리를 준비
    	front = (front + 1) % len(cQ)
        # 새로운 front 원소를 리턴하여 삭제와 동일한 기능 동작
        return cQ[front]
        
        
# 공백상태: front == rear
def isEmpty():
	return front == rear

 

 

✅ 공백상태 및 포화상태 검사: isEmpty(), isFull()

# 공백상태: front == rear
def isEmpty():
	return front == rear


# 포화상태: 삽입할 rear의 다음 위치 == 현재 front
def isFull():
	# (rear + 1) mod n == front
	return (rear + 1) % len(cQ) == front


💬 연결 큐

더보기

단순 연결 리스트(Linked List)를 이용한 큐

  • 큐의 원소: 단순 연결 리스트의 노드
  • 큐의 원소 순서: 노드의 연결 순서로, 링크로 연결되어 있음
  • front: 첫 번째 노드를 가리키는 링크
  • rear: 마지막 노드를 가리키는 링크

 

상태 표현

  • 초기 상태: front = rear = null
  • 공백 상태: front = rear = null

 

 

연결 큐의 연산

# 1. 공백 큐 생성 (front = rear = 0)
createLinkedQueue()

# 2. 원소 A 삽입
enQueue(A)

# 3. 원소 B 삽입
enQueue(B)

# 4. 원소 삭제
deQueue()

# 5. 원소 삭제 
deQueue()		# front = rear = 0 이 됨​

 

1. 공백 큐 생성
2. 원소 A 삽입
3. 원소 B 삽입
4. 원소 삭제
5. 원소 삭제

 

💬 우선순위 큐 (Priority Queue)

더보기

우선순위를 가진 항목들을 저장하는 큐로, FIFO 순서를 따르지 않고 우선순위가 높은 순서대로 먼저 출력됨

 

<details>
<summary>우선순위 큐 적용 분야</summary>
- 시뮬레이션 시스템

- 네트워크 트래픽 제어 (네트워크에서 패킷에 우선순위를 두는 것을 우선순위 큐로 구현함)

- 운영체제의 테스크 스케줄링</details>

 

 

우선순위 큐의 구현 방법

그래프나 트리 형태로 구현해야 하며, 일반적인 배열 형태로는 구현하기 어려움

  1. 배열 이용
    • 배열을 이용하여 자료를 저장하며, 원소 삽입 과정에서 우선순위를 비교하여 적절한 위치에 삽입함
      • 가장 앞에 최고 우선순위의 원소가 위치함
    • 문제점
      • 배열을 사용하므로, 삽입/삭제 연산이 일어날 때 원소의 재배치 발생 -> 이에 시간 및 메모리 낭비 발생
  2. 리스트(List) 이용

 

 

💬 큐의 활용: 버퍼(Buffer)

더보기
💡 버퍼(Buffer)란?

데이터를 한 곳에서 다른 한 곳으로 전송하는 동안 일시적으로 그 데이터를 보관하는 메모리의 영역


→ 데이터의 입출력에 가장 많은 리소스가 사용되므로, 이를 해결하기 위해 한 번에 많은 데이터를 입출력하도록 구현하는 것이 유리하다. 이 과정에서 버퍼링이 발생한다.
( 버퍼링 : 버퍼를 활용하는 방식 또는 버퍼를 채우는 동작을 의미)

 

 

버퍼의 자료 구조

버퍼는 일반적으로 입출력 및 네트워크 관련 기능에 활용된다. 따라서 순서대로 입력/출력/전달되어야 하므로 FIFO(First In First Out) 방식의 자료구조인 큐(Queue)가 활용된다.

입출력 기능에 활용되는 버퍼의 예시

 

💬 BFS (Breadth First Search, 너비 우선 탐색)

더보기

탐색 시작점의 인접한 정점들을 먼저 모두 차례로 방문한 후에, 방문했던 정점들을 시작점으로 하여 다시 인접한 정점들을 차례로 방문하는 방식 (인접한 정점들에 대해 탐색한 후, 차례로 다시 너비우선탐색(BFS)을 진행해야 하므로, 선입선출(FIFO) 형태의 자료구조인 큐(Queue)를 활용함)

  • 인접한 정점을 순차적으로 방문할 때에는, 데이터 값에 대해 내림차순으로 방문한다.
  • 완전 탐색이 가능하긴 하지만, 일반적으로 DFS와 달리 완전 탐색을 실행하지 않는다는 특징을 가진다.
📌 최소 몇 번의 연산을 해야하는가에 대한 문제를 해결할 땐 항상 DFS보단 BFS가 더 적합하다! 
(DFS는 가지치기에 유리함)

 

 

BFS의 장점

  • 정해진 개수만큼 큐를 생성해도 충분하다.
  • 재귀 호출에서의 장점을 가짐(강의 다시 보기..)
# G: 그래프, v: 탐색 시작점, n: 정점의 개수
def bfs(G, v, n):
    visited = [0] * (n + 1)
    # 큐 생성
    queue = []

    # 시작점 v를 큐에 삽입
    queue.append(v)

    # 큐가 비어있지 않는 동안
    while queue:
        # 큐의 첫 번째 원소를 반환 -> dequeue()
        t = queue.pop(0)
        # 방문하지 않은 곳인 경우,
        if not visited[t]:
            # 방문한 곳으로 표시해둠
            visited[t] = True
            # 정점 t에서 할 일 수행
            visit(t)

            # t와 연결된 모든 정점에 대해
            for i in G[t]:
                # 방문되지 않은 곳이라면
                if not visited[i]:
                    # 큐에 넣기
                    queue.append(i)
                    
                    
def visit(n):
	...
BFS의 그래프 탐색 순서

 

① 초기 상태

  • Visited 배열 초기화
  • Q (큐) 생성
  • enqueue(A)

 

② A 점부터 시작

  • dequeue()
  • A 점을 방문한 것으로 표시
  • A의 인접점을 euqueue()

 

 

③ 탐색 진행

  • dequeue()
  • B점 방문한 것으로 표시
  • B의 인접점을 enqueue()
  • dequeue()
  • C점 방문한 것으로 표시
  • C의 인접점을 enqueue()
  • dequeue()
  • D점 방문한 것으로 표시
  • D의 인접점을 enqueue()

 

  • 이후 Q에 대해 dequeue()를 반복하며 내보내진 점을 방문한 것으로 표시하고, 해당 점의 인접점을 enqueue() 함

④ Q가 비면 탐색 종료

 

 

✅ BFS 예제

 

# G: 그래프, v: 탐색 시작점, n: 정점의 개수
def BFS(G, v, n):
	visited = [0] * (n + 1)
    # 큐 생성
    queue = []					
    
    # 시작점 v를 큐에 삽입
    queue.append(v)				
    visited[v] = 1
    
    # 큐가 비어있지 않는 동안
    while queue:				
    	# 큐의 첫 번째 원소 반환
    	t = queue.pop(0)			
        visit(t)
        # t와 연결된 모든 정점에 대해
        for i in G[t]:				
        	# 방문되지 않은 곳이라면
        	if not visited[i]:		
            	# 큐에 넣음
            	queue.append(i)			
                # n으로부터 1만큼 이동
                visited[i] = visited[t] + 1

 

💬 데크 (deque)

더보기

컨테이너 자료형 중 하나

💡 deque 객체
    양쪽 끝에서 빠르게 추가 및 삭제를 할 수 있는 List류의 컨테이너이다.

 

 

데크 연산

from collections import deque

q = deque()

# append(x): 오른쪽에 x 요소를 추가함
q.append(1)				# enqueue()

# popleft(): 왼쪽에서 요소를 제거하여 반환함 (요소가 없으면 IndexError 발생)
t = q.popleft()			# dequeue()

 

 

예제

아래와 같이 시계 방향으로 1 ~ 5가 적힌 다이얼의 현재 가리키는 눈금이 1이라고 할 때, 이 다이얼을 오른쪽으로 2칸 돌려 가리키는 눈금이 4가 되도록 하려면 어떻게 해야 하는가? (출처: https://wikidocs.net/104977)

[1, 2, 3, 4, 5] → [4, 5, 1, 2, 3]

 

 

sol) 리스트(list)를 n만큼 회전하는 문제는 Python에서는 collections.deque 모듈을 사용하면 간단히 해결할 수 있다.

from collections import deque
a = [1, 2, 3, 4, 5]
q = deque(a)
q.rotate(2)  		#시계방향 회전은 양수, 그 반대는 음수
result = list(q)
print(result)		# [4, 5, 1, 2, 3]

 

📑 연습문제

더보기
  • 선형 큐
    • 큐를 구현하여 아래 동작을 확인해보자.
      • 세 개의 데이터 1, 2, 3을 차례로 큐에 삽입하고 큐에서 세 개의 데이터를 차례로 꺼내어 출력
        (1, 2, 3이 출력되어야 함)


  • Queue를 이용하여 마이쮸 나눠주기 시뮬레이션을 해보자.
    • 엔터(Enter)를 칠 때마다 아래의 정보를 화면에 출력
      • 큐에 있는 사람의 수
      • 현재 일인당 나눠주는 사탕의 수
      • 현재까지 나눠준 사탕의 수
마지막 마이쮸를 누가 가져가게 될지 출력해보자.

 

"""
- 엔터(Enter)를 칠 때마다 아래의 정보를 화면에 출력
    1. 큐에 있는 사람의 수
    2. 현재 일인당 나눠주는 사탕의 수
    3. 현재까지 나눠준 사탕의 수

"""

import time


def isFull():
    global queue
    global rear

    if rear == len(queue) -1 :
        return True
    
    return False


def enQueue(item):
    global queue
    global rear

    if isFull():
        print("Queue is Full now!")
    else:
        rear += 1
        queue[rear] = item


def deQueue():
    global queue
    global front
    global rear
    
    if front == rear:
        print("Queue is Empty now!")
    else:
        front += 1
        return queue[front]


def pressEnter():
    global front
    global rear
    global queue
    global myChew

    input("엔터를 입력해주세요.")

    who = None
    
    for i in range(front + 1, rear + 1):
        who = queue[i][0]
        myChew -= queue[i][1] 
        deQueue()

        print(f'큐에 있는 사람의 수: {rear - front}')
        print(f'현재 일인당 나눠주는 사탕의 수: {queue[i][1]}')
        print(f'현재까지 나눠준 사탕의 수:  {20 - myChew}')
        print()
    
    print('**************************************')
    print()
    time.sleep(0.1)
    return who
    

myChew = 20
front = rear = 0
queue = [None] * 1000000

cnt = 1

while myChew != 0:
    k = 1
    n = cnt + 1

    for i in range(cnt):
        enQueue((k, n - k))
        k += 1
    
    who = pressEnter()

    front = rear
    cnt += 1

print(f'사탕을 마지막으로 가져간 사람: {who}')

 

# 출력 결과
엔터를 입력해주세요.
큐에 있는 사람의 수: 0
현재 일인당 나눠주는 사탕의 수: 1
현재까지 나눠준 사탕의 수: 1
**************************************

엔터를 입력해주세요.
큐에 있는 사람의 수: 1
현재 일인당 나눠주는 사탕의 수: 2
현재까지 나눠준 사탕의 수: 3

큐에 있는 사람의 수: 0
현재 일인당 나눠주는 사탕의 수: 1
현재까지 나눠준 사탕의 수: 4
**************************************

엔터를 입력해주세요.
큐에 있는 사람의 수: 2
현재 일인당 나눠주는 사탕의 수: 3
현재까지 나눠준 사탕의 수: 7

큐에 있는 사람의 수: 1
현재 일인당 나눠주는 사탕의 수: 2
현재까지 나눠준 사탕의 수: 9

큐에 있는 사람의 수: 0
현재 일인당 나눠주는 사탕의 수: 1
현재까지 나눠준 사탕의 수: 10
**************************************

엔터를 입력해주세요.
큐에 있는 사람의 수: 3
현재 일인당 나눠주는 사탕의 수: 4
현재까지 나눠준 사탕의 수: 14

큐에 있는 사람의 수: 2
현재 일인당 나눠주는 사탕의 수: 3
현재까지 나눠준 사탕의 수: 17

큐에 있는 사람의 수: 1
현재 일인당 나눠주는 사탕의 수: 2
현재까지 나눠준 사탕의 수: 19

큐에 있는 사람의 수: 0
현재 일인당 나눠주는 사탕의 수: 1
현재까지 나눠준 사탕의 수: 20
**************************************

사탕을 마지막으로 가져간 사람: 4

 

  • BFS (너비 우선 탐색)
    • 아래는 연결되어 있는 두 개 정점 사이의 간선을 순서대로 나열해 둔 것이다. 모든 정점을 너비우선탐색(BFS)하여 경로를 출력하여라. (시작 정점은 1로 한다)
# 입력 값
1, 2, 1, 3, 2, 4, 2, 5, 4, 6, 5, 6, 6, 7, 3, 7

# 출력 값
1 - 2 - 3 - 4 - 5 - 7 - 6

 

나의 작성 코드!

# G: 그래프, v: 탐색 시작점, n: 정점의 개수
def bfs(G, v, n):
    visited = [0] * (n + 1)
    # 큐 생성
    queue = []

    # 시작점 v를 큐에 삽입
    queue.append(v)

    # 큐가 비어있지 않는 동안
    while queue:
        # 큐의 첫 번째 원소를 반환 -> dequeue()
        t = queue.pop(0)
        # 방문하지 않은 곳인 경우,
        if not visited[t]:
            # 방문한 곳으로 표시해둠
            visited[t] = True
            # 정점 t에서 할 일 수행
            visit(t)

            # t와 연결된 모든 정점에 대해
            for i in G[t]:
                # 방문되지 않은 곳이라면
                if not visited[i]:
                    # 큐에 넣기
                    queue.append(i)
            print(t, end = ' ')


def visit(t):
    pass


# 입력 데이터
input_data = '1 2 1 3 2 4 2 5 4 6 5 6 6 7 3 7'

# 7개의 노드로 구성
N = 7

# 그래프 생성
graph = [[] for i in range(N + 1)]
arr = list(map(int, input_data.split()))

for i in range(0, len(arr) - 1, 2):
    v1, v2 = arr[i], arr[i + 1]
    graph[v1].append(v2)
    graph[v2].append(v1)

# bfs 탐색 실시
bfs(graph, 1, N)