재귀에서 반복으로가는 방법


349

간단한 문제를 해결하기 위해 수년간의 프로그래밍에서 재귀를 많이 사용했지만 때로는 메모리 / 속도 문제로 인해 반복이 필요하다는 것을 완전히 알고 있습니다.

그래서 언젠가는 과거에 반복에 대한 일반적인 재귀 접근 방식을 변형시키는 "패턴"또는 교과서 방식이 존재하는지 찾아서 아무것도 찾지 못했습니다. 또는 적어도 내가 기억할 수있는 것은 도움이되지 않습니다.

  • 일반적인 규칙이 있습니까?
  • "패턴"이 있습니까?

답변:


334

일반적으로 재귀 함수에 일반적으로 전달되는 매개 변수를 스택으로 푸시하여 재귀 알고리즘을 반복 알고리즘으로 바꿉니다. 실제로, 프로그램 스택을 자신의 것으로 교체하고 있습니다.

Stack<Object> stack;
stack.push(first_object);
while( !stack.isEmpty() ) {
   // Do something
   my_object = stack.pop();

  // Push other objects on the stack.

}

참고 : 내부에 둘 이상의 재귀 호출이 있고 호출 순서를 유지하려면 스택에 역순으로 호출을 추가해야합니다.

foo(first);
foo(second);

에 의해 대체되어야한다

stack.push(second);
stack.push(first);

편집 : 기사 스택 및 재귀 제거 (또는 기사 백업 링크 )는이 주제에 대해 자세히 설명합니다.


4
스택을 대기열로 교체하면 추가 주문을 취소하는 문제가 해결되지 않습니까?
SamuelWarren

2
나는 종이 위에서 그것을 작업했고 두 가지 다른 것들입니다. 추가 한 순서를 반대로하면 평소와 같이 앞으로 순회하지만 순회는 여전히 깊이 우선 검색입니다. 그러나 모든 것을 대기열로 변경하면 깊이 우선 순회보다 너비 우선을 수행합니다.
pete

1
방금 노드 방문 기능 (node)->()(node)->[actions]action이 있는 위치 로 대체하여 최근에 일반적인 방식으로이 작업을 수행했습니다 () -> [actions]. 그런 다음 외부에서는 스택에서 액션 / 연속을 팝하고 적용 / 실행하고 스택에 반환 된 액션을 순서대로 밀고 반복합니다. 우발적 / 복잡한 순회, 당신은 당신이 당신의 썽크에서 닫은 참조 카운트 포인터에서 로컬 스택 변수였던 것을 포착하고, 후속 썽 크는 이전 하위 순회 등의 결과에 따라 우연히 될 수 있습니다.
experquisite

6
때때로 우리는 스택 오버플로를 피하기 위해 재귀를 피합니다. 그러나 자체 스택을 유지 관리하면 스택 오버플로가 발생합니다. 그렇다면 왜 우리 자신의 스택으로 재귀를 구현하고 싶습니까?
Zhu Li

8
@ZhuLi 사용 new하면 스택 대신 힙에 객체를 만들 수 있습니다. 스택과 달리 힙에는 메모리 제한이 없습니다. 참조 gribblelab.org/CBootCamp/7_Memory_Stack_vs_Heap.html
yuqli

77

실제로 가장 일반적인 방법은 자신의 스택을 유지하는 것입니다. C의 재귀 퀵 정렬 기능은 다음과 같습니다.

void quicksort(int* array, int left, int right)
{
    if(left >= right)
        return;

    int index = partition(array, left, right);
    quicksort(array, left, index - 1);
    quicksort(array, index + 1, right);
}

자체 스택을 유지하여 반복적으로 만드는 방법은 다음과 같습니다.

void quicksort(int *array, int left, int right)
{
    int stack[1024];
    int i=0;

    stack[i++] = left;
    stack[i++] = right;

    while (i > 0)
    {
        right = stack[--i];
        left = stack[--i];

        if (left >= right)
             continue;

        int index = partition(array, left, right);
        stack[i++] = left;
        stack[i++] = index - 1;
        stack[i++] = index + 1;
        stack[i++] = right;
    }
}

