본문 바로가기
Algorithm/취업을 위한 코딩테스트 with Python

DFS & BFS를 위한 (선형)자료구조의 기초

by millar 2023. 5. 11.

Data Structure

 

 

 탐색이란 많은 양의 데이터 중에서 원하는 데이터를 찾는 과정을 의미한다. 프로그래밍에서는 그래프, 트리 등의 자료구조 안에서 탐색을 하는 문제를 자주 다룬다. 대표적인 탐색 알고리즘으로 DFS와 BFS를 꼽을 수 있는데 이 두 알고리즘의 원리를 제대로 이해해야 코딩 테스트의 탐색 문제 유형을 풀 수 있다.

 

 자료구조데이터를 표현, 관리, 처리하기 위한 구조를 의미한다. 그중 스택과 큐는 자료구조의 기초 개념으로 다음 두 핵심적인 함수로 구성된다.

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

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

 

 

- Part 02 _ Chapter 05 DFS/BFS 124p


스택

 

 

 

 스택은 박스 쌓기에 비유할 수 있다. 흔히 박스는 아래에서부터 위로 쌓는다. 아래에 박스를 치우기 위해선, 위에 있는 박스를 먼저 내려야 한다. 이러한 구조를 선입후출, 후입선출 구조라고 한다.

 

파이썬 코드로 스택 구조를 표현해보면 다음과 같다.

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() 메서드는 리스트의 가장 뒤쪽에서 데이터를 꺼내기 때문이다.

 


 

 

 

 큐는 대기 줄에 비유할 수 있다. 우리가 흔히 놀이공원에 입장하기 위해 줄을 설 댸, 먼저 온 사람이 먼저 들어가게 된다. 나중에 온 사람일수록 나중에 들어가기 때문에 흔히 '공정한' 자료구조라고 비유한다. 이러한 구조를 선입선출 구조라고 한다.

 

파이썬 코드로 큐 구조를 표현하면 다음과 같다.

from collections import deque

queue = deque()

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 라이브러리를 이용하는 것보다 더 간단하다. 더불어 대부분의 코딩 테스트에서는 collections 모듈과 같은 기본 라이브러리 사용을 허용하므로 안심하고 사용해도 괜찮다. 또한 list(queue)를 사용하면 큐 자료구조를 리스트 자료형으로 반환할 수 있다.

 


재귀 함수

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

 

 재귀 함수를 문제 풀이에서 사용할 때는 재귀 함수가 언제 끝날지, 종료 조건을 꼭 명시해야 한다. 자칫 종료 조건을 명시하지 않으면 함수가 무한 호출될 수 있다. 예를 들어 다음은 재귀 함수를 100번 호출하도록 작성한 코드이다. 재귀 함수 초반에 등장하는 if문이 종료 조건 역할을 수행한다.

 

 컴퓨터 내부에서 재귀 함수의 수행은 스택 자료구조를 이용한다. 함수를 계속 호출했을 때 가장 마지막에 호출한 함수가 먼저 수행을 끝내야 그 앞의 함수 호출이 종료되기 때문이다. 컴퓨터의 구조 측면에서 보자면 연속해서 호출되는 함수는 메인 메모리의 스택 공간에 적재되므로 재귀 함수는 스택 자료구조와 같다는 말을 틀린 말이 아니다.

 

  재귀 함수를 이용하는 대표적 예제로는 팩토리얼 문제가 있다. 팩토리얼을 반복적으로 구현한 방식과 재귀적으로 구현한 두 방식을 비교해보자.

 

def factorial_iterative(n):
	result = 1
    for i in range(1, n + 1):
    	result *= 1
	return result

def factorial_recursive(n):
	if <= 1:
    	return 1
    return n * factorial_recursive(n - 1)

print('반복적으로 구현:', factorial_iterative(5))
print('재귀적으로 구현:', factorial_recursive(5))

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

 

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

댓글