Code/Algorithm

[Algorithm] 스택과 큐_2

heedy 2022. 12. 5. 14:14
728x90

04-2 큐란?

 큐 알아보기

  • 큐(queue): 스택과 같이 데이터를 임시 저장하는 자료구조
  • 큐는 가장 먼저 넣은 데이터를 가장 먼저 꺼내는 선입선출(FIFO) 구조입니다.
  • 큐에 데이터를 추가하는 작업을 인큐(enqueue), 데이터를 꺼내는 작업을 디큐(dequeue)라고 합니다.
  • 데이터를 꺼내는 쪽을 프런트(front), 데이터를 넣는 쪽을 리어(rear)라고 합니다.

배열로 큐 구현하기

 

배열로 큐를 구현한 예

  • 24를 인큐하기
    : 맨 끝 데이터가 저장되어 있는 que[3]의 다음 원소인 que[4]에 24를 저장합니다. 이때 처리의 복잡도는 O(1)이고 비교적 적은 비용으로 구현할 수 있습니다.
  • 19를 디큐하기
    : que[0]에 저장되어 있는 19를 꺼내면서 2번째 이후의 모든 원소를 위 그림의 c와 같이 앞쪽으로 옮겨야 합니다. 이때 처리의 복잡도는 O(n)입니다.

우선순위 큐(priority queue)

  • 우선순위 큐: 인큐할 때는 데이터에 우선순위를 부여하여 추가하고, 디큐할 때 우선순위가 가장 높은 데이터를 꺼내는 방식
  • 파이썬에서 우선순위를 부여하는 큐는 heapq 모듈에서 제공
  • heap에서 data의 인큐는 heapq.heappust(heap, data)로 수행
  • 디큐는 headpq.heappop(heap)으로 수행

링 버퍼(ring buffer)로 큐 구현하기

  • 링 버퍼(ring buffer): 배열 맨 끝의 원소 뒤에 맨 앞의 원소가 연결되는 자료구조
  • 어떤 원소가 맨 앞 원소이고, 어떤 원소가 맨 끝 원소인지 식별하는 변수가 각각 front와 rear입니다.
  • 프런트와 리어는 논리적인 데이터 순서일 뿐 배열의 물리적 원소의 순서는 아닙니다.

링 버퍼를 사용해서 큐 구현하기

인큐와 디큐 수행 시 front와 rear의 값이 변하는 과정

링 버퍼로 구현한 인큐와 디큐

  1. 7개의 데이터 35, 56, 24, 68, 95, 73, 19가 늘어선 순서대로 que[7], que[8], ···, que[11], que[0], que[1]에 저장됩니다. front 값은 7이고 rear 값은 2입니다.
  2. 1에서 82를 인큐한 다음의 상태입니다. 맨 끝의 다음에 위치한 que[rear], 즉 que[2]에 82를 저장하고 rear값을 1 증가시켜 3으로 만듭니다.
  3. 2에서 35를 디큐한 다음의 상태입니다. 맨 앞 원소인 que[front] 즉, que[7]의 값인 35를 꺼내고 front값을 1 증가시켜 8로 만듭니다.

위 그림과 같이 링 버퍼로 큐를 구현하면 원소를 옮길 필요 없이 front와 rear의 값을 업데이트하는 것만으로 인큐와 디큐를 구현할 수 있습니다. 모든 처리의 복잡도는 O(1)입니다.


실습 4-3 고정 길이 큐 클래스 FixedQueue 구현하기

# 고정 길이 큐 클래스 FixedQueue 구현하기

from typing import Any