분명히이 예제는 스택 경계를 확인하지 않습니다 ... 실제로 왼쪽과 오른쪽 값이 주어진 최악의 경우를 기준으로 스택의 크기를 조정할 수 있습니다. 그러나 당신은 아이디어를 얻습니다.


1
특정 재귀에 할당하기 위해 최대 스택을 해결하는 방법에 대한 아이디어가 있습니까?
lexicalscope

@lexicalscope는 재귀 알고리즘이 있다고 가정합니다 O(N) = O(R*L). 여기서 L"r 계층에 대한 복잡성"의 합계는 어디 입니까? 예를 들어,이 경우 O(N)분할을 수행하는 각 단계에서 작업을 수행합니다. 재귀 깊이는 평균 O(R)최악의 경우 입니다. O(N)O(logN)
Caleth

48

재귀 함수가 본문에서 두 번 이상 자신을 호출하는 곳을 아무도 다루지 않았으며 재귀의 특정 지점으로 돌아 오는 것을 처리하지 않습니다 (즉, 원시 재귀가 아님). 모든 재귀를 반복으로 바꿀 수 있다고 말하면 이것이 가능해야합니다.

방금 C # 예제를 생각해 냈습니다. postorder traversal처럼 작동하는 다음과 같은 재귀 함수가 있고 AbcTreeNode가 포인터 a, b, c가있는 3 진 트리라고 가정하십시오.

public static void AbcRecursiveTraversal(this AbcTreeNode x, List<int> list) {
        if (x != null) {
            AbcRecursiveTraversal(x.a, list);
            AbcRecursiveTraversal(x.b, list);
            AbcRecursiveTraversal(x.c, list);
            list.Add(x.key);//finally visit root
        }
}

반복 솔루션 :

        int? address = null;
        AbcTreeNode x = null;
        x = root;
        address = A;
        stack.Push(x);
        stack.Push(null)    

        while (stack.Count > 0) {
            bool @return = x == null;

            if (@return == false) {

                switch (address) {
                    case A://   
                        stack.Push(x);
                        stack.Push(B);
                        x = x.a;
                        address = A;
                        break;
                    case B:
                        stack.Push(x);
                        stack.Push(C);
                        x = x.b;
                        address = A;
                        break;
                    case C:
                        stack.Push(x);
                        stack.Push(null);
                        x = x.c;
                        address = A;
                        break;
                    case null:
                        list_iterative.Add(x.key);
                        @return = true;
                        break;
                }

            }


            if (@return == true) {
                address = (int?)stack.Pop();
                x = (AbcTreeNode)stack.Pop();
            }


        }

5
정말 유용합니다. 귀하의 게시물 덕분에 n 번 반복되는 반복 버전의 반복을 작성해야했습니다.
Wojciech Kulik

1
이것은 메서드 내에서 여러 재귀 호출이 수행되는 상황에서 호출 스택 재귀를 에뮬레이트하는 가장 좋은 예입니다. 좋은 작업.
CCS

1
"재귀 함수가 본문에서 두 번 이상 자신을 호출하는 부분을 다루지 않았고 재귀의 특정 지점으로 돌아가는 것을 처리하는 사람은 아무도 없었습니다." 자, 이제 나머지 답변을 읽고 조기 조기 투표가 정당한지 확인하겠습니다. (필요에 대한 답을 필사적으로 알아야하기 때문에).
mydoghasworms 2016 년

1
@mydoghasworms-오랜만에이 질문으로 돌아가서, 내가 생각했던 것을 기억하는 데 시간 이 걸렸 습니다 . 답변이 도움이 되었기를 바랍니다.
T. Webster

1
이 솔루션의 아이디어가 마음에 들었지만 혼란스러워 보였습니다. : 나는 어쩌면 누군가가 아이디어를 이해하는 데 도움이 될 것입니다, 파이썬에서 이진 트리에 대한 단순화 된 버전을 썼다 gist.github.com/azurkin/abb258a0e1a821cbb331f2696b37c3ac
azurkin

33

