재귀 란 무엇이며 언제 사용해야합니까?


121

메일 링리스트와 온라인 토론에서 정기적으로 올라 오는 주제 중 하나는 컴퓨터 과학 학위를 취득하는 것의 장점 (또는 부족)입니다. 부정적인 당사자에 대해 몇 번이고 반복되는 것처럼 보이는 주장은 그들이 몇 년 동안 코딩을 해왔고 재귀를 사용한 적이 없다는 것입니다.

그래서 질문은 :

  1. 재귀 란 무엇입니까?
  2. 재귀는 언제 사용합니까?
  3. 사람들이 재귀를 사용하지 않는 이유는 무엇입니까?

9
그리고 아마도 이것이 도움이 될 것입니다 : stackoverflow.com/questions/126756/…
kennytm

3
이것은 개념을 이해하는 데 도움이 될 수 있습니다.이 페이지에있는 질문의 두 번째 댓글에 제공된 링크로 이동하여 댓글이하는 작업을 수행하십시오. stackoverflow.com/questions/3021/…
dtmland

답변:


86

이 스레드 에는 재귀 에 대한 여러 가지 좋은 설명 이 있습니다.이 답변은 대부분의 언어에서 재귀 를 사용하지 말아야하는 이유에 대한 것입니다. * 대부분의 주요 명령형 언어 구현 (즉, C, C ++, Basic, Python의 모든 주요 구현)에서 , Ruby, Java 및 C #) 반복 은 재귀보다 훨씬 선호됩니다.

이유를 확인하려면 위의 언어에서 함수를 호출하는 데 사용하는 단계를 살펴보세요.

  1. 함수의 인수와 지역 변수를 위해 스택 에 공간이 조각되어 있습니다 .
  2. 함수의 인수가이 새 공간에 복사됩니다.
  3. 제어 기능으로 이동
  4. 함수의 코드가 실행됩니다.
  5. 함수의 결과는 반환 값으로 복사됩니다.
  6. 스택이 이전 위치로 되감 깁니다.
  7. 컨트롤은 함수가 호출 된 위치로 다시 이동합니다.

이 모든 단계를 수행하는 데는 일반적으로 루프를 반복하는 것보다 약간 더 많은 시간이 걸립니다. 그러나 실제 문제는 1 단계에 있습니다. 많은 프로그램이 시작될 때 스택에 단일 메모리 청크를 할당하고 해당 메모리가 부족하면 (종종 재귀로 인한 것은 아니지만) 스택 오버플 로로 인해 프로그램이 충돌 합니다 .

따라서 이러한 언어에서는 재귀가 느리고 충돌에 취약합니다. 그래도 사용에 대한 몇 가지 주장이 있습니다. 일반적으로 재귀 적으로 작성된 코드는 읽는 방법을 알게되면 더 짧고 우아합니다.

언어 구현자가 일부 클래스의 스택 오버플로를 제거 할 수있는 테일 호출 최적화 라는 기술을 사용할 수 있습니다 . 간결하게 말하면 함수의 반환 표현식이 단순히 함수 호출의 결과 인 경우 스택에 새 수준을 추가 할 필요가없는 경우 호출되는 함수에 대해 현재 수준을 재사용 할 수 있습니다. 안타깝게도 명령형 언어 구현에는 꼬리 호출 최적화가 내장되어 있습니다.

* 나는 재귀를 좋아합니다. 내가 가장 좋아하는 정적 언어 는 루프를 전혀 사용하지 않습니다. 재귀는 무언가를 반복적으로 수행하는 유일한 방법입니다. 나는 재귀가 일반적으로 그것에 맞게 조정되지 않은 언어에서 좋은 생각이라고 생각하지 않습니다.

** 그런데 Mario, ArrangeString 함수의 일반적인 이름은 "join"이며 선택한 언어에 이미 구현되어 있지 않다면 놀랄 것입니다.


