Гораздо проще придумать рекурсивные алгоритмы для многих задач. Единственная загвоздка в том, что рекурсия использует стековую память, которой не хватает, в то время как итеративные версии используют список, доступ к которому осуществляется по принципу «последний пришел – первый вышел». Здесь мы увидим три алгоритма, а именно: факториал, глубину бинарного дерева и генерацию всех n-выберите-k комбинаций набора.

Факториал

Факториал числа можно определить рекурсивно как:

f(0) = 1
f(n) = n* f(n-1)

Это позволяет прямой перевод в следующий код:

int factorial(int n) {
  if (n > 1) {
    // Recursive case
    return n * factorial(n - 1);
  } else {
    // Base case
    return 1;
  }
}

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

_Z9factoriali:
.LFB0:
 endbr64
 movl $1, %eax
 cmpl $1, %edi
 jle .L4
.L3:
 movl %edi, %edx
 subl $1, %edi
 imull %edx, %eax
 cmpl $1, %edi
 jne .L3
 ret  ;; redundant code
.L4:
 ret

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

Глубина бинарного дерева

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

int recursive_depth(Node* root){
  if (root ==nullptr) return 0;
  else {
      int dleft = recursive_depth(root->left);
      int dright = recursive_depth(root->right);
      return 1 + std::max(dleft,dright);
  }
}

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

_Z15recursive_depthP4Node:
.LFB6:
 endbr64
 testq %rdi, %rdi
 je .L6
 pushq %rbp
 pushq %rbx
 movq %rdi, %rbx
 subq $8, %rsp
 movq (%rdi), %rdi
 call _Z15recursive_depthP4Node
 movq 8(%rbx), %rdi
 movl %eax, %ebp
 call _Z15recursive_depthP4Node
 leal 1(%rbp), %ecx
 movl %eax, %edx
 leal 1(%rax), %eax
 cmpl %edx, %ebp
 cmovg %ecx, %eax
 addq $8, %rsp
 popq %rbx
 popq %rbp
 ret
.L6:
 xorl %eax, %eax
 ret

Один из способов преобразовать его в итеративную функцию — преобразовать его в стиль передачи продолжения (CPS). Переводя версию Ocaml на Gasche, мы получаем следующий код C++.

typedef std::function<int(int)> call_back;
int cps_depth( Node* root, call_back cps){

  if(root==nullptr) return cps(0);
  else return cps_depth(root->left,[&] (int dleft) {
                        return cps_depth(root->right, [&] (int dright){
                        return cps(1 + std::max(dleft,dright));
                        });});
}
int depth(Node* root){
  return cps_depth(root, [](int k){return k;});
}

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

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

enum class item_type { tree, height};
struct item {
  item_type type;
 union { Tree* root; unsigned height; };
 item(Tree* r):type(item_type::tree)
    {root=r;}
 item(unsigned h):type(item_type::height)
    {height=h;}
};
static int height_aux(Tree *root, stack<item>& items){
    if(root)
    {
      item it(root->right);
      items.push(it);
      return height_aux(root->left,items);
    } else {
      return eval(items, 0);
    }
  }
  static int eval(stack<items>, unsigned depth){
    if(items.empty()){
      return depth;
    }else{
 
      item cur = items.top();
      items.pop();
      switch (cur.type)
      {
        case item_type::tree:
          {
            item next(depth);
            items.push(next);
            return height_aux(cur.root, items);
          }
        case item_type::height:
          {
            return eval(items,  1 + std::max(depth, cur.height));
          }
      }
    }
 
  }
static unsigned height(Tree* root){
    stack<item> items;
    return height_aux(root,items);
}

Давайте расшифруем, что здесь происходит. Давайте снова посмотрим на обратный вызов версии CPS.

return cps_depth(root->left,[&] (int dleft) {
                        return cps_depth(root->right, [&] (int dright){
                        return cps(1 + std::max(dleft,dright));
                        });});

Второй или внутренний вызов cps_depth требует значения root-›right и ничего больше. Последующему вызову cps требуется только значение dleft, которое становится доступным только при вызове внешней функции. Следовательно, нам нужен союз, который указывает, какое из двух значений требуется.

Объединение eval и height_aux в одну итеративную функцию не так уж сложно, как показано ниже.

