Complessità computazionale della sequenza di Fibonacci


330

Capisco la notazione Big-O, ma non so come calcolarla per molte funzioni. In particolare, ho cercato di capire la complessità computazionale della versione ingenua della sequenza di Fibonacci:

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

Qual è la complessità computazionale della sequenza di Fibonacci e come viene calcolata?



3
Vedi la sezione del modulo matrice qui: en.wikipedia.org/wiki/Fibonacci_number . facendo questa matrice ^ n (in modo intelligente) puoi calcolare Fib (n) in O (lg n). Il trucco sta nel fare la funzione di potere. C'è un'ottima lezione su iTunesU su questo esatto problema e su come risolverlo in O (lg n). Il corso è un'introduzione agli algoritmi della lezione 3 del MIT (è assolutamente gratuito, quindi dai un'occhiata se sei interessato)
Aly,

1
Nessuno dei suddetti commenti affronta la domanda, che riguarda la complessità computazionale della versione ingenua (nel codice pubblicato), non sulle versioni più intelligenti come la forma a matrice o il calcolo non ricorsivo.
Josh Milthorpe,

Un video molto bello qui che parla sia della complessità del limite inferiore (2 ^ n / 2) che della complessità del limite superiore (2 ^ n) dell'implementazione ricorsiva.
RBT,

1
Una domanda a margine: l' implementazione ingenua della serie Fibonacci dovrebbe essere iterativa o ricorsiva ?
RBT

Risposte:


374

Modella la funzione tempo da calcolare Fib(n)come somma del tempo da calcolare Fib(n-1)più il tempo da calcolare Fib(n-2)più il tempo per sommarli ( O(1)). Ciò presuppone che valutazioni ripetute dello stesso Fib(n)richiedano lo stesso tempo, ovvero che non venga utilizzata alcuna memoizzazione.

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

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

Risolvi questa relazione di ricorrenza (usando le funzioni di generazione, ad esempio) e finirai con la risposta.

In alternativa, puoi disegnare l'albero di ricorsione, che avrà profondità ne capirà intuitivamente che questa funzione è asintotica . Puoi quindi provare la tua congettura per induzione.O(2n)

Base: n = 1è ovvio

Supponiamo , quindiT(n-1) = O(2n-1)

T(n) = T(n-1) + T(n-2) + O(1) che è uguale a

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

Tuttavia, come notato in un commento, questo non è un limite. Un fatto interessante riguardo a questa funzione è che T (n) è asintoticamente uguale al valore di Fib(n)poiché entrambi sono definiti come

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

Le foglie dell'albero di ricorsione restituiranno sempre 1. Il valore di Fib(n)è la somma di tutti i valori restituiti dalle foglie nell'albero di ricorsione che è uguale al conteggio delle foglie. Poiché ogni foglia richiederà O (1) per il calcolo, T(n)è uguale a Fib(n) x O(1). Di conseguenza, il limite stretto per questa funzione è la sequenza di Fibonacci stessa (~ ). Puoi scoprire questo stretto limite usando le funzioni di generazione come ho menzionato sopra.θ(1.6n)


29
Anche prova per induzione. Bello. +1
Andrew Rollings,

Sebbene il limite non sia stretto.
Capitano Segfault,

@Captain Segfault: Sì. Ho chiarito la risposta. Otterresti il ​​limite usando il metodo GF come avevo scritto sopra.
Mehrdad Afshari,

Prendo il tuo StackOverflowException come uno scherzo. Il tempo esponenziale è percepibile abbastanza facilmente con valori piuttosto piccoli per n.
David Rodríguez - dribeas,

1
"In alternativa, puoi disegnare l'albero di ricorsione, che avrà profondità n e capire intuitivamente che questa funzione è asintoticamente O (2n)." - Questo è completamente falso. La complessità temporale è O (golden_ratio ^ n). Non si avvicina mai a O (2 ^ n). Se potessi raggiungere l'infinito si avvicinerebbe a O (golden_ratio ^ n). Questo è quello che è un asintoto, la distanza tra le due linee deve avvicinarsi a 0.
bob

133

Chiediti quante istruzioni devi eseguire per F(n)completare.

Perché F(1), la risposta è 1(la prima parte del condizionale).

