Вычислительная сложность последовательности Фибоначчи

Я понимаю запись Big-O, но я не знаю, как вычислить ее для многих функций. В частности, я пытался выяснить вычислительную сложность наивной версии последовательности Фибоначчи:

int Fibonacci(int n)
{
    if (n <= 1)
        return n;
    else
        return Fibonacci(n - 1) + Fibonacci(n - 2);
}

Какова вычислительная сложность последовательности Фибоначчи и как она рассчитывается?

Ответ 1

Вы моделируете функцию времени, чтобы вычислить Fib(n) как сумму времени, чтобы вычислить Fib(n-1) плюс время, чтобы вычислить Fib(n-2) плюс время, чтобы сложить их вместе (O(1)). Это предполагает, что повторные оценки одного и того же Fib(n) занимают одно и то же время, то есть не используется запоминание.

T(n<=1) = O(1)

T(n) = T(n-1) + T(n-2) + O(1)

Вы решаете это рекуррентное отношение (например, с помощью генерирующих функций), и в итоге получите ответ.

Кроме того, вы можете нарисовать дерево рекурсии, которое будет иметь глубину n и интуитивно понять, что эта функция асимптотически O(2 n). Затем вы можете доказать свою гипотезу по индукции.

Основание: n = 1 очевидно

Предположим, что T(n-1) = O(2 n-1), поэтому

T(n) = T(n-1) + T(n-2) + O(1) что равно

T(n) = O(2 n-1) + O(2 n-2) + O(1) = O(2 n)

Однако, как отмечено в комментарии, это не жесткая граница. Интересный факт об этой функции заключается в том, что T (n) асимптотически совпадает со значением Fib(n) поскольку оба определены как

f(n) = f(n-1) + f(n-2).

Листья дерева рекурсии всегда будут возвращать 1. Значение Fib(n) - это сумма всех значений, возвращаемых листьями в дереве рекурсии, которая равна количеству листьев. Поскольку для вычисления каждого листа потребуется O (1), T(n) равно Fib(n) x O(1). Следовательно, жесткой границей для этой функции является сама последовательность Фибоначчи (~ θ(1.6 n)). Вы можете узнать эту жесткую границу, используя генерирующие функции, как я уже упоминал выше.

Ответ 2

Просто спросите себя, сколько операторов нужно выполнить для F(n) для завершения.

Для F(1) ответ 1 (первая часть условного выражения).

Для F(n) ответ F(n-1) + F(n-2).

Итак, какая функция удовлетворяет этим правилам? Попробуйте n (a > 1):

a n == a (n-1) + a (n-2)

Разделить на (n-2):

a 2 == a + 1

Решите для a, и вы получите (1+sqrt(5))/2 = 1.6180339887, иначе известный как

Ответ 3

Там очень хорошее обсуждение этой конкретной проблемы в MIT. На странице 5 они указывают, что если вы считаете, что добавление занимает одну вычислительную единицу, время, необходимое для вычисления Fib (N), очень тесно связано с результатом Fib (N).

В результате вы можете перейти непосредственно к очень близкому приближению серии Фибоначчи:

Fib(N) = (1/sqrt(5)) * 1.618^(N+1) (approximately)

и скажем, что наихудшая производительность наивного алгоритма

O((1/sqrt(5)) * 1.618^(N+1)) = O(1.618^(N+1))

PS: В Wikipedia обсуждается закрытое выражение формы номера Nth Fibonacci, если вы хотите получить дополнительную информацию.

Ответ 4

Я согласен с pgaur и rickerbh, сложность рекурсивного-фибоначчи - O (2 ^ n).

Я пришел к одному и тому же выводу довольно упрощенным, но я считаю, что все еще обоснованные рассуждения.

Во-первых, все это из-за выяснения того, сколько раз рекурсивная функция фибоначчи (F() с этого момента) вызывается при вычислении N-го числа фибоначчи. Если он вызывается один раз за число в последовательности от 0 до n, то мы имеем O (n), если он получает n раз для каждого числа, тогда мы получаем O (n * n) или O (n ^ 2), и т.д.

Итак, когда F() вызывается для числа n, число раз F() вызывается для заданного числа между 0 и n-1 растет по мере приближения к 0.

