Determinazione della complessità per le funzioni ricorsive (notazione Big O)


267

Domani ho un semestre di informatica e ho bisogno di aiuto per determinare la complessità di queste funzioni ricorsive. So come risolvere casi semplici, ma sto ancora cercando di imparare a risolvere questi casi più difficili. Questi erano solo alcuni dei problemi di esempio che non riuscivo a capire. Qualsiasi aiuto sarebbe molto apprezzato e sarebbe di grande aiuto nei miei studi, grazie!

int recursiveFun1(int n)
{
    if (n <= 0)
        return 1;
    else
        return 1 + recursiveFun1(n-1);
}

int recursiveFun2(int n)
{
    if (n <= 0)
        return 1;
    else
        return 1 + recursiveFun2(n-5);
}

int recursiveFun3(int n)
{
    if (n <= 0)
        return 1;
    else
        return 1 + recursiveFun3(n/5);
}

void recursiveFun4(int n, int m, int o)
{
    if (n <= 0)
    {
        printf("%d, %d\n",m, o);
    }
    else
    {
        recursiveFun4(n-1, m+1, o);
        recursiveFun4(n-1, m, o+1);
    }
}

int recursiveFun5(int n)
{
    for (i = 0; i < n; i += 2) {
        // do something
    }

    if (n <= 0)
        return 1;
    else
        return 1 + recursiveFun5(n-5);
}

4
Se non si desidera eseguire l'analisi ogni volta, esiste una tecnica di scatola nera chiamata metodo Master. Ma supponendo che tutte le suddivisioni ricorsive degli input abbiano le stesse dimensioni in ciascuna istanza.
Vivek Krishna,


Risposte:


345

La complessità temporale, in notazione O grande, per ciascuna funzione, è in ordine numerico:

  1. La prima funzione viene chiamata in modo ricorsivo n volte prima di raggiungere il caso base, quindi è O(n)spesso chiamata lineare .
  2. La seconda funzione si chiama n-5 per ogni volta, quindi ne deduciamo cinque da n prima di chiamare la funzione, ma anche n-5 lo è O(n). (In realtà chiamato ordine di n / 5 volte. E, O (n / 5) = O (n)).
  3. Questa funzione è log (n) base 5, per ogni volta che dividiamo per 5 prima di chiamare la funzione, quindi la sua O(log(n))(base 5), spesso chiamata logaritmica e molto spesso l'analisi della notazione e della complessità O utilizza la base 2.
  4. Nel quarto, è O(2^n), o esponenziale , poiché ogni chiamata di funzione chiama se stessa due volte a meno che non sia stata ripetuta n volte.
  5. Per quanto riguarda l'ultima funzione, il ciclo for prende n / 2 poiché stiamo aumentando di 2, e la ricorsione prende n-5 e poiché il ciclo for è chiamato ricorsivamente, quindi la complessità temporale è in (n-5) * (n / 2) = (2n-10) * n = 2n ^ 2- 10n, a causa del comportamento asintotico e delle considerazioni sullo scenario peggiore o del limite superiore per cui O grande sta lottando, siamo interessati solo al termine più ampio O(n^2).

    Buona fortuna per i tuoi intermedi;)


hai ragione circa il quinto, n diminuirà per il ciclo for ma per il quarto non penso che sia n ^ 2 per come un albero ogni volta che chiami la ricorsione due volte, quindi dovrebbe essere 2 ^ n più che era il tuo rispondere nel commento in precedenza.
coder

2
@MJGwater Lascia che il tempo di esecuzione del loop sia m. Quando la corsa ricorsiva 1 volta, ci vuole m per eseguire il ciclo. Quando la ricorsiva viene eseguita 2 volte, anche il loop viene eseguito 2 volte, quindi ci vogliono 2m ... e così via. Quindi è '*', non '^'.
BJC,

3
@coder La spiegazione per 5 sembra strana. Se l'incremento di 2 risulta in n/2iterazioni del forloop, perché una riduzione di 5 non comporterebbe n/5chiamate ricorsive? Ciò comporterebbe comunque, O(n^2)ma sembra una spiegazione più intuitiva. Perché mescolare sottrazione e divisione quando sono essenziali per fare la stessa cosa?
Jack,

