Immersion In Data

Python/이것이 코딩테스트다

[Python] 09. 스택, 큐, 재귀 함수

sungjunminn 2022. 11. 21. 15:50

탐색

많은 양의 데이터 중에서 원하는 데이터를 찾는 과정

 

자료구조 

데이터를 표현하고, 관리하고, 처리하기 위한 구조

스택(Stack)과 큐(Queue)를 구성하는 핵심 함수

  • 삽입(Push) : 데이터를 삽입한다. 
  • 삭제(Pop) : 데이터를 삭제한다. 

실제로는 스택(Stack)과 큐(Queue)를 사용할 때, 삽입과 삭제 외에도 오버플로와 언더플로를 고민해야 한다. 

  • 오버플로(Overflow) : 특정한 자료구조가 수용할 수 있는 데이터의 크기를 이미 가득 찬 상태에서 삽입 연산을 수행할 때 발생한다. 즉, 저장 공간을 벗어나 데이터가 넘쳐흐를 때 발생한다. 
  • 언더플로(Underflow) : 특정한 자료구조에 데이터가 전혀 들어 있지 않은 상태에서 삭제 연산을 수행해 데이터가 전혀 없는 상태이다. 

 

1. 스택(Stack)

스택은 박스 쌓기에 비유할 수 있다. 흔히 박스는 아래에서부터 위로 차곡차곡 쌓는다. 그리고 아래에 있는 박스를 치우기 위해서는 위에 있는 박스를 먼저 내려야 한다. 이러한 구조를 선입후출(First In Last Out) 구조 또는 후입선출(Last In First Out) 구조라고 한다. 

쉽게 말해, 먼저 들어온 수는 가장 나중에 나간다. 이때 시간 복잡도는 O(1)이다. 

stack = []

# 삽입(5) - 삽입(2) - 삽입(3) - 삽입(7) - 삭제() - 삽입(1) - 삽입(4) - 삭제()
stack.append(5)
stack.append(2)
stack.append(3)
stack.append(7)
stack.pop()
stack.append(1)
stack.append(4)
stack.pop()

print(stack) # 최하단 원소부터 출력
print(stack[::-1]) # 최상단 원소부터 출력
# [5, 2, 3, 1]
# [1, 3, 2, 5]

파이썬에서 스택을 이용할 때에는 별도의 라이브러리를 사용할 필요가 없다. 기본 리스트에서 append()와 pop() 메서드를 이용하면 스택 자료구조와 동일하게 동작한다. 

  • append() : 가장 뒤 쪽에 데이터 삽입
  • pop() : 가장 뒤 쪽에서 데이터를 꺼냄

 

2. 큐(Queue)

큐(Queue)는 대기 줄에 비유할 수 있다. 새치기가 없다고 가정하면, 먼저 줄을 선 사람이 먼저 들어갈 수 있다. 

이러한 구조를 선입선출(First In First Out) 구조라고 한다. 

from collections import deque

# 큐(Queue) 구현을 위해 deque 라이브러리 사용
queue = deque()

# 삽입(5) - 삽입(2) - 삽입(3) - 삽입(7) - 삭제() - 삽입(1) - 삽입(4) - 삭제()
queue.append(5)
queue.append(2)
queue.append(3)
queue.append(7)
queue.popleft()
queue.append(1)
queue.append(4)
queue.popleft()

print(queue)        # 먼저 들어온 순서대로 출력
queue.reverse()     # 다음 출력을 위해 역순으로 바꾸기
print(queue)        # 나중에 들어온 원소부터 출력
# deque([3, 7, 1, 4])
# deque([4, 1, 7, 3])

파이썬으로 큐를 구현할 때는 collections 모듈에서 제공하는 deque 자료구조를 활용하자. deque는 스택과 큐의 장점을 모두 채택한 것인데, 데이터를 넣고 빼는 속도가 리스트 자료형에 비해 효율적이며 queue 라이브러리를 이용하는 것보다 더 간단하다. deque 객체를 리스트 자료형으로 변경하고자 한다면 list() 메서드를 이용하자. - list(queue)

 

