[recursion] 재귀는 그 자체로 기능입니까?

… 아니면 그냥 연습입니까?

저는 교수님과의 논쟁 때문에 이것을 묻고 있습니다. 저는 수업에서 재귀를 다루지 않았기 때문에 함수를 재귀 적으로 호출 한 것에 대한 크레딧을 잃었고, 제 주장은 학습 return과 방법을 통해 암묵적으로 배웠다는 것 입니다.

나는 누군가가 확실한 답을 가지고 있다고 생각하기 때문에 여기에서 묻는 것입니다.

예를 들어 다음 두 방법의 차이점은 무엇입니까?

public static void a() {
    return a();
    }

public static void b() {
    return a();
    }

a영원히 계속”(실제 프로그램에서는 잘못된 입력이 제공 될 때 사용자에게 다시 메시지를 표시하는 데 올바르게 사용됨) 외에 a와 사이에 근본적인 차이점이 b있습니까? 최적화되지 않은 컴파일러에게는 어떻게 다르게 처리됩니까?

궁극적으로는 배우는 의해 여부에 온다 return a()에서 b우리는 또한 배운 그 때문에 그 return a()에서 a. 우리가?



답변

특정 질문에 답하려면 : 아니요, 언어 학습의 관점에서 재귀는 기능이 아닙니다. 만약 당신의 교수가 정말로 그가 아직 가르치지 않은 “기능”을 사용한다고 표시했다면 그것은 잘못된 것입니다.

줄 사이를 읽으면서, 한 가지 가능성은 재귀를 사용함으로써 그의 과정에 대한 학습 결과로 여겨지는 기능을 사용하지 않았을 것입니다. 예를 들어, 반복을 전혀 사용하지 않았거나 및 for을 모두 사용 하는 대신 루프 만 사용했을 수 있습니다 . 과제는 특정 작업을 수행하는 능력을 테스트하는 것을 목표로하는 것이 일반적이며,이를 피하면 교수가 해당 기능에 대해 따로 설정 한 점수를 부여 할 수 없습니다. 그러나 그것이 정말로 당신의 잃어버린 점수의 원인이라면, 교수는 이것을 자신의 학습 경험으로 받아 들여야합니다. 특정 학습 결과를 보여주는 것이 과제의 기준 중 하나라면 학생들에게 명확하게 설명해야합니다 .forwhile

그렇게 말하면서 반복이 여기서 재귀보다 더 나은 선택이라는 다른 대부분의 의견과 답변에 동의합니다. 몇 가지 이유가 있으며 다른 사람들이 어느 정도 그들에게 접촉했지만 그들이 그 뒤에있는 생각을 완전히 설명했는지 확신 할 수 없습니다.

스택 오버플로

더 분명한 것은 스택 오버플로 오류가 발생할 위험이 있다는 것입니다. 현실적으로, 사용자가 실제로 스택 오버플로를 트리거하기 위해 잘못된 입력을 여러 번 제공해야하기 때문에 작성한 메서드가 실제로 하나로 이어질 가능성은 거의 없습니다.

그러나 명심해야 할 한 가지는 메서드 자체뿐만 아니라 호출 체인에서 더 높거나 낮은 다른 메서드가 스택에 있다는 것입니다. 이 때문에 사용 가능한 스택 공간을 부담없이 차지하는 것은 모든 방법에 대해 매우 무례한 일입니다. 아무도 코드를 작성할 때마다 빈 스택 공간에 대해 계속 걱정할 필요가 없습니다. 다른 코드가 불필요하게 많은 공간을 사용할 위험이 있기 때문입니다.

이것은 추상화라고하는 소프트웨어 설계의보다 일반적인 원칙의 일부입니다. 기본적으로을 (를) 호출 할 때 DoThing()주의해야 할 것은 Thing이 완료된 것입니다. 어떻게 수행 되는지 에 대한 구현 세부 사항에 대해 걱정할 필요가 없습니다 . 그러나 스택을 탐욕스럽게 사용하면이 원칙이 깨집니다. 모든 코드는 콜 체인의 다른 곳에서 코드에 의해 스택이 얼마나 안전하게 남았는지에 대해 걱정해야하기 때문입니다.