Perché F(n)la risposta è F(n-1) + F(n-2).

Quindi quale funzione soddisfa queste regole? Prova a n (a> 1):

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

Dividi per un (n-2) :

a 2 == a + 1

Risolvi ae ottieni (1+sqrt(5))/2 = 1.6180339887, altrimenti noto come rapporto aureo .

Quindi ci vuole tempo esponenziale.


8
Prova per induzione. Bello. +1
Andrew Rollings,

2
30 voti positivi per una risposta sbagliata? :-) Segue che 1 = F (1) = (1 + sqrt (5)) / 2? E l'altra soluzione, (1 sqrt (5)) / 2?
Carsten S,

1
No, 1 non è uguale a 1 + 1. La funzione che soddisfa tali regole è menzionata nella domanda.
molbdnilo,

6
La risposta non è sbagliata È giusto in modo asintomatico. L'altra soluzione è negativa, quindi non ha senso fisicamente.
Da Teng,

10
Qualcuno può spiegare come a ^ n == a ^ (n-1) + a ^ (n-2) soddisfano queste regole? Come è soddisfatto esattamente, si prega di essere specifici.
franco

33

Concordo con pgaur e rickerbh, la complessità del fibonacci ricorsivo è O (2 ^ n).

Sono giunto alla stessa conclusione con un ragionamento piuttosto semplicistico, ma credo che sia ancora valido il ragionamento.

Innanzitutto, si tratta di capire quante volte la funzione fibonacci ricorsiva (F () da ora in poi) viene chiamata quando si calcola l'ennesimo numero di fibonacci. Se viene chiamato una volta per numero nella sequenza da 0 a n, allora abbiamo O (n), se viene chiamato n volte per ogni numero, allora otteniamo O (n * n) o O (n ^ 2), e così via.

Quindi, quando F () viene chiamato per un numero n, il numero di volte in cui F () viene chiamato per un dato numero tra 0 e n-1 aumenta man mano che ci avviciniamo a 0.

Come prima impressione, mi sembra che se la mettiamo in modo visivo, disegnando un'unità per volta in cui F () viene chiamato per un dato numero, bagniamo una sorta di forma piramidale (cioè se centriamo le unità in orizzontale ). Qualcosa come questo:

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

Ora, la domanda è: quanto velocemente si allarga la base di questa piramide man mano che n cresce?

Prendiamo un caso reale, ad esempio F (6)

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

Vediamo che F (0) viene chiamato 32 volte, che è 2 ^ 5, che per questo caso di esempio è 2 ^ (n-1).

Ora, vogliamo sapere quante volte viene chiamato F (x), e possiamo vedere il numero di volte che viene chiamato F (0) è solo una parte di ciò.

Se spostiamo mentalmente tutte le * da F (6) a F (2) linee in F (1), vediamo che le linee F (1) e F (0) sono ora uguali in lunghezza. Ciò significa che viene chiamato il totale dei tempi F () quando n = 6 è 2x32 = 64 = 2 ^ 6.

Ora, in termini di complessità:

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

3
F (3) viene chiamato solo 3 volte e non 4 volte. La seconda piramide è sbagliata.
Avik,

2
F (3) = 3, F (2) = 5, F (1) = 8, F (0) = 5. Lo riparerei, ma non credo di poter salvare questa risposta con una modifica.
Bernhard Barker,

31

C'è una bella discussione su questo problema specifico al MIT . A pagina 5, sottolineano che, se si assume che un'aggiunta richieda un'unità computazionale, il tempo necessario per calcolare Fib (N) è strettamente correlato al risultato di Fib (N).

Di conseguenza, puoi saltare direttamente all'approssimazione molto ravvicinata della serie Fibonacci:

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

e dire, quindi, che la peggiore delle prestazioni dell'algoritmo ingenuo è

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

PS: C'è una discussione sull'espressione in forma chiusa dell'ennesimo numero di Fibonacci su Wikipedia se desideri maggiori informazioni.


Grazie per il link del corso. Molto bella anche l'osservazione
SwimBikeRun

16

Puoi espanderlo e avere una visulizzazione

     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)

1
Capisco la prima riga. Ma perché c'è un personaggio meno che <alla fine? Come hai fatto T(n-1) + T(n-1)?
Quazi Irfan,