1
@coder quindi per # 4, se ci fossero 3 chiamate ricorsive nella definizione della funzione, avrebbe una complessità temporale di O (3 ^ n)? E per 5 chiamate ricorsive sarebbe O (5 ^ n), giusto?
rmutalik,

1
@ Jack Sì, mi chiedevo anche lo stesso. Dovrebbe essere n/5non n-5. E alla fine, tutto si ridurrà a O(N^2).
Anuj,

128

Per il caso in cui n <= 0, T(n) = O(1). Pertanto, la complessità temporale dipenderà da quando n >= 0.

Considereremo il caso n >= 0nella parte seguente.

1.

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

dove a è una costante.

Per induzione:

T(n) = n * a + T(0) = n * a + b = O(n)

dove a, b sono alcune costanti.

2.

T(n) = a + T(n - 5)

dove a è una costante

Per induzione:

T(n) = ceil(n / 5) * a + T(k) = ceil(n / 5) * a + b = O(n)

dove a, b sono alcune costanti e k <= 0

3.

T(n) = a + T(n / 5)

dove a è una costante

Per induzione:

T(n) = a * log5(n) + T(0) = a * log5(n) + b = O(log n)

dove a, b sono alcune costanti

4.

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

dove a è una costante

Per induzione:

T(n) = a + 2a + 4a + ... + 2^(n-1) * a + T(0) * 2^n 
     = a * 2^n - a + b * 2^n
     = (a + b) * 2^n - a
     = O(2 ^ n)

dove a, b sono alcune costanti.

5.

T(n) = n / 2 + T(n - 5)

dove n è una costante

Riscrivi n = 5q + rdove qer sono numeri interi e r = 0, 1, 2, 3, 4

T(5q + r) = (5q + r) / 2 + T(5 * (q - 1) + r)

Abbiamo q = (n - r) / 5, e poiché r <5, possiamo considerarlo una costante, quindiq = O(n)

Per induzione:

T(n) = T(5q + r)
     = (5q + r) / 2 + (5 * (q - 1) + r) / 2 + ... + r / 2 +  T(r)
     = 5 / 2 * (q + (q - 1) + ... + 1) +  1 / 2 * (q + 1) * r + T(r)
     = 5 / 4 * (q + 1) * q + 1 / 2 * (q + 1) * r + T(r)
     = 5 / 4 * q^2 + 5 / 4 * q + 1 / 2 * q * r + 1 / 2 * r + T(r)

Da r <4, possiamo trovare una costante b in modo che b >= T(r)

T(n) = T(5q + r)
     = 5 / 2 * q^2 + (5 / 4 + 1 / 2 * r) * q + 1 / 2 * r + b
     = 5 / 2 * O(n ^ 2) + (5 / 4 + 1 / 2 * r) * O(n) + 1 / 2 * r + b
     = O(n ^ 2)