1
재귀의 고유 한 오버 헤드에 대한 설명을 보니 좋습니다. 나는 내 대답에서도 그것에 대해 언급했습니다. 하지만 저에게 재귀의 가장 큰 장점은 호출 스택으로 할 수있는 것입니다. 반복적으로 분기되는 재귀를 사용하여 간결한 알고리즘을 작성할 수 있으므로 크롤링 계층 (상위 / 하위 관계)과 같은 작업을 쉽게 수행 할 수 있습니다. 예를 들어 내 대답을 참조하십시오.
Steve Wortham 2011-06-05

7
"재귀 란 무엇이며 언제 사용해야합니까?"라는 질문에 대한 최고 답변을 찾느 라 매우 실망했습니다. 하지 실제로 두 사람의 대답, 당신이 언급 한 언어의 대부분의 광범위한 사용에도 불구하고, 재귀에 대한 매우 바이어스 경고를 신경 쓰지 (이 당신이 말한 것에 대해 특별히 아무 잘못이 아니지만, 당신은 문제와 underexaggerating 과장 것 같다 유용성).
Bernhard Barker

2
당신은 아마 맞습니다 @Dukeling. 맥락을 위해이 답변을 작성했을 때 이미 재귀에 대한 많은 설명이 작성되었으며 상위 답변이 아닌 해당 정보에 대한 부속물이 될 의도로 작성했습니다. 실제로 트리를 걸어야하거나 다른 중첩 데이터 구조를 처리해야 할 때 일반적으로 재귀로 전환하고 아직 야생에서 직접 만든 스택 오버플로에 부딪히지 않았습니다.
Peter Burns

63

재귀의 간단한 영어 예.

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.

1
:) 마음에 감동을위한 + 최대
하일 뭄 타즈 아완

중국 설화에서 잠들지 않는 어린 아이들을위한 비슷한 이야기가 있습니다. 저는 그 이야기를 방금 기억했습니다. 그리고 그것은 현실 세계의 재귀가 어떻게 작동하는지 생각 나게합니다.
Harvey Lin

49

가장 기본적인 컴퓨터 과학적 의미에서 재귀는 자신을 호출하는 함수입니다. 연결 목록 구조가 있다고 가정합니다.

struct Node {
    Node* next;
};

그리고 당신은 재귀로 이것을 할 수있는 연결 목록의 길이를 알아 내고 싶습니다 :

int length(const Node* list) {
    if (!list->next) {
        return 1;
    } else {
        return 1 + length(list->next);
    }
}

(물론 for 루프로도 수행 할 수 있지만 개념을 설명하는 데 유용합니다.)


@Christopher : 이것은 재귀의 멋지고 간단한 예입니다. 특히 이것은 꼬리 재귀의 예입니다. 그러나 Andreas가 말했듯이 for 루프를 사용하여 (더 효율적으로) 쉽게 다시 작성할 수 있습니다. 내 대답에서 설명했듯이 재귀에 대한 더 나은 용도가 있습니다.
Steve Wortham

2
여기에 else 문이 정말로 필요합니까?
Adrien Be

1
아니요, 명확성을 위해서만 있습니다.
Andreas Brinck 2012

@SteveWortham : 이것은 쓰여진 것처럼 꼬리 재귀 적이 지 않습니다. 후자가 결과에 1을 더할 수 있도록 length(list->next)로 돌아 가야합니다 length(list). 길이를 전달하도록 작성 되었으면 호출자가 존재한다는 사실을 잊을 수 있습니다. 처럼 int length(const Node* list, int count=0) { return (!list) ? count : length(list->next, count + 1); }.
cHao

46

함수가 자신을 호출 할 때마다 루프를 생성하면 재귀입니다. 모든 것과 마찬가지로 재귀에는 좋은 용도와 나쁜 용도가 있습니다.

가장 간단한 예는 함수의 마지막 줄이 자신에 대한 호출 인 꼬리 재귀입니다.

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);
    }
}

반복으로 동일한 작업을 시도하면 수행하는 데 더 많은 코드가 필요하다는 것을 알게 될 것입니다.

다른 일반적인 사용 사례에는 웹 사이트 크롤러, 디렉토리 비교 등과 같은 계층 구조를 통과하는 것이 포함될 수 있습니다.

결론

실제로 반복적 인 분기가 필요할 때마다 재귀가 가장 적합합니다.


