재귀 대신 루프를 사용하거나 둘 다 동일한 목적을 수행 할 수있는 알고리즘에서 루프를 사용하면 성능이 저하됩니까? 예 : 주어진 문자열이 회문인지 확인하십시오. 간단한 반복 알고리즘이 계산서에 적합 할 때 보여주기위한 수단으로 재귀를 사용하는 많은 프로그래머를 보았습니다. 컴파일러는 무엇을 사용할지 결정하는 데 중요한 역할을합니까?
재귀 대신 루프를 사용하거나 둘 다 동일한 목적을 수행 할 수있는 알고리즘에서 루프를 사용하면 성능이 저하됩니까? 예 : 주어진 문자열이 회문인지 확인하십시오. 간단한 반복 알고리즘이 계산서에 적합 할 때 보여주기위한 수단으로 재귀를 사용하는 많은 프로그래머를 보았습니다. 컴파일러는 무엇을 사용할지 결정하는 데 중요한 역할을합니까?
답변:
재귀 함수가 꼬리 재귀인지 (마지막 줄은 재귀 호출)에 따라 재귀가 더 비쌀 수 있습니다 . 꼬리 재귀 해야 컴파일러에 의해 인식되고 반복 대응에 최적화되어야합니다 (코드에서 간결하고 명확한 구현을 유지하면서).
몇 개월 또는 몇 년 안에 코드를 유지해야하는 빈약 한 빨판 (자신 또는 다른 사람)에게 가장 적합한 방식으로 알고리즘을 작성합니다. 성능 문제가 발생하면 코드를 프로파일 링 한 다음 반복 구현으로 이동하여 최적화 만 살펴보십시오. 메모 및 동적 프로그래밍 을 살펴볼 수 있습니다 .
tail recursion is optimized by compilers
그러나 모든 컴파일러가 꼬리 재귀를 지원하는 것은 아닙니다.
루프는 프로그램 성능을 향상시킬 수 있습니다. 재귀는 프로그래머의 성능을 향상시킬 수 있습니다. 귀하의 상황에서 더 중요한 것을 선택하십시오!
재귀를 반복과 비교하는 것은 십자 드라이버를 일자 드라이버와 비교하는 것과 같습니다. 대부분 당신 이 할 수있는 경우 납작한 머리를 가진 모든 십자 나사를 제거 있지만 해당 나 사용으로 설계된 드라이버를 사용하면 더 쉬울까요?
일부 알고리즘은 설계된 방식 (피보나치 시퀀스, 구조와 같은 트리 탐색 등)으로 인해 재귀에 적합합니다. 재귀는 알고리즘을 간결하고 이해하기 쉽게 만듭니다 (따라서 공유 가능하고 재사용 가능).
또한 일부 재귀 알고리즘은 "지연 평가"를 사용하여 반복 형제보다 더 효율적입니다. 즉, 루프가 실행될 때마다 필요한 시간보다 비싼 계산 만 수행합니다.
시작하기에 충분해야합니다. 나는 당신을 위해 몇 가지 기사와 예제를 파헤칠 것입니다.
링크 1 : Haskel vs PHP (재귀 vs 반복)
다음은 프로그래머가 PHP를 사용하여 큰 데이터 세트를 처리해야하는 예입니다. 그는 재귀를 사용하여 Haskel에서 처리하는 것이 얼마나 쉬운 지 보여 주지만 PHP는 동일한 방법을 쉽게 수행 할 수있는 방법이 없기 때문에 반복을 사용하여 결과를 얻었습니다.
http://blog.webspecies.co.uk/2011-05-31/lazy-evaluation-with-php.html
링크 2 : 마스터 링 재귀
재귀의 나쁜 평판은 대부분 명령형 언어의 높은 비용과 비 효율성에서 비롯됩니다. 이 기사의 저자는 재귀 알고리즘을 더 빠르고 효율적으로 최적화하는 방법에 대해 설명합니다. 또한 전통적인 루프를 재귀 함수로 변환하는 방법과 테일 엔드 재귀 사용의 이점을 설명합니다. 그의 마지막 말은 실제로 내가 생각하는 핵심 요점 중 일부를 요약했습니다.
"재귀 프로그래밍은 프로그래머가 유지 관리 가능하고 논리적으로 일관된 방식으로 코드를 구성하는 더 나은 방법을 제공합니다."
링크 3 : 재귀가 루핑보다 훨씬 빠릅니까? (대답)
다음은 귀하와 유사한 stackoverflow 질문에 대한 답변 링크입니다. 저자는 되풀이 또는 반복과 관련된 많은 벤치 마크가 언어 마다 매우 다르다고 지적합니다 . 명령형 언어는 일반적으로 루프를 사용하여 더 빠르며 재귀를 사용하면 느리게 기능하고 그 반대도 마찬가지입니다. 이 링크에서 취해야 할 요점은 언어에 구애받지 않고 상황 맹목적으로 질문에 대답하기가 매우 어렵다는 것입니다.
일반적으로 성능 저하가 다른 방향으로 나타날 것으로 예상합니다. 재귀 호출은 추가 스택 프레임을 구성 할 수 있습니다. 이에 대한 형벌은 다양합니다. 또한 Python과 같은 일부 언어 (보다 정확하게는 일부 언어의 일부 구현에서는 ...)에서 트리 데이터 구조에서 최대 값 찾기와 같이 재귀 적으로 지정할 수있는 작업에 대해 스택 제한을 쉽게 실행할 수 있습니다. 이 경우 루프를 고수하려고합니다.
좋은 재귀 함수를 작성하면 테일 재귀 등을 최적화하는 컴파일러가 있다고 가정 할 때 성능 저하가 다소 줄어들 수 있습니다. 또한 함수가 실제로 테일 재귀인지 확인하기 위해 두 번 확인하십시오. 많은 사람들이 실수하는 것 중 하나입니다 의 위에.)
"가장자리"사례 (고성능 컴퓨팅, 매우 큰 재귀 깊이 등) 외에도 의도를 가장 명확하게 표현하고 설계가 잘되어 있으며 유지 관리가 가능한 접근 방식을 채택하는 것이 좋습니다. 요구를 파악한 후에 만 최적화하십시오.
재귀는 여러 개의 작은 조각 으로 나눌 수있는 문제에 대해 반복보다 낫습니다 .
예를 들어 재귀 피보나치 알고리즘을 만들려면 fib (n)을 fib (n-1) 및 fib (n-2)로 나누고 두 부분을 모두 계산합니다. 반복을 통해 단일 기능을 반복해서 반복 할 수 있습니다.
그러나 피보나치는 실제로 깨진 예이며 반복이 실제로 더 효율적이라고 생각합니다. fib (n) = fib (n-1) + fib (n-2) 및 fib (n-1) = fib (n-2) + fib (n-3)에 유의하십시오. fib (n-1)이 두 번 계산됩니다!
더 좋은 예는 트리의 재귀 알고리즘입니다. 부모 노드를 분석하는 문제는 각 자식 노드를 분석하는 여러 가지 작은 문제 로 나눌 수 있습니다 . 피보나치 예제와 달리 작은 문제는 서로 독립적입니다.
예, 재귀는 여러 개의 작고 독립적이며 유사한 문제로 나눌 수있는 문제에 대해 반복보다 낫습니다.
어떤 언어로든 메소드를 호출하면 많은 준비가 필요하기 때문에 재귀를 사용할 때 성능이 저하됩니다. 호출 코드는 리턴 주소, 호출 매개 변수, 프로세서 레지스터와 같은 기타 컨텍스트 정보가 어딘가에 저장 될 수 있으며 리턴 시간에 호출 된 메소드는 리턴 값을 게시 한 다음 호출자가 검색하며 이전에 저장된 컨텍스트 정보가 복원됩니다. 반복적 접근과 재귀 적 접근 사이의 성능 차이는 이러한 작업에 걸리는 시간에 있습니다.
구현 관점에서, 호출 컨텍스트를 처리하는 데 걸리는 시간이 메소드를 실행하는 데 걸리는 시간과 비교할 때 차이를 인식하기 시작합니다. 재귀 적 메서드가 호출 컨텍스트 관리 부분을 실행하는 데 시간이 오래 걸리면 코드를 일반적으로 더 읽기 쉽고 이해하기 쉽고 재귀 적 방식으로 수행하므로 성능 손실을 알 수 없습니다. 그렇지 않으면 효율성상의 이유로 반복적으로 진행하십시오.
Java의 꼬리 재귀가 현재 최적화되어 있지 않다고 생각합니다. 자세한 내용은 LtU 및 관련 링크에 대한 이 토론 전체에 뿌려 집니다. 그것은 수 다가오는 버전 7의 기능,하지만 분명히 특정 프레임이 누락 될 수 있기 때문에 스택 검사와 함께 특정 어려움을 선물한다. 스택 검사는 Java 2부터 세밀한 보안 모델을 구현하는 데 사용되었습니다.
재귀는 일부 상황에서 매우 유용합니다. 예를 들어 계승을 구하는 코드를 고려하십시오.
int factorial ( int input )
{
int x, fact = 1;
for ( x = input; x > 1; x--)
fact *= x;
return fact;
}
이제 재귀 함수를 사용하여 고려하십시오.
int factorial ( int input )
{
if (input == 0)
{
return 1;
}
return input * factorial(input - 1);
}
이 두 가지를 관찰하면 재귀를 이해하기 쉽다는 것을 알 수 있습니다. 그러나주의해서 사용하지 않으면 오류가 발생하기 쉽습니다. 우리가 놓치면 if (input == 0)
코드가 얼마 동안 실행되고 일반적으로 스택 오버플로로 끝납니다.
foldl (*) 1 [1..n]
습니다.
대부분의 경우 캐싱으로 인해 재귀가 빨라져 성능이 향상됩니다. 예를 들어, 다음은 전통적인 병합 루틴을 사용하는 반복 정렬 버전입니다. 캐싱 성능 향상으로 인해 재귀 구현보다 느리게 실행됩니다.
public static void sort(Comparable[] a)
{
int N = a.length;
aux = new Comparable[N];
for (int sz = 1; sz < N; sz = sz+sz)
for (int lo = 0; lo < N-sz; lo += sz+sz)
merge(a, lo, lo+sz-1, Math.min(lo+sz+sz-1, N-1));
}
private static void sort(Comparable[] a, Comparable[] aux, int lo, int hi)
{
if (hi <= lo) return;
int mid = lo + (hi - lo) / 2;
sort(a, aux, lo, mid);
sort(a, aux, mid+1, hi);
merge(a, aux, lo, mid, hi);
}
추신-이것은 코스 라에 제시된 알고리즘에 대한 과정에서 케빈 웨인 교수 (프린스턴 대학교)가 말한 것입니다.
재귀를 사용하면 각 "반복"마다 함수 호출 비용이 발생하지만 루프에서는 일반적으로 지불하는 것이 증가 / 감소뿐입니다. 따라서 루프 코드가 재귀 솔루션 코드보다 훨씬 복잡하지 않은 경우 일반적으로 루프가 재귀보다 우수합니다.
목록을 반복하는 중이라면 반복하십시오.
다른 몇 가지 대답은 (심도 우선) 나무 통과를 언급했습니다. 매우 일반적인 데이터 구조에 대해 수행하는 것이 매우 일반적이기 때문에 정말 좋은 예입니다. 재귀는이 문제에 대해 매우 직관적입니다.
여기서 "찾기"방법을 확인하십시오. http://penguin.ewu.edu/cscd300/Topic/BSTintro/index.html
재귀는 반복의 가능한 정의보다 더 단순하고 따라서 더 근본적입니다. 콤비 네이터 쌍 만으로 Turing-complete 시스템을 정의 할 수 있습니다 (예, 재귀 자체도 그러한 시스템의 파생 개념 임). Lambda 미적분학은 재귀 함수를 특징으로하는 강력한 기본 시스템입니다. 그러나 반복을 올바르게 정의하려면 시작하기 위해 훨씬 더 많은 프리미티브가 필요합니다.
코드에 관해서는, 대부분의 데이터 구조는 재귀 적이므로 재귀 코드는 순전히 반복적 인 코드보다 이해하고 유지하기가 훨씬 쉽습니다. 물론, 그것을 올바르게 얻으려면 최소한 모든 표준 결합기와 반복자를 깔끔하게 얻으려면 고차 함수와 클로저를 지원하는 언어가 필요합니다. 물론 C ++에서는 FC ++ 의 하드 코어 사용자가 아니라면 복잡한 재귀 솔루션이 약간 추한 것처럼 보일 수 있습니다 .
"재귀 깊이"에 따라 다릅니다. 함수 호출 오버 헤드가 총 실행 시간에 얼마나 영향을 미치는지에 달려 있습니다.
예를 들어, 재귀 방식으로 클래식 계승을 계산하는 것은 다음과 같은 이유로 매우 비효율적입니다.-데이터 오버플로 위험-스택 오버플로 위험-함수 호출 오버 헤드가 실행 시간의 80 %를 차지함
후속 N 이동을 분석 할 체스 게임에서 위치 분석을위한 최소-최대 알고리즘을 개발하는 동안 "분석 깊이"에 대한 재귀로 구현할 수 있습니다 (^_^)
재귀? Wiki는 어디서부터 시작합니까?“자기 비슷한 방식으로 항목을 반복하는 과정입니다”
내가 C를하고 있었을 때, C ++ 재귀는 "Tail recursion"과 같은 신이 보낸 것이 었습니다. 또한 많은 정렬 알고리즘에서 재귀를 사용합니다. 빠른 정렬 예 : http://alienryderflex.com/quicksort/
재귀는 특정 문제에 유용한 다른 알고리즘과 같습니다. 아마도 당신은 곧바로 또는 자주 사용하지 않을 수도 있지만 사용 가능한 것이 기쁠 것입니다.
C ++에서 재귀 함수가 템플릿 함수 인 경우 모든 유형 공제 및 함수 인스턴스화가 컴파일 시간에 발생하므로 컴파일러는이를 최적화 할 수 있습니다. 최신 컴파일러는 가능한 경우 함수를 인라인 할 수도 있습니다. 따라서 -O3
또는 -O2
in 과 같은 최적화 플래그를 사용하면 g++
재귀가 반복보다 빠를 수 있습니다. 반복 코드에서는 컴파일러가 이미 최적 상태 (충분히 작성된 경우)에 있으므로 최적화 할 기회가 줄어 듭니다.
필자의 경우, Armadillo 행렬 객체를 사용하여 재귀 및 반복 방식으로 행렬 지수를 구현하려고했습니다. 알고리즘은 https://en.wikipedia.org/wiki/Exponentiation_by_squaring 에서 찾을 수 있습니다 . 내 함수는 템플릿이었고 나는 1,000,000
12x12
거듭 제곱 행렬 을 계산했습니다 10
. 나는 다음과 같은 결과를 얻었다 :
iterative + optimisation flag -O3 -> 2.79.. sec
recursive + optimisation flag -O3 -> 1.32.. sec
iterative + No-optimisation flag -> 2.83.. sec
recursive + No-optimisation flag -> 4.15.. sec
이 결과는 c ++ 11 플래그 ( -std=c++11
)가있는 gcc-4.8 및 Intel mkl이있는 Armadillo 6.1을 사용하여 얻었습니다. 인텔 컴파일러도 비슷한 결과를 보여줍니다.
반복을 원자이며, 새로운 스택 프레임을 밀어 것보다 수십 배 더 비싼 경우 와 새로운 쓰레드를 생성 하고 여러 개의 코어를 가지고 및 런타임 환경이 그들 모두를 사용할 수 있습니다과 결합 될 때, 다음 재귀 방법은 큰 성능 향상을 얻을 수있다 멀티 스레딩. 평균 반복 횟수를 예측할 수없는 경우 스레드 할당을 제어하고 프로세스가 너무 많은 스레드를 작성하고 시스템을 호깅하지 못하게하는 스레드 풀을 사용하는 것이 좋습니다.
예를 들어, 일부 언어에는 재귀 멀티 스레드 병합 정렬 구현이 있습니다.
그러나 다시 멀티 스레딩을 재귀가 아닌 루핑과 함께 사용할 수 있으므로이 조합의 작동 방식은 OS 및 스레드 할당 메커니즘을 비롯한 더 많은 요소에 따라 다릅니다.
내가 아는 한 Perl은 꼬리 재귀 호출을 최적화하지는 않지만 가짜로 만들 수 있습니다.
sub f{
my($l,$r) = @_;
if( $l >= $r ){
return $l;
} else {
# return f( $l+1, $r );
@_ = ( $l+1, $r );
goto &f;
}
}
처음 호출되면 스택에 공간을 할당합니다. 그런 다음 인수를 변경하고 스택에 더 이상 아무것도 추가하지 않고 서브 루틴을 다시 시작합니다. 그러므로 그것은 결코 자기 자신을 부르지 않은 것처럼 가장하여 반복적 인 과정으로 바꾼다.
" my @_;
"또는 " local @_;
"가 없으면 더 이상 작동하지 않습니다.
Chrome 45.0.2454.85m 만 사용하면 재귀가 훨씬 빠릅니다.
코드는 다음과 같습니다.
(function recursionVsForLoop(global) {
"use strict";
// Perf test
function perfTest() {}
perfTest.prototype.do = function(ns, fn) {
console.time(ns);
fn();
console.timeEnd(ns);
};
// Recursion method
(function recur() {
var count = 0;
global.recurFn = function recurFn(fn, cycles) {
fn();
count = count + 1;
if (count !== cycles) recurFn(fn, cycles);
};
})();
// Looped method
function loopFn(fn, cycles) {
for (var i = 0; i < cycles; i++) {
fn();
}
}
// Tests
var curTest = new perfTest(),
testsToRun = 100;
curTest.do('recursion', function() {
recurFn(function() {
console.log('a recur run.');
}, testsToRun);
});
curTest.do('loop', function() {
loopFn(function() {
console.log('a loop run.');
}, testsToRun);
});
})(window);
결과
// 표준 for 루프를 사용하여 100 회 실행
루프 실행을위한 100x. 완료 시간 : 7.683ms
// 꼬리 재귀와 함께 기능 재귀 접근 방식을 사용하여 100 회 실행
100x 재귀 실행. 완료 시간 : 4.841ms
아래 스크린 샷에서 테스트 당 300 사이클로 실행하면 재귀가 더 큰 마진으로 다시 승리합니다.
나는 그 접근법들 사이에 또 다른 차이점을 발견했습니다. 간단하고 중요하지 않은 것처럼 보이지만 인터뷰 준비를하는 동안 매우 중요한 역할을하며이 주제가 발생하므로 자세히 살펴보십시오.
간단히 말하면 : 1) 반복적 인 주문 후 순회가 쉽지 않습니다.-DFT를 더 복잡하게 만듭니다. 2) 재귀로 사이클을 쉽게 확인합니다.
세부:
재귀 적 인 경우 사전 및 사후 순회를 쉽게 만들 수 있습니다.
"작업이 다른 작업에 의존 할 때 작업 5를 실행하기 위해 실행해야하는 모든 작업을 인쇄합니다"라는 매우 표준적인 질문을 상상해보십시오.
예:
//key-task, value-list of tasks the key task depends on
//"adjacency map":
Map<Integer, List<Integer>> tasksMap = new HashMap<>();
tasksMap.put(0, new ArrayList<>());
tasksMap.put(1, new ArrayList<>());
List<Integer> t2 = new ArrayList<>();
t2.add(0);
t2.add(1);
tasksMap.put(2, t2);
List<Integer> t3 = new ArrayList<>();
t3.add(2);
t3.add(10);
tasksMap.put(3, t3);
List<Integer> t4 = new ArrayList<>();
t4.add(3);
tasksMap.put(4, t4);
List<Integer> t5 = new ArrayList<>();
t5.add(3);
tasksMap.put(5, t5);
tasksMap.put(6, new ArrayList<>());
tasksMap.put(7, new ArrayList<>());
List<Integer> t8 = new ArrayList<>();
t8.add(5);
tasksMap.put(8, t8);
List<Integer> t9 = new ArrayList<>();
t9.add(4);
tasksMap.put(9, t9);
tasksMap.put(10, new ArrayList<>());
//task to analyze:
int task = 5;
List<Integer> res11 = getTasksInOrderDftReqPostOrder(tasksMap, task);
System.out.println(res11);**//note, no reverse required**
List<Integer> res12 = getTasksInOrderDftReqPreOrder(tasksMap, task);
Collections.reverse(res12);//note reverse!
System.out.println(res12);
private static List<Integer> getTasksInOrderDftReqPreOrder(Map<Integer, List<Integer>> tasksMap, int task) {
List<Integer> result = new ArrayList<>();
Set<Integer> visited = new HashSet<>();
reqPreOrder(tasksMap,task,result, visited);
return result;
}
private static void reqPreOrder(Map<Integer, List<Integer>> tasksMap, int task, List<Integer> result, Set<Integer> visited) {
if(!visited.contains(task)) {
visited.add(task);
result.add(task);//pre order!
List<Integer> children = tasksMap.get(task);
if (children != null && children.size() > 0) {
for (Integer child : children) {
reqPreOrder(tasksMap,child,result, visited);
}
}
}
}
private static List<Integer> getTasksInOrderDftReqPostOrder(Map<Integer, List<Integer>> tasksMap, int task) {
List<Integer> result = new ArrayList<>();
Set<Integer> visited = new HashSet<>();
reqPostOrder(tasksMap,task,result, visited);
return result;
}
private static void reqPostOrder(Map<Integer, List<Integer>> tasksMap, int task, List<Integer> result, Set<Integer> visited) {
if(!visited.contains(task)) {
visited.add(task);
List<Integer> children = tasksMap.get(task);
if (children != null && children.size() > 0) {
for (Integer child : children) {
reqPostOrder(tasksMap,child,result, visited);
}
}
result.add(task);//post order!
}
}
재귀적인 주문 후 순회에는 결과의 후속 역 분개가 필요하지 않습니다. 자녀가 먼저 인쇄하고 문제의 과제가 마지막에 인쇄됩니다. 다 괜찮아 재귀 사전 주문 순회 (위에 표시됨)를 수행 할 수 있으며 결과 목록을 취소해야합니다.
반복적 인 접근 방식으로는 그렇게 간단하지 않습니다! 반복적 인 (하나의 스택) 접근 방식에서는 사전 주문 순회 만 할 수 있으므로 마지막에 결과 배열을 되돌려 야합니다.
List<Integer> res1 = getTasksInOrderDftStack(tasksMap, task);
Collections.reverse(res1);//note reverse!
System.out.println(res1);
private static List<Integer> getTasksInOrderDftStack(Map<Integer, List<Integer>> tasksMap, int task) {
List<Integer> result = new ArrayList<>();
Set<Integer> visited = new HashSet<>();
Stack<Integer> st = new Stack<>();
st.add(task);
visited.add(task);
while(!st.isEmpty()){
Integer node = st.pop();
List<Integer> children = tasksMap.get(node);
result.add(node);
if(children!=null && children.size() > 0){
for(Integer child:children){
if(!visited.contains(child)){
st.add(child);
visited.add(child);
}
}
}
//If you put it here - it does not matter - it is anyway a pre-order
//result.add(node);
}
return result;
}
간단 해 보이죠?
그러나 일부 인터뷰에서는 함정입니다.
재귀 접근 방식을 사용하면 Depth First Traversal을 구현 한 다음 사전 또는 사후 필요한 순서를 선택할 수 있습니다 ( "결과 목록에 추가"의 경우 "인쇄"의 위치를 변경하여 간단히). ). 반복 (한 번의 스택) 접근 방식을 사용하면 사전 주문 순회 만 쉽게 수행 할 수 있으므로 어린이를 먼저 인쇄 해야하는 상황 (하단 노드에서 인쇄를 시작 해야하는 모든 상황) 문제. 그 문제가 있으면 나중에 되돌릴 수 있지만 알고리즘에 추가됩니다. 면접관이 자신의 시계를보고 있다면 문제가 될 수 있습니다. 반복적 인 주문 후 순회를 수행하는 복잡한 방법이 있지만 존재하지만 단순하지는 않습니다 . 예:https://www.geeksforgeeks.org/iterative-postorder-traversal-using-stack/
결론 : 인터뷰 중에 재귀를 사용하고 관리하고 설명하는 것이 더 간단합니다. 긴급한 상황에서 사전 주문 후 주문 순회로 쉽게 이동할 수 있습니다. 반복적으로 당신은 그렇게 유연하지 않습니다.
재귀를 사용하고 다음과 같이 말합니다. "좋아요. 그러나 반복적으로 사용 된 메모리를보다 직접적으로 제어 할 수 있습니다. 스택 크기를 쉽게 측정하고 위험한 오버플로를 허용하지 않습니다."
재귀의 또 다른 장점-그래프에서 사이클을 피 / 통지하는 것이 더 간단합니다.
예 (사전 코드) :
dft(n){
mark(n)
for(child: n.children){
if(marked(child))
explode - cycle found!!!
dft(child)
}
unmark(n)
}
재귀 또는 연습으로 작성하는 것이 재미있을 수 있습니다.
그러나 프로덕션에서 코드를 사용하려면 스택 오버플로 가능성을 고려해야합니다.
테일 재귀 최적화는 스택 오버플로를 제거 할 수 있지만 그렇게 만드는 데 어려움을 겪고 싶고 환경에서 최적화를 수행 할 수 있다는 것을 알아야합니다.
n
됩니까?데이터 크기를 줄이거 나 n
되풀이 할 때마다 절반 씩 줄이면 일반적으로 스택 오버플로에 대해 걱정할 필요가 없습니다. 말, 그것은 깊은 4,000 수준이나 스택 오버 플로우 프로그램에 대한 깊은 10,000 수준, 약이 될 다음 데이터 크기 요구해야 할 경우 4000 프로그램에 대한 스택 오버 플로우. 이를 고려할 때 가장 큰 저장 장치는 최근에 2 61 바이트를 보유 할 수 있으며 , 이러한 장치 가 2 61 개이면 2122 개의 데이터 크기 만 처리 합니다. 우주의 모든 원자를보고 있다면, 그것이 2보다 작을 것으로 추정됩니다 (84). 140 억 년 전에 우주가 탄생 한 이후 1 밀리 초마다 우주의 모든 데이터와 해당 상태를 처리해야하는 경우 2,153 만 될 수 있습니다 . 따라서 프로그램이 2 4000 단위의 데이터 또는를 처리 할 수 있으면 n
유니버스의 모든 데이터를 처리 할 수 있으며 프로그램에서 오버플로가 발생하지 않습니다. 2 4000 (4000 비트 정수) 만큼 큰 숫자를 처리 할 필요가없는 경우 일반적으로 스택 오버플로에 대해 걱정할 필요가 없습니다.
그러나 데이터 크기를 줄이거 나 n
되풀이 할 때마다 일정한 양 을 줄이면 프로그램 n
이 1000
제대로 실행되는 경우 가 있지만 어떤 상황에서는 n
단순히 일 때 스택 오버플로가 발생할 수 있습니다 20000
.
따라서 스택 오버플로가 발생할 가능성이 있으면 반복적 인 솔루션으로 만들어보십시오.
저는 재귀에 일종의 "이중"인 "유도"에 의해 Haskell 데이터 구조를 설계함으로써 귀하의 질문에 대답 할 것입니다. 그런 다음이 이원성이 어떻게 멋진 일을하는지 보여줄 것입니다.
간단한 트리를위한 타입을 소개합니다 :
data Tree a = Branch (Tree a) (Tree a)
| Leaf a
deriving (Eq)
우리는이 정의를 "나무는 가지 (두 개의 나무를 포함) 또는 잎 (데이터 값을 포함)"라고 말합니다. 잎은 최소한의 경우입니다. 나무가 잎이 아닌 경우 두 개의 나무가 포함 된 복합 트리 여야합니다. 이것 만이 유일한 경우입니다.
나무를 만들어 봅시다 :
example :: Tree Int
example = Branch (Leaf 1)
(Branch (Leaf 2)
(Leaf 3))
이제 트리의 각 값에 1을 더한다고 가정 해 봅시다. 다음을 호출하여이 작업을 수행 할 수 있습니다.
addOne :: Tree Int -> Tree Int
addOne (Branch a b) = Branch (addOne a) (addOne b)
addOne (Leaf a) = Leaf (a + 1)
첫째, 이것이 사실 재귀 적 정의임을 주목하십시오. 데이터 생성자 Branch 및 Leaf를 사례로 사용합니다. Leaf가 최소이고 가능한 유일한 사례이므로 함수가 종료 될 것입니다.
addOne을 반복적 인 스타일로 작성하려면 무엇이 필요합니까? 임의의 수의 분기로 반복되는 모양은 무엇입니까?
또한 이런 종류의 재귀는 종종 "펑터 (functor)"라는 관점에서 제외 될 수 있습니다. 다음을 정의하여 나무를 Functors로 만들 수 있습니다.
instance Functor Tree where fmap f (Leaf a) = Leaf (f a)
fmap f (Branch a b) = Branch (fmap f a) (fmap f b)
그리고 정의 :
addOne' = fmap (+1)
대수 데이터 형식에 대한 이변 형 (또는 접기)과 같은 다른 재귀 체계를 제외 할 수 있습니다. 이형 법을 사용하여 다음과 같이 작성할 수 있습니다.
addOne'' = cata go where
go (Leaf a) = Leaf (a + 1)
go (Branch a b) = Branch a b
스택 오버플로는 내장 메모리 관리 기능이없는 언어로 프로그래밍하는 경우에만 발생합니다. 그렇지 않으면 함수 (또는 함수 호출, STDLbs 등)에 무언가가 있는지 확인하십시오. 재귀가 없으면 단순히 구글이나 SQL과 같은 것을 가질 수 없으며 또는 대규모 데이터 구조 (클래스) 또는 데이터베이스를 효율적으로 정렬 해야하는 곳은 없습니다.
재귀는 파일을 반복하고 싶을 때가는 방법입니다. 'find * | ? grep * '작동합니다. Kinda 이중 재귀, 특히 파이프를 사용하십시오 (그러나 다른 사람들이 사용할 수있는 것이면 많은 사람들처럼 많은 syscall을 수행하지 마십시오).
고급 언어 및 clang / cpp조차 백그라운드에서 동일하게 구현할 수 있습니다.