@QuaziIrfan: D quella è una freccia. -> [(non inferiore a). Ci scusiamo per la confusione sull'ultima riga]. Per la prima riga, beh ... T(n-1) > T(n-2)Quindi posso cambiare T(n-2)e mettere T(n-1). Otterrò solo un limite superiore per il quale è ancora validoT(n-1) + T(n-2)
Tony Tannous il

10

È limitato sull'estremità inferiore da 2^(n/2)e sull'estremità superiore da 2 ^ n (come notato in altri commenti). E un fatto interessante di questa implementazione ricorsiva è che ha un limite asintotico stretto di Fib (n) stesso. Questi fatti possono essere riassunti:

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

Se lo desideri, il limite stretto può essere ulteriormente ridotto utilizzando la sua forma chiusa .


10

Le risposte di prova sono buone, ma devo sempre fare alcune iterazioni a mano per convincermi davvero. Così ho disegnato un piccolo albero chiamante sulla mia lavagna e ho iniziato a contare i nodi. Ho diviso i miei conteggi in nodi totali, nodi foglia e nodi interni. Ecco cosa ho ottenuto:

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

Ciò che salta immediatamente fuori è che il numero di nodi foglia è fib(n). Ciò che ha preso qualche altra iterazione da notare è che il numero di nodi interni è fib(n) - 1. Pertanto, il numero totale di nodi è 2 * fib(n) - 1.

Dal momento che si eliminano i coefficienti durante la classificazione della complessità computazionale, la risposta finale è θ(fib(n)).


(No, sulla mia lavagna non ho disegnato un albero delle chiamate con 10 profondità. Solo 5 di profondità.);)
benkc,

Bene, mi chiedevo quante aggiunte extra ha fatto Fib ricorsiva. Non si aggiunge solo 1ai tempi di un singolo accumulatore Fib(n), ma è interessante che sia ancora esattamente θ(Fib(n)).
Peter Cordes

Si noti che alcune (la maggior parte) implementazioni ricorsive impiegano molto tempo ad aggiungere 0, tuttavia: i casi base di ricorsione sono 0e 1, perché lo fanno Fib(n-1) + Fib(n-2). Quindi probabilmente il 3 * Fib(n) - 2da questa risposta link-solo è più preciso per il numero totale di nodi, non è 2 * Fib(n) - 1.
Peter Cordes

Non riesco ad ottenere gli stessi risultati sui nodi foglia. A partire da 0: F (0) -> 1 foglia (stessa); F (1) -> 1 foglia (stessa); F (2) -> 2 foglie (F (1) e F (0)); F (3) -> 3 foglie; F (5) -> 8 foglie; ecc.
alexlomba87

9

La complessità temporale dell'algoritmo ricorsivo può essere meglio stimata disegnando l'albero di ricorsione, in questo caso la relazione di ricorrenza per disegnare l'albero di ricorsione sarebbe T (n) = T (n-1) + T (n-2) + O (1) notare che ogni passaggio richiede O (1) che significa tempo costante, poiché esegue un solo confronto per verificare il valore di n in if block. L'albero di ricorsione sarebbe simile

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

Qui diciamo che ogni livello dell'albero sopra è indicato da i quindi

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)

diciamo al particolare valore di i, l'albero finisce, quel caso sarebbe quando ni = 1, quindi i = n-1, il che significa che l'altezza dell'albero è n-1. Ora vediamo quanto lavoro viene svolto per ciascuno degli n strati nell'albero. Nota che ogni passaggio richiede O (1) tempo come indicato nella relazione di ricorrenza.

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

poiché i = n-1 è l'altezza del lavoro dell'albero ad ogni livello sarà

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

Quindi il totale del lavoro svolto sommerà il lavoro svolto ad ogni livello, quindi sarà 2 ^ 0 + 2 ^ 1 + 2 ^ 2 + 2 ^ 3 ... + 2 ^ (n-1) poiché i = n-1. Per serie geometriche questa somma è 2 ^ n, quindi la complessità temporale totale qui è O (2 ^ n)


2