27

재귀는 분할 정복 사고 방식을 기반으로 문제를 해결하는 방법입니다. 기본 아이디어는 원래 문제를 가져 와서 더 작은 (더 쉽게 해결되는) 인스턴스로 나누고, 이러한 작은 인스턴스를 해결 한 다음 (일반적으로 동일한 알고리즘을 다시 사용하여) 최종 솔루션으로 재 조립하는 것입니다.

표준 예는 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 공식을 가지고 있습니다.

메서드를 호출 할 때마다 기본 사례가 트리거되거나 매개 변수가 기본 사례에 더 가까운 동일한 메서드에 대한 호출 (종종 재귀 호출이라고 함)이 발생한다는 점에 유의하는 것이 좋습니다. 그렇지 않은 경우 메서드는 영원히 실행됩니다.


2
좋은 설명이지만 이것은 단순히 꼬리 재귀이며 반복적 인 솔루션에 비해 이점이 없다는 점에 유의하는 것이 중요하다고 생각합니다. 대략 같은 양의 코드이며 함수 호출 오버 헤드로 인해 느리게 실행됩니다.
Steve Wortham 2011-06-05

1
@SteveWortham : 이것은 꼬리 재귀가 아닙니다. 재귀 단계에서는 결과를 반환하기 전에 FactRec()n해야합니다.
rvighne

12

재귀는 자신을 호출하는 함수로 문제를 해결합니다. 이에 대한 좋은 예는 계승 함수입니다. Factorial은 5의 factorial이 5 * 4 * 3 * 2 * 1 인 수학 문제입니다.이 함수는 C #에서 양의 정수에 대해이 문제를 해결합니다 (테스트되지 않음-버그가있을 수 있음).

public int Factorial(int n)
{
    if (n <= 1)
        return 1;

    return n * Factorial(n - 1);
}

9

재귀는 문제의 더 작은 버전을 해결 한 다음 그 결과와 다른 계산을 사용하여 원래 문제에 대한 답을 공식화하여 문제를 해결하는 방법을 말합니다. 종종 더 작은 버전을 해결하는 과정에서이 방법은 해결하기 쉬운 "기본 사례"에 도달 할 때까지 더 작은 버전의 문제를 해결합니다.

예를 들어 숫자에 대한 계승을 계산하기 위해 X이를로 나타낼 수 있습니다 X times the factorial of X-1. 따라서이 메서드는 "반복"하여의 계승을 찾은 X-1다음 얻은 값을 곱하여 X최종 답을 제공합니다. 물론의 계승을 찾기 위해 X-1먼저의 계승을 계산하는 식 X-2입니다. 때 기본 케이스는 것 X그것을 반환 할 알고있는 경우에, 0 또는 1 1부터 0! = 1! = 1.


1
당신이 언급하고있는 것은 재귀가 아니라 <a href=" en.wikipedia.org/wiki/… and Conquer</a> 알고리즘 설계 원리 라고 생각합니다 . <a href = " en.wikipedia를보십시오. org / wiki / Ackermann_function "> Ackermans 기능 </a>.
Gabriel Ščerbák

2
아니요, 저는 D & C를 말하는 것이 아닙니다. D & C는 2 개 이상의 하위 문제가 존재하고 재귀 자체는 존재하지 않는다는 것을 의미합니다 (예를 들어 여기에 제공된 계승 예제는 D & C가 아니라 완전히 선형입니다). D & C는 본질적으로 재귀의 하위 집합입니다.
Amber

3
링크 한 정확한 기사에서 인용 : "분할 및 정복 알고리즘은 문제를 동일한 (또는 관련) 유형의 두 개 이상의 하위 문제로 재귀 적으로 분해 하는 방식으로 작동 합니다."
Amber

엄밀히 말하면 재귀가 문제를 전혀 해결할 필요가 없기 때문에 이것이 훌륭한 설명이라고 생각하지 않습니다. 당신은 자신을 부를 수 있습니다 (그리고 오버플로).
UK-AL

나는 PHP 마스터를 위해 작성하는 기사에서 설명을 사용하고 있습니다. 괜찮 으시길 바랍니다.
frostymarvelous

