재귀를 언제 사용해야합니까?


26

루프 대신 재귀를 사용할 때 (상대적으로) 기본 (1 학년 대학 수준 CS 학생 생각) 인스턴스는 언제입니까?


2
재귀를 루프로 스택 할 수 있습니다.
Kaveh

답변:


18

저는 약 2 년 동안 학부생들에게 C ++을 가르쳤으며 재귀를 다루었습니다. 내 경험에 비추어 볼 때, 귀하의 질문과 감정은 매우 일반적입니다. 극단적으로, 일부 학생들은 재귀를 이해하기 어렵다고 생각하는 반면 다른 학생들은 거의 모든 것에 그것을 사용하기를 원합니다.

데이브가 잘 요약했다고 생각합니다. 적절한 곳에 사용하십시오. 즉, 자연스럽게 느껴질 때 사용하십시오. 잘 맞는 문제에 직면했을 때, 아마도 그것을 인식 할 것입니다. 그것은 반복적 인 해결책을 제시 할 수없는 것처럼 보일 것입니다. 또한 명확성은 프로그래밍의 중요한 측면입니다. 다른 사람들 (그리고 당신도!)은 당신이 생성 한 코드를 읽고 이해할 수 있어야합니다. 반복 루프가 재귀보다 첫눈에 이해하기 쉽다고 말하는 것이 안전하다고 생각합니다.

나는 당신이 일반적으로 프로그래밍이나 컴퓨터 과학을 얼마나 잘 알고 있는지 모르겠지만 가상 기능, 상속 또는 고급 개념에 대해 이야기하는 것이 타당하지 않다고 생각합니다. 필자는 종종 피보나치 수를 계산하는 고전적인 예를 시작했습니다. 피보나치 수는 재귀 적으로 정의 되기 때문에 여기에 잘 맞습니다 . 이 이해하기 쉽고 필요가 없습니다 어떤 공상 언어의 특징입니다. 학생들이 재귀에 대한 기본적인 이해를 얻은 후에, 우리는 우리가 이전에 구축 한 몇 가지 간단한 기능을 다시 살펴 보았습니다. 예를 들면 다음과 같습니다.

문자열에 문자 포함되어 있습니까?엑스

문자열을 반복하고 인덱스가 포함되어 있는지 확인하십시오 .엑스

bool find(const std::string& s, char x)
{
   for(int i = 0; i < s.size(); ++i)
   {
      if(s[i] == x)
         return true;
   }

   return false;
}

그렇다면 문제는 재귀 적으로 할 수 있습니까? 물론 우리가 할 수있는 방법은 다음과 같습니다.

bool find(const std::string& s, int idx, char x)
{
   if(idx == s.size())
      return false;

   return s[idx] == x || find(s, ++idx);
}

다음으로 자연스런 질문은 우리가 이렇게해야 하는가? 아마 아닙니다. 왜? 이해하기가 어렵고 생각해 내기가 더 어렵습니다. 따라서 오류가 발생하기 쉽습니다.


2
마지막 문단은 잘못이 아닙니다. 반복적 인 솔루션 (Quicksort!)에 대해 동일한 추론이 재귀를 선호한다는 점을 자주 언급하고 싶습니다.
Raphael

1
@Raphael 맞습니다. 어떤 것은 반복적으로 표현하는 것이 더 자연스럽고 다른 것은 재귀 적으로 표현하는 것이 더 자연 스럽습니다. 그것이 내가 만들려고했던 요점입니다 :)
Juho

음, 내가 틀렸다면 용서하십시오. 그러나 예제 코드에서 리턴 라인을 if 조건으로 분리하면 더 좋지 않을 것입니다 .x가 발견되면 true를 반환하고 그렇지 않으면 재귀적인 부분입니까? 나는 그것이 사실이라고해도 '또는'이 계속 실행되는지 모르겠지만, 그렇다면이 코드는 매우 비효율적입니다.
MindlessRanger

@MindlessRanger 아마도 재귀 버전이 이해하고 쓰기가 더 어려운 완벽한 예일까요? :-)
Juho

예, 그리고 이전의 코멘트는 'or'또는 '||' 첫 번째 조건이 참이면 다음 조건을 확인하지 않으므로 문제가 없습니다.
MindlessRanger

24

일부 문제에 대한 솔루션은 재귀를 사용하여보다 자연스럽게 표현됩니다.

예를 들어 두 종류의 노드가있는 트리 데이터 구조가 있다고 가정합니다. 잎은 정수 값을 저장합니다. 필드에 왼쪽 및 오른쪽 하위 트리가있는 분기. 가장 낮은 값이 가장 왼쪽 잎에 있도록 잎이 정렬되었다고 가정하십시오.

