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