В качестве первого впечатления мне кажется, что, если мы выразим его визуальным образом, для того, чтобы нарисовать единицу за время F(), нужно набрать определенное количество пирамиды (то есть, если мы центральных единиц по горизонтали). Что-то вроде этого:

n              *
n-1            **
n-2           ****  
...
2           ***********
1       ******************
0    ***************************

Теперь возникает вопрос: насколько быстро растет основание этой пирамиды с ростом n?

Возьмем вещественный случай, например F (6)

F(6)                 *  <-- only once
F(5)                 *  <-- only once too
F(4)                 ** 
F(3)                ****
F(2)              ********
F(1)          ****************           <-- 16
F(0)  ********************************    <-- 32

Мы видим, что F (0) называется 32 раз, что равно 2 ^ 5, что для этого примера является 2 ^ (n-1).

Теперь мы хотим узнать, сколько раз F (x) вообще вызвано, и мы можем видеть, сколько раз вызывается F (0), является лишь частью этого.

Если мы мысленно переместим все строки * из F (6) в F (2) в прямую F (1), мы увидим, что линии F (1) и F (0) теперь равны по длине. Это означает, что общее время F() вызывается, когда n = 6 равно 2x32 = 64 = 2 ^ 6.

Теперь, с точки зрения сложности:

O( F(6) ) = O(2^6)
O( F(n) ) = O(2^n)

Ответ 5

Вы можете развернуть его и получить визуализацию

     T(n) = T(n-1) + T(n-2) <
     T(n-1) + T(n-1) 

     = 2*T(n-1)   
     = 2*2*T(n-2)
     = 2*2*2*T(n-3)
     ....
     = 2^i*T(n-i)
     ...
     ==> O(2^n)

Ответ 6

Он ограничен в нижнем конце на 2^(n/2), а на верхнем конце - на 2 ^ n (как отмечалось в других комментариях). И интересным фактом этой рекурсивной реализации является то, что она имеет жесткую асимптотическую оценку самого Fib (n). Эти факты можно резюмировать:

T(n) = Ω(2^(n/2))  (lower bound)
T(n) = O(2^n)   (upper bound)
T(n) = Θ(Fib(n)) (tight bound)

Тесную границу можно уменьшить, используя закрытую форму, если хотите.

Ответ 7

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

IN | OUT | TOT | LEAF | INT
 1 |   1 |   1 |   1  |   0
 2 |   1 |   1 |   1  |   0
 3 |   2 |   3 |   2  |   1
 4 |   3 |   5 |   3  |   2
 5 |   5 |   9 |   5  |   4
 6 |   8 |  15 |   8  |   7
 7 |  13 |  25 |  13  |  12
 8 |  21 |  41 |  21  |  20
 9 |  34 |  67 |  34  |  33
10 |  55 | 109 |  55  |  54

Что сразу же происходит, так это то, что число листовых узлов fib(n). Еще несколько итераций заметили, что количество внутренних узлов fib(n) - 1. Поэтому общее число узлов 2 * fib(n) - 1.

Поскольку вы отбрасываете коэффициенты при классификации сложности вычислений, окончательный ответ θ(fib(n)).

Ответ 8

Временная сложность рекурсивного алгоритма может быть лучше оценена путем рисования дерева рекурсии. В этом случае отношение рекурсии для дерева рекурсии рисунка будет иметь вид T (n) = T (n-1) +T (n-2) +O (1) обратите внимание, что каждый шаг занимает O (1), что означает постоянное время, так как он выполняет только одно сравнение, чтобы проверить значение n в случае, если дерево block.Recursion будет выглядеть

          n
   (n-1)      (n-2)
(n-2)(n-3) (n-3)(n-4) ...so on

Здесь, скажем, каждый уровень выше дерева обозначается i, следовательно,

i
0                        n
1            (n-1)                 (n-2)
2        (n-2)    (n-3)      (n-3)     (n-4)
3   (n-3)(n-4) (n-4)(n-5) (n-4)(n-5) (n-5)(n-6)