작업이 트리의 값을 순서대로 인쇄한다고 가정하십시오. 이를위한 재귀 알고리즘은 매우 자연 스럽다.

class Node { abstract void traverse(); }
class Leaf extends Node { 
  int val; 
  void traverse() { print(val); }
} 
class Branch extends Node {
  Node left, right;
  void traverse() { left.traverse(); right.traverse(); }
}

재귀없이 동등한 코드를 작성하는 것은 훨씬 어렵습니다. 시도 해봐!

보다 일반적으로 재귀는 트리와 같은 재귀 데이터 구조의 알고리즘이나 자연스럽게 하위 문제로 나눌 수있는 문제에 효과적입니다. 예를 들어, 알고리즘을 나누고 정복하십시오 .

가장 자연스러운 환경에서 재귀를 실제로보고 싶다면 Haskell과 같은 기능적 프로그래밍 언어를 봐야합니다. 그러한 언어에는 반복 구조가 없으므로 모든 것이 재귀 (또는 고차 함수이지만 표현할 가치가있는 또 다른 이야기)를 사용하여 표현됩니다.

함수형 프로그래밍 언어는 최적화 된 테일 재귀를 수행합니다. 즉, 본질적으로 재귀를 루프로 변환 할 필요가없는 한 스택 프레임을 배치하지 않습니다. 실제적인 관점에서 자연스럽게 코드를 작성할 수 있지만 반복 코드의 성능을 얻을 수 있습니다. 레코드의 경우 C ++ 컴파일러는 tail call을 최적화하는 것으로 보이 므로 C ++에서 재귀 사용에 대한 추가 오버 헤드가 없습니다.


1
C ++에는 꼬리 재귀가 있습니까? 기능적 언어가 일반적이라는 점을 지적 할 가치가 있습니다.
Louis

3
고마워 루이스 일부 C ++ 컴파일러는 테일 호출을 최적화합니다. (꼬리 재귀는 언어가 아닌 프로그램의 속성입니다.) 내 답변을 업데이트했습니다.
Dave Clarke

적어도 GCC는 테일 콜 (및 일부 형태의 비 테일 콜)을 최적화합니다.
vonbrand

11

실제로 재귀에 사는 사람으로부터 나는 주제에 대해 약간의 빛을 비추려고 노력할 것입니다.

재귀에 처음 소개되었을 때, 자신을 호출하는 함수이며 기본적으로 트리 탐색과 같은 알고리즘으로 시연됩니다. 나중에 LISP 및 F #과 같은 언어의 함수형 프로그래밍 에 많이 사용됩니다 . 내가 쓴 F #을 사용하면 내가 쓰는 것의 대부분이 재귀 적이며 패턴 일치입니다.

F #과 같은 기능 프로그래밍에 대해 더 배우면 F # 목록 은 단일 링크 목록으로 구현됩니다. 즉, 목록의 헤드에만 액세스하는 작업은 O (1)이고 요소 액세스는 O (n)입니다. 일단 이것을 배우면 데이터를 목록으로 순회하는 경향이 있습니다. 새로운 목록을 역순으로 만든 다음 목록을 뒤집어 매우 효과적인 함수에서 돌아옵니다.

이제 이것에 대해 생각하기 시작하면 재귀 함수가 함수 호출이있을 때마다 스택 프레임을 푸시하고 스택 오버플로가 발생할 수 있음을 곧 깨닫게됩니다. 그러나 꼬리 호출을 수행 할 수 있도록 재귀 함수를 구성 하면 컴파일러는 꼬리 호출에 대한 코드를 최적화하는 기능을 지원합니다. 즉 .NET OpCodes.Tailcall 필드 스택 오버플로가 발생하지 않습니다. 이 시점에서 루프를 재귀 함수로 작성하고 모든 결정을 일치로 작성하기 시작합니다. 시대 ifwhile현재의 역사이다.

PROLOG와 같은 언어로 역 추적을 사용하여 AI로 이동하면 모든 것이 재귀 적입니다. 이를 위해서는 명령형 코드와는 전혀 다른 방식으로 생각해야하지만 PROLOG가 문제의 올바른 도구라면 많은 코드를 작성해야하는 부담을 덜고 오류 수를 크게 줄일 수 있습니다. 참조 : Amzi 고객 eoTek

재귀 사용시기에 대한 질문으로 돌아가려면; 프로그래밍을 보는 한 가지 방법은 한쪽 끝에 하드웨어를 사용하고 다른 쪽 끝에 추상 개념을 사용하는 것입니다. 문제는 하드웨어에 가까이가 더 내가 가진 명령형 언어에서 생각 if하고 while, 더 추상적 인 문제, 더 나는 재귀 높은 수준의 언어로 생각합니다. 그러나 저수준 시스템 코드 작성을 시작하고 유효한지 확인하려면 정리 프로 버 와 같은 솔루션 이 유용 하므로 재귀에 크게 의존합니다.

