동적 계획법(DP)와 비교해 함께 공부하면 좋다.
재귀함수의 정의
함수 안에 자신의 함수를 다시 호출하는 함수를 의미한다.
재귀함수는 자신의 로직을 내부적으로 반복하다가, 일정한 조건이 만족되면 함수를 이탈하여 결과를 도출한다.
➕분할 정복(Divide and Conquer) 방식의 하나로 재귀함수를 활용한다.
문제를 나눌 수 없을 때까지 나누어서 각각을 풀면서 다시 합병하여 문제의 답을 얻는 알고리즘을 분할 정복 알고리즘이라고 한다.
재귀함수를 활용해 Top-Down(하향식 접근)해결 방식을 사용하는 특징이 있다.
재귀함수를 구현하는 두가지 방법
1. 재귀함수를 호출하는 부분을 상단에 두고 표현할 수 있다.
# if문에서 보이는 것과 같이 일정한 조건을 만족하지 않으면
# 계속해서 자기 자신의 함수로 되돌리는 코드를 볼 수 있디
def function(입력)
if 입력 > 일정값: #입력이 일정 값 이상이면
return function(입력 - 1) #괄호 안의 값을 이용해 함수를 재호출 한다.
#입력보다 작은 값 #()안은 예시일 뿐 패턴은 여러가지이다.
else: #종료조건
return 일정값, 입력값, 또는 특정값 #재귀 호출 종료
#헤당 값을 리턴한다.
재귀함수로 표현하는 팩토리얼
def factorial(n):
if n > 1: #n값이 1 보다 크면
return n * factorial(n - 1) #n값을 곱하고 n-1 값으로 factorial을 재호출 한다.
else: #n값이 1이라면
return #리턴한다
#출력
for n in range(3)
print(factorial(n))
#입력 n=3 이라고 한다면
#결과
# 0
# 1
# 2
# 6
2. 재귀함수가 돌아가는 부분은 아래에 삽입해 표현 할 수 있다.
def function(입력):
if 입력 <= 일정값: #입력이 일정 값 보다 작으면
return 일정값, 입력값, 또는 특정값 #재귀 호출 종료
#해당 값을 리턴한다.
function(입력보다 작은 값) #해당 값으로 재귀 재호출 한다.
return 결과값
재귀함수로 표현하는 팩토리얼
def factorial(n)
if n > 1: #n값이 1보다 크다면
return n * factorial(n-1) #n값에 n-1값으로 재호출한 factorial 값을 곱한다
return n
#출력
for n in range(3)
print(factorial(n))
#입력 n=3이라고 한다면
#결과
# 0
# 1
# 2
# 6
개인적으로 보기 편하다는 이유로 1번의 방식을 선호한다.
재귀함수 동작과정
다음과 같은 함수가 있다고 가정하자.
def recur(n): #시작
if n == 0: #종료 조건
return #끝
recur(n-1) #종료 조건에 부합하지 않는다면 괄호 안의 값으로 다시 함수를 실행한다.
recur 함수는 n을 인자로 입력받는다.
def recur(3): #시작
if n == 0: #종료 조건
return #끝
recur(n-1) #종료 조건에 부합하지 않는다면 괄호 안의 값으로 다시 함수를 실행한다.
함수가 실행되고, 3 이라는 수를 인자로 받았다. 종료 조건에 부합하지 않으므로 recur(n-1)을 호출해 다시 함수를 실행한다.
def recur(2): #시작
if n == 0: #종료 조건
return #끝
recur(n-1) #종료 조건에 부합하지 않는다면 괄호 안의 값으로 다시 함수를 실행한다.
n의 값이 2로 다시 함수를 실행 했으나 2 또한 0이 아니므로 종료 조건에 부합하지 않는다. 따라서 위와 같이 recur(n-1)을 호출해 다시 함수를 실행한다.
def recur(1): #시작
if n == 0: #종료 조건
return #끝
recur(n-1) #종료 조건에 부합하지 않는다면 괄호 안의 값으로 다시 함수를 실행한다.
n의 값이 1인 recure 함수를 실행 했으나 종료 조건에 부합하지 않아 한번 더 recur(n-1)을 호출해 다시 함수를 실행한다.
def recur(0): #시작
if n == 0: #종료 조건
return #끝
recur(n-1) #종료 조건에 부합하지 않는다면 괄호 안의 값으로 다시 함수를 실행한다.
n==0으로 recur 함수 종료 조건에 부합했다. recur(0)은 return을 만난 즉시 종료되어 자신을 호출한 recur(1)로 돌아간다. recur(n-1) 아래로는 더 이상 실행 할 코드가 없기에 recur(1)은 그 즉시 종료된다.
recur(2)와 recur(3)또한 마찬가지이다.
이 처럼 재귀함수는 자기 자신을 호출해 실행하고 복제된 모든 함수가 종료될 때까지 끝나지 않는다.
리턴 이후?
예시로 든 함수에선 아무것도 리턴하지 않았지만, 만약 return n 이라고 한다면 몇을 리턴해야 하는가?
리턴 한 후 복사본이 종료되면 재귀를 호출한 전 단계 재귀로 돌아가는데 예시로 든 함수에선 recur(0)에서 종료되었다 그렇다면 recur(1)로 돌아가는데 이를 리턴해야 하는가?
def recur(n): #시작
if n == 0: #종료 조건
return #끝
recur(n-1) #종료 조건에 부합하지 않는다면 괄호 안의 값으로 다시 함수를 실행한다.
print(n)
#return
재귀함수는 recur(n-1)을 만난 즉시 실행된다.
위의 동작과정들에서 설명했듯 recur(3) → recur(2) → recur(1) → recur(0) 에서 return을 만나 recur(0)이 종료되고 recur(1)로 돌아간다.
recur(0)을 호출했던 recur(n-1) → recur(1-1) 밑에 print(n)이 있기에 recur(1)의 n 인 1을 호출한다.
print(n) 밑에 return이 생략되어 있다. c언어에서 main문을 쓸 때 마지막 return(0)을 안써도 문제가 되지 않는 것 처럼 파이썬 또한 문제가 되지 않는다.
결론은 종료조건을 호출한 함수로 돌아간 후 그 값을 출력한다.
재귀는 종료 조건을 만나야 종료가 되기 때문에 무조건 명시해야 한다.
그렇지 않으면 무한으로 실행되고 스택 오버플로우가 터진다.
재귀에 리턴 값이 있다면?
아래는 위에서 든 예이다.
def recur(n):
if n == 0:
return
recur(n-1)
리턴 값이 없는 경우였다.
다음은 리턴 값이 있는 예이다.
def recur(n):
if n == 0:
return n
recur(n-1)
위의 예를 살짝 바꾸어 보았다.
print(n)과 마찬가지로 n을 리턴 하는 것이기 때문에 리턴 값은 3이다.
하지만 위의 동작과정 처럼 recur(0)의 값인 0을 최종적으로 리턴해야한다면 다음과 같이 써야한다.
def recur(n):
if n == 0:
return n
return recur(n-1)
위에는 print(n)과 마찬가지로 n을 리턴 하는 것이였지만 앞에 return을 붙여줌으로써 호출된 재귀가 리턴한 값을 리턴하면 되는 것이다.
이 것을 가장 잘 표현한 예시가 피보나치수열 구하기 인데 아래에서 살펴 보겠다.
피보나치 수열
첫 번째 항의 값이 0이고 두 번째 항의 값이 1일 때, 이후의 항들은 이전의 두 항을 더한 값으로 이루어지는 수열을 말한다.
예)
def fibo(n):
if n <= 1: #n이 1과 같거나 작다면
return n #n 리턴
return fibo(n-1) + fibo(n-2) #괄호 안의 값으로 재호출 한다.
#피보나치 수는 점화식으로 나타낼 수 있다
#f(n) = f(n-1) + f(n-2)
#결과 출력
print(fibo(6))
#결과
#5
→ 위 코드 동작과정을 그림으로 나타냈다.
재귀함수로 피보나치 수열의 코드를 작성했을 때, f(n)함수에서 n이 커지면 커질수록 수행시간이 기하급수적으로 늘어나는 즉, 시간 복잡도가 엄청 커지는 문제 점이 생긴다.
위 그림에서도 f(2)가 두번이나 호출되는 것을 확인 할 수 있는데 n이 커지면 부분적으로 중복되는걸 더 많이 확인 할 수 있을 것이다.
피보나치 수열의 시간 복잡도는 O(2**N)인데, 그 예로 f(30)을 계산하기 위해 약 10억번의 연산을 수행해야하는 비효율적인 일이 생긴다.
따라서 DP(동적계획법)으로 풀이를 한다면 더 효율적으로 문제를 해결 할 수 있다.
[참조]
https://data-marketing-bk.tistory.com/27 # 팩토리얼을 이용한 재귀함수
https://veggie-garden.tistory.com/44 #백준 17478 이용한 재귀함수 설명
https://terms.naver.com/entry.naver?docId=1164771&cid=40942&categoryId=32209 #피보나치 수열의 정의 https://velog.io/@kimdukbae/다이나믹-프로그래밍-Dynamic-Programming #재귀함수로 푼 피보나치 수열의 문제점
'알고리즘 > 알고리즘 이론 정리' 카테고리의 다른 글
그리디(Greedy) 알고리즘 (0) | 2023.04.11 |
---|---|
동적 계획법(다이나믹 프로그래밍, DynamicProgramming), 다른 알고리즘들과의 비교 (0) | 2023.04.11 |
백트래킹(BackTracking)의 개념과 DFS와의 차이 (0) | 2023.04.11 |
그래프(Graph)의 탐색 👉 깊이 우선 탐색(DFS)와 너비 우선 탐색(BFS) (0) | 2023.04.11 |
그래프(Graph)의 구현 👉 인접 리스트(Adjacency List)와 인접 행렬(Adjacency Matrix) (0) | 2023.04.10 |
댓글