재귀 함수가 반복문으로 짠 것 보다는 가독성이 좋지만 느릴 수 있다는 것은 이미 알고 있다. 하지만 이 재귀 함수의 단점을 어느 정도 커버할 수 있는 꼬리 재귀(Tail recursion)란 것이 있는데, 이것에 대해 간단히 정리해보자.
우선 꼬리 재귀란 재귀 함수이긴 하지만 재귀 함수 호출 이후에 연산을 추가적으로 하지 않는 재귀 함수 형태를 의미한다. 예를 들어서 factorial함수를 재귀 함수로 짜보면
int factorial(int Num)
{
if(Num == 0)
return 1;
return Num * factorial(Num - 1);
}
아마 대부분 factorial함수를 재귀함수로 짤 때 이런식으로 만들 것이다. 하지만 보이다시피 factorial(Num - 1)을 호출하고 return되어도 Num과 곱하는 연산이 남아 있다. 따라서 이는 꼬리 재귀는 아니다. 꼬리 재귀로 구성하면
int factorialTail(int Num, int Acc)
{
if(Num == 0)
return Acc;
return factorialTail(Num - 1, Acc * Num);
}
int factorial(int Num)
{
return factorialTail(Num, 1);
}
이렇게 만들 수 있다. 원래 재귀 함수와 다르게, 재귀 함수 리턴 이후에 추가적으로 연산을 하는 게 아니라 바로 Acc를 리턴한다. 이렇게 하면 컴파일러가 꼬리 재귀 함수임을 인식하고 반복문으로 최적화 시켜준다. 이 말은 꼬리 재귀를 지원하지 않는 컴파일러는 꼬리 재귀로 코드를 짜봤자 성능 상 이득을 볼 수 없다는 것이다.
(C++, C#, Kotlin, Swift은 꼬리 재귀 최적화를 지원하지만, Java는 꼬리 재귀 최적화를 직접적으로 지원하지 않는다고 한다.)
컴파일러가 꼬리 재귀임을 인식해서 반복문으로 바꾸면 아래와 같은 형태일 것이다.
int FactorialTail(int Num)
{
int Acc = 1;
do {
if (Num == 0)
return Acc;
Acc = Acc * Num;
Num = Num - 1;
} while (true);
}
추가적으로 sum함수를 재귀와 꼬리 재귀 함수 두 가지 형태로 구성해보았다.
// 재귀
int sum(int Num)
{
if (Num == 0)
return Num;
return Num + sum(Num-1);
}
// 꼬리재귀
int sum(int Num, int Acc)
{
if (Num == 0)
{
return Acc;
}
return sum(Num - 1, Num + Acc);
}
int sum(int Num)
{
return sum(Num, 0);
}
Reference
'공부 > 그 외' 카테고리의 다른 글
레지스터(Register) vs 레지스트리(Registry) (0) | 2021.08.18 |
---|---|
C7510 : 종속적 형식 이름은 'typename' 접두사와 함께 사용해야 합니다. (1) | 2021.08.12 |
오브젝트 풀(Object Pool) (0) | 2021.07.19 |
Visual Studio "const char *" 형식의 값을 사용하여 "char *" 형식의 엔터티를 초기화할 수 없습니다. (0) | 2021.07.16 |
메모리 풀(Memory Pool) (0) | 2021.07.14 |