재귀 호출을 꼬리 재귀 (마지막 문이 재귀 호출 인 재귀) 로 만들기 위해 노력하십시오 . 일단 당신이 그것을 반복으로 변환하는 것은 일반적으로 매우 쉽습니다.


2
JIT의 일부 변환 꼬리 재귀 : ibm.com/developerworks/java/library/j-diag8.html
Liran Orevi

많은 통역사 (즉, 가장 잘 알려진 스킴)는 꼬리 재귀를 잘 최적화합니다. 특정 최적화를 사용하는 GCC는 꼬리 재귀를 수행한다는 것을 알고 있습니다 (C가 그러한 최적화에 대한 이상한 선택 임에도 불구하고).
new123456

19

일반적으로 단순히 스토리지 변수를 사용하여 반복으로 재귀를 모방 할 수 있습니다. 재귀와 반복은 일반적으로 동일합니다. 하나는 거의 항상 다른 것으로 변환 될 수 있습니다. 꼬리 재귀 함수는 반복적 인 함수로 매우 쉽게 변환됩니다. 어큐뮬레이터 변수를 로컬 변수로 만들고 재귀 대신 반복하십시오. 다음은 C ++의 예입니다 (C는 기본 인수를 사용하지 않았습니다).

// tail-recursive
int factorial (int n, int acc = 1)
{
  if (n == 1)
    return acc;
  else
    return factorial(n - 1, acc * n);
}

// iterative
int factorial (int n)
{
  int acc = 1;
  for (; n > 1; --n)
    acc *= n;
  return acc;
}

나를 알면 코드에서 실수를했을 수도 있지만 아이디어가 있습니다.


14

스택을 사용해도 재귀 알고리즘을 반복적으로 변환하지는 않습니다. 일반 재귀는 함수 기반 재귀이며 스택을 사용하면 스택 기반 재귀가됩니다. 그러나 여전히 재귀입니다.

재귀 알고리즘의 경우 공간 복잡도는 O (N)이고 시간 복잡도는 O (N)입니다. 반복 알고리즘의 경우 공간 복잡도는 O (1)이고 시간 복잡도는 O (N)입니다.

그러나 복잡성 측면에서 스택을 사용하면 동일하게 유지됩니다. 꼬리 재귀 만 반복으로 변환 할 수 있다고 생각합니다.


1
첫 비트에 동의하지만 두 번째 단락을 오해하고 있다고 생각합니다. 메모리 copy = new int[size]; for(int i=0; i<size; ++i) copy[i] = source[i];공간을 복사하여 어레이를 복제하는 것을 고려하십시오. 시간 복잡도는 데이터 크기를 기준으로 O (N)이지만 분명히 반복적 인 알고리즘입니다.
Ponkadoodle

13

스택과 재귀 제거 문서 캡처 힙 스택 프레임을 외부화의 생각은하지만, 제공하지 않습니다 간단하고 반복적 인 변환하는 방법을. 아래는 하나입니다.

반복 코드로 변환하는 동안 재귀 호출은 임의로 깊은 코드 블록에서 발생할 수 있음을 알고 있어야합니다. 그것은 매개 변수뿐만 아니라 실행될 논리와 후속 조건에 참여하는 변수의 상태로 돌아가는 지점이기도합니다. 아래는 최소한의 변경으로 반복 코드로 변환하는 매우 간단한 방법입니다.

이 재귀 코드를 고려하십시오.

struct tnode
{
    tnode(int n) : data(n), left(0), right(0) {}
    tnode *left, *right;
    int data;
};

void insertnode_recur(tnode *node, int num)
{
    if(node->data <= num)
    {
        if(node->right == NULL)
            node->right = new tnode(num);
        else
            insertnode(node->right, num);
    }
    else
    {
        if(node->left == NULL)
            node->left = new tnode(num);
        else
            insertnode(node->left, num);
    }    
}

반복 코드 :

// Identify the stack variables that need to be preserved across stack 
// invocations, that is, across iterations and wrap them in an object
struct stackitem 
{ 
    stackitem(tnode *t, int n) : node(t), num(n), ra(0) {}
    tnode *node; int num;
    int ra; //to point of return
};

