탐색
탐색(Search): 많은 양의 데이터 중에서 원하는 데이터를 찾는 과정
대표적인 탐색 알고리즘 - DFS, BFS
DFS와 BFS를 제대로 이해하려면 기본 자료구조인 스택과 큐에 대한 이해가 전재되어야 하므로 사전 학습으로 스택, 큐, 재귀함수를 간단히 알아보자.
자료구조(Data Structure)
자료구조(Data Structure): 데이터를 표현하고 관리하고 처리하기 위한 구조
스택과 큐의 두 핵심적인 함수
- 삽입(Push): 데이터를 삽입한다.
- 삭제(Pop): 데이터를 삭제한다.
삽입과 삭제 외에도 오버플로와 언더플로를 고민해야한다.
- 오버플로(OverFlow): 특정한 자료구조가 수용할 수 있는 데이터의 크기를 이미 가득 찬 상태에서 삽입 연산을 수행할 때 발생한다. 즉, 저장 공간을 벗어나 데이터가 넘쳐흐를 때 발생한다.
- 언더플로(Underfllow): 특정한 자료구조에 데이터가 전혀 들어있지 않은 상태에서 삭제 연산을 수행할 때 발생한다.
스택(Stack)
스택(Stack): 선입후출(First In Last Out) 구조 or 후입선출(Last In First Out) 구조
스택은 박스 쌓기에 비유할 수 있다. 흔히 박스는 아래에서부터 위로 차곡차곡 쌓는다. 그리고 아래에 있는 박스를 치우기 위해서는 위에 있는 박스를 먼저 내려야 한다.
스택 예제
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(): 리스트의 가장 뒤쪽에서 데이터를 꺼냄
큐(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(queue)를 하면 된다.
재귀함수(Recursive Function)
재귀함수(Recursive Function): 자기 자신을 다시 호출하는 함수
재귀함수 예제
def recursive_function():
print('재귀 함수를 호출합니다.')
recursive_function()
recursive_function()
재귀 함수를 호출합니다.
재귀 함수를 호출합니다.
.
.
재귀 함수를 호출합니다.
재귀 함수를 호출합니다.
RecursionError: maximum recursion depth exceeded while calling a Python object
위 코드를 싱행하면 recursive_function()이 자기 자신을 계속해서 추가로 불러오기 때문에 '재귀 함수를 호출합니다.'라는 문자열을 무한히 출력한다. 물론 재귀의 최대 깊이를 초과하면 오류 메시지가 뜬다. 즉, 무한대로 재귀 호출을 진행할 수는 없다.
재귀함수는 프랙털(Fractal) 구조와 흡사하다. 아래 시에르핀스키의 삼각형처럼 삼각형 안에 또 다른 삼각형이 무한히 존재하는 그림은 프랙털 구조의 대표적인 그림이다.
재귀 함수의 종료 조건
재귀 함수를 문제 풀이에서 사용할 때는 재귀함수가 언제 끝날지, 종료 조건을 꼭 명시해야 한다.
재귀 함수 종료 예제
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 번째 재귀 함수를 호출합니다.
.
.
98 번째 재귀 함수에서 99 번째 재귀 함수를 호출합니다.
99 번째 재귀 함수에서 100 번째 재귀 함수를 호출합니다.
99 번째 재귀 함수를 종료합니다.
98 번째 재귀 함수를 종료합니다.
.
.
2 번째 재귀 함수를 종료합니다.
1 번째 재귀 함수를 종료합니다.
컴퓨터 내부에서 재귀 함수의 수행은 스택 자료구조와 동일하다. 함수를 계속 호출했을 때 가장 마지막에 호출한 함수가 먼저 수행을 끝내야 그 앞의 함수 호출이 종료되기때문이다. 스택 자료구조를 활용해야 하는 상당수 알고리즘은 재귀 함수를 이용해서 간편하게 구현될 수 있다.
재귀 함수를 이용하는 대표적인 예제 - 팩토리얼(Factorial) 문제
n! = 1 x 2 x 3 x ... x (n-1) x n
# 반복적으로 구현한 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
수학적으로 0!과 1!의 값은 1로 같다는 성질을 이용해 팩토리얼 함수는 n이 1이하가 되었을 때 함수를 종료하는 재귀 함수의 형태로 구현할 수 있다.
그렇다면 반복문 대신에 재귀 함수를 사용했을 때 얻을 수 있는 장점은 무엇일까?
재귀 함수의 코드가 더 간결하다. 이렇게 간결해진 이유는 재귀 함수가 수학의 점화식(재귀식)을 그대로 소스코드로 옮겼기 때문이다. 수학에서 점화식은 특정한 함수를 자신보다 더 작은 변수에 대한 함수와의 관계로 표현한 것을 의미한다.
팩토리얼을 수학적 점화식으로 표현해보면 다음과 같다.
- n이 0 혹은 1일 때: factorial(n) = 1
- n이 1보다 클 때: factorial(n) = n x factorial(n-1)
일반적으로 우리는 점화식에서 종료 조건을 찾을 수 있는데, 앞 예시에서 종료 조건은 'n이 0 혹은 1일 때'이다. 팩토리얼은 n이 양의 정수일 때에만 유효하기 때문에 n이 1이하인 경우 1을 반환할 수 있도록 재귀 함수를 작성해야 한다. n이 1 이하인 경우를 고려하지 않으면 재귀 함수가 무한히 반복되어 결과를 출력하지 못할 것이다. 또한 n의 값으로 음수가 들어왔을 때는 입력 범위오류로, 오류 메시지를 띄우도록 코드를 작성할 수도 있다. 따라서 재귀 함수 내에서 특정 조건일 때 더 이상 재귀적으로 함수를 호출하지 않고 종료하도록 if문을 이용하여 꼭 종료 조건을 구현해주어야 한다.
'알고리즘(이코테)' 카테고리의 다른 글
[알고리즘] 정렬(Sorting) (0) | 2024.05.18 |
---|---|
[알고리즘] DFS/BFS - 탐색 알고리즘 DFS/BFS(3) (0) | 2024.05.15 |
[알고리즘] DFS/BFS - 그래프(2) (0) | 2024.05.14 |
[알고리즘] 구현(Implementation) (0) | 2024.05.09 |
[알고리즘] 그리디(Greedy) (0) | 2024.05.08 |