간단한 문제를 해결하기 위해 수년간의 프로그래밍에서 재귀를 많이 사용했지만 때로는 메모리 / 속도 문제로 인해 반복이 필요하다는 것을 완전히 알고 있습니다.
그래서 언젠가는 과거에 반복에 대한 일반적인 재귀 접근 방식을 변형시키는 "패턴"또는 교과서 방식이 존재하는지 찾아서 아무것도 찾지 못했습니다. 또는 적어도 내가 기억할 수있는 것은 도움이되지 않습니다.
- 일반적인 규칙이 있습니까?
- "패턴"이 있습니까?
간단한 문제를 해결하기 위해 수년간의 프로그래밍에서 재귀를 많이 사용했지만 때로는 메모리 / 속도 문제로 인해 반복이 필요하다는 것을 완전히 알고 있습니다.
그래서 언젠가는 과거에 반복에 대한 일반적인 재귀 접근 방식을 변형시키는 "패턴"또는 교과서 방식이 존재하는지 찾아서 아무것도 찾지 못했습니다. 또는 적어도 내가 기억할 수있는 것은 도움이되지 않습니다.
답변:
일반적으로 재귀 함수에 일반적으로 전달되는 매개 변수를 스택으로 푸시하여 재귀 알고리즘을 반복 알고리즘으로 바꿉니다. 실제로, 프로그램 스택을 자신의 것으로 교체하고 있습니다.
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);
편집 : 기사 스택 및 재귀 제거 (또는 기사 백업 링크 )는이 주제에 대해 자세히 설명합니다.
(node)->()
을 (node)->[actions]
action이 있는 위치 로 대체하여 최근에 일반적인 방식으로이 작업을 수행했습니다 () -> [actions]
. 그런 다음 외부에서는 스택에서 액션 / 연속을 팝하고 적용 / 실행하고 스택에 반환 된 액션을 순서대로 밀고 반복합니다. 우발적 / 복잡한 순회, 당신은 당신이 당신의 썽크에서 닫은 참조 카운트 포인터에서 로컬 스택 변수였던 것을 포착하고, 후속 썽 크는 이전 하위 순회 등의 결과에 따라 우연히 될 수 있습니다.
new
하면 스택 대신 힙에 객체를 만들 수 있습니다. 스택과 달리 힙에는 메모리 제한이 없습니다. 참조 gribblelab.org/CBootCamp/7_Memory_Stack_vs_Heap.html
실제로 가장 일반적인 방법은 자신의 스택을 유지하는 것입니다. 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;
}
}
분명히이 예제는 스택 경계를 확인하지 않습니다 ... 실제로 왼쪽과 오른쪽 값이 주어진 최악의 경우를 기준으로 스택의 크기를 조정할 수 있습니다. 그러나 당신은 아이디어를 얻습니다.
O(N) = O(R*L)
. 여기서 L
"r 계층에 대한 복잡성"의 합계는 어디 입니까? 예를 들어,이 경우 O(N)
분할을 수행하는 각 단계에서 작업을 수행합니다. 재귀 깊이는 평균 O(R)
최악의 경우 입니다. O(N)
O(logN)
재귀 함수가 본문에서 두 번 이상 자신을 호출하는 곳을 아무도 다루지 않았으며 재귀의 특정 지점으로 돌아 오는 것을 처리하지 않습니다 (즉, 원시 재귀가 아님). 모든 재귀를 반복으로 바꿀 수 있다고 말하면 이것이 가능해야합니다.
방금 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();
}
}
재귀 호출을 꼬리 재귀 (마지막 문이 재귀 호출 인 재귀) 로 만들기 위해 노력하십시오 . 일단 당신이 그것을 반복으로 변환하는 것은 일반적으로 매우 쉽습니다.
일반적으로 단순히 스토리지 변수를 사용하여 반복으로 재귀를 모방 할 수 있습니다. 재귀와 반복은 일반적으로 동일합니다. 하나는 거의 항상 다른 것으로 변환 될 수 있습니다. 꼬리 재귀 함수는 반복적 인 함수로 매우 쉽게 변환됩니다. 어큐뮬레이터 변수를 로컬 변수로 만들고 재귀 대신 반복하십시오. 다음은 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;
}
나를 알면 코드에서 실수를했을 수도 있지만 아이디어가 있습니다.
스택을 사용해도 재귀 알고리즘을 반복적으로 변환하지는 않습니다. 일반 재귀는 함수 기반 재귀이며 스택을 사용하면 스택 기반 재귀가됩니다. 그러나 여전히 재귀입니다.
재귀 알고리즘의 경우 공간 복잡도는 O (N)이고 시간 복잡도는 O (N)입니다. 반복 알고리즘의 경우 공간 복잡도는 O (1)이고 시간 복잡도는 O (N)입니다.
그러나 복잡성 측면에서 스택을 사용하면 동일하게 유지됩니다. 꼬리 재귀 만 반복으로 변환 할 수 있다고 생각합니다.
copy = new int[size]; for(int i=0; i<size; ++i) copy[i] = source[i];
공간을 복사하여 어레이를 복제하는 것을 고려하십시오. 시간 복잡도는 데이터 크기를 기준으로 O (N)이지만 분명히 반복적 인 알고리즘입니다.
스택과 재귀 제거 문서 캡처 힙 스택 프레임을 외부화의 생각은하지만, 제공하지 않습니다 간단하고 반복적 인 변환하는 방법을. 아래는 하나입니다.
반복 코드로 변환하는 동안 재귀 호출은 임의로 깊은 코드 블록에서 발생할 수 있음을 알고 있어야합니다. 그것은 매개 변수뿐만 아니라 실행될 논리와 후속 조건에 참여하는 변수의 상태로 돌아가는 지점이기도합니다. 아래는 최소한의 변경으로 반복 코드로 변환하는 매우 간단한 방법입니다.
이 재귀 코드를 고려하십시오.
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 가되어야 잘못된 동작이 발생합니다. 해결책은 ra
0 으로 초기화 하는 것입니다.
stackitem
는 초기화하지 않고 푸시해서는 안됩니다. 그러나 예, 0으로 초기화하면 오류가 발생합니다.
v.pop_back()
문으로 설정되지 않은 이유는 무엇 입니까?
그냥 시간을 죽이고 ... 재귀 함수
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);
}
}
일반적으로 스택 오버플로를 피하는 기술은 재귀 함수를위한 것입니다. 트램폴린 기술이라고하며 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);
}
실제로 스택이 필요한 것들을 생각하기 :
재귀 패턴을 다음과 같이 고려하면 :
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 실행 스택이 아닌 힙에 있다는 것을 의미하지만, 더 많은 것을 제어 할 수 있다는 점에서 유용 할 수 있습니다.
찾아야 할 한 가지 패턴은 함수 끝에서 재귀 호출입니다 (소위 재귀라고 함). 이것은 잠시 동안 쉽게 교체 할 수 있습니다. 예를 들어, 함수 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;
}
}
두 번째 재귀 호출을 제거합니다.
이 질문 으로 복제 된 질문 은 매우 구체적인 데이터 구조를가집니다.
노드의 구조는 다음과 같습니다.
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로 줄일 수있는 모든 데이터 링크 구조에 적용될 수 있습니다. 현재 노드 하위 항목은 마지막 하위 항목이 다른 모든 하위 항목을 채택하도록 재 배열됩니다. 그런 다음 현재 노드를 삭제하고 순회를 통해 나머지 자식을 반복 할 수 있습니다.
재귀는 하나의 함수를 다른 함수에서 호출하는 프로세스 일뿐입니다.이 프로세스 만 함수 자체를 호출하여 수행됩니다. 한 함수가 다른 함수를 호출 할 때 첫 번째 함수는 상태 (변수)를 저장 한 다음 컨트롤을 호출 된 함수에 전달합니다. 호출 된 함수는 동일한 이름의 변수를 사용하여 호출 할 수 있습니다. ex fun1 (a)는 fun2 (a)를 호출 할 수 있습니다. 우리가 재귀 호출을 할 때 새로운 일은 일어나지 않습니다. 하나의 함수는 이름 변수에서 동일한 유형과 유사한 변수를 전달하여 자신을 호출합니다 (그러나 변수에 저장된 값은 다르며 이름 만 동일합니다). 그러나 모든 호출 전에 함수는 상태를 저장하고이 저장 프로세스는 계속됩니다. 저축은 스택에 완료되었습니다.
이제 스택이 재생됩니다.
따라서 반복 프로그램을 작성하고 매번 스택에 상태를 저장 한 다음 필요할 때 스택에서 값을 표시하면 재귀 프로그램을 반복 프로그램으로 성공적으로 변환 한 것입니다!
증거는 간단하고 분석적입니다.
재귀에서 컴퓨터는 스택을 유지 관리하고 반복 버전에서는 수동으로 스택을 유지 관리해야합니다.
생각해보십시오. 깊이 우선 검색 (그래프에서) 재귀 프로그램을 dfs 반복 프로그램으로 변환하십시오.
모두 제일 좋다!
스택을 사용하여 재귀 함수를 반복 함수로 바꾸는 또 다른 간단하고 완전한 예입니다.
#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;
}
시스템이 재귀 함수를 가져 와서 스택을 사용하여 실행하는 방법에 대한 대략적인 설명 :
이것은 세부 사항없이 아이디어를 보여주기위한 것입니다. 그래프의 노드를 인쇄하는이 함수를 고려하십시오.
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 행으로 이동합니다.
코드를 실행하기 위해 시스템은 명령을 통해 실행됩니다. 함수 호출이 발생하면 시스템은 필요한 정보를 원래 위치로 푸시하고 함수 코드를 실행하며 함수가 완료되면 계속해야 할 위치에 대한 정보를 표시합니다.
여러 반복자 공급자 (반복자를 리턴하는 람다 표현식)를 연결하는 지연 반복자를 사용하여 재귀 순회를 반복자로 변환하는 일반적인 방법이 있습니다. 내 재귀 순회를 반복자로 변환을 참조하십시오 .
내 예제는 Clojure에 있지만 모든 언어로 쉽게 번역 할 수 있어야합니다.
StackOverflow
n의 큰 값에 대해이 함수가 주어지면 :
(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)))))