В моем недавнем сообщении в блоге о последовательности Фибоначчи были описаны итеративный и рекурсивный подходы к решению этой общей проблемы интервью. Напоминаем, что задача звучала так: Напишите функцию, возвращающую n элементов в последовательности Фибоначчи.
После изучения временной сложности для обоих решений был сделан вывод, что рекурсивное решение становилось значительно медленнее для каждого дополнительного числа в последовательности из-за экспоненциального роста во времени. Однако есть умный способ улучшить это решение - использовать Memoization.
«Мемоизация - это метод оптимизации, используемый в основном для ускорения компьютерных программ за счет сохранения результатов дорогостоящих вызовов функций и возврата кэшированного результата, когда те же входные данные повторяются снова». (Википедия)
Для нашего рекурсивного решения мы хотим сохранить аргументы каждого вызова функции, а также вывод функции, и повторно использовать их позже, если функция вызывалась с теми же аргументами.
Наше рекурсивное решение выглядело так:
function fib(n) { if (n < 2) { return n; } return fib(n - 1) + fib(n - 2); }
Давайте определим нашу функцию memoize. Он получит функцию в качестве аргумента, а также вернет функцию. Мы также создадим пустой объект, который будет хранить для нас вызовы функций.
function memoize(fn){ const cache = {} return function(){ } }
Функция, возвращаемая в memoize, должна получать тот же аргумент n, что и те, которые мы передали в fib. Однако, если бы мы написали многоразовый мемоизатор, который можно было бы применять к функциям, отличным от fib, мы бы хотели разрешить ему получать любое количество аргументов, например:
function memoize(fn){ const cache = {} return function(...args){ } }
Отсюда нам нужно будет заглянуть в наш банк памяти, нашу константу кеша, и если он содержит ключ с этим набором аргументов, мы вернем соответствующее значение.
function memoize(fn){ const cache = {} return function(...args){ if (cache[args]){ return cache[args]; } } }
Если функция никогда не вызывалась с текущим набором аргументов, нам придется передать их исходной медленной функции, то есть fn, переданной в memoize в качестве аргумента, а затем сохранить результат в кеш. . Чтобы передать массив аргументов, нам нужно использовать метод apply ().
function memoize(fn){ const cache = {} return function(...args){ if (cache[args]){ return cache[args]; } newCall = fn.apply(null, args); cache[args] = newCall; return newCall; } }
Чтобы использовать функцию memoize для нашей проблемы Фибоначчи, мы просто сохраняем вызов функции memoize как константу и передаем аргументы, которые в противном случае были бы переданы в fib этой константе.
const fastFib = memoize(fib);
И, наконец, нам нужно убедиться, что когда fib выполняет рекурсивные вызовы, он вызывает новую быструю версию самого себя, а не оригинального self.
const fastFib = memoize(fib); function fib(n) { if (n < 2) { return n; } return fastFib(n - 1) + fastFib(n - 2); }
Вот и все! Мы значительно сократили время, необходимое для выполнения рекурсивного решения для больших чисел, поскольку оно больше не будет расти экспоненциально.
Написанный здесь мемоайзер довольно универсален и может использоваться для улучшения времени выполнения практически любой функции.
Как всегда, спасибо за чтение и удачного кодирования!