Python 64 - DFS/BFS (3) 꼭 필요한 자료구조 기초_[재귀 함수]

2021. 6. 25. 18:02python

반응형

DFS 와 BFS 를 구현하려면 재귀함수도 이해하고 있어야 한다.

재귀 함수(Recursive Function)란 자기 자신을 다시 호출하는 함수를 의미한다.

가장 간단한 재귀 함수는 다음과 같다.

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

위 코드를 실행하면 '재귀 함수를 호출합니다'. 라는 문자열을 무한히 출력한다. 여기서 정의한 recursive_function()이 자기 자신을 계속해서 추가로 불러오기 때문이다. 물론 어느정도 출력 하다가 다음과 같은 오류 메시지를 출력하고 멈출 것이다. 

RecursionError: maximum recursion depth exceeded while calling a Python object

이 오류 메시지는 재귀(Recursion)의 최대 깊이를 초과했다는 내용이다. 보통 파이썬 인터프리터는 호출 횟수 제한이 있는데 이 한계를 벗어났기 때문이다. 따라서 무한대로 재귀 호출을 진행 할 수는 없다. (애초에 무한한 재귀호출을 요구하는 문제 또한 출제되지 않을 것이다.)

 

재귀 함수는 수학 시간에 한 번씩 언급되는 프랙털(Fractal) 구조와 흡사하다. 아래 그림은 시에르핀스키의 삼각형(sierpinski Triangle)이다. 삼각형 안에 또 다른 삼각형이 무한히 존재하는 이 그림은 프랙털 구조의 대표적인 그림으로 실제로 이러한 프랙털 이미지를 출력하는 프로그램을 작성할 때에도 재귀 함수를 이용한다.

재귀 함수의 종료 조건

재귀 함수를 문제 풀이에서 사용할 때는 재귀함수가 언제 끝날지, 종료 조건을 꼭 명시해야 한다.

칫 종료 조건을 명시하지 않으면 함수가 무한 호출될 수 있다.

예를 들어 다음은 재귀 함수를 100번 호출하도록 작성한 코드이다.

재귀 함수 초반에 등장하는 if문이 종료 조건 역할을 수행한다.

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

재귀 함수는 내부적으로 스택자료와 동일하다. 따라서 스택 자료구조를 활용해야 하는 상당수 알고리즘은 재귀 함수를 이용해서 간편하게 구현될 수 있다. DFS가 대표적인 예이다. 재귀 함수를 이용한 대표적인 예제로는 팩토리얼(Factorial)문제가 있다. 팩토리얼 기호는 느낌표(!) 를 사용하며 n! 는  1 x 2 x 3 x ..... x (n-1) x n 을 의미한다.

수학적으로 0! 와 1! 의  값은 1로 같다는 성질을 이용하여 팩토리얼 함수는 n이 1이하가 되었을 때 함수를 종료하는 재귀 함수의 형태로 구현할 수 있다.

 

팩토리얼을 반복적으로 구현한 방식재귀적으로 구현한 두 방식을 비교해보자. 소스코드는 다음과 같다.

# 반복적으로 구현한 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

실행 결과는 동일하다.

그렇다면 반복문 대신에 재귀함수를 사용했을 때 얻을 수 있는 장점은 무엇일까?

위의 코드를 비교했을 때 재귀 함수의 코드가 더 간결한 것을 알 수 있다. 이렇게 간결해진 이유는 재귀 함수가 수학의 점화식(재귀식)을 그대로 소스코드로 옮겼기 때문이다. 수학에서 점화식은 특정한 함수를 자신보다 더 작은 변수에 대한 함수와의 관계로 표현한 것을 의미한다. 이 개념은 이후에 배울 '다이나믹 프로그래밍'으로 이어지기 때문에 중요하다.

팩토리얼 수학적 점화식으로 표현해보면 다음과 같다.

  • n이 0 혹은 1일 때 : factorial(n) = 1
  • n이 1보다 클 때 : factorail(n) = n x factorial(n-1)

일반적으로 우리는 점화식에서 종료 조건을 찾을 수 있는데,  앞 예시에서  종료 조건은 'n이 0 혹은 1 일 때' 이다. 팩토리얼은 n이 양의 정수 일 때에만 유효하기 떄문에 n이 1이하인 경우 1을 반환할 수 있도록 재귀 함수를 작성해야한다.

n이 1이하 인 경우를 고려하지 않으면 재귀 함수가 무한히 반복되어 결과를 출력하지 못할 것이다. 또한 n의 값으로

음수가 들어왔을 때는 입력 범위 오류로, 오류메시지를 띄우도록 코드를 작성할 수도 있다. 따라서 재귀함수 내에서 특정 조건일 때 더이상 재귀적으로 함수를 호출하지 않고 종료하도록 if문을 이용하여 꼭 종료 조건을 구현해주어야 한다.

다시 한번 앞의 점화식과 조금 전에 작성했던 팩토리얼의 재귀 함수 버전을 비교해보자.

재귀 함수의 소스코드와 점화식이 매우 닮아 있는 것을 확인 할 수 있다. 다시 말해 재귀함수는 반복문을 이용하는 것과 비교했을 때 더욱 간결한 형태임을 이해할 수 있다.

 

 

 


본 포스팅은 ‘이것이 코딩 테스트다 with 파이썬’을 읽고 공부한 내용을 바탕으로 작성하였습니다.

 

 

 

작성자 : 엄코딩 eomcoding

반응형