큐와 우선순위 큐(Queue and Priority Queue)

큐란, 스택과 동일하게 Data를 임시 저장할 때 사용하는 자료구조지만, 스택과는 다르게 선입선출(FIFO) 방식이다. 데이터를 넣을땐 인큐(Enqueue), 데이터를 꺼낼땐 디큐(Dequeue) 이라 한다.

큐의 특징을 아래 정리해봤다.

  • 데이터를 꺼내는 쪽을 프론트(front), 데이터를 넣는 쪽을 리어(rear)라 함
  • 인덱스가 0인 곳이 프론트이다
  • 인큐시 처리의 복잡도는 O(1)으로 비용이 적은반면, 디큐같은 경우 2번째 이후의 원소를 앞쪽으로 옮겨야 하기 때문에 복잡도가 O(n)으로 나옴

위 큐의 특징 3번의 단점을 해결하고자 나온 방법으로 링 버퍼(Ring Buffer) 자료구조가 있다

  • 링 버퍼는 논리적인 데이터 순서일 뿐, 배열의 물리적 원소의 순서는 아님
  • front 와 rear(맨 끝 원소 바로 뒤의 인덱스)로 원소의 앞과 끝을 식별함
  • 인큐와 디큐시 front와 rear의 값은 변함

그러면 큐 클래스를 알아보자

큐의 Empty와 Full

예제 : 큐가 비어 있는지 확인해보자

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

이렇게 사용해도 되지만, 프로그램 오류로 인해 값이 0보다 작아질 수 있어서 아래처럼 작성하면 오류를 최소화 할수 있다

    return self.no <= 0

예제 : 큐가가 가득 차 있는지 확인해보자

def is_full(self) -> bool:
    """큐가 가득 찼는지 판단"""
    return self.no == self.capacity

이것도 마찬가지로 오류를 방지하기 위해 아래처럼 부등호를 사용하자

    return self.no >= self.capacity

스택과 동일하게 큐에서도 예외 처리를 진행한다. 예외 처리에 대해 다시 설명하자면, 예외 처리를 하면 실행되다가 중단되는것을 방지할 수 있다. 그리고 original code 에서 error가 발생했을때, 대처하는 코드를 분리할 수 있다는 장점이 있다.

예외 처리

예제 : Empty, deque()함수 또는 peek()함수를 호출할 때 스택이 비어 있으면 내보내는 예외 처리

class Empty(Exception):
    """비어 있는 FixedQueue에 대해 deque 또는 peek를 호출할 때 내보내는 예외처리"""
    pass

예제 : Full, enque() 함수를 호출할 때 스택이 가득 차 있으면 내보내는 예외 처리

class Full(Exception):
    """가득 찬 FixedQueue에 enque를 호출할 때 내보내는 예외처리"""
    pass

이제부터는 큐큐에 사용되는 함수들을 나열하겠다.

큐(Queue) 함수

예제 : init() 함수, 큐 배열을 생성한다. 이때 queue의 no == 0

def __init__(self, capacity: int) -> None:
    """초기화 선언"""
    self.no = 0     # 현재 데이터 개수, 처음에는 0이지만 가득 차게되면 capacity와 같아짐
    self.front = 0  # 맨앞 원소 커서
    self.rear = 0   # 맨끝 원소  커서, 여기서 왜 front와 같을지 의문이 들 수 있는데, 
                    # 그 이유는 rear는 마지막 원소의 다음값이기 때문임
    self.capacity = capacity      # 큐의 크기
    self.que = [None] * capacity  # 큐의 본체

그렇다면, self.front = self.rear 이면 큐가 빈것일까? 답은 비거나 가득차거나 둘중 하나이다.

예제 : len() 함수, 큐의 쌓여있는 데이터 개수를 반환

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

예제 : enque() 함수, 큐의 rear에 데이터 추가. 가득차 있으면 FixedQueue.Full로 예외처리

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

예제 : deque() 함수, 큐의 front data 반환. 스택이 비었으면 FixedQueue.Empty로 예외처리

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

예제 : peek() 함수, 큐의 front data 열람. 스택이 비었으면 FixedQueue.Empty로 예외처리

def peek(self) -> Any:
    """데이터를 피크합니다(맨 앞 데이터를 들여다 봄)"""
    if self.is_empty():
        raise FixedQueue.Empty  # 큐가 비어 있으면 예외처리를 발생
    return self.que[self.front]

예제 : clear() 함수, 큐의 모든 데이터를 삭제하여 빈 큐로 만듦

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

예제 : find() 함수, 큐에서 데이터를 찾으면 원소의 인덱스를 반환. 실패하면 -1 반환. 만약 중복되는 숫자가 있으면 front에 가까운 원소의 인덱스를 반환한다.

def find(self, value: Any) -> Any:
    """큐에서 value를 찾아 인덱스를 반환하고 없으면 -1을 반환합니다"""
    for i in range(self.no):
        idx = (i + self.front) % self.capacity     #self.capacity를 왜 나누냐 링 버퍼의 특성상 인덱스 값을
        if self.que[idx] == value:  # 검색 성공     #넘어버렸을때 앞으로 보내주기 위함이다.
            return idx
    return -1  # 검색 실패

예제 : count() 함수, 큐에서 특정 data의 개수를 반환. front쪽부터 센다.

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

예제 : contains() 함수, value가 들어 있는지를 판단

def __contains__(self, value: Any) -> bool:
    """스택에 value가 있는가?"""
    return self.count(value)

예제 : dump() 함수, 큐에 쌓여 있는 데이터를 front부터 rear까지 순차적으로 출력함.

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()

마지막 : FixedStack 같은 경우 아래 예시처럼 최대 64개까지를 저장하는 Queue을 만들고 싶다면, 아래처럼 명령어를 사용하면 된다

from fixed_queue import FixedQueue

q = FixedQueue(64)

이렇게 큐의 함수들을 알아보았다. 함수가 어떻게 구성되어 있는지 알아야 추후 사용할때 효율적으로 사용할 수 있을것이다.


© 2022 JeongHwan Yun.