9

오래되고 잘 알려진 문제를 고려하십시오 .

수학에서 두 개 이상의 0이 아닌 정수 의 최대 공약수 (gcd)…는 나머지없이 숫자를 나누는 최대 양의 정수입니다.

gcd의 정의는 놀랍도록 간단합니다.

gcd 고화질

여기서 mod는 모듈로 연산자 (즉, 정수 나누기 이후의 나머지)입니다.

영어에서이 정의는 어떤 수와 0의 최대 공약수는 그 숫자 이고 , 두 수 mn 의 최대 공약수는 n 의 최대 공약수 이고 mn으로 나눈 나머지는 나머지 입니다.

이것이 작동하는 이유를 알고 싶다면 유클리드 알고리즘 에 대한 Wikipedia 기사를 참조하십시오 .

예를 들어 gcd (10, 8)를 계산해 봅시다. 각 단계는 바로 전 단계와 같습니다.

  1. gcd (10, 8)
  2. gcd (10, 10 모드 8)
  3. gcd (8, 2)
  4. gcd (8, 8 모드 2)
  5. gcd (2, 0)
  6. 2

첫 번째 단계에서 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의 재귀 기사 에서 다른 알고리즘을 이해할 수 있습니다 !


8
  1. 자신을 호출하는 함수
  2. 함수가 (쉽게) 문제의 작은 부분에 대해 동일한 함수와 간단한 연산으로 분해 될 수있는 경우. 오히려 이것이 재귀에 대한 좋은 후보가된다고 말해야합니다.
  3. 그들이하다!

표준 예는 다음과 같은 계승입니다.

int fact(int a) 
{
  if(a==1)
    return 1;

  return a*fact(a-1);
}

일반적으로 재귀는 반드시 빠르지는 않습니다 (재귀 함수가 작은 경향이 있기 때문에 함수 호출 오버 헤드가 높은 경향이 있습니다. 위 참조). 몇 가지 문제가 발생할 수 있습니다 (누군가 스택 오버 플로우?). 어떤 사람들은 사소하지 않은 경우에 '올바르게'얻기가 어려운 경향이 있다고 말하지만 실제로는 그렇게 생각하지 않습니다. 어떤 상황에서는 재귀가 가장 의미가 있으며 특정 함수를 작성하는 가장 우아하고 명확한 방법입니다. 일부 언어는 재귀 솔루션을 선호하고 훨씬 더 최적화합니다 (LISP가 떠 오릅니다).


6

재귀 함수는 자신을 호출하는 함수입니다. 내가 그것을 사용하는 가장 일반적인 이유는 트리 구조를 순회하는 것입니다. 예를 들어, 체크 박스가있는 TreeView (새 프로그램 설치, "설치할 기능 선택"페이지)가있는 경우 다음과 같은 "모두 확인"버튼 (의사 코드)이 필요할 수 있습니다.

function cmdCheckAllClick {
    checkRecursively(TreeView1.RootNode);
}

function checkRecursively(Node n) {
    n.Checked = True;
    foreach ( n.Children as child ) {
        checkRecursively(child);
    }
}

따라서 checkRecursively가 먼저 전달 된 노드를 확인한 다음 해당 노드의 각 자식에 대해 자신을 호출하는 것을 볼 수 있습니다.

재귀에 대해 약간의주의가 필요합니다. 무한 재귀 루프에 들어가면 Stack Overflow 예외가 발생합니다. :)

적절한 때에 사람들이 사용하지 말아야하는 이유를 생각할 수 없습니다. 어떤 상황에서는 유용하지만 다른 상황에서는 유용하지 않습니다.

나는 이것이 흥미로운 기술이기 때문에 일부 코더들은 실제 정당화없이 그것을해야하는 것보다 더 자주 사용하게 될 것이라고 생각합니다. 이것은 일부 서클에서 재귀에 잘못된 이름을 부여했습니다.


5

재귀는 직접 또는 간접적으로 자신을 참조하는 표현식입니다.

