반복과 재귀 비교
본문피보나치 수열은 앞의 두 수를 더해 다음 수를 만든다. 정의가 'fib(n) = fib(n − 1) + fib(n − 2)'이니 재귀 함수로 옮기면 두 줄이면 된다. 그런데 이 함수로 fib(40)을 구하면 컴퓨터가 몇 초 동안 멈춰 있고, fib(50)은 몇 분을 기다려야 한다. 반복문으로 앞에서부터 차례로 더하면 fib(50)도 눈 깜짝할 사이에 나온다. 같은 정의를 옮긴 두 프로그램이 왜 이렇게 다를까?
1.3.1같은 계산의 되풀이
재귀로 fib(5)를 구하면 fib(4)와 fib(3)을 부른다. 그런데 fib(4)도 fib(3)과 fib(2)를 부른다. 그래서 fib(3)은 두 번, fib(2)는 세 번, fib(1)은 다섯 번 계산된다. n이 커질수록 같은 값을 계산하는 횟수가 폭발적으로 늘어난다. 호출들을 나무로 그리면 가지가 둘씩 갈라지는 커다란 나무가 되고, 그 나무의 마디 수가 곧 호출 수이다. 반복문은 fib(0)과 fib(1)에서 시작해 두 수를 기억하며 앞으로 나아가므로, 각 값을 한 번씩만 계산한다.
정의 그대로의 재귀로 fib(n)을 구할 때의 호출 수 \(C(n)\)은 \(C(0) = C(1) = 1\), \(C(n) = C(n-1) + C(n-2) + 1\)이고, 이를 풀면 \(C(n) = 2\,\text{fib}(n+1) - 1\)이다. fib(n)은 n이 1 늘 때마다 약 1.618배가 되므로 호출 수도 지수적으로 늘어난다. 반복문은 \(n\)번이면 된다.
그림 1에서 n을 바꾸어 보자. fib(30)은 재귀로 약 270만 번, fib(40)은 약 3억 3천만 번을 호출한다. 1초에 1억 번 호출한다고 해도 fib(40)은 약 3.3초, fib(50)은 약 400억 번으로 7분 가까이 걸린다. 반복문은 fib(50)을 50번 더하면 끝난다. 재귀와 반복의 차이가 아니라, 같은 계산을 되풀이하느냐 한 번만 하느냐의 차이이다.1
1.3.2알맞은 방식 고르기
재귀가 늘 느린 것은 아니다. 하노이의 탑이나 폴더 탐색처럼 부분 문제가 서로 겹치지 않으면 재귀가 자연스럽고 효율적이다. 피보나치처럼 부분 문제가 겹치면, 한 번 구한 답을 표에 적어 두었다가 다시 쓰는 메모이제이션을 더하면 재귀도 반복문만큼 빨라진다. 반복문은 스택 프레임을 쌓지 않아 메모리를 적게 쓰고, 아주 깊은 계산에서도 스택이 넘치지 않는다. 그래서 문제의 구조를 보고, 부분 문제가 겹치는지와 깊이가 얼마나 되는지를 따져 방식을 고른다.
1.3.3예제 풀이
정의 그대로의 재귀로 fib(6)을 구할 때 fib(2)는 몇 번 계산되는가? 전체 호출 수를 구하여라. fib(0) = 0, fib(1) = 1이다.
fib(6)은 fib(5)와 fib(4)를, fib(5)는 fib(4)와 fib(3)을 부른다. fib(k)가 계산되는 횟수를 세면 fib(6) 1번, fib(5) 1번, fib(4) 2번, fib(3) 3번, fib(2) 5번이다. 횟수 자체가 피보나치 수열을 따른다.
전체 호출 수는 \(C(6) = 2\,\text{fib}(7) - 1 = 2 \times 13 - 1 = 25\)번이다. 반복문이라면 fib(2)부터 fib(6)까지 5번 더하면 된다. n = 6에서도 다섯 배이고, n이 커질수록 그 차이가 지수적으로 벌어진다. 즉 짧고 우아한 코드가 늘 빠른 코드는 아니다. 그래서 프로그램을 짠 뒤에는 같은 계산을 되풀이하지 않는지 확인한다.
아래 탐구 카드에서 같은 문제를 반복과 재귀로 짜서 실행 시간을 비교하고, 문제마다 알맞은 방식을 골라 보자.
- 피보나치 수의 이웃한 두 수의 비는 황금비 \((1 + \sqrt 5)/2 \approx 1.618\)에 가까워진다. 그래서 fib(n)은 대략 \(1.618^n/\sqrt 5\)로 어림할 수 있다.
정의 그대로의 재귀로 피보나치 수를 구하면 같은 부분 문제를 수없이 되풀이해 호출 수가 \(2\,\text{fib}(n+1) - 1\)로 지수적으로 늘어나고, 반복문은 각 값을 한 번씩만 계산해 n번이면 끝난다. 부분 문제가 겹치면 메모이제이션이나 반복문을, 겹치지 않고 구조가 자기 자신을 품으면 재귀를 고른다. 그래서 처음 질문의 재귀 함수는 fib(40)을 구하려고 3억 번 넘게 호출하느라 멈춘 듯 보인다.