3. 재귀 함수(Recursive Function)

재귀함수(Recursive Function)는 자기 자신을 다시 호출하는 함수이다. 

def recursive_function():
    print('재귀 함수를 호출합니다.')
    recursive_function()

recursive_function()

위 코드를 실행시키면, 다음과 같은 오류 메시지를 볼 수 있다.

RecursionError: maximum recursion depth exceeded while calling a Python object

이 오류 메시지는 재귀의 최대 깊이를 초과했다는 내용이다. 보통 파이썬 인터프리터는 호출 횟수 제한이 있는데 이 한계를 벗어났기 때문이다. 따라서 무한대로 재귀 호출을 진행할 수는 없다. 

대신, 다음과 같이 종료조건을 준다. 

def recursive_function(i):
    # 100번째 출력했을 때 종료되도록 종료 조건 명시
    if i == 100:
        return
    print(i, '번째 재귀 함수에서', i + 1, '번째 재귀 함수를 호출합니다.')
    recursive_function(i + 1)
    print(i, '번째 재귀 함수를 종료합니다.')

recursive_function(1)
# 1 번째 재귀 함수에서 2 번째 재귀 함수를 호출합니다.
# 2 번째 재귀 함수에서 3 번째 재귀 함수를 호출합니다.
# 3 번째 재귀 함수에서 4 번째 재귀 함수를 호출합니다.
# 4 번째 재귀 함수에서 5 번째 재귀 함수를 호출합니다.
# 5 번째 재귀 함수에서 6 번째 재귀 함수를 호출합니다.
.
.
.
# 4 번째 재귀 함수를 종료합니다.
# 3 번째 재귀 함수를 종료합니다.
# 2 번째 재귀 함수를 종료합니다.
# 1 번째 재귀 함수를 종료합니다.

재귀 함수의 수행은 스택(Stack) 자료구조를 이용한다. 함수를 계속 호출했을 때, 가장 마지막에 호출한 함수가 먼저 수행을 끝내야 그 앞의 함수 호출이 종료되기 때문이다. 결론적으로 재귀 함수는 내부적으로 스택 자료구조와 동일하다는 것을 기억하자. 

재귀 함수를 이용하는 문제는 대표적으로 팩토리얼(factorial) 문제가 있다. 팩토리얼 문제는 반복문과 재귀 함수로 구현할 수 있다. 

# 반복적으로 구현한 n!
def factorial_iterative(n):
    result = 1
    # 1부터 n까지의 수를 차례대로 곱하기
    for i in range(1, n + 1):
        result *= i
    return result

# 재귀적으로 구현한 n!
def factorial_recursive(n):
    if n <= 1: # n이 1 이하인 경우 1을 반환
        return 1
    # n! = n * (n - 1)!를 그대로 코드로 작성하기
    return n * factorial_recursive(n - 1)

# 각각의 방식으로 구현한 n! 출력(n = 5)
print('반복적으로 구현 : ', factorial_iterative(5))
print('재귀적으로 구현 : ', factorial_recursive(5))

# 반복적으로 구현 :  120
# 재귀적으로 구현 :  120

위의 코드를 비교했을 때 재귀 함수의 코드가 더 간결한 것을 알 수 있다. 이렇게 간결해진 이유는 재귀 함수가 수학의 점화식(재귀식)을 그대로 소스코드로 옮겼기 때문이다. 수학의 점화식은 특정한 함수를 자신보다 더 작은 변수에 대한 함수와의 관계로 표현한 것을 의미한다. 

 

 

 

'이것이 코딩테스트다'를 읽고 공부한 내용을 바탕으로 작성하였습니다. 

'Python > 이것이 코딩테스트다' 카테고리의 다른 글

[Python] 11. BFS  (0) 2022.11.22
[Python] 10. DFS  (0) 2022.11.22
[Python] 8. 구현  (0) 2022.11.17
[Python] 7. 그리디  (0) 2022.11.15
[Python] 6. 순열과 조합  (0) 2022.11.14