재귀 약어를 간단한 예로 고려하십시오.

  • GNUGNU의 Not Unix를 의미합니다.
  • PHPPHP의 약자 : Hypertext Preprocessor
  • YAMLYAML Ai n't Markup Language의 약자입니다.
  • WINEWine Is Not an Emulator의 약자입니다.
  • VISAVisa International Service Association을 의미합니다.

Wikipedia에 대한 더 많은 예


4

재귀는 내가 "프랙탈 문제"라고 부르는 것에서 가장 잘 작동합니다. 여기서 여러분은 큰 것의 작은 버전으로 만들어진 큰 것을 다루고 있습니다. 각각은 큰 것의 더 작은 버전입니다. 트리 나 중첩 된 동일한 구조와 같은 것을 탐색하거나 검색해야하는 경우 재귀에 적합한 후보가 될 수있는 문제가 있습니다.

사람들은 여러 가지 이유로 재귀를 피합니다.

  1. 대부분의 사람들 (나 자신을 포함)은 함수형 프로그래밍이 아닌 절차 적 또는 객체 지향 프로그래밍에서 프로그래밍 이빨을 잘라냅니다. 그런 사람들에게는 반복적 인 접근 방식 (일반적으로 루프 사용)이 더 자연스럽게 느껴집니다.

  2. 절차 적 또는 객체 지향 프로그래밍에서 프로그래밍을 절단 한 사람들은 오류가 발생하기 쉬우므로 재귀를 피하라고 종종 들었습니다.

  3. 재귀가 느리다는 말을 자주 듣습니다. 루틴에서 반복적으로 호출하고 리턴하려면 루프보다 느린 스택 푸시 및 팝이 많이 필요합니다. 나는 어떤 언어가 다른 것보다 이것을 더 잘 처리한다고 생각하며, 그 언어는 지배적 인 패러다임이 절차 적이거나 객체 지향적 인 언어가 아닐 가능성이 큽니다.

  4. 내가 사용한 프로그래밍 언어 중 적어도 몇 개에 대해 스택이 그다지 깊지 않기 때문에 특정 깊이를 초과하면 재귀를 사용하지 말라는 권장 사항을 들었던 것을 기억합니다.


4

재귀 문은 입력과 이미 수행 한 작업의 조합으로 다음에 수행 할 작업의 프로세스를 정의하는 문입니다.

예를 들어 계승을 취하십시오.

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)

처음에는 머리를 돌리는 것이 약간 혼란 스러울 수 있지만, 여러 예제를 살펴보면 모두 합쳐 져야합니다. 개념에 대한 더 깊은 이해를 원한다면 수학적 귀납법을 공부하십시오. 또한 일부 언어는 재귀 호출에 최적화되지만 다른 언어는 그렇지 않습니다. 주의하지 않으면 엄청나게 느린 재귀 함수를 만드는 것은 매우 쉽지만 대부분의 경우 성능을 향상시키는 기술도 있습니다.

도움이 되었기를 바랍니다...


4

저는이 정의를 좋아합니다
. 재귀에서 루틴은 문제 자체의 작은 부분을 해결하고 문제를 작은 조각으로 나눈 다음 자신을 호출하여 각 작은 조각을 해결합니다.

나는 또한 Steve McConnells가 Code Complete에서 재귀에 대한 논의를 좋아하며, 그가 재귀에 관한 컴퓨터 과학 서적에 사용 된 예제를 비판합니다.

계승 또는 피보나치 수에 재귀를 사용하지 마십시오.

컴퓨터 과학 교과서의 한 가지 문제점은 재귀의 어리석은 예를 제시한다는 것입니다. 일반적인 예는 계승 계산 또는 피보나치 수열 계산입니다. 재귀는 강력한 도구이며 이러한 경우에 사용하는 것은 정말 어리석은 일입니다. 나를 위해 일한 프로그래머가 재귀를 사용하여 팩토리얼을 계산했다면 다른 사람을 고용 할 것입니다.

나는 이것이 매우 흥미로운 점이라고 생각했고 재귀가 종종 오해되는 이유 일 수 있습니다.

