[c] 꼬리 재귀는 정확히 어떻게 작동합니까?

나는 꼬리 재귀가 어떻게 작동하는지, 그리고 그것과 정상적인 재귀의 차이점을 거의 이해합니다. 나는 단지 그 이유를 이해하지 않습니다 그것의 반환 주소를 기억하기 위해 스택을 필요로한다.

// tail recursion
int fac_times (int n, int acc) {
    if (n == 0) return acc;
    else return fac_times(n - 1, acc * n);
}

int factorial (int n) {
    return fac_times (n, 1);
}

// normal recursion
int factorial (int n) {
    if (n == 0) return 1;
    else return n * factorial(n - 1);
}

꼬리 재귀 함수에서 함수 자체를 호출 한 후에는 할 일이 없지만 나에게는 의미가 없습니다.



답변

컴파일러는 간단히 이것을 변환 할 수 있습니다.

int fac_times (int n, int acc) {
    if (n == 0) return acc;
    else return fac_times(n - 1, acc * n);
}

다음과 같이 :

int fac_times (int n, int acc) {
label:
    if (n == 0) return acc;
    acc *= n--;
    goto label;
}


답변

“반환 주소를 기억하기 위해 스택이 필요하지 않은”이유를 묻습니다.

나는 이것을 바꾸고 싶다. 그것은 않습니다 반환 주소를 기억하기 위해 스택을 사용합니다. 비결은 꼬리 재귀가 발생하는 함수가 스택에 자체 반환 주소를 가지고 있으며 호출 된 함수로 점프 할 때이를 자체 반환 주소로 취급한다는 것입니다.

구체적으로 꼬리 호출 최적화없이 :

f: ...
   CALL g
   RET
g:
   ...
   RET

이 경우를 g호출하면 스택은 다음과 같습니다.

   SP ->  Return address of "g"
          Return address of "f"

반면에 마무리 호출 최적화를 사용하면 다음을 수행 할 수 있습니다.

f: ...
   JUMP g
g:
   ...
   RET

이 경우를 g호출하면 스택은 다음과 같습니다.

   SP ->  Return address of "f"

분명히 g돌아 오면 f호출 된 위치로 돌아갑니다 .

편집 : 위의 예는 한 함수가 다른 함수를 호출하는 경우를 사용합니다. 함수가 자신을 호출 할 때 메커니즘은 동일합니다.


답변

꼬리 재귀는 일반적으로 컴파일러에 의해 루프로 변환 될 수 있습니다. 특히 누산기가 사용될 때 더욱 그렇습니다.

// tail recursion
int fac_times (int n, int acc = 1) {
    if (n == 0) return acc;
    else return fac_times(n - 1, acc * n);
}

다음과 같이 컴파일됩니다.

// accumulator
int fac_times (int n) {
    int acc = 1;
    while (n > 0) {
        acc *= n;
        n -= 1;
    }
    return acc;
}


답변

재귀 함수에 있어야하는 두 가지 요소가 있습니다.

  1. 재귀 호출
  2. 반환 값의 개수를 유지하는 위치입니다.

“일반”재귀 함수는 스택 프레임에 (2)를 유지합니다.

일반 재귀 함수의 반환 값은 두 가지 유형의 값으로 구성됩니다.

  • 기타 반환 값
  • 자신의 함수 계산 결과

귀하의 예를 살펴 보겠습니다.

int factorial (int n) {
    if (n == 0) return 1;
    else return n * factorial(n - 1);
}