допустим, при конкретном значении я дерево заканчивается, этот случай будет, когда ni = 1, следовательно, я = n-1, что означает, что высота дерева равна n-1. Теперь давайте посмотрим, сколько работы проделано для каждого из n слоев дерева. Обратите внимание, что каждый шаг занимает O (1) время, как указано в отношении повторяемости.

2^0=1                        n
2^1=2            (n-1)                 (n-2)
2^2=4        (n-2)    (n-3)      (n-3)     (n-4)
2^3=8   (n-3)(n-4) (n-4)(n-5) (n-4)(n-5) (n-5)(n-6)    ..so on
2^i for ith level

поскольку я = n-1 - высота дерева, работа на каждом уровне будет

i work
1 2^1
2 2^2
3 2^3..so on

Следовательно, общая проделанная работа будет суммой проделанной работы на каждом уровне, следовательно, это будет 2 ^ 0 +2 ^ 1 +2 ^ 2 +2 ^ 3... +2 ^ (n-1), так как я = п-1. По геометрическим рядам эта сумма равна 2 ^ n, следовательно, общая сложность времени здесь равна O (2 ^ n).

Ответ 9

Ну, по мне, это O(2^n), так как в этой функции только рекурсия занимает значительное время (разделить и победить). Мы видим, что вышеуказанная функция будет продолжаться в дереве до тех пор, пока листья не приблизится, когда мы достигнем уровня F(n-(n-1)) i.e. F(1). Итак, здесь, когда мы записываем сложность времени, встречающуюся на каждой глубине дерева, серия суммирования:

1+2+4+.......(n-1)
= 1((2^n)-1)/(2-1)
=2^n -1

то есть порядок 2^n [ O(2^n) ].

Ответ 11

Наивная рекурсивная версия Фибоначчи экспоненциальна по дизайну из-за повторения в вычислении:

В корне, который вы вычисляете:

F (n) зависит от F (n-1) и F (n-2)

F (n-1) снова зависит от F (n-2) и F (n-3)

F (n-2) снова зависит от F (n-3) и F (n-4)

то у вас на каждом уровне 2 рекурсивных вызова, которые тратят много данных при вычислении, функция времени будет выглядеть так:

T (n) = T (n-1) + T (n-2) + C, с константой C

T (n-1) = T (n-2) + T (n-3) > T (n-2), то

T (n) > 2 * T (n-2)

...

T (n) > 2 ^ (n/2) * T (1) = O (2 ^ (n/2))

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

Кроме того, вы можете найти оптимизированные версии Fibonacci, используя динамическое программирование следующим образом:

static int fib(int n)
{
    /* memory */
    int f[] = new int[n+1];
    int i;

    /* Init */
    f[0] = 0;
    f[1] = 1;

    /* Fill */
    for (i = 2; i <= n; i++)
    {
        f[i] = f[i-1] + f[i-2];
    }

    return f[n];
}

Это оптимизировано и выполняется только n, но также экспоненциально.

Функции затрат определяются от размера ввода до количества шагов для решения проблемы. Когда вы видите динамическую версию шагов Fibonacci ( n для вычисления таблицы) или самый простой алгоритм, чтобы узнать, является ли число простым ( sqrt (n) для анализа действительного делителей числа). вы можете подумать, что эти алгоритмы O (n) или O (sqrt (n)), но это просто неверно по следующей причине: Вход в ваш алгоритм - это число: n, используя двоичную нотацию, размер ввода для целого числа n: log2 (n), затем выполните переменное изменение

m = log2(n) // your real input size

позволяет узнать количество шагов в зависимости от размера ввода

m = log2(n)
2^m = 2^log2(n) = n

то стоимость вашего алгоритма в зависимости от размера ввода:

T(m) = n steps = 2^m steps

и поэтому стоимость является экспоненциальной.

Ответ 12

Это лучше работает:

unsigned int Fib(unsigned int n)
{
    // first Fibonaci number is Fib(0)
    // second one is Fib(1) and so on

    // unsigned int m;  // m + current_n = original_n
    unsigned int a = 1; // Fib(m)
    unsigned int b = 0; // Fib(m-1)
    unsigned int c = 0; // Fib(m-2)

    while (n--)
    {
        c = b;
        b = a;
        a = b+c;
    }

    return a;
}