알고리즘 입문
개념 배우기직접 풀어보기
놀면 뭐 하니,
그냥 하는거야.

쌓거나 줄 세우거나! 스택과 큐

코딩냥

안녕, 나 코딩냥이다냥! 오늘 배우는 건 새로운 문법이 아니라 꺼내는 순서를 정하는 규칙이라냥. 리스트는 이미 알고 있으니, 거기에 규칙 하나만 얹으면 된다냥. 규칙이 두 개뿐인데 이게 없으면 못 푸는 문제가 잔뜩 있다냥.

오늘의 학습 목표

  • 스택과 큐의 차이를 꺼내는 순서로 설명한다.
  • 파이썬에서 큐를 만들 때 deque 를 쓰는 이유를 안다.
  • 스택으로 짝을 맞추고, 식을 계산한다.
  • 아직 답을 못 찾은 것을 쌓아두는 스택 활용을 익힌다.

1단계 · 스택은 쌓아 올린 접시라냥 (7분)

코딩냥

스택은 나중에 넣은 것이 먼저 나온다냥. 이미 리스트로 쓰고 있던 거라 새로 배울 게 거의 없다냥.

st = []
st.append(1)      # 넣기
st.append(2)
st.append(3)
print(st.pop())   # 3  — 마지막에 넣은 것
print(st.pop())   # 2
print(st[-1])     # 1  — 꺼내지 않고 맨 위만 보기

비유로 이해하기 — 쌓아 올린 접시

접시를 쌓을 때는 위에 올리고, 뺄 때도 위에서 뺀다냥. 맨 아래 접시를 쓰려면 위를 다 치워야 한다냥.

  • 넣기 — append — 접시를 위에 올린다
  • 꺼내기 — pop — 위에서 뺀다
  • 보기만 — st[-1] — 맨 위를 본다

빈 스택에서 pop 을 하면 IndexError 로 터진다냥.

꺼내기 전에 항상 비었는지 확인하자냥.

if st:
    x = st.pop()

오늘 괄호 검사 문제에서 이걸 빼먹으면 바로 에러가 난다냥.

2단계 · 큐는 줄 서기라냥 (10분)

코딩냥

큐는 반대라냥. 먼저 넣은 것이 먼저 나온다냥. 매표소 줄이랑 똑같다냥. 그런데 여기에 파이썬만의 함정이 하나 있다냥.

리스트로도 큐를 흉내낼 수는 있다냥.

q = []
q.append(1)
q.append(2)
print(q.pop(0))   # 1  — 맨 앞에서 꺼낸다냥

pop(0) 은 느리다냥. 이게 오늘 꼭 기억할 부분이라냥.

맨 앞을 빼면 뒤에 있는 원소가 전부 한 칸씩 앞으로 당겨진다냥. 원소가 n개면 한 번 꺼낼 때마다 n번 옮기는 셈이라냥.

pop() (맨 뒤)은 당길 게 없어서 빠르다냥. 앞에서 빼는 것만 느리다냥.

해결collections 의 deque 를 쓴다냥
from collections import deque

q = deque()
q.append(1)        # 뒤에 넣기
q.append(2)
print(q.popleft()) # 1  — 앞에서 꺼내기, 빠르다냥

deque 는 양쪽 끝이 다 빠른 자료구조라냥. 큐가 필요하면 그냥 이걸 쓰자냥. 스택으로 쓸 거면 리스트로 충분하다냥.

얼마나 차이 나는지 오늘 4번 문제에서 직접 확인할 수 있다냥. 사람 4000명을 4000번째마다 빼는 문제인데, 채점기에서 재보면 이렇다냥.

  • deque.rotate() — 약 0.03초
  • deque.popleft() — 약 1.6초
  • list.pop(0) — 시간 초과

같은 알고리즘인데 자료구조 하나로 통과와 실패가 갈린다냥.

3단계 · 스택으로 짝 맞추기 (10분)

코딩냥

스택이 제일 빛나는 곳이라냥. 괄호 검사는 스택을 배우면 몇 줄로 끝난다냥.

발상여는 괄호는 쌓고, 닫는 괄호는 하나 꺼낸다냥
  1. ( 를 만나면 스택에 쌓는다
  2. ) 를 만나면 스택에서 하나 꺼낸다
  3. 꺼낼 게 없는데 닫으려 하면 → 틀렸다
  4. 다 보고 났는데 스택에 남아 있으면 → 틀렸다

3번과 4번을 둘 다 확인해야 한다냥.

s = input().strip()
st = []
ok = True

for ch in s:
    if ch == '(':
        st.append(ch)
    else:
        if not st:        # 닫을 게 없다냥
            ok = False
            break
        st.pop()

print('YES' if ok and not st else 'NO')

개수만 세면 안 된다냥.

