자료구조 with Python : 스택 2개로 큐 구현하기


큐를 구현하는 방법 중 하나는 스택을 두 개 사용하는 것이다. 여기서는 스택 자료구조를 직접 사용하지 않고 파이썬 내장 리스트를 사용하여 스택을 대신한다.

큐의 구조

  • enqueue : 뒤 쪽으로 원소를 삽입하는 함수
  • dequeue : 가장 앞에 위치한 원소를 제거하면서 반환하는 함수
  • is_empty : 큐가 비어있는지 확인하는 함수
  • transfer : in_stack의 원소들을 모두 out_stack으로 옮기는 함수

스택 2개를 사용한 큐의 구현


class Queue(object):
    def __init__(self):
        self.in_stack = []
        self.out_stack = []

    def isEmpty(self):
        return not(bool(self.in_stack) or bool(self.out_stack))

    def transfer(self):
        while self.in_stack:
            self.out_stack.append(self.in_stack.pop())

    def enqueue(self, item):
        return self.in_stack.append(item)

    def dequeue(self):
        if not self.out_stack:
            self.transfer()
        if not self.isEmpty():
            return self.out_stack.pop()
        else:
            print("Queue is empty!")

    def __repr__(self):
        tmp = []
        tmp += self.out_stack[::-1]
        tmp += self.in_stack
        if tmp:
            return repr(tmp)
        else:
            return "Queue is empty!"

enqueue를 하게 되면 in_stack 리스트에 데이터를 입력한다. 그러다 dequeue를 실행했을 때 out_stack이 비어있으면 in_stack의 모든 원소들을 pop하여서 가져온다. 이후에 out_stack의 원소들을 pop하게 되면 큐의 동작처럼 FIFO로 데이터가 출력되는 것을 확인할 수 있다.

확인코드

queue = Queue()
for i in range(1, 4):
    queue.enqueue(i)
print(queue)
print("dequeue: {0}".format(queue.dequeue()))
print("dequeue: {0}".format(queue.dequeue()))
queue.enqueue(4)
queue.enqueue(5)
print(queue)
print("dequeue: {0}".format(queue.dequeue()))
print(queue)


고찰

가장 중요한 아이디어는 in_stack의 데이터들은 모두 out_stack보다 늦게 들어온 데이터라는 것이다. out_stack에 원소가 있다면 in_stack에서 원소를 가져오지 않고 pop을 해야했다. LIFO를 두 번 사용해서 FIFO로 만드는 게 꽤 흥미로웠다.

 

'프로그래밍 > 자료구조' 카테고리의 다른 글

[자료구조][Python] : Queue(큐)  (0) 2021.10.02
[자료구조][Python] : Stack(스택)  (0) 2021.09.15

자료구조 with Python : Queue(큐)


큐는 스택과 다르게 항목이 들어온 순서대로 접근이 가능하다. 먼저 들어온 데이터가 먼저 나가는 선입선출(FIFO) 구조다. 큐도 스택처럼 랜덤 액세스가 제한된다.

큐의 구조

  • front : 큐의 가장 앞을 가리키는 포인터
  • rear : 큐의 가장 뒤를 가리키는 포인터
  • enqueue : rear 뒤에 원소를 삽입하는 함수
  • dequeue : front 위치의 원소를 제거하면서 반환하는 함수
  • is_empty : 큐가 비어있는지 확인하는 함수
  • size : 큐의 크기를 확인하는 함수

큐의 구현


class Queue(object):
    def __init__(self):
        self.items = []
        self.front = 0
        self.rear = -1

    def isEmpty(self):
        return self.front > self.rear

    def enqueue(self, item):
        self.items.append(item)
        self.rear += 1

    def dequeue(self):
        if self.isEmpty():
            print("Queue is empty")
        else:
            self.front += 1
            return self.items[self.front - 1]

    def __repr__(self):
        return repr(self.items[self.front:self.rear+1])

items 리스트에 enqueue()함수로 데이터를 넣으면 가장 뒤를 나타내는 rear가 커진다.

dequeue를 하면 front가 가리키는 데이터를 반환하고 front가 1 증가한다.

큐가 비어있는 지 여부는 front > rear로 알 수 있다.

확인코드

queue = Queue()
for i in range(1, 4):
    queue.enqueue(i)
print(queue)
print("dequeue: {0}".format(queue.dequeue()))
print("dequeue: {0}".format(queue.dequeue()))
queue.enqueue(4)
queue.enqueue(5)
print(queue)
print("dequeue: {0}".format(queue.dequeue()))
print(queue)

이 방법은 큐의 동작이 어떻게 이뤄지는지 확인하기 위함이고 이렇게 계속 사용하게 되면 dequeue()를 하더라도 데이터가 남아있기 때문에 좋은 방법은 아니라고 생각한다.

쉬운 구현은 list자료형을 이용해서 append와 pop(0)를 사용하는 방법이지만 pop(0) 의 경우 시간복잡도가 O(N)이기 때문에 이 또한 비효율적이다.

자료구조 with Python : Stack(스택)

스택(Stack)은 마지막에 들어온 데이터가 가장 먼저 나가게 되는 후입선출(LIFO)의 자료구조이다. 스택도 결국에는 리스트의 특별한 케이스일 뿐이다. 입력과 출력이 한 쪽 끝단에서만 이루어진다.


스택의 구조

스택은 가장 위의 데이터를 알리는 top, 스택에 데이터를 삽입하는 push, top의 데이터를 내보내는 pop함수가 기본적인 구성이다. 이 외에도 peak나 is_empty를 구현하기도 하지만 이건 부수적인 것일 뿐이다.

Push(item)

push는 아이템을 스택에 삽입하는 함수이다. top은 1 증가시키고 top이 가리키는 위치에 아이템을 저장한다.