void insertnode_iter(tnode *node, int num) 
{
    vector<stackitem> v;
    //pushing a stackitem is equivalent to making a recursive call.
    v.push_back(stackitem(node, num));

    while(v.size()) 
    {
        // taking a modifiable reference to the stack item makes prepending 
        // 'si.' to auto variables in recursive logic suffice
        // e.g., instead of num, replace with si.num.
        stackitem &si = v.back(); 
        switch(si.ra)
        {
        // this jump simulates resuming execution after return from recursive 
        // call 
            case 1: goto ra1;
            case 2: goto ra2;
            default: break;
        } 

        if(si.node->data <= si.num)
        {
            if(si.node->right == NULL)
                si.node->right = new tnode(si.num);
            else
            {
                // replace a recursive call with below statements
                // (a) save return point, 
                // (b) push stack item with new stackitem, 
                // (c) continue statement to make loop pick up and start 
                //    processing new stack item, 
                // (d) a return point label
                // (e) optional semi-colon, if resume point is an end 
                // of a block.

                si.ra=1;
                v.push_back(stackitem(si.node->right, si.num));
                continue; 
ra1:            ;         
            }
        }
        else
        {
            if(si.node->left == NULL)
                si.node->left = new tnode(si.num);
            else
            {
                si.ra=2;                
                v.push_back(stackitem(si.node->left, si.num));
                continue;
ra2:            ;
            }
        }

        v.pop_back();
    }
}

코드의 구조가 어떻게 재귀 논리에 여전히 충실하고 수정이 최소화되어 버그 수가 줄어드는 지 살펴보십시오. 비교를 위해 변경 사항을 ++ 및-로 표시했습니다. v.push_back을 제외한 대부분의 새로 삽입 된 블록은 변환 된 반복 논리에 공통입니다.

void insertnode_iter(tnode *node, int num) 
{

+++++++++++++++++++++++++

    vector<stackitem> v;
    v.push_back(stackitem(node, num));

    while(v.size())
    {
        stackitem &si = v.back(); 
        switch(si.ra)
        {
            case 1: goto ra1;
            case 2: goto ra2;
            default: break;
        } 

------------------------

        if(si.node->data <= si.num)
        {
            if(si.node->right == NULL)
                si.node->right = new tnode(si.num);
            else
            {

+++++++++++++++++++++++++

                si.ra=1;
                v.push_back(stackitem(si.node->right, si.num));
                continue; 
ra1:            ;    

-------------------------

            }
        }
        else
        {
            if(si.node->left == NULL)
                si.node->left = new tnode(si.num);
            else
            {

+++++++++++++++++++++++++

                si.ra=2;                
                v.push_back(stackitem(si.node->left, si.num));
                continue;
ra2:            ;

-------------------------

            }
        }

+++++++++++++++++++++++++

        v.pop_back();
    }

-------------------------

}

이것은 많은 도움이되었지만 문제가 있습니다 : stackitem객체에 가비지 값이 할당되었습니다 ra. 모든 것이 여전히 가장 유사한 경우에 작동하지만 ra우연의 일치로 1 또는 2 가되어야 잘못된 동작이 발생합니다. 해결책은 ra0 으로 초기화 하는 것입니다.
JanX2

@ JanX2 stackitem는 초기화하지 않고 푸시해서는 안됩니다. 그러나 예, 0으로 초기화하면 오류가 발생합니다.
Chethan

반환 주소가 모두 v.pop_back()문으로 설정되지 않은 이유는 무엇 입니까?
is7s

7

"Continuation pass style"에 대해 Google을 검색하십시오. 꼬리 재귀 스타일로 변환하는 일반적인 절차가 있습니다. 꼬리 재귀 함수를 루프로 바꾸는 일반적인 절차도 있습니다.


6

그냥 시간을 죽이고 ... 재귀 함수

void foo(Node* node)
{
    if(node == NULL)
       return;
    // Do something with node...
    foo(node->left);
    foo(node->right);
}

로 변환 될 수있다

void foo(Node* node)
{
    if(node == NULL)
       return;

    // Do something with node...

    stack.push(node->right);
    stack.push(node->left);

    while(!stack.empty()) {
         node1 = stack.pop();
         if(node1 == NULL)
            continue;
         // Do something with node1...
         stack.push(node1->right);             
         stack.push(node1->left);
    }

}