static int height(Tree *root){
  enum class item_type { tree, height};
  struct item {
    item_type type;
    union {
      Tree* root;
      unsigned height;
    };
    item(Tree* r):type(item_type::tree)
    {root=r;}
    item(unsigned h):type(item_type::height)
    {height=h;}
 
  };
    bool go_on=true;
    unsigned depth = 0;
    std::stack<item> items;
    while (go_on) {
 
      if (root) {
        item it(root->right);
        items.push(it);
        root = root->left;
      } else {
        go_on=false;
        depth=0;
        while (!items.empty()&& !go_on) {
          item cur = items.top();
          items.pop();
          switch (cur.type) {
            case item_type::tree: {
                item next(depth);
                items.push(next);
                root = cur.root;
                go_on = true;
              }
            break;
            case item_type::height: {
                  depth = 1 + std::max(depth, cur.height);
                 }
             break;
          }
        }
      }
    }
    return depth;
  }

Сгенерировать все комбинации

Рассмотрим задачу генерации всех k-комбинаций множества, содержащего n элементов.

void print(const vector<int>& nums, ostream& fout)
{
    for (auto& a : nums) { fout << a << ' '; }
    fout << endl;
}
void combo(vector<int>& nums, int n, int k, vector<int>& result) 
{
    if ( k == 0 ) {
       print(result,cout); 
    }
    else if (n == k){
            result.insert(result.end(),nums.begin(),nums.begin()+k);
           print(result,cout);
            result.erase(result.end()-k,result.end());
    } else {
            result.push_back(nums[0]);
            //include num[0] and reduce n
            swap(nums[0],nums[n-1]);
            combo(nums,n-1,k-1,result);
            result.pop_back();
            //exclude old num[0]           
            combo(nums,n-1,k,result);
            swap(nums[0],nums[n-1]);
    }
}
void doCombo(vector<int>& nums, int n, int k){
  vector<int> result;
  combo(nums,n,k,result);
}

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

typedef std::function<void()> callback;
void combo_cps(vector<int>& nums, int n, int k,
  vector<int>& result, callback cps){
    if ( k == 0 ) {
        print(result,cout); 
        cps();
    } else if (n == k){
        result.insert(result.end(),nums.begin(),nums.begin()+k);
        print(result,cout);
        result.erase(result.end()-k,result.end());
        cps();
    } else {
        result.push_back(nums[0]);
        //include num[0] 
        swap(nums[0],nums[n-1]);
        combo_cps(nums,n-1,k-1,result, 
                  [&](){
                      result.pop_back();
                      //exclude old num[0]            
                      combo_cps(nums,n-1,k,result, [&]()
                                {
                                    swap(nums[0],nums[n-1]);
                                    cps();
                                });
                  });
    }

}
void combo(vector<int>& nums, int n, int k){
   combo_cps(nums,n,k,[](){}):
}

(От себя лично: я совершил ошибку новичка, не позвонив cps перед возвращением, и мне потребовалось некоторое время, чтобы решить эту проблему).

Чтобы преобразовать приведенную выше версию CPS в итеративную версию, нам нужно отслеживать два значения, n и k, и состояние, которое означает, включено или исключено num[0]. Результат показан ниже:

struct iter{
  int n;
  int k;
  enum { include, exclude, none } state;
  
};

void combo_iter(vector<int>& nums, int n, int k){
    vector<int> result;
    vector<iter> items;
    iter it{0,0,iter::none};
    items.push_back(it);
    for(;;){
        while(!(k==0 || k==n)){
            result.push_back(nums[0]);
            swap(nums[0],nums[n-1]);
            iter it{n-1,k-1,iter::include};
            items.push_back(it);
            --n;
            --k;
        }

    if ( k == 0 ) {
        print(result,cout); 
    } else if (n == k){
        result.insert(result.end(),nums.begin(),nums.begin()+k);
        print(result,cout);
        result.erase(result.end()-k,result.end());
    } 
        bool go_on = true;
        while(go_on){
            iter it = items.back();
            items.pop_back();
            switch (it.state){
                case iter::exclude:
                    n=it.n;
                    k= it.k;
                    swap(nums[0],nums[n]);
                    break;
                case iter::include:
                    result.pop_back();
                    it = iter{it.n,it.k+1,iter::exclude};
                    items.emplace_back(it);
                    n=it.n;
                    k= it.k;
                    go_on = false;
                    break;
                case iter::none:
                    return;
                    break;
            }
        }
    }
}

Обратите внимание, что структура кода почти такая же, как и для задачи о глубине дерева. Чтобы включить num[0], мы уменьшаем k и добавляем num[0] к результату. Чтобы исключить num[0], мы возвращаемся к старому значению k и извлекаем верхнее значение из result. Обратите внимание, что оператор switch выполняется только после того, как будет вычислен хотя бы один полный результат.