1
Recentemente ho fallito una domanda di intervista (ed estendendo l'intervista) che ha a che fare con l'analisi della complessità temporale e spaziale di una funzione ricorsiva dei fibonacci. Questa risposta è epica e ha aiutato molto, lo adoro, vorrei poterti votare due volte. So che è vecchio ma hai qualcosa di simile per il calcolo dello spazio - forse un link, qualcosa?
Dimitar Dimitrov,

Per No.4, anche se il risultato è lo stesso, l'induzione non dovrebbe essere la seguente? T (n) = a + 2T (n-1) = a + 2a + 4T (n-1) = 3a + 4a + 8T (n-1) = a * (2 ^ n - 1) + 2 ^ n * T (0) = a * (2 ^ n - 1) + b * 2 ^ n = (a + b) * 2 ^ n - a = O (2 ^ n)
Snowfish

27

Uno dei modi migliori che trovo per approssimare la complessità dell'algoritmo ricorsivo è disegnare l'albero di ricorsione. Una volta che hai l'albero ricorsivo:

Complexity = length of tree from root node to leaf node * number of leaf nodes
  1. La prima funzione avrà lunghezza ne numero del nodo foglia 1quindi la complessità saràn*1 = n
  2. La seconda funzione avrà di nuovo la lunghezza n/5e il numero di nodi foglia, 1quindi la complessità sarà n/5 * 1 = n/5. Dovrebbe essere approssimato an

  3. Per la terza funzione, poiché nviene diviso 5 per ogni chiamata ricorsiva, la lunghezza dell'albero ricorsivo sarà log(n)(base 5)e il numero di nodi foglia di nuovo 1, quindi la complessità saràlog(n)(base 5) * 1 = log(n)(base 5)

  4. Per la quarta funzione poiché ogni nodo avrà due nodi figlio, il numero di nodi foglia sarà uguale (2^n)e la lunghezza dell'albero ricorsivo sarà ntale che la complessità sarà (2^n) * n. Ma dal momento che nè insignificante di fronte (2^n), può essere ignorato e si può solo dire che la complessità lo è (2^n).

  5. Per la quinta funzione, ci sono due elementi che introducono la complessità. Complessità introdotta dalla natura ricorsiva della funzione e complessità introdotta dal forloop in ciascuna funzione. Facendo il calcolo di cui sopra, la complessità introdotta dalla natura ricorsiva della funzione sarà ~ ne la complessità dovuta a per il ciclo n. La complessità totale sarà n*n.

Nota: questo è un modo rapido e sporco di calcolare la complessità (niente di ufficiale!). Mi piacerebbe ricevere feedback su questo. Grazie.


Risposta eccellente! Ho una domanda sulla quarta funzione. Se avesse avuto tre chiamate ricorsive, la risposta sarebbe (3 ^ n). O vorresti ancora dire (2 ^ n)?
Ben Forsrup,

@Shubham: # 4 non mi sembra giusto. Se il numero di foglie è 2^nallora l'altezza dell'albero deve essere n, no log n. L'altezza sarebbe solo log nse nrappresentasse il numero totale di nodi nella struttura. Ma non lo fa.
Giuliano A.

@BenForsrup: sarà 3 ^ n perché ogni nodo avrà tre nodi figlio. Il modo migliore per essere sicuri di questo è disegnare l'albero ricorsivo con valori fittizi.
Shubham,

# 2 dovrebbe essere n-5 non n / 5
Fintasys,

7

Possiamo dimostrarlo matematicamente che è qualcosa che mi mancava nelle risposte di cui sopra.

Può aiutarti notevolmente a capire come calcolare qualsiasi metodo. Consiglio di leggerlo dall'alto verso il basso per comprendere appieno come farlo:

  1. T(n) = T(n-1) + 1Significa che il tempo necessario per il completamento del metodo è uguale allo stesso metodo ma con n-1 che è T(n-1)e ora aggiungiamo + 1perché è il tempo necessario per il completamento delle operazioni generali (tranne T(n-1)). Ora, stiamo andando a trovare T(n-1)come segue: T(n-1) = T(n-1-1) + 1. Sembra che ora possiamo formare una funzione che può darci una sorta di ripetizione in modo da poter comprendere appieno. Metteremo il lato destro T(n-1) = ...invece che T(n-1)all'interno del metodo T(n) = ...che ci darà: T(n) = T(n-1-1) + 1 + 1che è T(n) = T(n-2) + 2o, in altre parole, abbiamo bisogno di trovare il nostro mancante k: T(n) = T(n-k) + k. Il prossimo passo è prendere n-ke affermare che, n-k = 1poiché alla fine della ricorsione ci vorrà esattamente O (1) quandon<=0. Da questa semplice equazione ora lo sappiamo k = n - 1. Posizioniamo knel nostro metodo finale: T(n) = T(n-k) + kche ci darà: T(n) = 1 + n - 1che è esattamente no O(n).
  2. È lo stesso di 1. Puoi testarlo da solo e vedere che ottieni O(n).
  3. T(n) = T(n/5) + 1come prima, il tempo per il completamento di questo metodo equivale al tempo dello stesso metodo, ma con n/5quale motivo è limitato T(n/5). Scopriamo T(n/5)come in 1: T(n/5) = T(n/5/5) + 1che è T(n/5) = T(n/5^2) + 1. Posto di Let T(n/5)all'interno T(n)per il calcolo finale: T(n) = T(n/5^k) + k. Di nuovo come prima, n/5^k = 1che è n = 5^kesattamente come chiedere cosa al potere di 5, ci darà n, la risposta è log5n = k(log della base 5). Posizioniamo i nostri risultati T(n) = T(n/5^k) + kcome segue: T(n) = 1 + lognche èO(logn)
  4. T(n) = 2T(n-1) + 1quello che abbiamo qui è sostanzialmente lo stesso di prima, ma questa volta stiamo invocando il metodo in modo ricorsivo 2 volte, quindi lo moltiplichiamo per 2. Scopriamo T(n-1) = 2T(n-1-1) + 1quale è T(n-1) = 2T(n-2) + 1. Il nostro prossimo posto come prima, posizioniamo la nostra scoperta: T(n) = 2(2T(n-2)) + 1 + 1che è T(n) = 2^2T(n-2) + 2ciò che ci dà T(n) = 2^kT(n-k) + k. Scopriamo kaffermando ciò n-k = 1che è k = n - 1. Posizioniamo kcome segue: T(n) = 2^(n-1) + n - 1che è approssimativamenteO(2^n)
  5. T(n) = T(n-5) + n + 1È quasi uguale a 4 ma ora aggiungiamo nperché abbiamo un forloop. Scopriamo T(n-5) = T(n-5-5) + n + 1qual è T(n-5) = T(n - 2*5) + n + 1. Posto di Ammettiamolo: T(n) = T(n-2*5) + n + n + 1 + 1)che è T(n) = T(n-2*5) + 2n + 2)e per il k: T(n) = T(n-k*5) + kn + k)di nuovo: n-5k = 1che è n = 5k + 1che è più o meno n = k. Questo ci darà: T(n) = T(0) + n^2 + nche è approssimativamente O(n^2).

