Шлях від рекурсії до ітерації


349

Я багато використовував рекурсії в своїх багаторічних програмуваннях для вирішення простих проблем, але я цілком усвідомлюю, що іноді потрібна ітерація через проблеми з пам'яттю / швидкістю.

Отже, колись у дуже далекому минулому я спробував знайти, чи існував якийсь «візерунок» чи підручник із тексту, що трансформував загальний рекурсійний підхід до ітерації, і нічого не знайшов. Або хоча б нічого, що я пам’ятаю, це не допомогло б.

  • Чи є загальні правила?
  • Чи є "візерунок"?

4
Я знайшов цю серію інформативною: blog.moertel.com/posts/2013-05-11-recursive-to-iterative.html
orionrush

Відповіді:


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
Я опрацював це на папері, і це дві різні речі. Якщо ви скасуєте порядок, який ви додали до них, це змусить вас рухатись вперед, як завжди, але ваш пошук все ще є глибинним пошуком. Але якщо ви поміняєте все в чергу, тепер ви робите першу широту, а не першу глибину.
Піт

1
Я нещодавно зробив це загальним способом, замінивши свою функцію відвідування вузла (node)->()на те, (node)->[actions]де дія () -> [actions]. Потім на зовнішній стороні ви просто вискочите дію / продовження зі стека, застосуєте / виконаєте, натисніть дії, які він повернув на стеку, у зворотному порядку, і повторіть. Контингентні / складні обходи, ви просто фіксуєте те, що були б локальними змінними стека в посилальних лічильниках, які ви закриваєте у своєму гроні, то наступні грона можуть залежати від результатів попередніх
субтраверсій

6
Іноді ми уникаємо рекурсії, щоб уникнути потокового потоку. Але збереження власного стека також спричинить stackoverflow. То чому ми хочемо реалізувати рекурсію власним стеком?
Чжу Лі

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 припустимо, що у вас є рекурсивний алгоритм O(N) = O(R*L), де Lсума складнощів "для шару r", наприклад, у цьому випадку ви O(N)працюєте на кожному кроці, роблячи перегородки, рекурсивна глибина - O(R)тобто найгірший випадок O(N), середній випадок O(logN)тут.
Калет

48

Здається, ніхто не звертався до того, де рекурсивна функція викликає себе не один раз у тілі, і обробляє повернення до певної точки рекурсії (тобто не примітивно-рекурсивної). Кажуть, що кожна рекурсія може бути перетворена на ітерацію , тому, здається, це має бути можливим.

Я щойно придумав приклад 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();
            }


        }

5
Це справді корисно, мені довелося написати ітераційну версію реквізиту, яка викликає себе n-разів, завдяки вашій посаді я це зробив.
Войцех Кулик

1
Це повинен бути найкращим прикладом, який я коли-небудь бачив для емуляції рекурсії стека викликів у ситуаціях, коли в рамках методу здійснюються кілька рекурсивних викликів. Хороша робота.
CCS

1
Ви мали мене в "Здається, ніхто не звертався до того, де рекурсивна функція викликає себе не один раз в тілі, і обробляє повернення до певної точки рекурсії", і тоді я вже схвалив. Гаразд, зараз я прочитаю решту вашої відповіді і побачу, чи виправдана моя передчасна пропозиція. (Тому що мені відчайдушно потрібно знати відповідь на це).
mydoghasworms

1
@mydoghasworms - Повернувшись до цього питання через стільки часу, мені навіть знадобилася хвилина, щоб згадати, що я думав. Сподіваюся, відповідь допомогла.
Т. Вебстер

1
Ідея цього рішення мені сподобалась, але це здавалося мені заплутаним. Я написав спрощену версію для бінарного дерева в python, можливо, це допоможе комусь зрозуміти ідею: gist.github.com/azurkin/abb258a0e1a821cbb331f2696b37c3ac
azurkin

33

Прагніть зробити ваш рекурсивний виклик Tail Recursion (рекурсія, де останнє твердження - рекурсивний виклик). Коли ви це зробите, перетворити його на ітерацію, як правило, досить просто.


2
Деякі
рецидиви

Багато перекладачів (тобто схема є найбільш відомою) добре оптимізує рекурсію хвоста. Я знаю, що GCC з певною оптимізацією робить хвостові рекурсії (навіть незважаючи на те, що C є дивним вибором для такої оптимізації).
new123456

19

Ну, загалом, рекурсію можна імітувати як ітерацію, просто використовуючи змінну зберігання. Зауважте, що рекурсія та ітерація, як правило, рівнозначні; одне майже завжди може бути перетворене на інше. Хвостово-рекурсивна функція дуже легко перетворюється на ітеративну. Просто зробіть змінну акумулятора локальною та повторіть, а не повторюйте. Ось приклад в 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, ви отримаєте неправильну поведінку. Рішення полягає в ініціалізації raдо 0.
JanX2

@ JanX2, stackitemне слід натискати без ініціалізації. Але так, ініціалізація до 0 призведе до помилок.
Четман

Чому натомість обидві зворотні адреси не встановлені в v.pop_back()операторі?
is7s

7

Пошук у 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);
    }

}

Наведений вище приклад є прикладом рекурсивного до ітеративного dfs на дереві двійкового пошуку :)
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об'єктів, які роблять складні речі. Або ви можете піти в зворотному напрямку і скласти його список більш простих типів (наприклад, "завдання" може бути 4 елемента на стеку int, а не один елемент в стеку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
Все ще виглядає рекурсивно для мене ... :)
Натхан

2
Ну так - але це наполовину рекурсивно. Позбавлення від іншої рекурсії потребує використання іншої методики ...
Марк Бессі

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

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

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

Для виконання коду система виконує інструкції. Коли виникає виклик функції, система підштовхує інформацію, необхідну для повернення туди, де вона була, запускає функціональний код, а коли функція завершується, з'являється інформація про те, куди потрібно йти, щоб продовжувати.


0

Це посилання дає деяке пояснення та пропонує ідею збереження "розташування", щоб мати можливість дістатися до точного місця між кількома рекурсивними дзвінками:

Однак усі ці приклади описують сценарії, в яких рекурсивний виклик робиться фіксовану кількість разів. Речі стають складнішими, коли у вас є щось на кшталт:

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


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

Це працює і для більш складних функцій, наприклад, функції 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)))))
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.