8.3. Демонстрация - Чем можно заменить рекурсию: циклы
Шаг 1
Мы уже видели бинарный поиск без рекурсии (в разделе про поиск) и с рекурсией (в разделе про рекурсию). На самом деле, каждое решение задачи рекурсией можно переписать на циклы, и наоборот. Чтобы максимально упростить демонстрацию, посмотрим на ещё одну рекурсивную функцию, которую мы уже видели (хоть она и не очень интересная с точки зрения логики) — функцию для вычисления последовательности Фибоначчи. Здесь мы можем наглядно посмотреть на то, сколько вызовов функций произошло, а на каждый из них выделялась память!
import {benchmark} from "./benchmarker";
import {logCall, printLogs} from "./logger"
/*
Примеры:
fibonacci(1) // 0
fibonacci(2) // 1
fibonacci(13) // 144
*/
function fibonacci(index) {
logCall(index);
if (index === 1) {
return 0;
}
if (index === 2) {
return 1;
}
return fibonacci(index - 1) + fibonacci(index - 2);
}
benchmark(() => fibonacci(20), 'fibonacci(20)');
printLogs();
Шаг 2
Этот код лишён главного недостатка рекурсии: стека вызовов. А значит, мы можем вызывать его вообще с любыми аргументами и не бояться упереться в максимальное количество подвызовов. Однако и читается он сложнее: вместо репрезентативного «текущее — сумма двух предыдущих» у нас появляются какие-то вспомогательные переменные, в логике обновления которых надо разбираться. Конечно, из-за простоты примера это не очень усложняет задачу. Но потому этот пример и был выбран: чтобы не переусложнять логику для демонстрации концепта. Главный вывод из этого демо: если код чуть сложнее по асимптотике и расходам памяти, но при этом легче для поддержки — используйте его. Он хоть и потратит немного времени вашего пользователя, но зато сэкономит время и нервы вам, если придётся погружать в него нового члена команды или себя самого через несколько месяцев.
/*
Примеры:
fibonacci(1) // 0
fibonacci(2) // 1
fibonacci(13) // 144
*/
import {benchmark} from "./benchmarker";
function fibonacci(index) {
if (index === 1) {
return 0;
}
if (index === 2) {
return 1;
}
// Переменные для хранения предыдущих двух чисел
let grandParent = 0;
let parent = 1;
let current;
for (let i = 3; i <= index; i++) {
current = grandParent + parent;
grandParent = parent;
parent = current;
}
return parent;
}
benchmark(() => fibonacci(100), 'fibonacci(100)');
Шаг 3
А ещё в этом конкретном случае мы уменьшаем сложность алгоритма. В рекурсивном случае она была θ(1.6ⁿ) (почитать почему можно здесь), а в итеративном — всего O(n)!
import {benchmark} from "./benchmarker";
/*
Примеры:
fibonacci(1) // 0
fibonacci(2) // 1
fibonacci(13) // 144
*/
function recursiveFibonacci(index) {
if (index === 1) {
return 0;
}
if (index === 2) {
return 1;
}
return recursiveFibonacci(index - 1) + recursiveFibonacci(index - 2);
}
function iterativeFibonacci(index) {
if (index === 1) {
return 0;
}
if (index === 2) {
return 1;
}
// Переменные для хранения предыдущих двух чисел
let grandParent = 0;
let parent = 1;
let current;
for (let i = 3; i <= index; i++) {
current = grandParent + parent;
grandParent = parent;
parent = current;
}
return parent;
}
benchmark(() => recursiveFibonacci(42), "recursiveFibonacci");
benchmark(() => iterativeFibonacci(42), "iterativeFibonacci");
File benchmarker.js
const results = document.getElementById('results')
export function benchmark(callback, name) {
const start = performance.now();
const result = callback();
const end = performance.now();
const time = (end - start) / 1000;
log(`${name} вернула ${result} за ${time.toFixed(5)} секунд`);
}
function log(message) {
const result = document.createElement('li');
result.innerHTML = message;
results.appendChild(result);
}
File logger.js
const results = document.getElementById('results');
const logs = [];
export function logCall(argument) {
const result = document.createElement('li');
result.innerHTML = `fibonacci(${argument})`;
logs.push(result);
}
export function printLogs() {
const list = document.createElement('ol');
logs.forEach(log => list.appendChild(log));
results.appendChild(list);
}