꼬리 재귀는 무엇입니까?


52

나는 재귀의 일반적인 개념을 알고있다. 퀵 정렬 알고리즘을 연구하면서 테일 재귀 개념을 발견했습니다 . 이 년 MIT에서 빠른 정렬 알고리즘의 비디오 18시 반 초 교수의이 꼬리 재귀 알고리즘이라고 말했다. 꼬리 재귀가 실제로 무엇을 의미하는지는 분명하지 않습니다.

누군가 적절한 개념으로 개념을 설명 할 수 있습니까?

여기 SO 커뮤니티 에서 제공하는 답변이 있습니다 .


꼬리 재귀 라는 용어가 발생한 상황에 대해 자세히 알려주십시오 . 링크? 소환?
A.Schulz

@ A.Schulz 컨텍스트에 대한 링크를 넣었습니다.
Geek

5
stackoverflow 에서 " 꼬리 재귀는 무엇입니까? "
Vor

2
@ajmartin 문제는 Stack Overflow의 경계선 이지만 Computer Science에 대해서는 주제가 많으므로 원칙적으로 Computer Science 는 더 나은 답변을 제공해야합니다. 여기에서는 발생하지 않았지만 더 나은 답변을 얻기 위해 여기에 다시 질문해도됩니다. 괴짜, 당신은 사람들이 이미 말한 것을 반복하지 않도록 SO에 대한 이전 질문을 언급했을 것입니다.
Gilles 'SO- 악의를 멈추십시오'

1
또한 모호한 부분이 무엇인지 또는 이전 답변으로 만족하지 못하는 이유를 말해야합니다 .SO 사람들이 좋은 답변을 제공하지만 다시 요청하게 된 이유는 무엇입니까?

답변:


52

테일 재귀는 재귀 호출을 한 후 호출 함수가 더 이상 계산을 수행하지 않는 특별한 재귀 사례입니다. 예를 들어, 함수

int f (int x, int y) {
  if (y == 0) {
    x를 반환;
  }

  반환 f (x * y, y-1);
}

꼬리 재귀입니다 (마지막 명령어는 재귀 호출이므로).이 함수는 꼬리 재귀가 아닙니다.

int g (int x) {
  if (x == 1) {
    리턴 1;
  }

  int y = g (x-1);

  x * y를 반환;
}

재귀 호출이 반환 된 후 계산을 수행하기 때문입니다.

테일 재귀는 일반적인 재귀보다 더 효율적으로 구현 될 수 있기 때문에 중요합니다. 정상적인 재귀 호출을 할 때 반환 주소를 호출 스택으로 푸시 한 다음 호출 된 함수로 이동해야합니다. 즉, 재귀 호출 깊이에서 크기가 선형 인 호출 스택이 필요합니다. 꼬리 재귀가있을 때 우리는 재귀 호출에서 돌아 오는 즉시 즉시 돌아올 것이므로 재귀 함수의 전체 체인을 건너 뛰고 원래 호출자로 곧바로 돌아갈 수 있음을 알고 있습니다. 즉, 모든 재귀 호출에 대해 호출 스택이 전혀 필요하지 않으며 최종 호출을 간단한 점프로 구현하여 공간을 절약 할 수 있습니다.


2
"이것은 모든 재귀 호출에 대해 호출 스택이 전혀 필요하지 않음을 의미합니다." 콜 스택은 항상 거기에 있습니다. 단지 리턴 주소를 콜 스택에 쓸 필요가 없습니다.
Geek

2
그것은 어느 정도 계산 모델에 달려 있습니다 :) 그렇습니다. 실제 컴퓨터에서는 콜 스택이 여전히 남아 있습니다. 우리는 단지 그것을 사용하지 않습니다.
매트 루이스

마지막 호출이지만 for 루프 인 경우 어떻게됩니까? 따라서 위의 모든 계산을 수행하지만 일부는 다음과 같은 for 루프에서 수행됩니다.def recurse(x): if x < 0 return 1; for i in range 100{ (do calculations) recurse(x)}
thed0ctor

13

간단히 말해서, 꼬리 재귀는 컴파일러가 재귀 호출을 "goto"명령으로 대체 할 수있는 재귀이므로 컴파일 된 버전은 스택 깊이를 증가시킬 필요가 없습니다.

경우에 따라 꼬리 재귀 함수를 디자인하려면 추가 매개 변수를 사용하여 도우미 함수를 만들어야합니다.

예를 들어, 이것은 꼬리 재귀 함수 가 아닙니다 .

