메일 링리스트와 온라인 토론에서 정기적으로 올라 오는 주제 중 하나는 컴퓨터 과학 학위를 취득하는 것의 장점 (또는 부족)입니다. 부정적인 당사자에 대해 몇 번이고 반복되는 것처럼 보이는 주장은 그들이 몇 년 동안 코딩을 해왔고 재귀를 사용한 적이 없다는 것입니다.
그래서 질문은 :
- 재귀 란 무엇입니까?
- 재귀는 언제 사용합니까?
- 사람들이 재귀를 사용하지 않는 이유는 무엇입니까?
메일 링리스트와 온라인 토론에서 정기적으로 올라 오는 주제 중 하나는 컴퓨터 과학 학위를 취득하는 것의 장점 (또는 부족)입니다. 부정적인 당사자에 대해 몇 번이고 반복되는 것처럼 보이는 주장은 그들이 몇 년 동안 코딩을 해왔고 재귀를 사용한 적이 없다는 것입니다.
그래서 질문은 :
답변:
이 스레드 에는 재귀 에 대한 여러 가지 좋은 설명 이 있습니다.이 답변은 대부분의 언어에서 재귀 를 사용하지 말아야하는 이유에 대한 것입니다. * 대부분의 주요 명령형 언어 구현 (즉, C, C ++, Basic, Python의 모든 주요 구현)에서 , Ruby, Java 및 C #) 반복 은 재귀보다 훨씬 선호됩니다.
이유를 확인하려면 위의 언어에서 함수를 호출하는 데 사용하는 단계를 살펴보세요.
이 모든 단계를 수행하는 데는 일반적으로 루프를 반복하는 것보다 약간 더 많은 시간이 걸립니다. 그러나 실제 문제는 1 단계에 있습니다. 많은 프로그램이 시작될 때 스택에 단일 메모리 청크를 할당하고 해당 메모리가 부족하면 (종종 재귀로 인한 것은 아니지만) 스택 오버플 로로 인해 프로그램이 충돌 합니다 .
따라서 이러한 언어에서는 재귀가 느리고 충돌에 취약합니다. 그래도 사용에 대한 몇 가지 주장이 있습니다. 일반적으로 재귀 적으로 작성된 코드는 읽는 방법을 알게되면 더 짧고 우아합니다.
언어 구현자가 일부 클래스의 스택 오버플로를 제거 할 수있는 테일 호출 최적화 라는 기술을 사용할 수 있습니다 . 간결하게 말하면 함수의 반환 표현식이 단순히 함수 호출의 결과 인 경우 스택에 새 수준을 추가 할 필요가없는 경우 호출되는 함수에 대해 현재 수준을 재사용 할 수 있습니다. 안타깝게도 명령형 언어 구현에는 꼬리 호출 최적화가 내장되어 있습니다.
* 나는 재귀를 좋아합니다. 내가 가장 좋아하는 정적 언어 는 루프를 전혀 사용하지 않습니다. 재귀는 무언가를 반복적으로 수행하는 유일한 방법입니다. 나는 재귀가 일반적으로 그것에 맞게 조정되지 않은 언어에서 좋은 생각이라고 생각하지 않습니다.
** 그런데 Mario, ArrangeString 함수의 일반적인 이름은 "join"이며 선택한 언어에 이미 구현되어 있지 않다면 놀랄 것입니다.
재귀의 간단한 영어 예.
A child couldn't sleep, so her mother told her a story about a little frog,
who couldn't sleep, so the frog's mother told her a story about a little bear,
who couldn't sleep, so the bear's mother told her a story about a little weasel...
who fell asleep.
...and the little bear fell asleep;
...and the little frog fell asleep;
...and the child fell asleep.
가장 기본적인 컴퓨터 과학적 의미에서 재귀는 자신을 호출하는 함수입니다. 연결 목록 구조가 있다고 가정합니다.
struct Node {
Node* next;
};
그리고 당신은 재귀로 이것을 할 수있는 연결 목록의 길이를 알아 내고 싶습니다 :
int length(const Node* list) {
if (!list->next) {
return 1;
} else {
return 1 + length(list->next);
}
}
(물론 for 루프로도 수행 할 수 있지만 개념을 설명하는 데 유용합니다.)
length(list->next)
로 돌아 가야합니다 length(list)
. 길이를 전달하도록 작성 되었으면 호출자가 존재한다는 사실을 잊을 수 있습니다. 처럼 int length(const Node* list, int count=0) { return (!list) ? count : length(list->next, count + 1); }
.
함수가 자신을 호출 할 때마다 루프를 생성하면 재귀입니다. 모든 것과 마찬가지로 재귀에는 좋은 용도와 나쁜 용도가 있습니다.
가장 간단한 예는 함수의 마지막 줄이 자신에 대한 호출 인 꼬리 재귀입니다.
int FloorByTen(int num)
{
if (num % 10 == 0)
return num;
else
return FloorByTen(num-1);
}
그러나 이것은 좀 더 효율적인 반복으로 쉽게 대체 될 수 있기 때문에 거의 무의미한 예입니다. 결국 재귀는 함수 호출 오버 헤드로 인해 어려움을 겪습니다. 위의 예에서는 함수 자체 내부의 작업에 비해 상당 할 수 있습니다.
따라서 반복보다는 재귀를 수행하는 모든 이유는 호출 스택 을 활용하여 영리한 작업을 수행하는 것입니다. 예를 들어 동일한 루프 내에서 다른 매개 변수를 사용하여 함수를 여러 번 호출하면 분기 를 수행하는 방법 입니다. 고전적인 예는 Sierpinski 삼각형 입니다.
호출 스택이 세 방향으로 분기되는 재귀를 사용하여 매우 간단하게 그 중 하나를 그릴 수 있습니다.
private void BuildVertices(double x, double y, double len)
{
if (len > 0.002)
{
mesh.Positions.Add(new Point3D(x, y + len, -len));
mesh.Positions.Add(new Point3D(x - len, y - len, -len));
mesh.Positions.Add(new Point3D(x + len, y - len, -len));
len *= 0.5;
BuildVertices(x, y + len, len);
BuildVertices(x - len, y - len, len);
BuildVertices(x + len, y - len, len);
}
}
반복으로 동일한 작업을 시도하면 수행하는 데 더 많은 코드가 필요하다는 것을 알게 될 것입니다.
다른 일반적인 사용 사례에는 웹 사이트 크롤러, 디렉토리 비교 등과 같은 계층 구조를 통과하는 것이 포함될 수 있습니다.
결론
실제로 반복적 인 분기가 필요할 때마다 재귀가 가장 적합합니다.
재귀는 분할 정복 사고 방식을 기반으로 문제를 해결하는 방법입니다. 기본 아이디어는 원래 문제를 가져 와서 더 작은 (더 쉽게 해결되는) 인스턴스로 나누고, 이러한 작은 인스턴스를 해결 한 다음 (일반적으로 동일한 알고리즘을 다시 사용하여) 최종 솔루션으로 재 조립하는 것입니다.
표준 예는 n의 계승을 생성하는 루틴입니다. n의 계승은 1과 n 사이의 모든 숫자를 곱하여 계산됩니다. C #의 반복 솔루션은 다음과 같습니다.
public int Fact(int n)
{
int fact = 1;
for( int i = 2; i <= n; i++)
{
fact = fact * i;
}
return fact;
}
반복적 솔루션에 대해 놀라운 것은 없으며 C #에 익숙한 사람이라면 누구나 이해할 수 있습니다.
재귀 적 해는 n 번째 팩토리얼이 n * Fact (n-1)임을 인식하여 구합니다. 또는 다른 방법으로 말하자면, 특정 팩토리얼 숫자가 무엇인지 안다면 다음 숫자를 계산할 수 있습니다. 다음은 C #의 재귀 솔루션입니다.
public int FactRec(int n)
{
if( n < 2 )
{
return 1;
}
return n * FactRec( n - 1 );
}
이 함수의 첫 번째 부분은 Base Case (또는 때때로 Guard Clause) 로 알려져 있으며 알고리즘이 영원히 실행되는 것을 방지합니다. 함수가 1 이하의 값으로 호출 될 때마다 값 1을 반환합니다. 두 번째 부분은 더 흥미롭고 재귀 적 단계 로 알려져 있습니다. 여기서 우리는 약간 수정 된 매개 변수를 사용하여 동일한 메서드를 호출 한 다음 (1 씩 감소) 결과에 n의 복사본을 곱합니다.
처음 만났을 때 이것은 다소 혼란 스러울 수 있으므로 실행할 때 어떻게 작동하는지 조사하는 것이 좋습니다. FactRec (5)를 호출한다고 상상해보십시오. 우리는 루틴에 들어가고, 기본 케이스에 의해 선택되지 않으므로 다음과 같이 끝납니다.
// In FactRec(5)
return 5 * FactRec( 5 - 1 );
// which is
return 5 * FactRec(4);
매개 변수 4로 메소드를 다시 입력하면 다시 가드 절에 의해 중지되지 않으므로 다음과 같이됩니다.
// In FactRec(4)
return 4 * FactRec(3);
이 반환 값을 위의 반환 값으로 대체하면
// In FactRec(5)
return 5 * (4 * FactRec(3));
이렇게하면 최종 솔루션에 도달하는 방법에 대한 단서를 얻을 수 있으므로 아래로가는 각 단계를 빠르게 추적하고 보여 드리겠습니다.
return 5 * (4 * FactRec(3));
return 5 * (4 * (3 * FactRec(2)));
return 5 * (4 * (3 * (2 * FactRec(1))));
return 5 * (4 * (3 * (2 * (1))));
최종 대체는 기본 케이스가 트리거 될 때 발생합니다. 이 시점에서 우리는 처음에 팩토리얼의 정의와 직접적으로 동일한 풀기위한 간단한 algrebraic 공식을 가지고 있습니다.
메서드를 호출 할 때마다 기본 사례가 트리거되거나 매개 변수가 기본 사례에 더 가까운 동일한 메서드에 대한 호출 (종종 재귀 호출이라고 함)이 발생한다는 점에 유의하는 것이 좋습니다. 그렇지 않은 경우 메서드는 영원히 실행됩니다.
FactRec()
곱 n
해야합니다.
재귀는 문제의 더 작은 버전을 해결 한 다음 그 결과와 다른 계산을 사용하여 원래 문제에 대한 답을 공식화하여 문제를 해결하는 방법을 말합니다. 종종 더 작은 버전을 해결하는 과정에서이 방법은 해결하기 쉬운 "기본 사례"에 도달 할 때까지 더 작은 버전의 문제를 해결합니다.
예를 들어 숫자에 대한 계승을 계산하기 위해 X
이를로 나타낼 수 있습니다 X times the factorial of X-1
. 따라서이 메서드는 "반복"하여의 계승을 찾은 X-1
다음 얻은 값을 곱하여 X
최종 답을 제공합니다. 물론의 계승을 찾기 위해 X-1
먼저의 계승을 계산하는 식 X-2
입니다. 때 기본 케이스는 것 X
그것을 반환 할 알고있는 경우에, 0 또는 1 1
부터 0! = 1! = 1
.
오래되고 잘 알려진 문제를 고려하십시오 .
수학에서 두 개 이상의 0이 아닌 정수 의 최대 공약수 (gcd)…는 나머지없이 숫자를 나누는 최대 양의 정수입니다.
gcd의 정의는 놀랍도록 간단합니다.
여기서 mod는 모듈로 연산자 (즉, 정수 나누기 이후의 나머지)입니다.
영어에서이 정의는 어떤 수와 0의 최대 공약수는 그 숫자 이고 , 두 수 m 과 n 의 최대 공약수는 n 의 최대 공약수 이고 m 을 n으로 나눈 나머지는 나머지 입니다.
이것이 작동하는 이유를 알고 싶다면 유클리드 알고리즘 에 대한 Wikipedia 기사를 참조하십시오 .
예를 들어 gcd (10, 8)를 계산해 봅시다. 각 단계는 바로 전 단계와 같습니다.
첫 번째 단계에서 8은 0이 아니므로 정의의 두 번째 부분이 적용됩니다. 10 mod 8 = 2 왜냐하면 8은 나머지 2와 함께 10으로 들어가기 때문입니다. 3 단계에서 두 번째 부분이 다시 적용되지만, 이번에는 2가 나머지없이 8을 나누기 때문에 8 mod 2 = 0입니다. 5 단계에서 두 번째 인수는 0이므로 답은 2입니다.
등호의 왼쪽과 오른쪽 모두에 gcd가 나타납니다. 수학자는 정의하는 표현식이 정의 내에서 반복 되기 때문에이 정의가 재귀 적이라고 말할 것 입니다.
재귀 정의는 우아한 경향이 있습니다. 예를 들어, 목록 합계에 대한 재귀 적 정의는 다음과 같습니다.
sum l =
if empty(l)
return 0
else
return head(l) + sum(tail(l))
여기서 head
목록의 첫 번째 요소이며, tail
리스트의 나머지이다. 그 주 sum
말의 정의 내부의 재발을.
대신 목록의 최대 값을 선호 할 수 있습니다.
max l =
if empty(l)
error
elsif length(l) = 1
return head(l)
else
tailmax = max(tail(l))
if head(l) > tailmax
return head(l)
else
return tailmax
음이 아닌 정수의 곱셈을 반복적으로 정의하여 일련의 덧셈으로 바꿀 수 있습니다.
a * b =
if b = 0
return 0
else
return a + (a * (b - 1))
곱셈을 일련의 덧셈으로 변환하는 것이 의미가 없다면 몇 가지 간단한 예제를 확장하여 작동 방식을 확인하십시오.
병합 정렬 에는 멋진 재귀 정의가 있습니다.
sort(l) =
if empty(l) or length(l) = 1
return l
else
(left,right) = split l
return merge(sort(left), sort(right))
재귀 적 정의는 무엇을 찾아야하는지 안다면 주위에 있습니다. 이 모든 정의는 매우 간단한 기본 케이스를 얼마나 공지 사항, 예를 들어 , GCD (m, 0) = m. 재귀 적 사례는 쉬운 답을 찾기 위해 문제를 해결합니다.
이러한 이해를 바탕으로 Wikipedia의 재귀 기사 에서 다른 알고리즘을 이해할 수 있습니다 !
표준 예는 다음과 같은 계승입니다.
int fact(int a)
{
if(a==1)
return 1;
return a*fact(a-1);
}
일반적으로 재귀는 반드시 빠르지는 않습니다 (재귀 함수가 작은 경향이 있기 때문에 함수 호출 오버 헤드가 높은 경향이 있습니다. 위 참조). 몇 가지 문제가 발생할 수 있습니다 (누군가 스택 오버 플로우?). 어떤 사람들은 사소하지 않은 경우에 '올바르게'얻기가 어려운 경향이 있다고 말하지만 실제로는 그렇게 생각하지 않습니다. 어떤 상황에서는 재귀가 가장 의미가 있으며 특정 함수를 작성하는 가장 우아하고 명확한 방법입니다. 일부 언어는 재귀 솔루션을 선호하고 훨씬 더 최적화합니다 (LISP가 떠 오릅니다).
재귀 함수는 자신을 호출하는 함수입니다. 내가 그것을 사용하는 가장 일반적인 이유는 트리 구조를 순회하는 것입니다. 예를 들어, 체크 박스가있는 TreeView (새 프로그램 설치, "설치할 기능 선택"페이지)가있는 경우 다음과 같은 "모두 확인"버튼 (의사 코드)이 필요할 수 있습니다.
function cmdCheckAllClick {
checkRecursively(TreeView1.RootNode);
}
function checkRecursively(Node n) {
n.Checked = True;
foreach ( n.Children as child ) {
checkRecursively(child);
}
}
따라서 checkRecursively가 먼저 전달 된 노드를 확인한 다음 해당 노드의 각 자식에 대해 자신을 호출하는 것을 볼 수 있습니다.
재귀에 대해 약간의주의가 필요합니다. 무한 재귀 루프에 들어가면 Stack Overflow 예외가 발생합니다. :)
적절한 때에 사람들이 사용하지 말아야하는 이유를 생각할 수 없습니다. 어떤 상황에서는 유용하지만 다른 상황에서는 유용하지 않습니다.
나는 이것이 흥미로운 기술이기 때문에 일부 코더들은 실제 정당화없이 그것을해야하는 것보다 더 자주 사용하게 될 것이라고 생각합니다. 이것은 일부 서클에서 재귀에 잘못된 이름을 부여했습니다.
재귀는 직접 또는 간접적으로 자신을 참조하는 표현식입니다.
재귀 약어를 간단한 예로 고려하십시오.
재귀는 내가 "프랙탈 문제"라고 부르는 것에서 가장 잘 작동합니다. 여기서 여러분은 큰 것의 작은 버전으로 만들어진 큰 것을 다루고 있습니다. 각각은 큰 것의 더 작은 버전입니다. 트리 나 중첩 된 동일한 구조와 같은 것을 탐색하거나 검색해야하는 경우 재귀에 적합한 후보가 될 수있는 문제가 있습니다.
사람들은 여러 가지 이유로 재귀를 피합니다.
대부분의 사람들 (나 자신을 포함)은 함수형 프로그래밍이 아닌 절차 적 또는 객체 지향 프로그래밍에서 프로그래밍 이빨을 잘라냅니다. 그런 사람들에게는 반복적 인 접근 방식 (일반적으로 루프 사용)이 더 자연스럽게 느껴집니다.
절차 적 또는 객체 지향 프로그래밍에서 프로그래밍을 절단 한 사람들은 오류가 발생하기 쉬우므로 재귀를 피하라고 종종 들었습니다.
재귀가 느리다는 말을 자주 듣습니다. 루틴에서 반복적으로 호출하고 리턴하려면 루프보다 느린 스택 푸시 및 팝이 많이 필요합니다. 나는 어떤 언어가 다른 것보다 이것을 더 잘 처리한다고 생각하며, 그 언어는 지배적 인 패러다임이 절차 적이거나 객체 지향적 인 언어가 아닐 가능성이 큽니다.
내가 사용한 프로그래밍 언어 중 적어도 몇 개에 대해 스택이 그다지 깊지 않기 때문에 특정 깊이를 초과하면 재귀를 사용하지 말라는 권장 사항을 들었던 것을 기억합니다.
재귀 문은 입력과 이미 수행 한 작업의 조합으로 다음에 수행 할 작업의 프로세스를 정의하는 문입니다.
예를 들어 계승을 취하십시오.
factorial(6) = 6*5*4*3*2*1
그러나 factorial (6)도 쉽게 볼 수 있습니다.
6 * factorial(5) = 6*(5*4*3*2*1).
따라서 일반적으로 :
factorial(n) = n*factorial(n-1)
물론 재귀의 까다로운 점은 이미 수행 한 작업을 정의하고 싶다면 시작할 곳이 필요하다는 것입니다.
이 예에서는 factorial (1) = 1을 정의하여 특별한 경우를 만듭니다.
이제 우리는 아래에서 위로 봅니다.
factorial(6) = 6*factorial(5)
= 6*5*factorial(4)
= 6*5*4*factorial(3) = 6*5*4*3*factorial(2) = 6*5*4*3*2*factorial(1) = 6*5*4*3*2*1
factorial (1) = 1을 정의 했으므로 "하단"에 도달합니다.
일반적으로 재귀 프로시 저는 두 부분으로 구성됩니다.
1) 동일한 절차를 통해 "이미 수행 한"작업과 결합 된 새로운 입력 측면에서 일부 절차를 정의하는 재귀 부분. (예 factorial(n) = n*factorial(n-1)
)
프로세스가 시작 그것을 어떤 장소를 제공하여 영원히 반복되지 않는 것을 확인합니다 2)베이스 부분 (예 factorial(1) = 1
)
처음에는 머리를 돌리는 것이 약간 혼란 스러울 수 있지만, 여러 예제를 살펴보면 모두 합쳐 져야합니다. 개념에 대한 더 깊은 이해를 원한다면 수학적 귀납법을 공부하십시오. 또한 일부 언어는 재귀 호출에 최적화되지만 다른 언어는 그렇지 않습니다. 주의하지 않으면 엄청나게 느린 재귀 함수를 만드는 것은 매우 쉽지만 대부분의 경우 성능을 향상시키는 기술도 있습니다.
도움이 되었기를 바랍니다...
저는이 정의를 좋아합니다
. 재귀에서 루틴은 문제 자체의 작은 부분을 해결하고 문제를 작은 조각으로 나눈 다음 자신을 호출하여 각 작은 조각을 해결합니다.
나는 또한 Steve McConnells가 Code Complete에서 재귀에 대한 논의를 좋아하며, 그가 재귀에 관한 컴퓨터 과학 서적에 사용 된 예제를 비판합니다.
계승 또는 피보나치 수에 재귀를 사용하지 마십시오.
컴퓨터 과학 교과서의 한 가지 문제점은 재귀의 어리석은 예를 제시한다는 것입니다. 일반적인 예는 계승 계산 또는 피보나치 수열 계산입니다. 재귀는 강력한 도구이며 이러한 경우에 사용하는 것은 정말 어리석은 일입니다. 나를 위해 일한 프로그래머가 재귀를 사용하여 팩토리얼을 계산했다면 다른 사람을 고용 할 것입니다.
나는 이것이 매우 흥미로운 점이라고 생각했고 재귀가 종종 오해되는 이유 일 수 있습니다.
편집 : 이것은 Dav의 대답에 대한 발굴이 아니 었습니다-나는 이것을 게시했을 때 그 대답을 보지 못했습니다.
1.) 자신을 호출 할 수있는 메서드는 재귀 적입니다. 직접 :
void f() {
... f() ...
}
또는 간접적으로 :
void f() {
... g() ...
}
void g() {
... f() ...
}
2.) 재귀를 사용하는 경우
Q: Does using recursion usually make your code faster?
A: No.
Q: Does using recursion usually use less memory?
A: No.
Q: Then why use recursion?
A: It sometimes makes your code much simpler!
3.) 사람들은 반복적 인 코드를 작성하는 것이 매우 복잡한 경우에만 재귀를 사용합니다. 예를 들어, preorder, postorder와 같은 트리 순회 기술은 반복적이고 재귀 적으로 만들 수 있습니다. 그러나 일반적으로 단순성 때문에 재귀를 사용합니다.
다음은 간단한 예입니다. 집합에있는 요소의 수입니다. (수를 세는 더 좋은 방법이 있지만 이것은 멋지고 간단한 재귀 예제입니다.)
먼저 두 가지 규칙이 필요합니다.
다음과 같은 세트가 있다고 가정합니다 : [xxx]. 얼마나 많은 항목이 있는지 세어 보겠습니다.
이것을 다음과 같이 나타낼 수 있습니다.
count of [x x x] = 1 + count of [x x]
= 1 + (1 + count of [x])
= 1 + (1 + (1 + count of []))
= 1 + (1 + (1 + 0)))
= 1 + (1 + (1))
= 1 + (2)
= 3
재귀 솔루션을 적용 할 때 일반적으로 최소 2 개의 규칙이 있습니다.
위를 의사 코드로 변환하면 다음과 같은 결과가 나타납니다.
numberOfItems(set)
if set is empty
return 0
else
remove 1 item from set
return 1 + numberOfItems(set)
다른 사람들이 다룰 것이라고 확신하는 훨씬 더 유용한 예제 (예 : 나무 횡단)가 있습니다.
평이한 영어로 : 3 가지 일을 할 수 있다고 가정합니다.
당신은 테이블에 당신 앞에 많은 사과가 있고 얼마나 많은 사과가 있는지 알고 싶습니다.
start
Is the table empty?
yes: Count the tally marks and cheer like it's your birthday!
no: Take 1 apple and put it aside
Write down a tally mark
goto start
끝날 때까지 같은 일을 반복하는 과정을 재귀라고합니다.
나는 이것이 당신이 찾고있는 "평범한 영어"대답이기를 바랍니다!
재귀 함수는 자신에 대한 호출을 포함하는 함수입니다. 재귀 구조체는 자신의 인스턴스를 포함하는 구조체입니다. 두 가지를 재귀 클래스로 결합 할 수 있습니다. 재귀 항목의 핵심 부분은 자신의 인스턴스 / 호출을 포함한다는 것입니다.
서로 마주 보는 두 개의 거울을 고려하십시오. 우리는 그들이 만드는 깔끔한 무한 효과를 보았습니다. 각 반사는 거울의 인스턴스이며 다른 거울 인스턴스 내에 포함됩니다. 자체 반사를 포함하는 거울은 재귀입니다.
이진 검색 트리 재귀의 좋은 프로그램의 예입니다. 구조는 노드의 인스턴스 2 개를 포함하는 각 노드에서 재귀 적입니다. 이진 검색 트리에서 작동하는 함수도 재귀 적입니다.
"망치가 있으면 모든 것이 못처럼 보이게하세요."
재귀는 대규모 의 문제 해결 전략입니다. 문제에 , 모든 단계에서 매번 같은 망치로 "작은 것 2 개를 큰 것으로 바꾸는 것"입니다.
당신의 책상이 1024 개의 종이로 뒤덮여 있다고 가정 해보자. 재귀를 사용하여 어떻게 깔끔하고 깨끗한 종이 더미를 엉망으로 만들까요?
이것은 모든 것을 세는 것 (엄격히 필요하지 않음)을 제외하고는 매우 직관적입니다. 실제로는 1 장 스택으로 끝까지 내려 가지 않을 수도 있지만 여전히 작동 할 수 있습니다. 중요한 부분은 해머입니다. 팔을 사용하면 항상 스택 하나를 다른 스택 위에 올려서 더 큰 스택을 만들 수 있으며 두 스택의 크기는 중요하지 않습니다.
이봐, 내 의견이 누군가와 동의한다면 미안하다. 나는 단지 평범한 영어로 재귀를 설명하려고 노력하고있다.
세 명의 관리자 (Jack, John, Morgan)가 있다고 가정합니다. Jack은 2 명의 프로그래머, John-3 및 Morgan-5를 관리합니다. 모든 관리자에게 300 달러를주고 비용이 얼마인지 알고 싶습니다. 대답은 분명합니다.하지만 Morgan의 직원 2 명이 관리자이기도 한 경우에는 어떨까요?
여기에 재귀가 있습니다. 계층 구조의 맨 위에서 시작합니다. 여름 비용은 0 $입니다. 잭부터 시작해서 그에게 직원으로 관리자가 있는지 확인합니다. 그들 중 하나라도 있으면 직원으로 관리자가 있는지 확인하십시오. 관리자를 찾을 때마다 여름 비용에 300 $를 추가하십시오. Jack과 작업을 마치면 John, 그의 직원, Morgan으로 이동합니다.
당신은 얼마나 많은 관리자가 있고 얼마나 많은 예산을 쓸 수 있는지 알고 있지만 대답을 얻기 전에 얼마나 많은 사이클을 갈 것인지 결코 알 수 없습니다.
재귀는 나뭇 가지와 잎이있는 나무로, 각각 부모와 자식이라고합니다. 재귀 알고리즘을 사용할 때, 당신은 다소 의식적으로 데이터에서 트리를 구축하고 있습니다.
평이한 영어에서 재귀는 몇 번이고 반복하는 것을 의미합니다.
프로그래밍에서 한 가지 예는 자체 내에서 함수를 호출하는 것입니다.
숫자의 계승 계산에 대한 다음 예를 살펴보십시오.
public int fact(int n)
{
if (n==0) return 1;
else return n*fact(n-1)
}
기본적으로 데이터 유형의 각 케이스에 대한 케이스가있는 switch-statement로 구성된 경우 모든 알고리즘은 데이터 유형에 대한 구조적 재귀를 나타냅니다 .
예를 들어, 유형에 대해 작업 할 때
tree = null
| leaf(value:integer)
| node(left: tree, right:tree)
구조적 재귀 알고리즘은 다음과 같은 형식을 갖습니다.
function computeSomething(x : tree) =
if x is null: base case
if x is leaf: do something with x.value
if x is node: do something with x.left,
do something with x.right,
combine the results
이것은 데이터 구조에서 작동하는 알고리즘을 작성하는 가장 확실한 방법입니다.
이제 Peano 공리를 사용하여 정의 된 정수 (자연수)를 살펴보면
integer = 0 | succ(integer)
정수에 대한 구조적 재귀 알고리즘은 다음과 같습니다.
function computeSomething(x : integer) =
if x is 0 : base case
if x is succ(prev) : do something with prev
너무 잘 알려진 요인 함수는이 형식의 가장 사소한 예에 관한 것입니다.
함수를 호출하거나 자체 정의를 사용합니다.