가독성

다른 이유는 가독성입니다. 코드가 바라는 이상은 사람이 읽을 수있는 문서가되는 것입니다. 여기서 각 줄은 자신이하는 일을 간단히 설명합니다. 다음 두 가지 접근 방식을 사용하십시오.

private int getInput() {
    int input;
    do {
        input = promptForInput();
    } while (!inputIsValid(input))
    return input;
}

private int getInput() {
    int input = promptForInput();
    if(inputIsValid(input)) {
        return input;
    }
    return getInput();
}

예, 둘 다 작동하며 예, 둘 다 이해하기 쉽습니다. 그러나 두 가지 접근 방식을 영어로 어떻게 설명 할 수 있습니까? 나는 그것이 다음과 같을 것이라고 생각합니다.

입력이 유효 할 때까지 입력을 요청한 다음 반환합니다.

입력을 요청하고 입력이 유효하면 반환하고, 그렇지 않으면 입력을 받고 그 결과를 대신 반환합니다.

아마도 후자에 대해 약간 덜 투박한 표현을 생각할 수 있지만 항상 첫 번째가 개념적으로 실제로 수행하려는 작업에 대한보다 정확한 설명이 될 것임을 항상 발견 할 것입니다. 이것은 재귀가 항상 가독성 이 낮다 는 것을 의미하지는 않습니다 . 트리 순회와 같이 빛나는 상황의 경우 재귀와 다른 접근 방식 사이에서 동일한 종류의 나란히 분석을 수행 할 수 있으며 재귀가 한 줄씩 더 명확하게 설명하는 코드를 제공한다는 사실을 거의 확실하게 찾을 수 있습니다.

분리해서 두 가지 모두 작은 점입니다. 이것이 실제로 스택 오버플로로 이어질 가능성은 매우 낮으며 가독성 향상은 미미합니다. 그러나 모든 프로그램은 이러한 작은 결정의 모음이 될 것이므로 격리되어 있어도 그다지 중요하지 않더라도 올바른 결정의 원칙을 배우는 것이 중요합니다.


답변

메타 질문이 아니라 문자적인 질문에 답하기 위해 : 재귀 모든 컴파일러 및 / 또는 언어가 반드시 허용하는 것은 아니라는 점에서 기능입니다. 실제로는 모든 (일반적인) 최신 컴파일러와 확실히 모든 Java 컴파일러에서 예상됩니다! -그러나 그것은 보편적으로 사실 이 아닙니다 .

재귀가 지원되지 않는 이유에 대한 인위적인 예로서 함수의 반환 주소를 정적 위치에 저장하는 컴파일러를 고려하십시오. 예를 들어 스택이없는 마이크로 프로세서 용 컴파일러의 경우 일 수 있습니다.

이러한 컴파일러의 경우 다음과 같은 함수를 호출하면

a();

그것은 다음과 같이 구현됩니다.

move the address of label 1 to variable return_from_a
jump to label function_a
label 1

그리고 a ()의 정의,

function a()
{
   var1 = 5;
   return;
}

다음과 같이 구현됩니다.

label function_a
move 5 to variable var1
jump to the address stored in variable return_from_a

a()그러한 컴파일러에서 재귀 적 으로 호출하려고 할 때의 문제 가 분명하기를 바랍니다. 반환 주소가 덮어 써 졌기 때문에 컴파일러는 더 이상 외부 호출에서 반환하는 방법을 알지 못합니다.

