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

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

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

  • Есть общие правила?
  • Есть "выкройка"?

Я нашел эту серию информативной: blog.moertel.com/posts/2013-05-11-recursive-to-iterative.htm‌ l

orionrush 03.07.2018 14:47
Учебная записка [Medium] Leetcode#22 Generate Parentheses
Учебная записка [Medium] Leetcode#22 Generate Parentheses
На этот раз мы собираемся решить еще одну классическую проблему, связанную с парными скобками, так называемую генерацию скобок.
382
1
145 734
19
Перейти к ответу Данный вопрос помечен как решенный

Ответы 19

Стремитесь сделать ваш рекурсивный вызов Рекурсия хвоста (рекурсия, где последний оператор является рекурсивным вызовом). Как только у вас есть это, преобразовать его в итерацию, как правило, довольно просто.

Некоторая рекурсия хвоста преобразования JIT: ibm.com/developerworks/java/library/j-diag8.html

Liran Orevi 15.05.2009 18:03

Множество интерпретаторов (т. Е. Scheme является наиболее известной) хорошо оптимизируют хвостовую рекурсию. Я знаю, что GCC с определенной оптимизацией выполняет хвостовую рекурсию (хотя C - странный выбор для такой оптимизации).

new123456 07.04.2011 19:20

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

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

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

Мне все еще кажется рекурсивным ... :)

nathan 02.10.2008 00:51

Ну да, но это наполовину рекурсивно. Чтобы избавиться от другой рекурсии, потребуется использовать другую технику ...

Mark Bessey 02.10.2008 01:10

Ну, в общем, рекурсию можно имитировать как итерацию, просто используя переменную хранения. Обратите внимание, что рекурсия и итерация обычно эквивалентны; один почти всегда можно преобразовать в другой. Хвостовая рекурсивная функция очень легко конвертируется в итеративную. Просто сделайте аккумуляторную переменную локальной и выполняйте итерацию вместо рекурсии. Вот пример на 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;
}

Зная меня, я, наверное, ошибся в коде, но идея есть.

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

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

Любые идеи о том, как определить максимальный стек для конкретной рекурсии?

lexicalscope 19.03.2012 19:36

@lexicalscope предположим, что у вас есть рекурсивный алгоритм в O(N) = O(R*L), где L - это сумма сложности "для уровня r", например в этом случае у вас есть O(N), который работает на каждом шаге, выполняя разбиение, рекурсивная глубина - O(R), то есть в худшем случае O(N), в среднем O(logN).

Caleth 20.09.2017 14:30
Ответ принят как подходящий

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

var stack = [];
stack.push(firstObject);

// while not empty
while (stack.length) {

    // Pop off end of stack.
    obj = stack.pop();

    // Do stuff.
    // Push other objects on the stack as needed.
    ...

}

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

foo(first);
foo(second);

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

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

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

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

Chethan 29.04.2013 18:49

Если вы замените свой стек очередью, разве это не решит проблему изменения порядка добавления?

SamuelWarren 30.05.2013 19:11

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

pete 01.11.2013 00:33

Возможно, стоит упомянуть, что если между двумя foo вы добавите некоторую work (), вы также можете поместить ее в свой стек, но вы захотите добавить дополнительный параметр, например isWork.

user420667 28.01.2014 01:57

Я совсем недавно сделал это в общих чертах, заменив мою функцию посещения узла (node)->() на (node)->[actions], где действие - () -> [actions]. Затем снаружи вы просто извлекаете действие / продолжение из стека, применяете / выполняете его, помещаете возвращенные им действия в стек в обратном порядке и повторяете. Условные / сложные обходы, вы просто фиксируете то, что было бы локальными переменными стека в указателях со счетчиком ссылок, которые вы закрываете в своих преобразователях, тогда последующие преобразователи могут зависеть от результатов предыдущих промежуточных обходов и т. д.

experquisite 10.04.2015 01:49

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

Adam Arold 28.05.2015 01:16

@SamuelWarren Если вы используете очередь вместо стека, вы превращаете обход в глубину исходного дерева вызовов в обход в ширину. например, если f (1) вызывает f (2), затем f (3), а f (2) вызывает f (4), вы получаете должен 1,2,4,3, но, помещая вызовы в очередь, вы получите 1,2,3,4 (поскольку 1 поставит в очередь 3, прежде чем 2 получит возможность поставить в очередь 4).

amalloy 28.07.2015 22:05

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

Zhu Li 26.08.2016 04:37

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

dontloo 19.06.2017 09:41

Почему это лучше, чем внутренний стек вызовов?

Gusev Slava 13.07.2018 23:42

@ZhuLi Если мы используем new, мы можем создать объект в куче, а не в стеке. В отличие от стека, куча не имеет ограничений по памяти. См. gribblelab.org/CBootCamp/7_Memory_Stack_vs_Heap.html

yuqli 28.08.2018 06:22

@SamuelWarren Если вы гарантируете, что ничего не выскочит до того, как все будет отправлено, вы можете преобразовать стек в очередь. Однако в большинстве случаев (в том числе и в этом) выскакивание происходит в процессе нажатия. Одним словом, стеки и очереди вообще не конвертируемы.

aafulei 17.12.2018 08:49

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

Bernhard Barker 26.04.2019 18:08

Я добавил ответ, который показывает вопрос как с итеративным, так и с рекурсивным решением.

user12170609 02.11.2019 04:32