class FixedQueue:

  class Empty(Exception):
    """비어 있는 FixedQueue에서 디큐 또는 피크할 때 내보내는 예외 처리"""
    pass

  class Full(Exception):
    """가득 차 있는 FixedQueue에서 인큐할 때 내보내는 예외 처리"""
    pass

  def __init__(self, capacity: int) -> None:
    """큐 초기화"""
    self.no = 0                   # 현재 데이터 개수
    self.front = 0                # 맨 앞 원소 커서
    self.rear = 0                 # 맨 끝 원소 커서
    self.capacity = capacity      # 큐의 크기
    self.que = [None] * capacity  # 큐의 본체

  def __len__(self) -> int:
    """큐에 있는 모든 데이터 개수를 반환"""
    return self.no

  def is_empty(self) -> bool:
    """큐가 비어 있는지 판단"""
    return self.no <= 0

  def is_full(self) -> bool:
    """큐가 가득 차 있는지 판단"""
    return self.no >= self.capacity
  • 예외 처리 클래스 Empty와 Full
    : 비어 있는 큐에 deque(), peek() 함수를 호출할 때 내보내는 예외 처리는 Empty 클래스이고, 가득 차 있는 큐에 enque() 함수를 호출할 때 내보내는 예외처리는 Full 클래스입니다.
  • 초기화하는 __init__() 함수
    : __init__() 함수는 큐 배열을 생성하는 등의 준비 작업을 하며 다음과 같이 5개의 변수에 값을 설정합니다.
    • que: 큐의 배열로서 밀어 넣는 데이터를 저장하는 list형 배열입니다.
    • capacity: 큐의 최대 크기를 나타내는 int형 정수입니다. 이 값은 배열 que의 원소 수와 일치합니다.
    • front, rear: 맨 앞의 원소, 맨 끝의 원소를 나타내는 인덱스입니다. 큐에 넣은 데이터 중에 가장 처음에 넣은 맨 앞 원소의 인덱스가 front이고, 가장 마지막에 넣은 맨 끝의 원소의 바로 다음 인덱스가 rear입니다. rear는 다음에 인큐할 때 데이터를 저장하는 원소의 인덱스입니다.
    • no: 큐에 쌓여 있는 데이터 개수를 나타내는 int형 정수입니다. 변수 front와 rear의 값이 같을 경우 큐가 비어 있는지 또는 가득 차 있는지 구별하기 위해 필요합니다. 큐가 비어 있는 경우에는 no가 0이 되고, 가득 차 있는 경우에는 capacity와 같은 값이 됩니다.
  • 추가한 데이터 개수를 알아내는 __len__() 함수
    : __len__() 함수는 큐에 추가한 데이터 개수를 반환합니다. no의 값은 그대로 반환합니다.
  • 큐가 비어 있는지를 판단하는 is_empty() 함수
    : is empty() 함수는 큐가 비어 있는지를 판단합니다. 비어 있으면 True를, 그렇지 않으면 False를 반환합니다.
  • 큐가 가득 차 있는지를 판단하는 is_full() 함수
    : is_full() 함수는 큐가 가득 차 있어서 더 이상 데이터를 추가할 수 없는 상태인지 검사합니다. 가득 차 있으면 True를, 그렇지 않으면 False를 반환합니다.
def enque(self, x: Any) -> None:
    """데이터 x를 인큐"""
    if self.is_full():
      raise FixedQueue.Full       # 큐가 가득 차 있는 경우 예외 처리 발생
    self.que[self.rear] = x
    self.rear += 1
    self.no += 1
    if self.rear == self.capacity:
      self.rear = 0

  def deque(self) -> Any:
    """데이터를 디큐"""
    if self.is_empty():
      raise FixedQueue.Empty      # 큐가 비어 있는 경우 예외 처리 발생
    x = self.que[self.front]
    self.front += 1
    self.no -= 1
    if self.front == self.capacity:
      self.front = 0
    return x
  • 데이터를 넣는 enque() 함수
    : enque() 함수는 큐에 데이터를 인큐합니다. 하지만 큐가 가득 차서 인큐할 수 없는 경우 예외 처리인 FixedQueue.Full을 내보냅니다.
  • 데이터를 꺼내는 deque() 함수
    : deque() 함수는 큐의 맨 앞부터 데이터를 디큐하여 그 값을 반환합니다. 그러나 큐가 비어 있어 디큐할 수 없는 경우 예외 처리인 FixedQueue.Empty를 내보냅니다.
  def peek(self) -> Any:
    """큐에서 데이터를 피크(맨 앞 데이터를 들여다봄)"""
    if self.is_empty():
      raise FixedQueue.Empty
    return self.que[self.front]

  def find(self, value: Any) -> Any:
    """큐에서 value를 찾아 인덱스를 반환(없으면 -1을 반환)"""
    for i in range(self.no):
      idx = (i + self.front) % self.capacity
      if self.que[idx] == value:  # 검색 성공
        return idx
    return -1                     # 검색 실패

  def count(self, value: Any) -> bool:
    """큐에 있는 value의 개수를 반환"""
    c = 0
    for i in range(self.no):      # 큐 데이터를 선형 검색
      idx = (i + self.front) % self.capacity
      if self.que[idx] == value:  # 검색 성공
        c += 1                    # 들어 있음
    return c

  def __contains__(self, value: Any) -> bool:
    """큐에 value가 있는지 판단"""
    return self.count(value)

  def clear(self) -> None:
    """큐의 모든 데이터를 비움"""
    self.no = self.front = self.rear = 0

  def dump(self) -> None:
    """모든 데이터를 맨 앞부터 맨 끝 순으로 출력"""
    if self.is_empty():           # 큐가 비어 있음
      print('큐가 비어 있습니다.')
    else:
      for i in range(self.no):
        print(self.que[(i + self.front) % self.capacity], end='')
      print()
  • 데이터를 들여다보는 peek() 함수
    : peek() 함수는 맨 앞 데이터, 즉 다음 디큐에서 꺼낼 데이터를 들여다봅니다. que[front]의 값을 반환할 뿐 데이터를 꺼내지는 않으므로 front, rear, no의 값은 변하지 않습니다. 큐가 비어 있을 때는 예외 처리 Fixed Queue.Empty를 내보냅니다.
  • 검색하는 find() 함수
    : find() 함수는 큐의 배열에서 value와 같은 데이터가 포함되어 있는 위치를 알아냅니다. 맨 앞에서 맨 끝쪽으로 선형 검색을 수행합니다. 물론 스캔은 배열의 맨 앞 원소가 아니라 큐의 맨 앞 원소(front)부터 시작합니다. 따라서 스캔할 때 주목하는 인덱스 idx를 구하는 식은 (i + front) % capacity입니다. 검색에 성공하면 찾은 원소의 인덱스를 반환하고 실패하면 -1을 반환합니다.
  • 데이터 개수를 세는 count() 함수
    : count() 함수는 큐에 있는 데이터(value)의 개수를 구하여 반환합니다.
  • 데이터가 포함되어 있는지를 판단하는 __contains__() 함수
    : __contains__() 함수는 큐에 데이터(value)가 들어 있는지를 판단합니다. 들어 있으면 True를, 그렇지 않으면 False를 반환합니다. 내부의 count() 함수를 호출하여 구현합니다.
  • 큐의 전체 원소를 삭제하는 clear() 함수
    : clear() 함수는 현재 큐에 들어 있는 모든 데이터를 삭제합니다.
  • 큐의 전체 데이터를 출력하는 dump() 함수
    : dump() 함수는 큐에 들어 있는 모든 데이터를 맨 앞부터 맨 끝 쪽으로 순서대로 출력합니다. 하지만 큐가 비어 있으면 '큐가 비어 있습니다.'를 출력합니다.


