나는 재귀의 일반적인 개념을 알고있다. 퀵 정렬 알고리즘을 연구하면서 테일 재귀 개념을 발견했습니다 . 이 년 MIT에서 빠른 정렬 알고리즘의 비디오 18시 반 초 교수의이 꼬리 재귀 알고리즘이라고 말했다. 꼬리 재귀가 실제로 무엇을 의미하는지는 분명하지 않습니다.
누군가 적절한 개념으로 개념을 설명 할 수 있습니까?
나는 재귀의 일반적인 개념을 알고있다. 퀵 정렬 알고리즘을 연구하면서 테일 재귀 개념을 발견했습니다 . 이 년 MIT에서 빠른 정렬 알고리즘의 비디오 18시 반 초 교수의이 꼬리 재귀 알고리즘이라고 말했다. 꼬리 재귀가 실제로 무엇을 의미하는지는 분명하지 않습니다.
누군가 적절한 개념으로 개념을 설명 할 수 있습니까?
답변:
테일 재귀는 재귀 호출을 한 후 호출 함수가 더 이상 계산을 수행하지 않는 특별한 재귀 사례입니다. 예를 들어, 함수
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를 반환; }
재귀 호출이 반환 된 후 계산을 수행하기 때문입니다.
테일 재귀는 일반적인 재귀보다 더 효율적으로 구현 될 수 있기 때문에 중요합니다. 정상적인 재귀 호출을 할 때 반환 주소를 호출 스택으로 푸시 한 다음 호출 된 함수로 이동해야합니다. 즉, 재귀 호출 깊이에서 크기가 선형 인 호출 스택이 필요합니다. 꼬리 재귀가있을 때 우리는 재귀 호출에서 돌아 오는 즉시 즉시 돌아올 것이므로 재귀 함수의 전체 체인을 건너 뛰고 원래 호출자로 곧바로 돌아갈 수 있음을 알고 있습니다. 즉, 모든 재귀 호출에 대해 호출 스택이 전혀 필요하지 않으며 최종 호출을 간단한 점프로 구현하여 공간을 절약 할 수 있습니다.
def recurse(x): if x < 0 return 1; for i in range 100{ (do calculations) recurse(x)}
간단히 말해서, 꼬리 재귀는 컴파일러가 재귀 호출을 "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;
. 그러나 재귀 호출에 의해 반환 된 값이 직접 반환 된 경우에만 수행 할 수 있습니다.
경우 모든 함수의 재귀 호출은 다음과 같이 교체 할 수 있습니다, 그것은 꼬리 재귀 함수입니다.
저의 대답은 컴퓨터 프로그램의 구조와 해석에 주어진 설명을 바탕으로합니다 . 나는이 책을 컴퓨터 과학자들에게 강력히 추천한다.
(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
(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에는 인터프리터가 유지하는 더 숨겨진 정보가 있으며 이는 기본적으로 연기 된 작업 체인입니다.
꼬리 재귀는 재귀 호출이 함수의 마지막 명령어 인 꼬리 재귀의 한 형태입니다 (꼬리 부분의 출처). 또한, 재귀 호출은 이전 값 (함수의 파라미터 이외의 참조)을 저장하는 메모리 셀에 대한 참조로 구성되어서는 안됩니다. 이런 식으로, 우리는 이전 값에 신경 쓰지 않고 모든 재귀 호출에 대해 하나의 스택 프레임으로 충분합니다. 꼬리 재귀는 재귀 알고리즘을 최적화하는 한 가지 방법입니다. 다른 장점 / 최적화는 테일 재귀 알고리즘을 재귀 대신 반복을 사용하는 동등한 알고리즘으로 쉽게 변환 할 수 있다는 것입니다. 예, 퀵 정렬을위한 알고리즘은 실제로 꼬리 재귀입니다.
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