Даже использование стека не преобразует рекурсивный алгоритм в итеративный. Обычная рекурсия - это рекурсия на основе функций, и если мы используем стек, то она становится рекурсией на основе стека. Но это все еще рекурсия.

Для рекурсивных алгоритмов пространственная сложность составляет 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) в зависимости от размера данных, но это явно итеративный алгоритм.

Ponkadoodle 29.08.2013 03:52

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

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

}

Приведенный выше пример является примером рекурсивного к итеративному dfs в двоичном дереве поиска :)

Amit 11.10.2011 23:39

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

Я только что придумал пример на C# того, как это сделать. Предположим, у вас есть следующая рекурсивная функция, которая действует как обход по порядку, и что 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();
            }


        }

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

Wojciech Kulik 19.02.2012 03:02

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

CCS 18.12.2014 20:46

Вы говорили мне: «Кажется, никто не обратился к тому, где рекурсивная функция вызывает себя более одного раза в теле и обрабатывает возврат к определенной точке рекурсии», и тогда я уже проголосовал за. Хорошо, теперь я собираюсь прочитать оставшуюся часть вашего ответа и посмотреть, был ли мой преждевременный голос оправданным. (Потому что мне отчаянно нужно знать ответ на этот вопрос).

mydoghasworms 25.06.2015 13:05

@mydoghasworms - Возвращаясь к этому вопросу спустя столько времени, меня потребовалось даже время, чтобы вспомнить, о чем я думал. Надеюсь, ответ помог.

T. Webster 04.07.2015 04:38

@ T.Webster Что ж, это действительно направило меня на определенный путь мышления, хотя у меня до сих пор нет ответа. На данный момент я собираюсь использовать рекурсивную функцию, хотя в конце концов мне бы хотелось от нее отказаться. Однако спасибо; голос остается :-)

mydoghasworms 05.07.2015 20:48

@mydoghasworms, спасибо. Сейчас я потратил некоторое время на то, чтобы освоить Python 3.x (поэтому Microsoft продвигает кроссплатформенную разработку), и может быть более простой способ выразить это.

T. Webster 14.07.2015 09:44

@ T.Webster Что касается меня, я прохожу курс nand2tetris на Coursera, чтобы попытаться расширить свой кругозор и, надеюсь, найти ответ, который я ищу. (Потому что я считаю, что в какой-то момент это произойдет).

mydoghasworms 14.07.2015 09:55

Идея этого решения мне понравилась, но она показалась мне запутанной. Я написал упрощенную версию бинарного дерева на питоне, возможно, это поможет кому-то разобраться в идее: gist.github.com/azurkin/abb258a0e1a821cbb331f2696b37c3ac

azurkin 14.08.2018 14:46

Рекурсия - это не что иное, как процесс вызова одной функции из другой, только этот процесс выполняется путем вызова самой функции. Как мы знаем, когда одна функция вызывает другую, первая функция сохраняет свое состояние (свои переменные), а затем передает управление вызываемой функции. Вызываемая функция может быть вызвана с использованием того же имени переменных, что ex fun1 (a) может вызывать fun2 (a). Когда мы выполняем рекурсивный вызов, ничего нового не происходит. Одна функция вызывает себя, передавая один и тот же тип и похожие по именам переменные (но, очевидно, значения, хранящиеся в переменных, разные, остается только имя) самой себе. Но перед каждым вызовом функция сохраняет свое состояние, и этот процесс сохранения продолжается. ЭКОНОМИЯ СДЕЛАНА НА СТЕКЕ.

ТЕПЕРЬ СТЕК ПРИХОДИТ В ИГРУ.

Итак, если вы пишете итеративную программу и каждый раз сохраняете состояние в стеке, а затем при необходимости извлекаете значения из стека, вы успешно преобразовали рекурсивную программу в итеративную!

Доказательство простое и аналитическое.

В рекурсии компьютер поддерживает стек, а в итеративной версии вам придется поддерживать стек вручную.

Подумайте над этим, просто преобразуйте рекурсивную программу поиска в глубину (на графах) в итеративную программу dfs.

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

Обычно метод предотвращения переполнения стека для рекурсивных функций называется методом трамплина, который широко применяется разработчиками 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);
}

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

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

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

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.

JanX2 01.01.2014 22:28

@ JanX2, stackitem нельзя отправлять без инициализации. Но да, инициализация 0 приведет к обнаружению ошибок.

Chethan 02.01.2014 10:14

Почему вместо этого не заданы оба обратных адреса для оператора v.pop_back()?

is7s 07.02.2015 00:56

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

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

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

Например график: А-> В А-> С show (A) напечатает B, A, C

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

Например, предположим, что шоу (A) начинает работать. Вызов функции в строке 3. show (B) означает - Добавить элемент в стек, означающий, что «вам нужно будет продолжить со строки 2 с локальным состоянием переменной node = A». - Перейти к строке 0 с node = B.

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

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

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

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

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

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

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

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

Подумайте о вещах, которым действительно нужен стек:

Если мы рассмотрим схему рекурсии как:

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, но это может быть полезно, поскольку у вас есть больший контроль над ней.

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

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

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

Мои примеры написаны на Clojure, но их довольно легко перевести на любой язык.

Учитывая эту функцию, StackOverflows для больших значений 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))

Это работает и для более сложных функций, например функция Аккермана:

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

Другие вопросы по теме