Способ перехода от рекурсии к итерации

Я использовал рекурсию довольно много на моих многих годах программирования для решения простых проблем, но я полностью осознаю, что иногда вам нужна итерация из-за проблем с памятью/скоростью.

Итак, когда-то в очень далеком прошлом я попытался найти, существует ли какой-либо "шаблон" или способ преобразования обычного рекурсивного подхода к итерации, и ничего не нашел. Или, по крайней мере, ничего из того, что я помню, не помогло бы.

  • есть общие правила?
  • есть "шаблон"?

19 ответов


обычно я заменяю рекурсивный алгоритм итерационным алгоритмом, нажимая параметры, которые обычно передаются рекурсивной функции в стек. Фактически, вы заменяете стек программы одним из своих собственных.

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

должен быть заменен by

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

Edit: статья стеки и устранение рекурсии (или ссылка на резервную копию статьи) подробнее на эту тему.


действительно, самый распространенный способ сделать это-сохранить свой собственный стек. Вот рекурсивная функция quicksort в 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;
    }
}

очевидно, что этот пример не проверяет границы стека... и действительно, Вы можете определить размер стека на основе наихудшего случая с учетом левого и правого значений. Но вы поняли идею.


Кажется, никто не обращался, где рекурсивная функция вызывает себя более одного раза в теле, и обрабатывает возврат к определенной точке рекурсии (т. е. не примитивно-рекурсивной). Говорят, что каждая рекурсия может быть превращена в итерацию, так что кажется, что это должно быть возможно.

Я только что придумал пример C#, как это сделать. Предположим, у вас есть следующая рекурсивная функция, которая действует как обход postorder, и что AbcTreeNode-это 3-арное дерево с указателями a, b, c.

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).

но если мы используем вещи стека с точки зрения сложности, остается тем же. Я думаю, только хвостовая рекурсия можно преобразовать в итерацию.


на стеки и устранение рекурсии статья отражает идею воплощения кадра стека в куче, но не дает простой и повторяемый способ преобразования. Ниже один.

при преобразовании в итеративный код необходимо знать, что рекурсивный вызов может произойти из произвольно глубокого блока кода. Его не только параметры, но и точка, чтобы вернуться к логике, которая еще предстоит выполнить, и состояние переменные, которые участвуют в последующих условных обозначениях, которые имеют значение. Ниже приведен очень простой способ преобразования в итеративный код с наименьшими изменениями.

рассмотрим этот рекурсивный код:

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

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

}

поиск google для " продолжения прохождения стиля."Существует общая процедура преобразования в хвостовой рекурсивный стиль; существует также общая процедура превращения хвостовых рекурсивных функций в циклы.


просто убить время... Рекурсивная функция

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

один шаблон для поиска-это вызов рекурсии в конце функции (так называемая хвостовая рекурсия). Это можно легко заменить на некоторое время. Например, функция 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;
     }
}

что исключает второй рекурсивный вызов.


Я просто поддержал ответ, предлагающий использовать явный стек, который, по моему мнению, является правильным решением и имеет общую применимость.

Я имею в виду, что вы можете использовать его для преобразования любой рекурсивной функции в итеративную функцию. Просто проверьте, какие значения сохраняются в рекурсивных вызовах, это те, которые есть чтобы быть локальным для рекурсивной функции и заменять вызовы циклом, где вы будете нажимать их в стеке. Когда стек пуст, рекурсивный функция была бы прекращена.

Я не могу удержаться, чтобы не сказать, что доказательство того, что каждая рекурсивная функция эквивалентна итеративной функции на другом типе данных, это одно из моих самых дорогих воспоминаний моих университетских времен. Это был курс (и профессор), который действительно заставил меня понять, что такое компьютерное программирование.


A вопрос который был закрыт как дубликат этого, имел очень специфическую структуру данных:

enter image description here

узел имел следующую структуру:

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.

всего наилучшего!


думая о вещах, которые на самом деле нуждаются в стеке:

если мы рассмотрим шаблон рекурсия как:

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 объекты, которые делают сложные вещи. Или вы можете пойти в противоположном направлении и сделать его списком более простых типов (например, "задача" может быть 4 элемента в стеке int, а не один элемент в стеке Task).

все это означает, что память для стека в куче а не в стеке выполнения Java, но это может быть полезно в том, что у вас больше контроля над ним.


примерное описание того, как система принимает любую рекурсивную функцию и выполняет ее с помощью стека:

Это должно было показать идею без деталей. Рассмотрим эту функцию, которая будет выводить узлы графа:

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) начинает работать. Вызов функции на линии 3. показать (B) означает - Добавить элемент в стек означает "вам нужно будет продолжить в строке 2 с узлом состояния локальной переменной=A" - Гото строка 0 с узлом=B.

для выполнения кода система выполняет инструкции. При обнаружении вызова функции система выталкивает информацию, которая должна вернуться туда, где она была, запускает код функции, а когда функция завершается, выводит информацию о том, где она должна продолжайте.


этой ссылке дает некоторое объяснение и предлагает идею сохранения "местоположения", чтобы иметь возможность добраться до точного места между несколькими рекурсивными вызовами:

однако, все эти примеры иллюстрируют ситуации, в которых рекурсивный вызов в основные количество раз. Все становится сложнее, когда у вас есть что-то вроде:

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

существует общий способ преобразования рекурсивного обхода в итератор с помощью ленивого итератора, который объединяет несколько поставщиков итераторов (лямбда-выражение, которое возвращает итератор). Смотрите мой преобразование рекурсивного обхода в итератор.


еще один простой и полный пример превращения рекурсивной функции в итерационном с помощью стека.

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