Ряд Фибоначчи

Чи́сла Фибона́ччи (вариант написания — Фибона́чи[2]) — элементы числовой последовательности

0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946, 17711, … (последовательность A000045 в OEIS),

в которой первые два числа равны 0 и 1, а каждое последующее число равно сумме двух предыдущих чисел[3]. Названы в честь средневекового математика Леонардо Пизанского (известного как Фибоначчи)[4].

- Википедия

Ряд Фибоначчи
Ряд Фибоначчи
Ряд Фибоначчи в природе
Ряд Фибоначчи в природе

Ряд Фибоначчи часто упоминается на собеседованиях, потому что в нем демонстрируется множество мощных методов, включая рекурсию. Он является отличным примером того, что мы называем мемоизацией(запоминанием). Понимание ряда Фибоначчи и его работы очень полезно.
Математически ряд Фибоначчи представляет собой последовательность чисел, которые следуют этому уравнению: F(n) = F(n-1) + F(n-2). Эта последовательность встречается в различных интересных контекстах, например, в природе, в раковинах, спиральных структурах и галактиках, а также в дизайне римских полов, и даже брокеры используют ряд Фибоначчи для прогнозирования акций.

func fib(_ n: Int) -> Int {
    if n == 0 {
        return 0
    } else if n == 1 {
        return 1
    } else {
        return fib(n - 1) + fib(n - 2)
    }
}

В коде серия Фибоначчи выглядит следующим образом: для F(0) и F(1) возвращаем их значения напрямую, а затем алгоритм сводится к этой строке - return fib(n-1) + fib(n-2). Это является ключевым моментом алгоритма и демонстрирует использование рекурсии.
Однако у ряда Фибоначчи есть проблема: с увеличением чисел в последовательности алгоритм требует все больше времени для вычисления. Это связано с тем, что время, необходимое для вычислений, растет экспоненциально, и сложность алгоритма составляет O(2^n). Это делает вычисления очень затратными.
Наша задача - найти способ оптимизировать этот алгоритм и сделать его быстрее. Именно здесь на помощь приходит мощная техника, известная как мемоизация, которую важно знать и понимать.

Мемоизация

Чтобы понять мемоизацию, нужно рассмотреть проблему с рядом Фибоначчи. Для простых чисел, таких как число до F(5), ряд Фибоначчи вычисляется достаточно быстро. Однако, при вычислении чисел более высокого порядка, повторное вычисление чисел становится очень затратным по времени.

Мемоизация - это техника оптимизации, ускоряющая алгоритмы за счет кэширования или хранения результатов вычислений для их использования в будущих вычислениях. В случае ряда Фибоначчи, мы можем создать словарь (назовем его "Memo") для хранения ранее вычисленных чисел. Затем, когда мы вычисляем каждое число ряда Фибоначчи, мы сохраняем результат в массиве и возвращаем его. Таким образом, в следующий раз нам не нужно вычислять это число снова и снова.

var memo = [Int: Int]()

func fib(_ n: Int) -> Int {
    if n == 0 { return 0 }
    else if n == 1 { return 1 }
  
    if let result = memo[n] { return result }
  
    memo[n] = fib(n - 1) + fib(n - 2)
    return memo [n]!
}

Благодаря мемоизации, эффективность алгоритма возрастает значительно. Теперь вместо того, чтобы за 19 секунд вычислить ряд Фибоначчи из 30 чисел, мы можем вычислить тысячи чисел. Это увеличение на 3333%. Мы превратили алгоритм из экспоненциального (O(2^n)) в линейный (O(n)).

Именно поэтому ряд Фибоначчи и мемоизация являются отличными примерами для интервью. Они объединяют рекурсию и мемоизацию, показывая, как сделать затратный алгоритм быстрее. Знание и понимание ряда Фибоначчи и мемоизации помогут вам решать подобные задачи быстро и эффективно.

import UIKit

func fibNaive(_ n: Int) -> Int {
    print(n)
    if n == 0 {
        return 0
    } else if n == 1 {
        return 1
    } else {
        return fibNaive(n - 1) + fibNaive(n - 2)
    }
}

fibNaive(20) // 20 = 13s / 22 = 54 s
var memo = [Int: Int]()

func fib(_ n: Int) -> Int {
    if n == 0 { return 0}
    else if n == 1 { return 1 }

    if let result = memo[n] { return result }

    memo[n] = fib(n - 1) + fib(n - 2)

    return memo[n]!
}

fib(22) // 90 max

Что касается создания одного из них с нуля, то это очень просто. Представляю к вашему вниманию две реализации: одну наивную, которая ничего не запоминает и не хранит, и вторую, где мы используем мемоизацию и храним наши результаты по мере их вычисления.

На мгновение остановимся на мемоизированном варианте, чтобы показать, сколько времени требуется для выполнения очень маленького вычисления Фибоначчи для числа 20. Если мы запустим его для простого числа, например, 20, вы увидите, что счетчик работает, пересчитывая одни и те же числа снова и снова. В среднем это занимает около 13 секунд, что не так уж и много. Теперь, если я увеличу его на один или два, это займет еще больше времени. Это экспоненциальный рост, увеличивающийся в размерах, и для очень маленьких приращений числа требуется все больше и больше времени. Именно так работает ряд Фибоначчи, и именно поэтому он так затратен.

Теперь давайте сравним это с мемоизированной версией и посмотрим, каково это - хранить эти значения. Во втором случае мы будем хранить наши результаты в словаре. Каждый раз, когда мы вычисляем F(n-1) + F(n-2), мы будем хранить его в ключе, представленном N, каким бы ни было число в этот момент (20, 21, 22 и т. д.). Мы просто сохраним этот результат. Затем, по мере выполнения последовательных вычислений, если мы можем извлечь результат из словаря, мы просто вернем его, не вычисляя его снова.