재귀를 지원하지 않고 실제로 사용한 컴파일러 (70 년대 후반 또는 80 년대 초반)의 경우 문제는 그보다 약간 더 미묘했습니다. 반환 주소는 현대 컴파일러와 마찬가지로 스택에 저장되지만 로컬 변수는 ‘티. (이론적으로 이것은 비 정적 지역 변수가없는 함수에 대해 재귀가 가능하다는 것을 의미해야하지만 컴파일러가이를 명시 적으로 지원했는지 여부를 기억하지 못합니다. 어떤 이유로 암시 적 지역 ​​변수가 필요할 수 있습니다.)

앞으로 모든 스레드에 대해 스택을 제공 할 필요가없는 것이 유리할 수 있고 컴파일러가 루프로 리팩토링 할 수있는 경우에만 재귀가 허용되는 경우와 같은 고도의 병렬 시스템과 같은 특수 시나리오를 상상할 수 있습니다. (물론 위에서 논의한 기본 컴파일러는 코드 리팩토링과 같은 복잡한 작업을 수행 할 수 없었습니다.)


답변

선생님은 당신이 공부했는지 여부를 알고 싶어합니다. 분명히 당신은 그가 당신에게 가르쳐 준 방식 ( 좋은 방법 , 반복) 으로 문제를 해결하지 못했고 , 따라서 당신이하지 않았다고 생각합니다. 저는 모두 창의적인 해결책을 원하지만이 경우에는 다른 이유로 선생님과 동의해야합니다
. 사용자가 잘못된 입력을 너무 많이 제공하면 (즉, Enter 키를 길게 눌러) 스택 오버플로 예외가 발생하고 솔루션이 충돌합니다. 또한 반복 솔루션이 더 효율적이고 유지 관리하기 쉽습니다. 그것이 당신의 선생님이 당신에게 주었어야하는 이유라고 생각합니다.


답변

“우리가 수업에서 재귀를 다루지 않았기 때문에”점수를 차감하는 것은 끔찍합니다. B로 돌아가는 함수 C를 호출하는 함수 B를 호출하는 함수 A를 호출하는 방법을 배운 경우, 호출자에게 다시 돌아가는 A로 돌아가는 함수 C를 호출하고 교사는 이들이 다른 함수 여야한다는 것을 명시 적으로 말하지 않았습니다. 예를 들어 이전 FORTRAN 버전의 경우) A, B 및 C가 모두 동일한 기능이 될 수는 없습니다.

다른 한편으로, 우리는 당신의 특정한 경우에 재귀를 사용하는 것이 정말로 옳은 일인지 결정하기 위해 실제 코드를 봐야합니다. 세부 사항은 많지 않지만 잘못 들립니다.


답변

당신이 질문 한 구체적인 질문과 관련하여 많은 관점이 있지만 제가 말할 수있는 것은 언어 학습의 관점에서 볼 때 재귀는 그 자체로 기능이 아니라는 것입니다. 만약 당신의 교수가 정말로 그가 아직 가르치지 않은 “특징”을 사용했다는 표시를했다면 그것은 틀렸지 만 제가 말했듯이, 실제로 교수가 점수를 감점 할 때 옳다고 여기는 다른 관점이 있습니다.

귀하의 질문에서 추론 할 수있는 것에서 입력 실패의 경우 재귀 함수를 사용하여 입력을 요청하는 것은 모든 재귀 함수 호출이 스택으로 푸시되기 때문에 좋은 습관이 아닙니다. 이 재귀는 사용자 입력에 의해 구동되기 때문에 무한 재귀 함수를 가질 수 있으며 따라서 StackOverflow가 발생합니다.

귀하의 질문에서 언급 한 두 가지 예는 그들이하는 일의 의미에서 차이가 없습니다 (그러나 다른 방식으로 다릅니다)-두 경우 모두 반환 주소와 모든 메서드 정보가 스택에로드됩니다. 재귀의 경우 반환 주소는 단순히 메서드 호출 바로 뒤의 줄입니다 (물론 코드 자체에서 볼 수있는 것과 정확히 일치하는 것이 아니라 컴파일러가 만든 코드에서). Java, C 및 Python에서 재귀는 새 스택 프레임을 할당해야하므로 반복 (일반)에 비해 상당히 비쌉니다. 입력이 너무 여러 번 유효하지 않으면 스택 오버플로 예외가 발생할 수 있습니다.