프레임 f (5)는 자신의 계산 결과 (5)와 f (4)의 값을 “저장”합니다. factorial (5)를 호출하면 스택 호출이 붕괴되기 직전에 다음이 있습니다.

 [Stack_f(5): return 5 * [Stack_f(4): 4 * [Stack_f(3): 3 * ... [1[1]]

각 스택은 내가 언급 한 값 외에 함수의 전체 범위를 저장합니다. 따라서 재귀 함수 f의 메모리 사용량은 O (x)입니다. 여기서 x는 내가 만들어야하는 재귀 호출의 수입니다. 따라서 factorial (1) 또는 factorial (2)을 계산하는 데 1kb의 RAM이 필요한 경우 factorial (100)을 계산하려면 ~ 100k가 필요합니다.

Tail Recursive 함수는 인수에 (2)를 넣습니다.

Tail Recursion에서는 매개 변수를 사용하여 각 재귀 프레임의 부분 계산 결과를 다음 프레임으로 전달합니다. 팩토리얼 예제 인 Tail Recursive를 보겠습니다.

int factorial (int n) {int helper (int num, int 누적) {if num == 0 return 누적 else return helper (num-1, 누적 * num)} return helper (n, 1)
}

factorial (4)에서 프레임을 살펴 보겠습니다.

[Stack f(4, 5): Stack f(3, 20): [Stack f(2,60): [Stack f(1, 120): 120]]]]

차이점이 보이십니까? “일반”재귀 호출에서 반환 함수는 최종 값을 재귀 적으로 구성합니다. Tail Recursion에서는 기본 케이스 (마지막으로 평가 된 케이스) 만 참조합니다 . 우리는 accumulator를 이전 값을 추적하는 인수 라고 부릅니다 .

재귀 템플릿

일반 재귀 함수는 다음과 같습니다.

type regular(n)
    base_case
    computation
    return (result of computation) combined with (regular(n towards base case))

Tail 재귀로 변환하려면 다음을 수행합니다.

  • 누산기를 운반하는 도우미 기능을 소개합니다.
  • 누산기가 기본 케이스로 설정된 주 함수 내에서 도우미 함수를 실행합니다.

보기:

type tail(n):
    type helper(n, accumulator):
        if n == base case
            return accumulator
        computation
        accumulator = computation combined with accumulator
        return helper(n towards base case, accumulator)
    helper(n, base case)

차이점이 보이십니까?

테일 콜 최적화

Tail Call 스택의 Non-Border-Cases에 저장된 상태가 없기 때문에 중요하지 않습니다. 일부 언어 / 통역사는 이전 스택을 새 스택으로 대체합니다. 따라서 호출 수를 제한하는 스택 프레임이 없기 때문에 Tail Calls 는 이러한 경우 for 루프처럼 작동합니다 .

최적화하는 것은 컴파일러의 몫입니다.


답변

다음은 재귀 함수가 작동하는 방식을 보여주는 간단한 예입니다.

long f (long n)
{

    if (n == 0) // have we reached the bottom of the ocean ?
        return 0;

    // code executed in the descendence

    return f(n-1) + 1; // recurrence

    // code executed in the ascendence

}

Tail 재귀는 함수의 끝에서 반복이 수행되는 간단한 재귀 함수이므로 어센 던스에서 코드가 수행되지 않으므로 대부분의 고급 프로그래밍 언어 컴파일러가 Tail Recursion Optimization으로 알려진 작업을 수행하는 데 도움 이됩니다. Tail recursion modulo로 알려진 더 복잡한 최적화


답변

재귀 함수는 자체적으로 호출 하는 함수입니다.

이를 통해 프로그래머는 최소한의 코드를 사용하여 효율적인 프로그램을 작성할 수 있습니다 .

단점은 적절하게 작성하지 않으면 무한 루프 및 기타 예기치 않은 결과 가 발생할있다는 입니다.

Simple Recursive 함수와 Tail Recursive 함수를 모두 설명하겠습니다.

단순 재귀 함수 를 작성하려면

  1. 고려해야 할 첫 번째 사항은 if 루프 인 루프에서 나올 때를 결정할 때입니다.
  2. 두 번째는 우리가 우리 자신의 기능인 경우해야 할 과정입니다.

주어진 예에서 :

public static int fact(int n){
  if(n <=1)
     return 1;
  else
     return n * fact(n-1);
}

위의 예에서

if(n <=1)
     return 1;

루프를 종료 할시기를 결정하는 요소입니다.

else
     return n * fact(n-1);

수행 할 실제 처리입니까?

쉽게 이해할 수 있도록 작업을 하나씩 나누겠습니다.

내가 실행하면 내부적으로 어떤 일이 일어나는지 보자 fact(4)

  1. n = 4 대체
public static int fact(4){
  if(4 <=1)
     return 1;
  else
     return 4 * fact(4-1);
}

If루프가 실패하여 else루프 로 이동하여 반환합니다.4 * fact(3)

  1. 스택 메모리에는 4 * fact(3)

    n = 3 대체

public static int fact(3){
  if(3 <=1)
     return 1;
  else
     return 3 * fact(3-1);
}

If루프가 실패하여 else루프 로 이동 합니다.

그래서 그것은 반환 3 * fact(2)

“`4 * fact (3)“라고 불렀다는 것을 기억하십시오.

출력 fact(3) = 3 * fact(2)

지금까지 스택은 4 * fact(3) = 4 * 3 * fact(2)

  1. 스택 메모리에는 4 * 3 * fact(2)

    n = 2 대체

public static int fact(2){
  if(2 <=1)
     return 1;
  else
     return 2 * fact(2-1);
}

If루프가 실패하여 else루프 로 이동 합니다.

그래서 그것은 반환 2 * fact(1)

우리가 전화했던 기억 4 * 3 * fact(2)

출력 fact(2) = 2 * fact(1)

지금까지 스택은 4 * 3 * fact(2) = 4 * 3 * 2 * fact(1)

  1. 스택 메모리에는 4 * 3 * 2 * fact(1)

    n = 1 대체

public static int fact(1){
  if(1 <=1)
     return 1;
  else
     return 1 * fact(1-1);
}

If 루프가 참

그래서 그것은 반환 1

우리가 전화했던 기억 4 * 3 * 2 * fact(1)

출력 fact(1) = 1

지금까지 스택은 4 * 3 * 2 * fact(1) = 4 * 3 * 2 * 1

마지막으로 fact (4) = 4 * 3 * 2 * 1 = 24의 결과

여기에 이미지 설명 입력

꼬리 재귀는

public static int fact(x, running_total=1) {
    if (x==1) {
        return running_total;
    } else {
        return fact(x-1, running_total*x);
    }
}
  1. n = 4 대체
public static int fact(4, running_total=1) {
    if (x==1) {
        return running_total;
    } else {
        return fact(4-1, running_total*4);
    }
}

If루프가 실패하여 else루프 로 이동하여 반환합니다.fact(3, 4)

  1. 스택 메모리에는 fact(3, 4)

    n = 3 대체

public static int fact(3, running_total=4) {
    if (x==1) {
        return running_total;
    } else {
        return fact(3-1, 4*3);
    }
}

If루프가 실패하여 else루프 로 이동 합니다.

그래서 그것은 반환 fact(2, 12)

  1. 스택 메모리에는 fact(2, 12)

    n = 2 대체

public static int fact(2, running_total=12) {
    if (x==1) {
        return running_total;
    } else {
        return fact(2-1, 12*2);
    }
}

If루프가 실패하여 else루프 로 이동 합니다.

그래서 그것은 반환 fact(1, 24)

  1. 스택 메모리에는 fact(1, 24)

    n = 1 대체

public static int fact(1, running_total=24) {
    if (x==1) {
        return running_total;
    } else {
        return fact(1-1, 24*1);
    }
}

If 루프가 참

그래서 그것은 반환 running_total

출력 running_total = 24

마지막으로 fact (4,1) = 24 의 결과

여기에 이미지 설명 입력


답변

재귀는 내부 구현과 관련이 있기 때문에 내 대답은 추측에 가깝습니다.

꼬리 재귀에서 재귀 함수는 동일한 함수의 끝에서 호출됩니다. 아마도 컴파일러는 다음과 같이 최적화 할 수 있습니다.

  1. 진행중인 기능이 종료되도록합니다 (예 : 사용 된 스택이 호출 됨).
  2. 함수에 대한 인수로 사용될 변수를 임시 저장소에 저장하십시오.
  3. 그런 다음 임시 저장된 인수로 함수를 다시 호출하십시오.

보시다시피, 동일한 함수의 다음 반복 전에 원래 함수를 마무리하므로 실제로 스택을 “사용”하지 않습니다.

그러나 함수 내부에 호출 할 소멸자가 있으면이 최적화가 적용되지 않을 수 있다고 생각합니다.