공부/그 외

꼬리 재귀(Tail recursion)

sudo 2021. 8. 4. 00:45

재귀 함수가 반복문으로 짠 것 보다는 가독성이 좋지만 느릴 수 있다는 것은 이미 알고 있다. 하지만 이 재귀 함수의 단점을 어느 정도 커버할 수 있는 꼬리 재귀(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

https://velog.io/@dldhk97/%EC%9E%AC%EA%B7%80%ED%95%A8%EC%88%98%EC%99%80-%EA%BC%AC%EB%A6%AC-%EC%9E%AC%EA%B7%80

 

재귀함수와 꼬리 재귀

일반적으로 재귀함수보다 반복문의 실행 속도가 더 빠른 것으로 알고 있는데, 어째서 그러한 차이가 나는지 궁금해졌다. 그래서 이번 포스팅에서 재귀과 반복의 차이, 그리고 꼬리 재귀 최적화

velog.io