[Stack/Queue/Recursive Function] 꼭 필요한 자료구조 기초

Update:     Updated:

카테고리:

태그:

1. 꼭 필요한 자료구조 기초

탐색이란 많은 양의 데이터 중에서 원하는 데이터를 찾는 과정을 의미한다. DSF와 BFS를 제대로 이해하려면 기본 자료구조인 스택과 큐에 대한 이해가 전제되어야 하므로 사전 학습이 필요하다.
자료구조란 데이터를 표현하고 관리하고 처리하기 위한 구조를 의미한다. 그 중 스택과 큐는 자료구조의 기초 개념으로 다음의 두 핵심적인 함수로 구성된다.

  • 삽입(push) : 데이터를 삽입한다.
  • 삭제(pop) : 데이터를 삭제한다.

스택과 큐를 사용할 때는 삽입과 삭제 이외에도 오버플로와 언더플로를 고민해야 한다.
오버플로는 특정한 자료구조가 수용할 수 있는 데이터의 크기를 이미 가득 찬 상태에서 삽입연산을 수행할 때 발생한다.
언더플로는 데이터가 전혀 들어 있지 않은 상태에서 삭제 연산을 수행할 때 발생한다.

스택

스택(stack)은 박스 쌓기에 비유할 수 있다. 박스를 쌓을 때에는 위에서부터 차곡차곡 쌓지만 박스를 치울때는 위에서부터 하나씩 빼야한다. 이러한 구조를 선입후출(First In Last Out) 또는 후입선출(Last In First Out) 구조라고 한다.

image

한번 스택의 과정을 예로 들어보자!
삽입(5) - 삽입(2) - 삽입(3) - 삽입(7) - 삭제() - 삽입(1) - 삽입(4) - 삭제()

step 0 초기 단계

step 1 삽입(5) - 삽입(2) - 삽입(3) - 삽입(7) - 삭제() - 삽입(1) - 삽입(4) - 삭제()
5

step 2 삽입(5) - 삽입(2) - 삽입(3) - 삽입(7) - 삭제() - 삽입(1) - 삽입(4) - 삭제()
5 2

step 3 삽입(5) - 삽입(2) - 삽입(3) - 삽입(7) - 삭제() - 삽입(1) - 삽입(4) - 삭제()
5 2 3

step 4 삽입(5) - 삽입(2) - 삽입(3) - 삽입(7) - 삭제() - 삽입(1) - 삽입(4) - 삭제()
5 2 3 7

step 5 삽입(5) - 삽입(2) - 삽입(3) - 삽입(7) - 삭제() - 삽입(1) - 삽입(4) - 삭제()
5 2 3

step 6 삽입(5) - 삽입(2) - 삽입(3) - 삽입(7) - 삭제() - 삽입(1) - 삽입(4) - 삭제()
5 2 3 1

step 7 삽입(5) - 삽입(2) - 삽입(3) - 삽입(7) - 삭제() - 삽입(1) - 삽입(4) - 삭제()
5 2 3 1 4

step 8 삽입(5) - 삽입(2) - 삽입(3) - 삽입(7) - 삭제() - 삽입(1) - 삽입(4) - 삭제()
5 2 3 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]) # 최상단 원소부터 출력

별도의 라이브러리 필요없이 append()와 pop() 함수를 이용하면 스택 자료구조를 구현할 수 있다.
append()는 리스트 가장 뒤쪽에 데이터를 삽입하는 것이고, pop()은 리스트의 가장 뒤쪽에서 데이터를 꺼내는 것이다.

큐(Queue)는 대기 줄에 비유할 수 있다. 예를 들어 놀이동산에서 줄을 설 경우 먼저 온 사람이 먼저들어가고, 나중에 온 사람은 나중에 들어온다. 이와 같은 구조를 선입선출(First In First Out)라고 한다.

image

한번 큐의 과정을 예로 들어보자.
삽입(5) - 삽입(2) - 삽입(3) - 삽입(7) - 삭제() - 삽입(1) - 삽입(4) - 삭제()

step 0 초기 단계

step 1 삽입(5) - 삽입(2) - 삽입(3) - 삽입(7) - 삭제() - 삽입(1) - 삽입(4) - 삭제()
5

step 2 삽입(5) - 삽입(2) - 삽입(3) - 삽입(7) - 삭제() - 삽입(1) - 삽입(4) - 삭제()
2 5

step 3 삽입(5) - 삽입(2) - 삽입(3) - 삽입(7) - 삭제() - 삽입(1) - 삽입(4) - 삭제()
3 2 5

step 4 삽입(5) - 삽입(2) - 삽입(3) - 삽입(7) - 삭제() - 삽입(1) - 삽입(4) - 삭제()
7 3 2 5

step 5 삽입(5) - 삽입(2) - 삽입(3) - 삽입(7) - 삭제() - 삽입(1) - 삽입(4) - 삭제()
7 3 2

step 6 삽입(5) - 삽입(2) - 삽입(3) - 삽입(7) - 삭제() - 삽입(1) - 삽입(4) - 삭제()
1 7 3 2

step 7 삽입(5) - 삽입(2) - 삽입(3) - 삽입(7) - 삭제() - 삽입(1) - 삽입(4) - 삭제()
4 1 7 3 2

step 8 삽입(5) - 삽입(2) - 삽입(3) - 삽입(7) - 삭제() - 삽입(1) - 삽입(4) - 삭제()
4 1 7 3

이를 파이썬 코드로 표현하면 아래와 같다.

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) # 나중에 들어온 원소부터 출력

collections 모듈에서 제공하는 deque는 스택과 큐의 장점을 모두 채택한 것인데 데이터를 넣고 빼는 속도가 리스트 자료형에 비해서 효율적이며 queue 라이브러리를 이용하는 것보다 더 간단하다. 대부분의 코딩테스트에서 기본 라이브러리 사용 허용

재귀 함수

DFS와 BFS를 구현하려면 재귀 함수도 이해하고 있어야 한다. 재귀 함수(Recursive Function)란 자기 자신을 다시 호출하는 함수를 의미한다.

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

recursive_function()

재귀 함수의 종료 조건
재귀 함수를 문제 풀이에서 사용할 때는 무한 호출을 방지하기 위해서 재귀 함수가 언제 끝날지. 종료 조건을 꼭 명시해야 한다.

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

recursive_function(1)

컴퓨터 내부에서 재귀 함수의 수행은 스택 자료구조를 이용한다. 그 이유는 함수를 계속 호출했을 때 가장 마지막에 호출한 함수가 먼저 수행을 끝내야 그 앞의 함수 호출이 종료되기 때문이다.
재귀함수는 내부적으로 스택 자료구조와 동일하다.
재귀 함수를 이용하는 대표적인 예제로는 팩토리얼(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)  

반복적으로 구현하는 방법보다 재귀적으로 구현했을 때 얻을 수 있는 장점은 무엇일까?

코드의 간결성 간결해진 이유는 재귀 함수가 수학의 점화식을 그대로 소스코드로 옮겼기 때문이다. 추후, 다이나믹 프로그래밍으로 이어진다.


🐢 현재 공부하고 있는 이것이 취업을 위한 코딩 테스트다 with 파이썬 - 나동빈 저자 의 책을 학습하며 기록 및 정리를 하기위한 내용들입니다. 🐢

감사합니다.😊

Algorithm 카테고리 내 다른 글 보러가기

댓글남기기