쌓거나 줄 세우거나! 스택과 큐
안녕, 나 코딩냥이다냥! 오늘 배우는 건 새로운 문법이 아니라 꺼내는 순서를 정하는 규칙이라냥. 리스트는 이미 알고 있으니, 거기에 규칙 하나만 얹으면 된다냥. 규칙이 두 개뿐인데 이게 없으면 못 푸는 문제가 잔뜩 있다냥.
오늘의 학습 목표
- 스택과 큐의 차이를 꺼내는 순서로 설명한다.
- 파이썬에서 큐를 만들 때
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() (맨 뒤)은 당길 게 없어서 빠르다냥. 앞에서 빼는 것만 느리다냥.
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분)
스택이 제일 빛나는 곳이라냥. 괄호 검사는 스택을 배우면 몇 줄로 끝난다냥.
(를 만나면 스택에 쌓는다)를 만나면 스택에서 하나 꺼낸다- 꺼낼 게 없는데 닫으려 하면 → 틀렸다
- 다 보고 났는데 스택에 남아 있으면 → 틀렸다
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 * 를 따라가보자냥.
끝나면 스택에 답 하나만 남는다냥.
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 이라냥.
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 에 비례한다냥. 겉모습이 아니라 총 몇 번 꺼내지냐로 세는 게
요령이라냥.
오늘 배운 내용 정리
먼저 스스로 답해보고, "정답 보기"를 눌러서 맞는지 확인해보자냥!
이제 직접 풀어볼 차례다냥
오늘 문제는 다섯 개라냥. 12번은 짝 맞추기, 3번은 계산기, 4번은 큐,
5번은 오큰수라냥. 4번과 5번은 자료구조를 잘못 고르면 시간 초과가
나니까 오늘 배운 걸 꼭 떠올리자냥. 가보자냥