링 버퍼로 큐 프로그램 만들기

실습 4-4 고정 길이 큐 클래스(FixedQueue)를 사용하기

# 고정 길이 큐 클래스(FixedQueue)를 사용하기

from enum import Enum
from fixed_queue import FixedQueue

Menu = Enum('Menu', ['인큐','디큐','피크','검색','덤프','종료'])

def select_menu() -> Menu:
  """메뉴 선택"""
  s = [f'({m.value}){m.name}' for m in Menu]
  while True:
    print(*s, sep = ' ', end='')
    n = int(input(': '))
    if 1 <= n <= len(Menu):
      return Menu(n)

q = FixedQueue(64)                                           # 최대 64개를 인큐할 수 있는 큐

while True:
  print(f'현재 데이터 개수: {len(q)} / {q.capacity}')
  menu = select_menu()                                       # 메뉴 선택

  if menu == Menu.인큐:                                      # 인큐
    x = int(input('인큐할 데이터를 입력하세요.: '))
    try:
      q.enque(x)
    except FixedQueue.Full:
      print('큐가 가득 찼습니다.')

  elif menu == Menu.디큐:                                    # 디큐
    try:
      x = q.deque()
      print(f'디큐한 데이터는 {x}입니다.')
    except FixedQueue.Empty:
      print('큐가 비어 있습니다.')

  elif menu == Menu.피크:                                    # 피크
    try:
      x = q.peek()
      print(f'피크한 데이터는 {x}입니다.')
    except FixedQueue.Empty:
      print('큐가 비어 있습니다.')

  elif menu == Menu.검색:                                   # 검색
    x = int(input('검색할 값을 입력하세요.: '))
    if x in q:
      print(f'{q.count(x)}개 포함되고, 맨 앞의 위치는 {q.find(x)}입니다.')
    else:
      print('검색값을 찾을 수 없습니다.')

  elif menu == Menu.덤프:                                   # 덤프
    q.dump()
  else:
    break