나는 재귀가 그 자체의 주제로 간주되고 프로그래밍 경험이없는 사람이 재귀를 생각할 가능성이 없기 때문에 교수님이 점수를 감점했다고 생각합니다. (물론 그렇지 않다는 의미는 아니지만 가능성이 낮습니다).

IMHO, 교수님이 점수를 빼서 옳다고 생각합니다. 유효성 검사 부분을 다른 방법으로 쉽게 가져와 다음과 같이 사용할 수 있습니다.

public bool foo()
{
  validInput = GetInput();
  while(!validInput)
  {
    MessageBox.Show("Wrong Input, please try again!");
    validInput = GetInput();
  }
  return hasWon(x, y, piece);
}

당신이 한 일이 실제로 그런 방식으로 해결 될 수 있다면, 당신이 한 일은 나쁜 습관이었고 피해야합니다.


답변

교수님이 아직 가르치지 않았을 수도 있지만 재귀의 장점과 단점을 배울 준비가 된 것 같습니다.

재귀의 가장 큰 장점은 재귀 알고리즘이 작성하기가 훨씬 쉽고 빠르다는 것입니다.

재귀의 가장 큰 단점은 재귀 알고리즘이 스택 오버플로를 유발할 수 있다는 것입니다. 각 재귀 수준에는 스택에 추가 스택 프레임을 추가해야하기 때문입니다.

스케일링으로 인해 프로그래머의 단위 테스트보다 프로덕션에서 더 많은 수준의 재귀가 발생할 수있는 프로덕션 코드의 경우 일반적으로 단점이 장점보다 크며 실용적 일 때 재귀 코드를 피하는 경우가 많습니다.


답변

구체적인 질문과 관련하여 재귀는 기능이므로 예라고 말하고 싶지만 질문을 다시 해석 한 후에. 재귀를 가능하게하는 언어와 컴파일러의 일반적인 디자인 선택이 있으며, 재귀를 전혀 허용하지 않는 Turing-complete 언어가 존재 합니다 . 즉, 재귀는 언어 / 컴파일러 디자인의 특정 선택에 의해 활성화되는 기능입니다.

  • 일류 함수를 지원 하면 최소한의 가정 하에서 재귀가 가능합니다. 예를 들어 Unlambda에서 루프 작성을 참조 하거나 자체 참조, 루프 또는 할당이없는이 둔한 Python 표현식을 참조하십시오.

    >>> map((lambda x: lambda f: x(lambda g: f(lambda v: g(g)(v))))(
    ...   lambda c: c(c))(lambda R: lambda n: 1 if n < 2 else n * R(n - 1)),
    ...   xrange(10))
    [1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880]
    
  • 후기 바인딩 을 사용 하거나 정방향 선언 을 정의 하는 언어 / 컴파일러는 재귀를 가능하게합니다. 예를 들어 Python은 아래 코드를 허용하지만 Turing-complete 시스템에 대한 요구 사항이 아닌 디자인 선택 (늦은 바인딩) 입니다. 상호 재귀 함수는 종종 전방 선언 지원에 의존합니다.

    factorial = lambda n: 1 if n < 2 else n * factorial(n-1)
    
  • 재귀 적으로 정의 된 유형 을 허용하는 정적으로 유형이 지정된 언어재귀 활성화에 기여합니다. Go에서 Y Combinator 구현을 참조하십시오 . 재귀 적으로 정의 된 유형이 없어도 Go에서 재귀를 사용할 수는 있지만 Y 결합자는 특별히 불가능하다고 생각합니다.