본문 바로가기
알고리즘/알고리즘 이론 정리

재귀함수(Recursion)

by jxhxxn_ 2023. 4. 11.

동적 계획법(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 #재귀함수로 푼 피보나치 수열의 문제점

 

 

 

 

 

 

 

 

 

 

 

댓글