int factorial(int x) {
    if (x > 0) {
        return x * factorial(x - 1);
    }
    return 1;
}

그러나 이것은 꼬리 재귀 함수입니다.

int factorial(int x) {
    return tailfactorial(x, 1);
}

int tailfactorial(int x, int multiplier) {
    if (x > 0) {
        return tailfactorial(x - 1, x * multiplier);
    }
    return multiplier;
}

컴파일러는 다음과 같은 것을 사용하여 재귀 함수를 비 재귀 함수로 다시 작성할 수 있기 때문에 (의사 코드) :

int tailfactorial(int x, int multiplier) {
    start:
    if (x > 0) {
        multiplier = x * multiplier;
        x--;
        goto start;
    }
    return multiplier;
}

컴파일러의 규칙은 매우 간단합니다. " return thisfunction(newparameters);" 를 찾으면 " "로 바꾸십시오 parameters = newparameters; goto start;. 그러나 재귀 호출에 의해 반환 된 값이 직접 반환 된 경우에만 수행 할 수 있습니다.

경우 모든 함수의 재귀 호출은 다음과 같이 교체 할 수 있습니다, 그것은 꼬리 재귀 함수입니다.


13

저의 대답은 컴퓨터 프로그램의 구조와 해석에 주어진 설명을 바탕으로합니다 . 나는이 책을 컴퓨터 과학자들에게 강력히 추천한다.

접근법 A : 선형 재귀 프로세스

(define (factorial n)
 (if (= n 1)
  1
  (* n (factorial (- n 1)))))

접근법 A 의 프로세스 모양은 다음과 같습니다.

(factorial 5)
(* 5 (factorial 4))
(* 5 (* 4 (factorial 3)))
(* 5 (* 4 (* 3 (factorial 2))))
(* 5 (* 4 (* 3 (* 2 (factorial 1)))))
(* 5 (* 4 (* 3 (* 2 (* 1)))))
(* 5 (* 4 (* 3 (* 2))))
(* 5 (* 4 (* 6)))
(* 5 (* 24))
120

접근법 B : 선형 반복 프로세스

(define (factorial n)
 (fact-iter 1 1 n))

(define (fact-iter product counter max-count)
 (if (> counter max-count)
  product
  (fact-iter (* counter product)
             (+ counter 1)
             max-count)))

접근법 B 의 프로세스 모양은 다음과 같습니다.

(factorial 5)
(fact-iter 1 1 5)
(fact-iter 1 2 5)
(fact-iter 2 3 5)
(fact-iter 6 4 5)
(fact-iter 24 5 5)
(fact-iter 120 6 5)
120

선형 반복 프로세스 (접근법 B)는 프로세스가 재귀 절차 인 경우에도 일정한 공간에서 실행됩니다. 이 접근법에서, 설정 변수는 임의의 시점에서 프로세스의 상태를 정의한다는 것에 주목해야한다. {product, counter, max-count}. 테일 재귀가 컴파일러 최적화를 가능하게하는 기술이기도합니다.

접근법 A에는 인터프리터가 유지하는 더 숨겨진 정보가 있으며 이는 기본적으로 연기 된 작업 체인입니다.


5

꼬리 재귀는 재귀 호출이 함수의 마지막 명령어 인 꼬리 재귀의 한 형태입니다 (꼬리 부분의 출처). 또한, 재귀 호출은 이전 값 (함수의 파라미터 이외의 참조)을 저장하는 메모리 셀에 대한 참조로 구성되어서는 안됩니다. 이런 식으로, 우리는 이전 값에 신경 쓰지 않고 모든 재귀 호출에 대해 하나의 스택 프레임으로 충분합니다. 꼬리 재귀는 재귀 알고리즘을 최적화하는 한 가지 방법입니다. 다른 장점 / 최적화는 테일 재귀 알고리즘을 재귀 대신 반복을 사용하는 동등한 알고리즘으로 쉽게 변환 할 수 있다는 것입니다. 예, 퀵 정렬을위한 알고리즘은 실제로 꼬리 재귀입니다.

QUICKSORT(A, p, r)
    if(p < r)
    then
        q = PARTITION(A, p, r)
        QUICKSORT(A, p, q–1)
        QUICKSORT(A, q+1, r)

반복 버전은 다음과 같습니다.

QUICKSORT(A)
    p = 0, r = len(A) - 1
    while(p < r)
        q = PARTITION(A, p, r)
        r = q - 1

    p = 0, r = len(A) - 1
    while(p < r)
        q = PARTITION(A, p, r)
        p = q + 1
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.