Pop()

pop은 아이템을 스택에서 꺼내는 함수이다. 단순 참조가 아니라 스택에서 아예 나오게 되는 것이다. top이 가리키는 위치의 아이템을 꺼낸 후 top을 1 감소시킨다.

두 함수 모두 시간 복잡도는 O(1)이고 스택은 원하는 아이템을 찾을 때는 top에서 부터 차례대로 찾아야해서 random 접근이 불가능하다.


스택의 예시

스택을 쉽게 설명하기 위해서 스택은 상자, 데이터는 상자와 너비가 같은 책이라고 예시를 든다 (책은 항상 눕혀서 상자에 넣는다). Top은 상자의 위에서 확인하는 것이라 생각한다.

스택이라는 상자에 처음 위에서 바라보면 상자의 밑바닥을 확인할 수 있다. 스택에 처음에는 자료구조라는 책을 넣는다. 상자를 위에서 봤을 때 우리는 자료구조가 있다는 것을 확인할 수 있다. 그 다음 운영체제 책을 넣고 컴퓨터 구조 책을 넣는다. 이 때 상자의 위에서 볼 수 있는 것은 컴퓨터 구조이다. Stack의 Top이 가리키는 것이 컴퓨터 구조라는 의미이다.

상자에서 우리가 꺼낼 수 있는 책은 마지막에 넣은 컴퓨터 구조이고 다른 책을 넣을 수 있는 공간 역시 컴퓨터 구조 위이다. 우리는 상자에서 컴퓨터구조, 운영체제, 자료구조 순으로 꺼낼 수 있으며 위에서 봤을 때 상자의 밑바닥이 보이면 책을 꺼내는 것이 불가능해진다.

빈 상자와 넣을 책이 3권일 때
자료구조 책을 상자에 넣었을 때
모든 책을 다 넣은 상태

상자에 자료구조가 있는 지 확인하려면 컴퓨터 구조 책부터 순서대로 꺼내가면서 확인을 해야한다.


Stack의 구현

이론적인 Stack (list자료형의 append(), pop() 사용하지 않음)

주소에 의한 참조가 가능하다는 조건에서 만들기 위해 dictionary 자료형을 사용했습니다. del 함수는 사전 자료형에서 시간복잡도가 1입니다.

class Stack:
    def __init__(self):     # 스택의 정의
        self.stack = dict()
        self.top = -1

    def push(self, data):   
        self.top += 1 
        self.stack[self.top] = data

    def pop(self):          
        if self.is_empty(): # 스택이 비었으면 pop이 불가능
            return "Stack is Empty"
        tmp = self.stack[self.top]
        del self.stack[self.top]
        self.top -= 1       

        return tmp

    def size(self):
        return self.top+1

    def is_empty(self):     # stack이 비어있는지 확인
        if self.top == -1:
            return True
        else:
            return False

    def peak(self):         # top이 가리키는 값
        if not self.is_empty():
            return self.stack[self.top]
        else:
            return "Stack is Empty"

    def __repr__(self):
        return repr(self.stack)

확인 코드

stack = Stack()
stack.push("자료구조")
print("top : {}, peak : {}, stack : {}".format(stack.top, stack.peak(), stack))
stack.push("운영체제")
print("top : {}, peak : {}, stack : {}".format(stack.top, stack.peak(), stack))
stack.push("컴퓨터구조")
print("top : {}, peak : {}, stack : {}".format(stack.top, stack.peak(), stack))
print("pop : {}".format(stack.pop()))
print("top : {}, peak : {}, stack : {}".format(stack.top, stack.peak(), stack))
print("pop : {}".format(stack.pop()))
print("top : {}, peak : {}, stack : {}".format(stack.top, stack.peak(), stack))
stack.push("알고리즘")
print("top : {}, peak : {}, stack : {}".format(stack.top, stack.peak(), stack))
print("pop : {}".format(stack.pop()))
print("top : {}, peak : {}, stack : {}".format(stack.top, stack.peak(), stack))
print("pop : {}".format(stack.pop()))
print("top : {}, peak : {}, stack : {}".format(stack.top, stack.peak(), stack))

결과


위의 코드는 이론적인 부분을 확인하기 위한 코드로 실제 파이썬에서는 list 자료형의 popappend를 이용하여 구현하는 것이 쉽습니다.

Stack 객체 구현 (list자료형 pop(), append() 사용 )

class Stack:
    def __init__(self):
        self.stack = []
        self.top = -1
    def push(self, data):
        self.stack.append(data)
        self.top += 1
    def pop(self):
        if self.is_empty():
            return "Stack is Empty"
        self.top -= 1
        return self.stack.pop()

    def size(self):
        return len(self.stack)

    def is_empty(self):
        if not self.stack:
            return True
        else:
            return False

    def peak(self):
        if not self.is_empty():
            return self.stack[-1]
        else:
            return "Stack is Empty"

    def __repr__(self):
        return repr(self.stack)

확인 코드 결과


파이썬에서 스택을 굳이 직접 구현해야하는가?

파이썬의 list자료형은 위의 예제에서 봤듯이 pop()함수가 사용가능하고 push대신 append() 함수를 사용하면 스택처럼 리스트의 사용이 가능하다. 두 함수 모두 시간 복잡도는 O(1)이기 때문에 스택에서의 구현과 동일하다. top 또한 len - 1로, peak 또한 list[-1] 로 구현이 가능한 만큼 제공되는 함수를 사용하는 것이 유리하다고 생각한다.

찾는 item이 있는지 확인하는 부분이 리스트는 앞에서 확인하지만 스택은 뒤에서부터 확인한다는 점이 조금 다르긴 할 것이다.

+ Recent posts