편집 : 이것은 Dav의 대답에 대한 발굴이 아니 었습니다-나는 이것을 게시했을 때 그 대답을 보지 못했습니다.


6
팩토리얼이나 피보나치 시퀀스가 ​​예제로 사용되는 대부분의 이유는 그것들이 재귀 적 방식으로 정의 된 공통 항목이기 때문에 그것들 을 계산하기 위해 자연스럽게 재귀 예제에 빌려주기 때문입니다-그것이 실제로 최선의 방법은 아니지만 CS 관점에서.
Amber

동의합니다. 책을 읽을 때 재귀에 대한 섹션 중간에 올리는 것이 흥미로운 점이라는 것을 방금 발견했습니다.
Robben_Ford_Fan_boy

4

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와 같은 트리 순회 기술은 반복적이고 재귀 적으로 만들 수 있습니다. 그러나 일반적으로 단순성 때문에 재귀를 사용합니다.


성능과 관련하여 나누고 정복 할 때 복잡성을 줄이는 것은 어떻습니까?
mfrachet 2016 년

4

다음은 간단한 예입니다. 집합에있는 요소의 수입니다. (수를 세는 더 좋은 방법이 있지만 이것은 멋지고 간단한 재귀 예제입니다.)

먼저 두 가지 규칙이 필요합니다.

  1. 세트가 비어 있으면 세트의 항목 수는 0입니다 (duh!).
  2. 세트가 비어 있지 않은 경우 개수는 1에 하나의 항목이 제거 된 후 세트의 항목 수를 더한 값입니다.

다음과 같은 세트가 있다고 가정합니다 : [xxx]. 얼마나 많은 항목이 있는지 세어 보겠습니다.

  1. 세트는 비어 있지 않은 [xxx]이므로 규칙 2를 적용합니다. 항목 수는 [xx]의 항목 수를 더한 값입니다 (즉, 항목을 제거했습니다).
  2. 세트는 [xx]이므로 규칙 2를 다시 적용합니다 : 하나 + [x]의 항목 수.
  3. 세트는 [x]이며, 이는 여전히 규칙 2 : []에있는 하나 + 항목 수와 일치합니다.
  4. 이제 세트는 규칙 1과 일치하는 []입니다. 개수는 0입니다!
  5. 이제 4 단계 (0)에서 답을 알았으므로 3 단계 (1 + 0)를 풀 수 있습니다.
  6. 마찬가지로 이제 3 단계 (1)에서 답을 알았으므로 2 단계 (1 + 1)를 풀 수 있습니다.
  7. 마지막으로 2 단계 (2)에서 답을 알았으므로 1 단계 (1 + 2)를 풀고 [xxx]의 항목 수, 즉 3을 얻을 수 있습니다. 만세!

이것을 다음과 같이 나타낼 수 있습니다.

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 개의 규칙이 있습니다.

  • 기초, 모든 데이터를 "사용"했을 때 발생하는 일을 나타내는 간단한 사례입니다. 일반적으로 "처리 할 데이터가 부족한 경우 답변은 X"의 일부 변형입니다.
  • 데이터가있는 경우 발생하는 상황을 나타내는 재귀 규칙. 이는 일반적으로 "데이터 세트를 더 작게 만들고 더 작은 데이터 세트에 규칙을 다시 적용"하는 일종의 규칙입니다.

위를 의사 코드로 변환하면 다음과 같은 결과가 나타납니다.

numberOfItems(set)
    if set is empty
        return 0
    else
        remove 1 item from set
        return 1 + numberOfItems(set)

다른 사람들이 다룰 것이라고 확신하는 훨씬 더 유용한 예제 (예 : 나무 횡단)가 있습니다.


3

음, 그것은 당신이 가지고있는 꽤 괜찮은 정의입니다. 그리고 위키피디아도 좋은 정의를 가지고 있습니다. 그래서 나는 당신을 위해 또 다른 (아마 더 나쁜) 정의를 추가 할 것입니다.

사람들이 "재귀"를 언급 할 때, 그들은 보통 작업이 끝날 때까지 반복적으로 자신을 호출하는 자신이 작성한 함수에 대해 이야기합니다. 재귀는 데이터 구조에서 계층 구조를 탐색 할 때 유용 할 수 있습니다.