위의 예는 이진 검색 트리에서 반복 df로 재귀하는 예입니다. :)
Amit

5

일반적으로 스택 오버플로를 피하는 기술은 재귀 함수를위한 것입니다. 트램폴린 기술이라고하며 Java 개발자가 널리 채택하고 있습니다.

그러나 C #을위한 약간의 도우미 방법이 여기에 변화의 논리에 요구하거나에서-이해할 수있는 코드를 만들없이 반복적으로 재귀 함수를 켭니다. C #은 놀라운 언어로 가능합니다.

헬퍼 메소드로 메소드의 일부를 랩핑하여 작동합니다. 예를 들어 다음과 같은 재귀 함수입니다.

int Sum(int index, int[] array)
{
 //This is the termination condition
 if (int >= array.Length)
 //This is the returning value when termination condition is true
 return 0;

//This is the recursive call
 var sumofrest = Sum(index+1, array);

//This is the work to do with the current item and the
 //result of recursive call
 return array[index]+sumofrest;
}

로 바뀝니다 :

int Sum(int[] ar)
{
 return RecursionHelper<int>.CreateSingular(i => i >= ar.Length, i => 0)
 .RecursiveCall((i, rv) => i + 1)
 .Do((i, rv) => ar[i] + rv)
 .Execute(0);
}

4

실제로 스택이 필요한 것들을 생각하기 :

재귀 패턴을 다음과 같이 고려하면 :

if(task can be done directly) {
    return result of doing task directly
} else {
    split task into two or more parts
    solve for each part (possibly by recursing)
    return result constructed by combining these solutions
}

예를 들어, 하노이의 고전 탑

if(the number of discs to move is 1) {
    just move it
} else {
    move n-1 discs to the spare peg
    move the remaining disc to the target peg
    move n-1 discs from the spare peg to the target peg, using the current peg as a spare
}

이것은 다음과 같이 명시 적 스택에서 작동하여 루프로 변환 될 수 있습니다.

place seed task on stack
while stack is not empty 
   take a task off the stack
   if(task can be done directly) {
      Do it
   } else {
      Split task into two or more parts
      Place task to consolidate results on stack
      Place each task on stack
   }
}

하노이 타워의 경우 다음과 같습니다.

stack.push(new Task(size, from, to, spare));
while(! stack.isEmpty()) {
    task = stack.pop();
    if(task.size() = 1) {
        just move it
    } else {
        stack.push(new Task(task.size() -1, task.spare(), task,to(), task,from()));
        stack.push(new Task(1, task.from(), task.to(), task.spare()));
        stack.push(new Task(task.size() -1, task.from(), task.spare(), task.to()));
    }
}

스택을 정의하는 방법에는 상당한 유연성이 있습니다. 스택을 Command정교한 일 을하는 객체 목록으로 만들 수 있습니다 . 또는 반대 방향으로 가서 더 간단한 유형의 목록으로 만들 수 있습니다 (예 : "작업"은 스택에있는 int하나의 요소가 아니라 스택에있는 4 개의 요소 일 수 있음 Task).

이 모든 것은 스택의 메모리가 Java 실행 스택이 아닌 힙에 있다는 것을 의미하지만, 더 많은 것을 제어 할 수 있다는 점에서 유용 할 수 있습니다.


3

찾아야 할 한 가지 패턴은 함수 끝에서 재귀 호출입니다 (소위 재귀라고 함). 이것은 잠시 동안 쉽게 교체 할 수 있습니다. 예를 들어, 함수 foo :

void foo(Node* node)
{
    if(node == NULL)
       return;
    // Do something with node...
    foo(node->left);
    foo(node->right);
}

foo 호출로 끝납니다. 이것은 다음으로 대체 될 수 있습니다.

void foo(Node* node)
{
    while(node != NULL)
    {
        // Do something with node...
        foo(node->left);
        node = node->right;
     }
}

두 번째 재귀 호출을 제거합니다.