print('YES' if s.count('(') == s.count(')') else 'NO')   # 틀렸다냥!

)( 는 개수가 똑같이 하나씩이지만 올바르지 않다냥. 순서가 틀렸기 때문이라냥. 개수 세기로는 순서를 볼 수 없다냥.

괄호가 (), [], {} 세 종류면 스택에 종류까지 담아두고 꺼낼 때 대조하면 된다냥.

pair = {')': '(', ']': '[', '}': '{'}

if not st or st[-1] != pair[ch]:
    ok = False

이렇게 해야 ([)] 처럼 엇갈린 것을 잡아낸다냥. 종류를 안 보면 이게 통과해버린다냥.

4단계 · 스택으로 계산하기 (10분)

코딩냥

스택은 계산기도 된다냥. 후위 표기식이라는 걸 쓰면 괄호 없이도 순서가 정해진다냥.

3 4 + 는 3 + 4 라는 뜻이라냥. 연산자가 뒤에 온다냥.

발상숫자는 쌓고, 연산자는 두 개 꺼내서 합친다냥

3 4 + 2 * 를 따라가보자냥.

읽은 토큰스택
3[3]
4[3, 4]
+[7]
2[7, 2]
*[14]

끝나면 스택에 답 하나만 남는다냥.

for t in tokens:
    if t in ('+', '-', '*'):
        b = st.pop()      # 나중에 꺼낸 게 오른쪽이라냥
        a = st.pop()
        st.append(a + b if t == '+' else a - b if t == '-' else a * b)
    else:
        st.append(int(t))

빼기에서 순서가 뒤집히기 쉽다냥.

스택에서 먼저 꺼낸 것이 오른쪽 피연산자라냥. 10 3 - 는 10 - 3 이니 7 이라냥.

a = st.pop()      # 이러면 a 가 3 이 된다냥
b = st.pop()
st.append(a - b)  # 3 - 10 = -7  틀렸다냥!

더하기와 곱하기는 순서를 바꿔도 답이 같아서 빼기에서만 티가 난다냥. 그래서 더 못 찾는다냥.

5단계 · 답을 못 찾은 것을 쌓아둔다냥 (12분)

코딩냥

오늘의 마지막이자 제일 재밌는 쓰임새라냥. 스택에 숫자가 아니라 아직 처리 못 한 일을 쌓아두는 거라냥.

각 수의 오른쪽에서 자기보다 큰 첫 번째 수를 찾는 문제라냥. 이걸 오큰수라고 한다냥.

그냥 오른쪽으로 하나씩 훑으면 되지 않냥? 되긴 된다냥. 느려서 문제라냥.

수가 n개면 최악의 경우 n × n 번 비교한다냥. n 이 12000만 돼도 시간 초과가 난다냥. 오늘 5번 문제의 숨겨진 테스트가 정확히 그거라냥.

발상아직 답을 못 찾은 인덱스를 쌓아둔다냥

왼쪽부터 한 번만 훑는다냥.

  1. 새 수를 본다
  2. 스택 맨 위의 수가 새 수보다 작으면, 그 수의 답은 새 수라냥. 꺼내서 답을 적는다
  3. 작은 게 계속 나오면 계속 꺼낸다
  4. 그리고 새 수의 인덱스를 쌓는다

끝까지 갔는데 스택에 남아 있으면 그것들은 답이 -1 이라냥.

ans = [-1] * n
st = []

for i in range(n):
    while st and arr[st[-1]] < arr[i]:
        ans[st.pop()] = arr[i]
    st.append(i)

비유로 이해하기 — 대기 명단

스택에 쌓인 건 "내 오큰수 아직 못 찾았어요" 하고 기다리는 번호표라냥.

새 수가 나타나면 기다리던 사람 중 그 수보다 작은 사람들은 전부 답을 찾은 것이라냥. 한꺼번에 처리하고 명단에서 지운다냥.

각 번호표는 한 번 쌓이고 한 번 지워질 뿐이라 전체가 빠르다냥.

while 안에 pop 이 있어서 이중 반복문처럼 보이지만 느리지 않다냥.

각 원소는 평생 한 번 쌓이고 한 번 꺼내진다냥. 그래서 전체 작업량이 n 에 비례한다냥. 겉모습이 아니라 총 몇 번 꺼내지냐로 세는 게 요령이라냥.

오늘 배운 내용 정리

코딩냥

먼저 스스로 답해보고, "정답 보기"를 눌러서 맞는지 확인해보자냥!

Q1.스택과 큐는 뭐가 다르냥?
Q2.파이썬에서 큐를 만들 때 리스트의 pop(0) 을 쓰면 왜 안 되냥?
Q3.괄호 검사에서 개수만 세면 왜 틀리냥?
Q4.후위 표기식에서 뺄셈을 할 때 주의할 점이 뭐냥?
Q5.오큰수에서 while 안에 pop 이 있는데 왜 느리지 않냥?

이제 직접 풀어볼 차례다냥

코딩냥

오늘 문제는 다섯 개라냥. 12번은 짝 맞추기, 3번은 계산기, 4번은 큐, 5번은 오큰수라냥. 4번과 5번은 자료구조를 잘못 고르면 시간 초과가 나니까 오늘 배운 걸 꼭 떠올리자냥. 가보자냥