Jane Street 를 보면 기능 언어 OCaml 을 사용하는 것을 볼 수 있습니다 . 코드에 대해 언급 한 내용을 읽음으로써 코드를 전혀 보지 못했지만 재귀 적으로 생각하고 있습니다.

편집하다

사용 목록을 찾고 있기 때문에 코드에서 무엇을 찾아야하는지에 대한 기본 아이디어와 기본 이외 의 카타 모피 즘 (Catamorphism) 개념을 기반으로하는 기본 사용 목록을 제공합니다 .

C ++의 경우 : 동일한 구조 또는 클래스에 대한 포인터가있는 구조 또는 클래스를 정의하는 경우 포인터를 사용하는 순회 메소드에 대해 재귀를 고려해야합니다.

간단한 경우는 단방향 연결 목록입니다. 머리 나 꼬리에서 시작하여 목록을 처리 한 다음 포인터를 사용하여 목록을 재귀 적으로 탐색합니다.

나무는 재귀가 자주 사용되는 또 다른 경우입니다. 재귀없이 나무 순회를 볼 경우 왜 그렇게 묻기 시작해야합니까? 그것은 잘못된 것이 아니라 의견에 언급해야 할 것입니다.

재귀의 일반적인 용도는 다음과 같습니다.


2
그것은 정말 큰 대답처럼 들리지만, 나는 그것이 곧 믿기 만하면 내 수업에서 가르치는 것보다 조금 위에 있습니다.
Taylor Huston

1
@TaylorHuston 고객임을 기억하십시오. 선생님에게 이해하고 싶은 개념을 물어보십시오. 그는 수업 시간에 대답하지는 않지만 근무 시간 중에 그를 잡아서 앞으로 많은 배당금을 지불 할 수 있습니다.
Guy Coder

좋은 대답이지만 함수형 프로그래밍에 대해 모르는 사람에게는 너무 진보 된 것 같습니다 :).
pad

2
... 순진한 질문자가 함수형 프로그래밍을 공부하게합니다. 승리!
JeffE

8

다른 답변에 주어진 것보다 덜 신비한 사용 사례를 제공하기 위해 재귀는 공통 소스에서 파생되는 트리와 같은 (객체 지향) 클래스 구조와 매우 잘 혼합됩니다. C ++ 예제 :

class Expression {
public:
    // The "= 0" means 'I don't implement this, I let my subclasses do that'
    virtual int ComputeValue() = 0;
}

class Plus : public Expression {
private:
    Expression* left
    Expression* right;
public:
    virtual int ComputeValue() { return left->ComputeValue() + right->ComputeValue(); }
}

class Times : public Expression {
private:
    Expression* left
    Expression* right;
public:
    virtual int ComputeValue() { return left->ComputeValue() * right->ComputeValue(); }
}

class Negate : public Expression {
private:
    Expression* expr;
public:
    virtual int ComputeValue() { return -(expr->ComputeValue()); }
}

class Constant : public Expression {
private:
    int value;
public:
    virtual int ComputeValue() { return value; }
}

위의 예는 재귀를 사용합니다. ComputeValue는 재귀 적으로 구현됩니다. 예제를 작동시키기 위해 가상 함수와 상속을 사용합니다. Plus 클래스의 왼쪽과 오른쪽 부분이 정확히 무엇인지는 모르지만 신경 쓰지 마십시오. 자체 값을 계산할 수있는 것입니다.

위의 접근 방식의 중요한 장점은 모든 클래스가 자체 계산을 처리 한다는 것 입니다. 가능한 모든 하위 표현식의 서로 다른 구현을 완전히 분리합니다. 서로의 작업에 대한 지식이 없습니다. 이를 통해 프로그램에 대한 추론이 쉬워지고 프로그램을 이해, 유지 및 확장하기가 더 쉬워집니다.


1
나는 당신이 어떤 'arcane'예를 참조하는지 잘 모르겠습니다. 그럼에도 불구하고 OO와의 통합에 대한 좋은 토론.
Dave Clarke

3

처음 프로그래밍 클래스에서 재귀를 가르치는 데 사용한 첫 번째 예는 모든 숫자를 역순으로 숫자로 개별적으로 나열하는 함수였습니다.

void listDigits(int x){
     if (x <= 0)
        return;
     print x % 10;
     listDigits(x/10);
}

또는 그와 비슷한 것 (여기서는 메모리에서 가고 테스트하지 않습니다). 또한 높은 수준의 클래스에 들어가면 특히 검색 알고리즘, 정렬 알고리즘 등에서 LOT 재귀를 사용합니다.

따라서 현재 언어에서 쓸모없는 기능처럼 보이지만 장기적으로는 매우 유용합니다.

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