3

예 : 계단의 재귀 적 정의는 다음과 같습니다. 계단은 다음으로 구성됩니다.-단일 계단과 계단 (재귀)-또는 단일 계단 만 (종료)


2

해결 된 문제를 되풀이하려면 : 아무것도하지 마십시오.
열려있는 문제에 대해 반복하려면 다음 단계를 수행 한 다음 나머지 단계를 반복합니다.


2

평이한 영어로 : 3 가지 일을 할 수 있다고 가정합니다.

  1. 사과 하나 가져가
  2. 집계 표시 기록
  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

끝날 때까지 같은 일을 반복하는 과정을 재귀라고합니다.

나는 이것이 당신이 찾고있는 "평범한 영어"대답이기를 바랍니다!


1
잠깐, 내 앞에 테이블 위에 많은 집계 표시가 있는데 이제 몇 개의 집계 표시가 있는지 알고 싶습니다. 이것을 위해 어떻게 든 사과를 사용할 수 있습니까?
Christoffer Hammarström 2010 년

만약 당신이 땅에서 사과를 가져다가 (과정 동안 거기에 놓았을 때), 탈리 마크가 남지 않을 때까지 목록의 하나의 탈리 마크를 긁을 때마다 테이블 위에 놓는다면, 나는 당신을 확신합니다. 당신이 가지고있는 탤리 마크의 수와 같은 양의 사과가 테이블에 남게됩니다. 이제 즉각적인 성공을 위해 그 사과를 세십시오! (참고 :이 프로세스는 더 이상 재귀가 아니라 무한 루프입니다.)
Bastiaan Linders

2

재귀 함수는 자신에 대한 호출을 포함하는 함수입니다. 재귀 구조체는 자신의 인스턴스를 포함하는 구조체입니다. 두 가지를 재귀 클래스로 결합 할 수 있습니다. 재귀 항목의 핵심 부분은 자신의 인스턴스 / 호출을 포함한다는 것입니다.

서로 마주 보는 두 개의 거울을 고려하십시오. 우리는 그들이 만드는 깔끔한 무한 효과를 보았습니다. 각 반사는 거울의 인스턴스이며 다른 거울 인스턴스 내에 포함됩니다. 자체 반사를 포함하는 거울은 재귀입니다.

이진 검색 트리 재귀의 좋은 프로그램의 예입니다. 구조는 노드의 인스턴스 2 개를 포함하는 각 노드에서 재귀 적입니다. 이진 검색 트리에서 작동하는 함수도 재귀 적입니다.


2

이것은 오래된 질문이지만 물류 관점에서 답을 추가하고 싶습니다 (예 : 알고리즘 정확성 관점이나 성능 관점이 아님).

작업에 Java를 사용하고 Java는 중첩 기능을 지원하지 않습니다. 따라서 재귀를 수행하려면 외부 함수를 정의해야하거나 (내 코드가 Java의 관료적 규칙에 맞서기 때문에 존재하는 경우에만 존재 함) 코드를 모두 리팩토링해야 할 수도 있습니다 (정말 싫어함).

따라서 재귀 자체가 본질적으로 스택 작업이기 때문에 종종 재귀를 피하고 대신 스택 작업을 사용합니다.


1

트리 구조가있을 때마다 사용하고 싶습니다. XML을 읽을 때 매우 유용합니다.


1

프로그래밍에 적용되는 재귀는 기본적으로 작업을 수행하기 위해 다른 매개 변수를 사용하여 자체 정의 내부 (자체 내부)에서 함수를 호출하는 것입니다.


1

"망치가 있으면 모든 것이 못처럼 보이게하세요."

재귀는 대규모 의 문제 해결 전략입니다. 문제에 , 모든 단계에서 매번 같은 망치로 "작은 것 2 개를 큰 것으로 바꾸는 것"입니다.