3
아직도 나에게 재귀 보인다 ... :)
nathan

2
글쎄요.하지만 재귀는 절반입니다. 다른 재귀를 제거하기는 ... 다른 기술을 사용하여 요구하는 것입니다
마크 Bessey

2

질문 으로 복제 된 질문 은 매우 구체적인 데이터 구조를가집니다.

여기에 이미지 설명을 입력하십시오

노드의 구조는 다음과 같습니다.

typedef struct {
    int32_t type;
    int32_t valueint;
    double  valuedouble;
    struct  cNODE *next;
    struct  cNODE *prev;
    struct  cNODE *child;
} cNODE;

재귀 삭제 기능은 다음과 같습니다.

void cNODE_Delete(cNODE *c) {
    cNODE*next;
    while (c) {
        next=c->next;
        if (c->child) { 
          cNODE_Delete(c->child)
        }
        free(c);
        c=next;
    }
}

일반적으로 한 번 이상 (또는 한 번) 자신을 호출하는 재귀 함수의 스택을 항상 피할 수있는 것은 아닙니다. 그러나이 특정 구조에 대해서는 가능합니다. 아이디어는 모든 노드를 단일 목록으로 병합하는 것입니다. 이것은 현재 노드를 child맨 위 행 목록의 끝에 놓아서 수행됩니다 .

void cNODE_Delete (cNODE *c) {
    cNODE *tmp, *last = c;
    while (c) {
        while (last->next) {
            last = last->next;   /* find last */
        }
        if ((tmp = c->child)) {
            c->child = NULL;     /* append child to last */
            last->next = tmp;
            tmp->prev = last;
        }
        tmp = c->next;           /* remove current */
        free(c);
        c = tmp;
    }
}

이 기술은 결정 론적 토폴로지 순서로 DAG로 줄일 수있는 모든 데이터 링크 구조에 적용될 수 있습니다. 현재 노드 하위 항목은 마지막 하위 항목이 다른 모든 하위 항목을 채택하도록 재 배열됩니다. 그런 다음 현재 노드를 삭제하고 순회를 통해 나머지 자식을 반복 할 수 있습니다.


1

재귀는 하나의 함수를 다른 함수에서 호출하는 프로세스 일뿐입니다.이 프로세스 만 함수 자체를 호출하여 수행됩니다. 한 함수가 다른 함수를 호출 할 때 첫 번째 함수는 상태 (변수)를 저장 한 다음 컨트롤을 호출 된 함수에 전달합니다. 호출 된 함수는 동일한 이름의 변수를 사용하여 호출 할 수 있습니다. ex fun1 (a)는 fun2 (a)를 호출 할 수 있습니다. 우리가 재귀 호출을 할 때 새로운 일은 일어나지 않습니다. 하나의 함수는 이름 변수에서 동일한 유형과 유사한 변수를 전달하여 자신을 호출합니다 (그러나 변수에 저장된 값은 다르며 이름 만 동일합니다). 그러나 모든 호출 전에 함수는 상태를 저장하고이 저장 프로세스는 계속됩니다. 저축은 스택에 완료되었습니다.

이제 스택이 재생됩니다.

따라서 반복 프로그램을 작성하고 매번 스택에 상태를 저장 한 다음 필요할 때 스택에서 값을 표시하면 재귀 프로그램을 반복 프로그램으로 성공적으로 변환 한 것입니다!

증거는 간단하고 분석적입니다.

재귀에서 컴퓨터는 스택을 유지 관리하고 반복 버전에서는 수동으로 스택을 유지 관리해야합니다.

생각해보십시오. 깊이 우선 검색 (그래프에서) 재귀 프로그램을 dfs 반복 프로그램으로 변환하십시오.

모두 제일 좋다!


1

스택을 사용하여 재귀 함수를 반복 함수로 바꾸는 또 다른 간단하고 완전한 예입니다.

#include <iostream>
#include <stack>
using namespace std;

int GCD(int a, int b) { return b == 0 ? a : GCD(b, a % b); }

struct Par
{
    int a, b;
    Par() : Par(0, 0) {}
    Par(int _a, int _b) : a(_a), b(_b) {}
};