현재 데이터 개수: 0 / 64
(1)인큐 (2)디큐 (3)피크 (4)검색 (5)덤프 (6)종료: 1
인큐할 데이터를 입력하세요.: 1
현재 데이터 개수: 1 / 64
(1)인큐 (2)디큐 (3)피크 (4)검색 (5)덤프 (6)종료: 1
인큐할 데이터를 입력하세요.: 2
현재 데이터 개수: 2 / 64
(1)인큐 (2)디큐 (3)피크 (4)검색 (5)덤프 (6)종료: 1
인큐할 데이터를 입력하세요.: 3
현재 데이터 개수: 3 / 64
(1)인큐 (2)디큐 (3)피크 (4)검색 (5)덤프 (6)종료: 1
인큐할 데이터를 입력하세요.: 1
현재 데이터 개수: 4 / 64
(1)인큐 (2)디큐 (3)피크 (4)검색 (5)덤프 (6)종료: 1
인큐할 데이터를 입력하세요.: 5
현재 데이터 개수: 5 / 64
(1)인큐 (2)디큐 (3)피크 (4)검색 (5)덤프 (6)종료: 5
12315
현재 데이터 개수: 5 / 64
(1)인큐 (2)디큐 (3)피크 (4)검색 (5)덤프 (6)종료: 4
검색할 값을 입력하세요.: 1
2개 포함되고, 맨 앞의 위치는 0입니다.
현재 데이터 개수: 5 / 64
(1)인큐 (2)디큐 (3)피크 (4)검색 (5)덤프 (6)종료: 3
피크한 데이터는 1입니다.
현재 데이터 개수: 5 / 64
(1)인큐 (2)디큐 (3)피크 (4)검색 (5)덤프 (6)종료: 2
디큐한 데이터는 1입니다.
현재 데이터 개수: 4 / 64
(1)인큐 (2)디큐 (3)피크 (4)검색 (5)덤프 (6)종료: 2
디큐한 데이터는 2입니다.
현재 데이터 개수: 3 / 64
(1)인큐 (2)디큐 (3)피크 (4)검색 (5)덤프 (6)종료: 5
315
현재 데이터 개수: 3 / 64
(1)인큐 (2)디큐 (3)피크 (4)검색 (5)덤프 (6)종료: 6

링 버퍼의 활용

링 버퍼는 오래된 데이터를 버리는 용도로 활용할 수 있습니다. 예를 들면 원소 수가 n인 배열에 데이터를 계속해서 입력할 때 가장 최근에 들어온 데이터 n개만 저장하고 나머지 오래된 데이터는 버리는 경우입니다.

실습 4C-2 원하는 개수(n)만큼 값을 입력받아 마지막 n개를 저장

# 원하는 개수(n)만큼 값을 입력받아 마지막 n개를 저장

n = int(input('정수를 몇 개 저장할까요?: '))
a = [None] * n                        # 입력받은 값을 저장하는 배열

cnt = 0                               # 정수를 입력받은 개수
while True:
  a[cnt % n] = int(input((f'{cnt + 1}번째 정수를 입력하세요.: ')))
  cnt += 1


  retry = input(f'계속 할까요?(Y … Yes / N … No): ')
  if retry in {'N','n'}:              # N이나 n을 입력하면 더 이상 값을 받지 않음
    break

i = cnt - n
if i < 0: i = 0

while i < cnt:
  print(f'{i +1}번째 = {a[i % n]}')
  i += 1
정수를 몇 개 저장할까요?: 10
1번째 정수를 입력하세요.: 15
계속 할까요?(Y … Yes / N … No): 17
2번째 정수를 입력하세요.: 19
계속 할까요?(Y … Yes / N … No): 21
3번째 정수를 입력하세요.: 23
계속 할까요?(Y … Yes / N … No): 25
4번째 정수를 입력하세요.: 59
계속 할까요?(Y … Yes / N … No): 55
5번째 정수를 입력하세요.: 84
계속 할까요?(Y … Yes / N … No): 30
6번째 정수를 입력하세요.: 53
계속 할까요?(Y … Yes / N … No): 20
7번째 정수를 입력하세요.: 49
계속 할까요?(Y … Yes / N … No): 38
8번째 정수를 입력하세요.: 29
계속 할까요?(Y … Yes / N … No): n
1번째 = 15
2번째 = 19
3번째 = 23
4번째 = 59
5번째 = 84
6번째 = 53
7번째 = 49
8번째 = 29

 

- 출처: Do it! 자료구조와 함께 배우는 알고리즘 입문 - 파이썬 편
- notebook ipynb file: https://github.com/heejvely/Algorithm/blob/main/Do_it_algorithm/Do_it_%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98_%EC%8A%A4%ED%83%9D%EA%B3%BC%20%ED%81%90.ipynb

 

GitHub - heejvely/Algorithm: 파이썬 알고리즘 인터뷰 책을 참고한 파이썬 알고리즘 연습

파이썬 알고리즘 인터뷰 책을 참고한 파이썬 알고리즘 연습. Contribute to heejvely/Algorithm development by creating an account on GitHub.

github.com

 

728x90