당신의 책상이 1024 개의 종이로 뒤덮여 있다고 가정 해보자. 재귀를 사용하여 어떻게 깔끔하고 깨끗한 종이 더미를 엉망으로 만들까요?

  1. 나누기: 모든 시트를 펼쳐서 각 "스택"에 하나의 시트 만 있습니다.
  2. 정복하다:
    1. 돌아 다니면서 각 시트를 다른 시트 위에 놓습니다. 이제 2 개의 스택이 있습니다.
    2. 주위를 돌아 다니면서 각각의 2- 스택을 다른 2- 스택 위에 놓습니다. 이제 4 개의 스택이 있습니다.
    3. 주위를 돌아 다니면서 각 4 스택을 다른 4 스택 위에 놓습니다. 이제 8 개의 스택이 있습니다.
    4. ... 계속해서 ...
    5. 이제 1024 장의 거대한 스택이 있습니다!

이것은 모든 것을 세는 것 (엄격히 필요하지 않음)을 제외하고는 매우 직관적입니다. 실제로는 1 장 스택으로 끝까지 내려 가지 않을 수도 있지만 여전히 작동 할 수 있습니다. 중요한 부분은 해머입니다. 팔을 사용하면 항상 스택 하나를 다른 스택 위에 올려서 더 큰 스택을 만들 수 있으며 두 스택의 크기는 중요하지 않습니다.


6
당신은 분열과 정복을 묘사하고 있습니다. 이것은 재귀 의 이지만 결코 유일한 것은 아닙니다.
Konrad Rudolph

괜찮아. 저는 여기 [재귀의 세계] [1]를 문장으로 포착하려고하는 것이 아닙니다. 직관적 인 설명을 원합니다. [1] : facebook.com/pages/Recursion-Fairy/269711978049
Andres Jaan Tack

1

재귀는 특정 작업을 수행 할 수 있도록 메서드가 iself를 호출하는 프로세스입니다. 코드의 중복을 줄입니다. 대부분의 재귀 함수 또는 메서드에는 반향 호출을 중단하기위한 조건이 있어야합니다. 즉, 조건이 충족되면 자체 호출을 중지합니다. 이렇게하면 무한 루프가 생성되지 않습니다. 모든 함수가 재귀 적으로 사용하기에 적합한 것은 아닙니다.


1

이봐, 내 의견이 누군가와 동의한다면 미안하다. 나는 단지 평범한 영어로 재귀를 설명하려고 노력하고있다.

세 명의 관리자 (Jack, John, Morgan)가 있다고 가정합니다. Jack은 2 명의 프로그래머, John-3 및 Morgan-5를 관리합니다. 모든 관리자에게 300 달러를주고 비용이 얼마인지 알고 싶습니다. 대답은 분명합니다.하지만 Morgan의 직원 2 명이 관리자이기도 한 경우에는 어떨까요?

여기에 재귀가 있습니다. 계층 구조의 맨 위에서 시작합니다. 여름 비용은 0 $입니다. 잭부터 시작해서 그에게 직원으로 관리자가 있는지 확인합니다. 그들 중 하나라도 있으면 직원으로 관리자가 있는지 확인하십시오. 관리자를 찾을 때마다 여름 비용에 300 $를 추가하십시오. Jack과 작업을 마치면 John, 그의 직원, Morgan으로 이동합니다.

당신은 얼마나 많은 관리자가 있고 얼마나 많은 예산을 쓸 수 있는지 알고 있지만 대답을 얻기 전에 얼마나 많은 사이클을 갈 것인지 결코 알 수 없습니다.

재귀는 나뭇 가지와 잎이있는 나무로, 각각 부모와 자식이라고합니다. 재귀 알고리즘을 사용할 때, 당신은 다소 의식적으로 데이터에서 트리를 구축하고 있습니다.


1

평이한 영어에서 재귀는 몇 번이고 반복하는 것을 의미합니다.

프로그래밍에서 한 가지 예는 자체 내에서 함수를 호출하는 것입니다.

숫자의 계승 계산에 대한 다음 예를 살펴보십시오.

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

1
평이한 영어에서는 무언가를 반복하는 것을 반복이라고합니다.
toon81

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

너무 잘 알려진 요인 함수는이 형식의 가장 사소한 예에 관한 것입니다.


당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.