[프로그래머스] 피보나치 수
문제 설명
피보나치 수는 F(0) = 0, F(1) = 1일 때, 1 이상의 n에 대하여 F(n) = F(n-1) + F(n-2) 가 적용되는 수 입니다.
예를들어
- F(2) = F(0) + F(1) = 0 + 1 = 1
- F(3) = F(1) + F(2) = 1 + 1 = 2
- F(4) = F(2) + F(3) = 1 + 2 = 3
- F(5) = F(3) + F(4) = 2 + 3 = 5
와 같이 이어집니다.
2 이상의 n이 입력되었을 때, n번째 피보나치 수를 1234567으로 나눈 나머지를 리턴하는 함수, solution을 완성해 주세요.
제한사항
- n은 1이상, 100000이하인 자연수입니다.
풀이
15분
별 생각없이 long long형으로 선언해두고, return 문에서만 mod연산을 해주어서 바로 제출하니 문제를 아예 틀렸다. 이 간단한 문제 풀이에서 뭐가 잘못된건지 고민을 잠깐 하다가, 계산 과정에서 오버플로우가 발생한다는 것을 알아차리고 고쳤다.
피보나치를 처음 배울 때 재귀를 이용하는 방법을 배우는데, 문제 조건을 봤을 때 딱 보아도 스택오버플로우가 발생할 것 같았다. 그런데 알고리즘 문제를 풀 때 재귀를 사용할지 결정하기 위해 가늠하는 방법이 궁금해서 찾아보았고, 꼬리 재귀 최적화라는 방법을 알게 되었다. 재귀 호출의 대표적인 예시인 factorial의 일반항을 보면,
int Factorial(int n){
if (n == 1) return 1;
return n * Factorial(n-1);
}
위와 같은 코드는 컴파일러는 다음과 같이 해석한다.
int Factorial(int n){
if (n == 1) return 1;
int result = Factorial(n - 1);
return n * result;
}
이 때, 코드를 다음과 같이 바꾸어 주면 반환할 때 추가적인 연산이 필요하지 않게 되고,
int FactorialTail(int n, int acc){
if (n == 1) return acc;
return FactorialTail(n - 1, acc * n);
}
int Factorial(int n){
return FactorialTail(n, 1);
}
컴파일러는 이것을 인식하여 다음과 같이 해석해준다.
int FactorialTail(int n){
int acc = 1;
do{
if (n == 1) return;
acc = acc * n;
n = n - 1;
} while (true);
}
실제로 동작하는 것을 살펴보아도 스택을 한번만 호출하는데, 이 꼬리 재귀 개념을 사용하기 위해서는 컴파일러가 이 기능을 지원해주어야 한다.
그리고 처음 궁금했던
재귀는 몇 번까지 호출할 수 있을까
의 해답은, 파이썬 언어 같은 경우는 재귀함수 호출 횟수가 제한되어 있기도 하지만, 작성한 코드에 따라 스택에 올라가는 크기도 달라지므로 예측이 매우 어려우니 통상 1만번은 넘기지 않도록 어림잡는 방법이 있다고 한다.
코드
// 09:53 ~ 10:08
#include <string>
#include <vector>
using namespace std;
int solution(int n) {
int pprev = 0, prev = 1, temp;
for(int i = 0; i < n; ++i){
temp = prev;
prev = (pprev + prev) % 1234567;
pprev = temp % 1234567;
}
return pprev;
}
꼬리 재귀 코드
#include <string>
#include <vector>
using namespace std;
int fibo(int n, int pprev, int prev){
if(n == 0) return pprev;
return fibo(n - 1, prev, (prev + pprev) % 1234567);
}
int fibo_tail(int n){
return fibo(n, 0, 1);
}
int solution(int n) {
return fibo_tail(n);
}