Заключение:

Это удобно не только для рядов Фибоначчи, но и для всего, что связано с дорогостоящими вычислениями, которые можно сохранить, кэшировать и использовать в будущих результатах.

В этом и заключается сила мемоизации.

Она используется для самых разных вещей.

Ряд Фибоначчи - отличный пример. Когда дело доходит до реального интервью.

Я слышал, как людей просили воспроизвести ряд Фибоначчи.

Это не огромный алгоритм, он сводится к одной строчке и двум случаям: конец равен нулю и единице. Но мой совет - просто запомните эту строчку, потому что она демонстрирует такую вещь как рекурсия.

Как только вы это поймете, очень просто перейти к мемоизированному примеру, где вы сможете продемонстрировать как взять трудозатратный алгоритм и сделать его более эффективным. Вы знаете - это называется мемоизацией.

Комментарии (11)


  1. vadimr
    23.04.2023 19:01
    -1

    Это всё хорошо как объяснение техники мемоизации на некотором примере, но эффективная рекурсивная реализация вычисления чисел Фибоначчи не требует ни экспоненциальной рекурсии, ни мемоизации. Алгоритм содержит один хвостовой рекурсивный вызов, не использует память для временных переменных и, соответственно, не требует в ней поиска:

    (defun fib1 (n s1 s2)
       (cond
          ((eq n 0) 0) 
          ((eq n 1) 1) 
          ((eq n 2) (+ s1 s2))
          (t (fib1 (- n 1) s2 (+ s1 s2)))))
    
    (defun fib (n)
       (fib1 n 0 1))

    Я бы лично не очень порадовался ответу человека, который решил бы написать эффективное вычисление ряда Фибоначчи через мемоизацию. Это своего рода горе от ума.


    1. Dinozavr2005 Автор
      23.04.2023 19:01

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

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


    1. Zenitchik
      23.04.2023 19:01
      +3

      Если мне память не изменяет, число Фибоначчи можно вычислить через матрицу и быстрое возведение в степень. Без вычисления всех предыдущих членов.


      1. Dinozavr2005 Автор
        23.04.2023 19:01

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


  1. Pastoral
    23.04.2023 19:01
    -2

    За такие рекурсию и мемоизацию я бы попросил охрану завершить собеседование в стиле обижать но не калечить. Почему ряд Фибоначчи не преподают как пример неверного применения рекурсии - не знаю.


    1. Dinozavr2005 Автор
      23.04.2023 19:01

      Спасибо за ваше мнение. Вы правильно отмечаете, что есть более эффективные подходы к вычислению чисел Фибоначчи, такие как хвостовая рекурсия или итеративные методы. Рекурсия и мемоизация могут быть не самыми оптимальными подходами в этом случае, но они всё же могут быть полезными для демонстрации основных концепций и возможностей в других задачах.

      Цель статьи была продемонстрировать технику мемоизации на примере ряда Фибоначчи, хотя я понимаю, что это может быть не лучшим примером. Важно помнить, что хотя мемоизация не является оптимальным решением для вычисления чисел Фибоначчи, она может быть очень полезной в других ситуациях, где экспоненциальная рекурсия является неизбежной, а хвостовая рекурсия или итеративные методы не подходят.


  1. ednersky
    23.04.2023 19:01

    в реальной жизни — числа Фибоначчи являются ярчайшим примером или эталоном "не нужно".


    То есть на собеседованиях, действительно, часто спрашивают про этот ряд. Вот только с числами Фибоначчи программист сталкивается крайне редко. И даже тогда, когда сталкивается — то чаще всего в каком-нибудь легаси коде, куда этот ряд кто-то затащил не в силу необходимости, а потому что хотелось, наконец, куда-то его затащить.


    Как-то так, имхо.


    1. Dinozavr2005 Автор
      23.04.2023 19:01

      Спасибо за ваше мнение! Вы правы в том, что числа Фибоначчи могут быть редко применимы в реальных задачах программирования. Однако, причина, по которой числа Фибоначчи часто упоминаются на собеседованиях, заключается в том, что они служат хорошим примером для проверки понимания кандидатом базовых концепций, таких как рекурсия, динамическое программирование и мемоизация. Цель моей статьи была в демонстрации техники мемоизации на примере, который знаком многим программистам. Хотя числа Фибоначчи могут быть не самым распространенным примером в реальной жизни, их использование в данном контексте помогает быстро объяснить и продемонстрировать работу мемоизации.


      1. ednersky
        23.04.2023 19:01
        +1

        Вы отвечаете так, будто, статья — это программа в play.store, а комменты пользователей — отзывы, на которые выложивший программу обязан ответить


        :)


        PS: я вообще о чём. О том, что и проверять знание важных концепций стоит на более реалистичных примерах/задачах. Если ничего кроме фибоначчи для рекурсии в голову не лезет — ну и не нужная значит это концепция.


        За многие годы программирования я, по моему, с рекурсией сталкивался ну может быть раза два. И, думаю, ткни в любого программиста — у него будет то же самое.


        1. Dinozavr2005 Автор
          23.04.2023 19:01
          +1

          Я думал будет не культурно не отвечать на комментарии)

          В этом замечании, скорее всего, вы правы по всем пунктам. Остается только надеятся, что кому-то, всё-таки, пригодится!


        1. vadimr
          23.04.2023 19:01

          Это дело привычки и используемого языка. В некоторых языках рекурсия является основным или даже единственным инструментом управления вычислительным процессом (из небанальных примеров назову DB2 SQL; хотя не буду утверждать, что мне доводилось коротать время за написанием циклических алгоритмов на SQL). В целом рекурсию частенько доводилось использовать.