- Рекурсивно определяем числа Фибоначчи с помощью мемоизации в JavaScript
- Фибоначчи и рекурсия
- Мемоизация
- Email подписка!
- Вывести числа фибоначчи javascript
- Фибоначчи на собеседовании
- 1. Быть проще, и люди к вам потянутся.
- 2. Чтобы понять рекурсию, надо понять рекурсию
- 3. Мемная функция
- 4. Мистер Бине
- 5. Следуй за белым кроликом.
Рекурсивно определяем числа Фибоначчи с помощью мемоизации в JavaScript
В этой статье рассказывается о том, как определить и оперировать числами Фибоначчи с помощью рекурсии и мемоизации на JavaScript.
Я постепенно расскажу про каждый момент в этой задаче, с объяснениями и примерами кода.
P.S Очень надеюсь, что эта статья поможет явисту Ростиславу узнать, что такое мемоизация и рекурсивный ряд чисел Фибоначчи.
Фибоначчи и рекурсия
Вообще, рекурсивный подход в решении этой задачи считается плохой практикой из-за высокого runtime complexity. Это решение будет O(log n). А если представить большие числа, то сложно представить насколько тяжелой будет операция.
Давайте посмотрим на рекурсивное решение этой задачки:
Суть в том, что мы последовательно запускаем функцию внутри себя же, делая ряд вычислений. Но давайте разберемся, что же там происходит на самом деле. Вот визуальное представление:
Давайте взглянем на три блока слева. А именно f(2) и затем на f(1) и f(0) . Тут мы находим второй элемент в ряде чисел, начинающимся с 0 .
Напоминаю, ряд чисел Фибоначчи это:
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144 и так далее.
Тут мы отдаем fibo значение 2 , так как мы хотим высчитать fibo(2) . Как вы видите сверху мы видим специальное условие когда число равно 2-м и не меньше двух.
Дальше отработают следующие строчки кода:
fibo(n — 1) + fibo(n — 2)
Замените n на 2 и мы получим fibo(1) + fibo(0)
Следующим шагом будет нахождение значений двух условий, а именно fibo(0) и fibo(1) . Тут для каждого случая нам придётся заново запускать метод fibo и именно в этом моменте и начинается рекурсия.
В общем, мы получаем буквально следующее:
fibo(2) = fibo(1) + fibo(0) = 1 + 0 = 1
Давайте ещё немного разберем рекурсию, но уже с fibo(3) .
fibo(3) = fibo(2) + fibo(1)
Используя рекурсию мы получаем мы получаем следующее продолжение:
fibo(2) + fibo(1), где fibo(2) рекурсивно станет (fibo(1) + fibo(0)) + fibo(1) , где в результате мы получим 1 + 0 + 1 = 2 .
Сейчас структура древа, описанного выше должна стать для вас более понятной. Рекурсия будет происходить вплоть до конца каждой ветви пока значение не будет 1 или 0 .
А теперь самое интересное. Посмотрите на количество одинаковых аргументов в этом древе. Их несколько: 4, 3, 2 и все они будут отрабатывать снова и снова при вызове рекурсии.
И тут нам на помощь приходит мемоизация.
Мемоизация
Мемоизация это имплементация сложных рекурсивных или же итеративных функций более быстрым способом. Как? Кэшируя значения отдаваемые функцией при её первоначальном запуске.
Когда мы отдаём функции мемоизации уже использованное значение, то оно отдаст готовое значение, сохраненное в кэше и не будет запускать функцию снова. К чему это приведет? К тому ускорению производительности, очевидно же. Вашему коду не надо будет пересчитывать то, что уже давно посчитано.
Круто звучит, да? Тогда давайте посмотрим на пример мемоизации, что понять его более детально:
Мы отдаём так называемое анонимное замыкание, которое может наследовать любую переменную или в нашем случае функцию, переданную memo. Затем мы можем смело играться с объектом аргументов этой функции.
Например, запустив её с memoFib(8) мы получим 21 , а объект с мемоизированными значениями будет выглядеть так:
Email подписка!
- Интересные новости
- Полезные статьи
- Сниппеты кода
- Много интересного
Источник
Вывести числа фибоначчи javascript
Последовательность чисел Фибоначчи определяется формулой Fn = Fn-1 + Fn-2 . То есть, следующее число получается как сумма двух предыдущих.
Первые два числа равны 1 , затем 2(1+1) , затем 3(1+2) , 5(2+3) и так далее: 1, 1, 2, 3, 5, 8, 13, 21. .
Числа Фибоначчи тесно связаны с золотым сечением и множеством природных явлений вокруг нас.
Напишите функцию fib(n) которая возвращает n-е число Фибоначчи.
P.S. Все запуски функций из примера выше должны работать быстро. Вызов fib(77) должен занимать не более доли секунды.
Сначала решим через рекурсию.
Числа Фибоначчи рекурсивны по определению:
При больших значениях n такое решение будет работать очень долго. Например, fib(77) может повесить браузер на некоторое время, съев все ресурсы процессора.
Это потому, что функция порождает обширное дерево вложенных вызовов. При этом ряд значений вычисляется много раз снова и снова.
Например, посмотрим на отрывок вычислений для fib(5) :
Здесь видно, что значение fib(3) нужно одновременно и для fib(5) и для fib(4) . В коде оно будет вычислено два раза, совершенно независимо.
Полное дерево рекурсии:
Можно заметить, что fib(3) вычисляется дважды, а fib(2) – трижды. Общее количество вычислений растёт намного быстрее, чем n , что делает его огромным даже для n=77 .
Можно это оптимизировать, запоминая уже вычисленные значения: если значение, скажем, fib(3) вычислено однажды, затем мы просто переиспользуем это значение для последующих вычислений.
Другим вариантом было бы отказаться от рекурсии и использовать совершенно другой алгоритм на основе цикла.
Вместо того, чтобы начинать с n и вычислять необходимые предыдущие значения, можно написать цикл, который начнёт с 1 и 2 , затем из них получит fib(3) как их сумму, затем fib(4) как сумму предыдущих значений, затем fib(5) и так далее, до финального результата. На каждом шаге нам нужно помнить только значения двух предыдущих чисел последовательности.
Вот детальные шаги нового алгоритма.
Теперь мы хотим получить fib(4) = fib(2) + fib(3) .
Переставим переменные: a,b , присвоим значения fib(2),fib(3) , тогда c можно получить как их сумму:
Следующий шаг даёт новое число последовательности:
…И так далее, пока не получим искомое значение. Это намного быстрее рекурсии и не требует повторных вычислений.
Цикл начинается с i=3 , потому что первое и второе значения последовательности заданы a=1 , b=1 .
Источник
Фибоначчи на собеседовании
Вычисление ряда Фибоначчи — это классическая алгоритмическая задача, потому её нередко дают на собеседованиях, когда хотят проверить, что кандидат в принципе хоть как-то умеет в алгоритмы. Предположим, вы тот самый кандидат. Вам дали задание: на языке JavaScript написать функцию fib(n) , возвращающую энное число Фибоначчи. Считаем, что нулевое число Фибоначчи — это нуль. Проверка корректности аргумента не требуется. Какие у вас есть варианты?
1. Быть проще, и люди к вам потянутся.
Самое простое решение — это банальный цикл.
Шутка. Разумеется, так писать не нужно — если, конечно, вы не собеседуетесь на должность штатного обфускатора.
У вас кончились талончики на переменные? cypok
Ладно, ладно, для ещё большей читаемости напишем так:
Это — вариант классический, простой и элегантный. Но, возможно, вы хотите продемонстрировать своё знание ещё каких-то концепций? Например…
2. Чтобы понять рекурсию, надо понять рекурсию
Например, да, вы можете продемонстрировать, что умеете в рекурсию. Например, так:
Запомните этот вариант. Так делать не стоит. Не следует. Нельзя. Никогда. Это хуже, чем пинать щеночков, и сравнимо с небольшим холокостом.
Возможно, вы спросите, почему. В таком случае просто запустите этот код и попытайтесь посчитать, скажем, пятидесятое число Фибоначчи. Полагаю, вы ощутите некую задержку. Шутка. Полагаю, если вы запускаете этот код не на суперкомпьютере, то попросту не дождётесь результата. При том, что простой, не рекурсивный код из предыдущих примеров посчитает пятидесятый член последовательности Фибоначчи быстрее, чем вы успеете произнести слово «пятьдесят» или любой его слог.
Выражаясь грубым языком O-нотации, такое решение имеет временную сложность O(e n ). То есть — время выполнения этой функции растёт экспоненциально при увеличении n. То есть — когда n увеличивается на, время выполнения увеличивается в. Грубо говоря, если fib(45) вам пришлось ждать час, то fib(46) вы будете ждать два часа, fib(47) — 4 часа, и так далее. Я разжёвываю так подробно, чтобы каждый читатель, даже верстальщик, впервые попробовавший свои силы в написании скриптов, мог осознать ужас ситуации.
Это правильно, но слишком грубо. Можно получить более точную оценку числа вызов функции
(1+sqrt(5)) fib(n) и красивое замечание «Для вычисления числа Фибонначи наивным рекуррентным методом понадобится вызовов функции в 3.2 раза больше чем само число Фибонначи». Taus
И мы получаем ещё один метод его вычисления. Надо просто запустить наивный рекурректный метод, подсчитать количество вызовов функции и разделить на 3.2! Cerberuser
Если от вас на собеседовании потребуют рекурсивного решения этой задачи, скорее всего, это ловушка. «Правильная» рекурсия, работающая за линейное время, может выглядеть, например, так:
Подводя итог: несмотря на то, что числа Фибоначчи являются классическим, хрестоматийным примером рекурсии, в действительности это не самый удобный случай для применения рекурсии. Но можно попробовать блеснуть ещё какими-нибудь знаниями.
3. Мемная функция
Существует волшебный способ, превращающий чудовищно неэффективное решение из прошлого параграфа в потенциально очень быстрое (хотя и не лишённое проблем). Имя ему — мемоизация. А если говорить по-русски — мы просто запоминаем результаты предыдущих вызовов вместо того, чтобы вычислять их заново.
В принципе, мы можем даже ничего не менять внутри того решения — просто добавить функцию-обёртку memoize . Здесь я для наглядности использую её упрощённую версию для функции с единственным аргументом.
Вуаля! Теперь функция fib имеет через замыкание доступ к объекту cache . Если её вызывают с аргументом, который ранее не встречался, вычисленное значение сохраняется в cache . При новых вызовах функции с тем же аргументом значение не придётся вычислять заново, оно будет просто взято из кэша. Основная проблема «плохой» старой функции fib была в том, что одни и те же значения в ней вычислялись заново несколько раз. Например, для вычисления fib(45) нужно было один раз вычислить f(44) , два раза — f(43) , три раза — f(42) , пять раз — f(41) , и так далее.
Так вот, теперь предыдущие значения будут вычисляться по одному разу, а при их повторном запросе — просто браться из кэша. Представляете, во сколько раз быстрее мы сможем теперь вычислить сорок пятое число Фибоначчи? Серьёзно, как вы думаете, во сколько?
На самом деле — чуть-чуть медленнее. Я сознательно допустил классическую ошибку, часто совершаемую при мемоизации рекурсивных функций. При вызове fib(45) «под капотом» вызывается oldFib(45) , которая для своих нужд вызывает oldFib(44) и oldFib(43) … Вы чувствуете подвох? Здесь и далее идут уже вызовы обычной, не мемоизированной функции. Конечно, при повторном вызове fib(45) мы мгновенно получим результат из кэша — однако первый вызов ничуть не ускорился. Чтобы это поправить, придётся всё-таки влезть oldFib под днище с гаечным ключом:
Замечательно! Теперь первый вызов fib(45) отработает со скоростью, сравнимой с версией с циклом. А дальнейшие вызовы вообще сработают за константное время… Оп! Опять обманул. Получение значения свойства объекта по ключу — это операция быстрая, но всё-таки O(1) только в среднем, в худшем случае она может деградировать до O(n). Чтобы стало совсем хорошо, в нашем случае мы можем сменить тип cache с объекта на массив.
Разумеется, не стоит также забывать, что мемоизация требует памяти. И пока мы уменьшаем сложность по времени, сложность по памяти растёт с O(1) до O(n).
Как ещё мы можем выпендриться? Например, продемонстрировав своё глубокое знание математики
4. Мистер Бине
Существует особая прекрасная наука о том, как рекуррентные соотношения превращать в явные формулы. Здесь мы не будем вдаваться в её детали. Скажем лишь, что для чисел Фибоначчи с помощью достаточно несложных рассуждений можно вывести следующую формулу, известную как формула Бине:
Однако довольно языка математики, запишем это на языке JavaScript:
Прогоним на первых нескольких числах. Замечательно, кажется, всё работает. Вот 13, вот 21, вот 34, вот… 54.99999999999999?
Да, разумеется, такой результат закономерен. Формула Бине точна математически, но компьютер оперирует дробями конечной точности, и при действиях над ними может накопиться ошибка, что и произошло в данном случае. Однако мы можем всё исправить. Зная, что вычитаемое в числителе всегда будет маленьким по модулю, мы можем упростить формулу до следующего состояния:
Здесь странные недоделанные квадратные скобки означают ближайшее целое число, то есть — округление. Перепишем наш код:
Да, так гораздо лучше. Мы сможем увидеть и 55, и 89, и даже моё любимое число Фибоначчи — 144 (которое я люблю за то, что оно равняется двенадцати в квадрате). Всё будет хорошо до числа за номером 76. Которое должно быть равно 3416454622906707, а наша функция вычислит 3416454622906706. Потому что проблема ограниченной точности дробных чисел никуда не делась, мы просто затолкали её поглубже и надеялись, что она не всплывёт. Как показывает данный пример — надеялись напрасно.
На самом деле мы можем сделать ещё кое-что, чтобы спасти этот метод. Но об этом ниже. А пока — шутки в сторону. Поговорим о методе суровом, хардкорном и брутальном.
5. Следуй за белым кроликом.
Говорят, если у вас есть проблема и вам пришла в голову идея, что можно решить её с помощью регулярных выражений, то теперь у вас две проблемы. Матрицы — это регулярные выражения наоборот. Многие проблемы, если их переформулировать на языке матриц, решаются просто сами собой.
Что касается чисел Фибоначчи, для них на матричном языке можно записать вот такое очевидное тождество:
То есть если взять пару подряд идущих чисел Фибоначчи и умножить их на такую вот незамысловатую матрицу, мы получим следующую пару. А отсюда логично следует вывод: если мы возьмём пару из нулевого и первого числа Фибоначчи, то есть нуля и единицы, и умножим их на эту матрицу в энной степени, мы получим пару из энного и эн плюс первого числа Фибоначчи. То есть, говоря по-человечески:
Можно это ещё немного упростить, отказавшись от векторов. На самом деле все необходимые значения содержатся в самой матрице:
Замечательно, не правда ли? Осталось понять, нафига попу гармонь, если он не филармонь. В смысле — зачем такие сложности на ровном месте. А ответ прост — быстрое возведение в степень.
Сколько нужно выполнить элементарных умножений, чтобы вычислить, скажем, 2 10 ? Нормальный человек скажет, что девять. Дважды два — четыре. Дважды четыре — восемь. Дважды восемь — шестнадцать. И так далее. Хитрый человек скажет, что четыре.
Программист скажет. что он это число помнит наизусть, и ничего умножать не нужно. Однако вопросы мемоизации мы рассмотрели выше.
Так вот, быстрое возведение в степень применимо и к матрицам, и таким образом позволяет уменьшить асимптотическую временную сложность нашей функции с O(n) до O(log n). И это очень круто — если, конечно, нам действительно так важна эта сложность. Давайте напишем код:
Вот мы и получили самый быстрый алгоритм на Диком Западе. И его, в отличие от большинства предыдущих, можно неиронично продемонстрировать на собеседовании. А в каких-нибудь математико-ёмких местах именно его от вас и будут ждать.
Я обещал ремарку относительно того, как же нам спасти метод, основанный на формуле Бине. Ответ кроется вот в этой моей статье. Там я для нужд народного хозяйства написал специальный класс корень-из-пяти-рациональных чисел, которые могут без потери точности хранить результаты арифметических действий над целыми числами и корнем из пяти. Можно взять этот класс, дополнить его методом округления и использовать для поиска чисел Фибоначчи по формуле Бине. А затем впрыснуть закись азота, применив быстрое возведение в степень.
И что самое интересное: если внимательно посмотреть, какие числа будут получаться в процессе, какие будут выполняться операции, то станет понятно, что этот метод — это то же самое матричное умножение, только под другим фасадом. Разница лишь в том, храним мы числа в двумерных массивах или в полях объекта специального класса.
На этом всё. Если вы считаете, что я упустил ещё какие-то интересные способы найти никому не нужные числа, обязательно напишите об этом в комментариях.
Есть ещё такой способ как fast doubling. Работает как и матричное умножение за O(log), но с меньшей константой в асимптотике (и на практике). Если кратко, то там используется две формулы, опираясь на которые можно быстро рекурсивно откатываться к вдвое меньшим индексам:
Реализация, кстати, получается довольно компактная.
Источник