int GCDIter(int a, int b)
{
    stack<Par> rcstack;

    if (b == 0)
        return a;
    rcstack.push(Par(b, a % b));

    Par p;
    while (!rcstack.empty()) 
    {
        p = rcstack.top();
        rcstack.pop();
        if (p.b == 0)
            continue;
        rcstack.push(Par(p.b, p.a % p.b));
    }

    return p.a;
}

int main()
{
    //cout << GCD(24, 36) << endl;
    cout << GCDIter(81, 36) << endl;

    cin.get();
    return 0;
}

0

시스템이 재귀 함수를 가져 와서 스택을 사용하여 실행하는 방법에 대한 대략적인 설명 :

이것은 세부 사항없이 아이디어를 보여주기위한 것입니다. 그래프의 노드를 인쇄하는이 함수를 고려하십시오.

function show(node)
0. if isleaf(node):
1.  print node.name
2. else:
3.  show(node.left)
4.  show(node)
5.  show(node.right)

예를 들어 그래프 : A-> B A-> C show (A)는 B, A, C를 인쇄합니다.

함수 호출은 로컬 상태와 연속 점을 저장하여 다시 돌아올 수 있고 호출하려는 함수를 건너 뛰는 것을 의미합니다.

예를 들어 show (A)가 실행되기 시작한다고 가정합니다. 3 행의 함수 호출 show (B)는-스택에 항목을 추가 함을 의미합니다. "로컬 변수 상태 node = A 인 2 행에서 계속해야 함"-node = B 인 0 행으로 이동합니다.

코드를 실행하기 위해 시스템은 명령을 통해 실행됩니다. 함수 호출이 발생하면 시스템은 필요한 정보를 원래 위치로 푸시하고 함수 코드를 실행하며 함수가 완료되면 계속해야 할 위치에 대한 정보를 표시합니다.


0

링크 는 몇 가지 설명을 제공하고 여러 재귀 호출 사이의 정확한 위치에 도달 할 수 있도록 "위치"를 유지하는 아이디어를 제안합니다.

그러나 이러한 모든 예는 재귀 호출이 고정 된 횟수 로 수행되는 시나리오를 설명 합니다. 다음과 같은 것이 있으면 까다로워집니다.

function rec(...) {
  for/while loop {
    var x = rec(...)
    // make a side effect involving return value x
  }
}

0

여러 반복자 공급자 (반복자를 리턴하는 람다 표현식)를 연결하는 지연 반복자를 사용하여 재귀 순회를 반복자로 변환하는 일반적인 방법이 있습니다. 내 재귀 순회를 반복자로 변환을 참조하십시오 .


0

내 예제는 Clojure에 있지만 모든 언어로 쉽게 번역 할 수 있어야합니다.

StackOverflown의 큰 값에 대해이 함수가 주어지면 :

(defn factorial [n]
  (if (< n 2)
    1
    (*' n (factorial (dec n)))))

다음과 같은 방식으로 자체 스택을 사용하는 버전을 정의 할 수 있습니다.

(defn factorial [n]
  (loop [n n
         stack []]
    (if (< n 2)
      (return 1 stack)
      ;; else loop with new values
      (recur (dec n)
             ;; push function onto stack
             (cons (fn [n-1!]
                     (*' n n-1!))
                   stack)))))

여기서 return다음과 같이 정의됩니다.

(defn return
  [v stack]
  (reduce (fn [acc f]
            (f acc))
          v
          stack))

이것은 ackermann 함수와 같이 더 복잡한 함수에서도 작동 합니다 .

(defn ackermann [m n]
  (cond
    (zero? m)
    (inc n)

    (zero? n)
    (recur (dec m) 1)

    :else
    (recur (dec m)
           (ackermann m (dec n)))))

다음과 같이 변형 될 수 있습니다.

(defn ackermann [m n]
  (loop [m m
         n n
         stack []]
    (cond
      (zero? m)
      (return (inc n) stack)

      (zero? n)
      (recur (dec m) 1 stack)

      :else
      (recur m
             (dec n)
             (cons #(ackermann (dec m) %)
                   stack)))))
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.