Last In, First Out

개념 확인: Stacks


LIFO 순서로 연산을 추적하고 스택이 필요한 문제를 푼다.

문제 1

연산 열 추적

빈 스택에 다음 연산을 차례로 호출한다. 상태는 왼쪽이 바닥, 오른쪽이 맨 위다.

push(3) → push(7) → peek() → push(1) → pop() → pop() → size()
  1. 반환값이 있는 호출마다 반환값을 적는다.
  2. 모든 호출을 마친 뒤의 최종 스택 상태를 적는다.
  3. peek()과 pop()이 스택을 다르게 바꾸는 점을 적는다.
  4. 빈 스택에서 pop()을 호출하는 경우를 위해 명세가 정해야 하는 것을 적는다.
해설 펼치기
  1. peek()은 7, 첫 pop()은 1, 둘째 pop()은 7, size()는 1이다.
  2. [3]이다.
  3. peek()은 맨 위 값을 반환하고 스택을 바꾸지 않지만 pop()은 맨 위 값을 반환하며 삭제한다.
  4. 빈 스택에서 호출할 수 있는지와, 호출하면 어떤 결과(오류 등)가 되는지를 정해야 한다.
문제 2

맨 위를 어느 끝에 둘까

Python list _items로 스택을 구현한다. 원소가 n = 1,000개 있다. 각 연산은 처음 상태에서 따로 실행한다.

  1. 오른쪽 끝을 맨 위로 둘 때 push와 pop에 쓰는 메서드와 비용을 적는다.
  2. 왼쪽 앞을 맨 위로 둘 때 같은 연산의 메서드와 한 번에 옮기는 원소 수를 적는다.
  3. 두 구현이 모두 LIFO 결과를 내는 이유를 적는다.
  4. SLList의 head를 맨 위로 둔 스택에서 push와 pop의 비용을 적고 순회가 필요 없는 이유를 적는다.
  5. 오른쪽 끝 구현에서 한 번의 push가 Θ(n)이 될 수 있는 경우를 적는다.
해설 펼치기
  1. append(x)와 pop() 모두 상각 Θ(1)이다.
  2. insert(0, x)는 기존 원소 1,000개를, pop(0)은 뒤에 남은 999개를 옮긴다. 둘 다 Θ(n)이다.
  3. push와 pop이 같은 끝을 사용하기 때문이다.
  4. 둘 다 Θ(1)이다. 머리 앞에 연결하고 머리를 다음 노드로 옮기므로 앞 삽입·앞 삭제만 쓴다.
  5. 용량이 부족해 재할당하는 push는 원소를 모두 복사해 Θ(n)이다.
문제 3

괄호 검사

여는 괄호는 push, 닫는 괄호는 스택이 비어 있으면 실패하고 아니면 pop, 입력 끝에서 스택이 비어 있으면 성공이다.

  1. (()(()))를 끝까지 읽은 결과를 적는다.
  2. (()))(는 몇 번째 기호를 읽을 때 실패하는지와 그 이유를 적는다.
  3. ((())는 언제 실패가 드러나는지와 그때 스택에 남은 괄호 수를 적는다.
  4. ([)]처럼 괄호 종류가 섞이면 소괄호만 보는 코드에 무엇을 더해야 하는지 적는다.
해설 펼치기
  1. 모든 닫는 괄호가 짝을 찾고 끝에서 스택이 비어 있어 성공이다.
  2. 5번째 기호 )다. 앞의 네 기호에서 여는 괄호 두 개가 모두 짝을 이뤄 스택이 비었는데 닫는 괄호를 만났다.
  3. 입력을 모두 읽은 뒤 스택에 여는 괄호 1개가 남아 실패한다.
  4. 닫는 괄호를 만나면 꺼낸 여는 괄호와 종류가 짝인지 비교해야 한다. ([)]는 )를 만날 때 맨 위가 [라 실패한다.

오개념 점검: 여는 괄호와 닫는 괄호의 개수가 같다는 것만으로는 짝이 맞는다고 할 수 없다.

문제 4

후위식

  1. 중위 표기식 (A + B) × C를 후위식으로 바꾼다.
  2. 후위식 6 2 3 + ×를 평가하며 각 단계의 스택을 적는다.
  3. 후위식 9 3 - 2 /를 평가한다.
  4. 3에서 연산자를 만났을 때 두 번 pop한 값의 순서를 바꿔 계산하면 결과가 어떻게 되는지 적는다.
해설 펼치기
  1. A B + C ×다. 괄호 안의 +는 닫는 괄호에서 출력되고 ×는 입력 끝에서 출력된다.
  2. [6] → [6, 2] → [6, 2, 3] → +에서 3, 2를 pop해 5를 push [6, 5] → ×에서 30을 push [30]. 결과는 30이다.
  3. [9, 3]에서 -로 9 − 3 = 6, [6, 2]에서 /로 6 / 2 = 3이다.
  4. 처음 pop한 값이 오른쪽 피연산자다. 순서를 바꾸면 3 − 9 = −6, 2 / (−6) = −1/3이 된다. 뺄셈과 나눗셈은 순서가 중요하다.
문제 5

호출 스택과 진법 변환

  1. main이 a()를 부르고, a가 b()를 부른 뒤 b가 반환하고, 이어서 a가 c()를 부른다. 각 사건 뒤의 호출 스택(오른쪽이 맨 위)을 적는다.
  2. to_str(6, 2)가 재귀로 부르는 호출을 순서대로 적고, 가장 깊은 호출이 반환되는 순간 남아 있는 호출 프레임을 적는다.
  3. 10진수 13을 2로 계속 나눈 나머지를 스택에 push하고 맨 위부터 pop해 2진수를 만든다.
  4. 재귀 호출마다 같은 이름의 변수 n이 서로 섞이지 않는 이유를 적는다.
해설 펼치기
  1. [main] → [main, a] → [main, a, b] → b 반환 [main, a] → [main, a, c]다.
  2. to_str(6, 2) → to_str(3, 2) → to_str(1, 2)다. to_str(1, 2)가 반환하는 순간 6과 3을 받은 두 호출 프레임이 남아 있다. 결과는 "110"이다.
  3. 나머지는 1, 0, 1, 1 순서로 push되고 pop하면 1101이다.
  4. 호출 프레임이 호출마다 인자와 지역 변수를 따로 저장하기 때문이다.