Ora raccomando di leggere il resto delle risposte che ora ti daranno una prospettiva migliore. Buona fortuna a vincere quelle grandi O :)


1

La chiave qui è di visualizzare l'albero delle chiamate. Fatto ciò, la complessità è:

nodes of the call tree * complexity of other code in the function

quest'ultimo termine può essere calcolato nello stesso modo in cui lo facciamo per una normale funzione iterativa.

Invece, i nodi totali di un albero completo vengono calcolati come

                  C^L - 1
                  -------  , when C>1
               /   C - 1
              /
 # of nodes =
              \    
               \ 
                  L        , when C=1

Dove C è il numero di figli di ciascun nodo e L è il numero di livelli dell'albero (radice inclusa).

È facile visualizzare l'albero. Inizia dalla prima chiamata (nodo radice) quindi disegna un numero di figli uguale al numero di chiamate ricorsive nella funzione. È anche utile scrivere il parametro passato alla chiamata secondaria come "valore del nodo".

Quindi, negli esempi sopra:

  1. l'albero delle chiamate qui è C = 1, L = n + 1. La complessità del resto della funzione è O (1). Pertanto la complessità totale è L * O (1) = (n + 1) * O (1) = O (n)
n     level 1
n-1   level 2
n-2   level 3
n-3   level 4
... ~ n levels -> L = n
  1. l'albero delle chiamate qui è C = 1, L = n / 5. La complessità del resto della funzione è O (1). Pertanto la complessità totale è L * O (1) = (n / 5) * O (1) = O (n)
n
n-5
n-10
n-15
... ~ n/5 levels -> L = n/5
  1. l'albero delle chiamate qui è C = 1, L = log (n). La complessità del resto della funzione è O (1). Pertanto la complessità totale è L * O (1) = log5 (n) * O (1) = O (log (n))
n
n/5
n/5^2
n/5^3
... ~ log5(n) levels -> L = log5(n)
  1. l'albero delle chiamate qui è C = 2, L = n. La complessità del resto della funzione è O (1). Questa volta utilizziamo la formula completa per il numero di nodi nella struttura delle chiamate perché C> 1. Pertanto la complessità totale è (C ^ L-1) / (C-1) * O (1) = (2 ^ n - 1 ) * O (1) = O (2 ^ n) .
               n                   level 1
      n-1             n-1          level 2
  n-2     n-2     n-2     n-2      ...
n-3 n-3 n-3 n-3 n-3 n-3 n-3 n-3    ...     
              ...                ~ n levels -> L = n
  1. l'albero delle chiamate qui è C = 1, L = n / 5. La complessità del resto della funzione è O (n). Pertanto la complessità totale è L * O (1) = (n / 5) * O (n) = O (n ^ 2)
n
n-5
n-10
n-15
... ~ n/5 levels -> L = n/5
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.