본문 바로가기
Hobby/Hobby_4 - Coding

[코딩공부] 필수알고리즘w파이썬 - 2. 스택(Stack)와 큐(Que)

by 와우멍 2021. 1. 6.

안녕하세요 와우멍입니다.

오늘은 알고리즘 기초지식을 쌓기 위해 읽고 있는 '필수알고리즘 with 파이썬'을 이어서 정리하겠습니다.

이전글 링크


 

스택(Stack) 알고리즘 

출처: 위키피디아 (https://ko.wikipedia.org/wiki/스택)

- 입력과 출력을 한 방향으로 제한한 알고리즘

 (배열은 인덱스를 통해 어느 곳이든 엑세스가 가능)

 (연결리스트는 링크 통해서 노드를 검색하고, 새로운 노드를 중간에 삽입하거나 기존의 노드를 삭제하는 것이 가능, 삽입될 위치를 검색하고 링크를 연결시켜야 하므로 간단하지는 않지만)

 - 스택은 차곡차곡 쌓는 개념, LIFO(Last In First Out)

 - 위(Back)에 쌓는 것이 push, 꺼내는 것이 pop.

 * Undo (Ctrl + Z)할 때 사용하면 효율적이겠지

 

스택(Stack) 예제 - 1. 기본

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def push(item):
    stack.append(item)
def pop():
    return stack.pop()
 
if __name__=='__main__':
    stack=[]
    push(1)
    push(2)
    push(3)
    print("Stack now:", stack)
    
    while stack:
        pop()
        print("Stack now: {}".format(stack))
cs

  __main__에서 빈 리스트인 stack1, 2, 3이 순서대로 쌓여서 [1, 2, 3]이 된 것을 확인

  while문에서 stack의 원소가 있는 동안 pop을 하는데, 역순으로 3, 2, 1순으로 버려지는 것을 볼 수 있다.


 

스택 예제 - 2. 계산기

1
2
3
4
5
6
7
8
if operator == '+':
    ret = operand_1 + operand_2
elif operator == '-':
    ret = operand_1 - operand_2
elif operator == '*':
    ret = operand_1 * operand_2
elif operator == '/':
    ret = operand_1 // operand_2
cs

 - 계산기라는걸 단순히 생각해보면 다음과 같지만, 계산할 때 곱하기와 나누기가 먼저 수행되어야 하고 또 괄호가 있으면 괄호 안의 계산이 먼저 수행되어야 한다.

 - 이 과정에서는 피연산자(operand)와 연산자(operator)를 나눠서 stack함.  첫 단계에서, 피연산자 2개와 연산자 1개를 각각 pop해서 계산 후 다시 stack, 다음에 피연산자 2개와 연산자 1개를 pop해서 결과 계산. 다만, 이러면 우선순위가 순서와 다른 1+2*3의 결과는 달라지겠지.

 - stack할 때, 제일 위의 연산자와 push할 연산자를 비교하여 새로 저장할 연산자의 우선순위가 높으면 그대로 push하는 방식으로 연산자와 피연사자를 쌓는다. 자세한 건 아래 두 예제를 비교하면서 이해해봅시다.

 

예제) 2 * 3 + 1 과정

 1. (2 * 3)을 피연산자와 연산자 그룹에 각각 push

    - 피연산자(Operand): [2, 3]

    - 연산자(Operator): [*]  

2. 1과 +을 push하는데, +는 *보다 우선순위가 낮으니 있던 애들을 꺼내서 계산후 먼저 push

    - 피연산자(Operand): [6, 1]

    - 연산자(Operator): [+]  

3. [6, 1]과 [+]가 꺼내서 계산

   - 6 + 1 해서 7이 나옴.

 

예제) 2 + 3 * 1 과정

1. (2 + 3)을 피연산자와 연산자 그룹에 각각 push

    - 피연산자(Operand): [2, 3]

    - 연산자(Operator): [+]  

2. *와 1을 push하는데, 우선순위 *가 높으니 그대로 push

   - 피연산자(Operand): [2, 3, 1]

   - 연산자(Operator): [+, *]

3. 위부터 차례대로 3, 1, *를 꺼내서 계산후 피연산자에 push

   - 피연산자(Operand): [2, 3]

   - 연산자(Operator): [+]

4. [2, 3]과 [+]를 꺼내서 계산

  - 2 + 3 해서 5가 나옴.

 

**그럼 더 긴 경우에 대해서는 어떻게 되는거지? 연산자끼리만 말고 뒤에 괄호도 있고 하면..? (TBA)


 

큐(Queue) 알고리즘

 - Stack와 비슷하지만, 큐(Queue)는 LIFO가 아닌 FIFO(First In First Out)

 - 매표소에서 줄서는 것과 비슷, 데이터가 들어오는 위치는 뒤(Rear or Back), 나가는 부분은 앞(Front)라 정의.

 - 입력은 Enqueue(파이썬에서 put): 매표소에 줄을 서는 것

 - 출력은 Dequeue(파이썬에서 get): 온 순서대로 표를 사는 것

  (보급창고가 생각나네요 ㅋㅋㅋㅋ 선입선출)

 - 스택과 달리 배열에서 더 편리하게 이용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def put(item):
    queue.append(item)
def get():
    return queue.pop(0)
 
if __name__ == '__main__':
    queue = []
    put(1)
    put(2)
    put(3)
    print('Queue Now: ', queue)
    
    while queue:
        get()
        print("Queue now: {}".format(queue))
cs

 위의 stack에서의 pop에서와 달리, 먼저 입력된 순으로 1, 2, 3이 순차적으로 방출되는 것을 볼 수 있다.

 ** 여기서 get을 구현하기 위해서는 pop(0)로 해줘야 한다 (0번째 성분을 뽑는다는 뜻). 아니면 queue 모듈의 Queue()라는 Class를 이용하는 것으로 쉽게 할 수도 있다(put과 get이 Cython으로 자체구현되어 있음)

 ** 단순하게 생각하면 맨 앞의 한칸을 뺀다는 개념이지만, 실제 과정을 보면 맨앞의 한칸을 뽑으면서 비우고, 그 뒤의 데이터들을 한칸씩 앞으로 당기는 작업이 따르기 때문에, n번의 프로세스를 거치며 퍼포먼스가 떨어진다.

출처: '파이썬 알고리즘 인터뷰'를 인용한 나무위키

 - 큐(Queue)는 미리 정해놓은 배열의 크기만큼으로 사용할 수 밖에 없음. 그런데 이걸 원형 큐(Circular Queue)의 형태로 사용하면, 배열 전체 크기와 상관없이 효율적으로 사용할 수 있다.

 - 앞부분이 비는 것을 활용해서, 배열의 마지막 부분을 쓰면 다시 처음부터 큐를 채우기 시작하는 형태의 큐 (마지막 위치가 시작위치와 연결된다는 점에서 Ring Buffer라고도 함)

 - dequeue(=get)을 하게 되면 front포인터가 뒤로 이동하고, enqueue(=put)을 하게 되면 rear포인터가 앞으로 이동하고, 이 두개의 포인터가 만나면 공간이 없다는 에러가 발생하니 잘 조절해서 떨어지게 컨트롤한다.  (포인터는 위치를 지정한다고 생각하면 됨, C에서의 개념)


스택(Stack)와 큐(Queue)에 대해서는 이정도로 정리하겠습니다.

혹시 잘못된 것이나 추가하면 좋은 것이 있다면 편히 글 남겨주세요 :-)

댓글