Bene, secondo me è O(2^n)come in questa funzione solo la ricorsione sta impiegando molto tempo (dividere e conquistare). Vediamo che, la funzione di cui sopra continuerà in un albero fino a quando le foglie non si avvicinano quando raggiungiamo il livello F(n-(n-1))cioè F(1). Quindi, qui quando annotiamo la complessità temporale incontrata ad ogni profondità dell'albero, la serie di sommatoria è:

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

questo è l'ordine di 2^n [ O(2^n) ].


1

L'ingenua versione ricorsiva di Fibonacci è esponenziale in base alla progettazione a causa della ripetizione nel calcolo:

Alla radice stai calcolando:

F (n) dipende da F (n-1) e F (n-2)

F (n-1) dipende nuovamente da F (n-2) e F (n-3)

F (n-2) dipende nuovamente da F (n-3) e F (n-4)

quindi ad ogni livello 2 si verificano chiamate ricorsive che stanno sprecando molti dati nel calcolo, la funzione del tempo sarà simile a questa:

T (n) = T (n-1) + T (n-2) + C, con costante C.

T (n-1) = T (n-2) + T (n-3)> T (n-2) quindi

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

...

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

Questo è solo un limite inferiore che ai fini della tua analisi dovrebbe essere sufficiente, ma la funzione in tempo reale è un fattore di una costante con la stessa formula di Fibonacci e la forma chiusa è nota per essere esponenziale del rapporto aureo.

Inoltre, puoi trovare versioni ottimizzate di Fibonacci usando una programmazione dinamica come questa:

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];
}

Questo è ottimizzato e fa solo n passaggi ma è anche esponenziale.

Le funzioni di costo sono definite dalla dimensione di input al numero di passaggi per risolvere il problema. Quando vedi la versione dinamica di Fibonacci ( n passaggi per calcolare la tabella) o l'algoritmo più semplice per sapere se un numero è primo ( sqrt (n) per analizzare i divisori validi del numero). potresti pensare che questi algoritmi siano O (n) o O (sqrt (n)) ma questo non è vero per il seguente motivo: l'input per il tuo algoritmo è un numero: n , usando la notazione binaria la dimensione di input per un il numero intero n è log2 (n) quindi esegue una modifica variabile di

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

cerchiamo di scoprire il numero di passaggi in funzione della dimensione di input

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

quindi il costo del tuo algoritmo in funzione della dimensione di input è:

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

ed è per questo che il costo è esponenziale.


1

È semplice calcolare calcolando le chiamate delle funzioni. Aggiungi semplicemente le chiamate di funzione per ogni valore di n e osserva come cresce il numero.

La O grande è O (Z ^ n) dove Z è il rapporto aureo o circa 1,62.

Sia i numeri di Leonardo che i numeri di Fibonacci si avvicinano a questo rapporto mentre aumentiamo n.

A differenza di altre domande su Big O, non c'è variabilità nell'input e sia l'algoritmo che l'implementazione dell'algoritmo sono chiaramente definiti.

Non è necessario un gruppo di matematica complessa. Basta schematizzare le chiamate di funzione di seguito e adattare una funzione ai numeri.

Oppure, se conosci il rapporto aureo, lo riconoscerai come tale.

Questa risposta è più corretta della risposta accettata che afferma che si avvicinerà a f (n) = 2 ^ n. Non lo farà mai. Si avvicinerà a f (n) = golden_ratio ^ n.

2 (2 -> 1, 0)

4 (3 -> 2, 1) (2 -> 1, 0)

8 (4 -> 3, 2) (3 -> 2, 1) (2 -> 1, 0)
            (2 -> 1, 0)


14 (5 -> 4, 3) (4 -> 3, 2) (3 -> 2, 1) (2 -> 1, 0)
            (2 -> 1, 0)

            (3 -> 2, 1) (2 -> 1, 0)

22 (6 -> 5, 4)
            (5 -> 4, 3) (4 -> 3, 2) (3 -> 2, 1) (2 -> 1, 0)
                        (2 -> 1, 0)

                        (3 -> 2, 1) (2 -> 1, 0)

            (4 -> 3, 2) (3 -> 2, 1) (2 -> 1, 0)
                        (2 -> 1, 0)

1
Potete fornire qualche fonte per tale affermazione sul rapporto aureo?
Nico Haase,

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.