Come posso determinare il runtime di una doppia funzione ricorsiva?


15

Data una funzione arbitrariamente doppia ricorsiva, come si calcolerebbe il suo tempo di esecuzione?

Ad esempio (in pseudocodice):

int a(int x){
  if (x < = 0)
    return 1010;
  else
    return b(x-1) + a(x-1);
}
int b(int y){
  if (y <= -5)
    return -2;
  else
    return b(a(y-1));
}

O qualcosa del genere.

Quali metodi si potrebbero o si dovrebbero usare per determinare qualcosa del genere?


2
Sono compiti?
Bernard,

5
No, è estate e mi piace imparare. Immagino di andare avanti invece di lasciare che il mio cervello si trasformi in poltiglia.
if_zero_equals_one,

11
Ok, capito. A coloro che votano per migrare questo su Stack Overflow: questo è in argomento qui, e off-topic su Stack Overflow. Programmers.SE è per domande concettuali su lavagna bianca; Stack Overflow è per domande di implementazione, mentre problema mentre sto codificando.

3
Grazie, questo è il motivo per cui l'ho fatto qui in primo luogo. Inoltre è meglio sapere come pescare che ricevere un pesce.
if_zero_equals_one,

1
In questo caso particolare è ancora generalmente una ricorsione infinita perché b (a (0)) invoca infinitamente molti altri termini b (a (0)). Sarebbe stato diverso se fosse stata una formula matematica. Se la tua configurazione fosse stata diversa, avrebbe funzionato diversamente. Proprio come in matematica, in cs alcuni problemi hanno una soluzione, alcuni no, alcuni ne hanno uno facile, altri no. Esistono molti casi reciprocamente ricorsivi in ​​cui esiste la soluzione. A volte, per non far saltare in aria uno stack, si dovrebbe usare uno schema a trampolino.
Giobbe

Risposte:


11

Continui a cambiare la tua funzione. Ma continua a scegliere quelli che funzioneranno per sempre senza conversione.

La ricorsione diventa complicata, veloce. Il primo passo per analizzare una funzione doppiamente ricorsiva proposta è provare a rintracciarla su alcuni valori di esempio, per vedere cosa fa. Se il calcolo entra in un ciclo infinito, la funzione non è ben definita. Se il tuo calcolo entra in una spirale che continua a ottenere numeri più grandi (cosa che accade molto facilmente), probabilmente non è ben definito.

Se rintracciarlo dà una risposta, allora provi a trovare un modello o una relazione di ricorrenza tra le risposte. Una volta che hai quello, puoi provare a capire il suo runtime. Capirlo può essere molto, molto complicato, ma abbiamo risultati come il teorema del Maestro che ci permettono di capire la risposta in molti casi.

Attenzione che anche con una singola ricorsione, è facile trovare funzioni di cui non sappiamo calcolare il tempo di esecuzione. Ad esempio, considerare quanto segue:

def recursive (n):
    if 0 == n%2:
        return 1 + recursive(n/2)
    elif 1 == n:
        return 0
    else:
        return recursive(3*n + 1)

Al momento non è noto se questa funzione sia sempre ben definita, per non parlare del suo runtime.


5

Il runtime di quella particolare coppia di funzioni è infinito perché nessuno dei due ritorna senza chiamare l'altro. Il valore di ritorno di aè sempre dipendente dal valore di ritorno di una chiamata a bcui da sempre chiama a... e questo è ciò che è noto come ricorsione infinita .


Non sto cercando le funzioni particolari proprio qui. Sto cercando un modo generale per trovare il tempo di esecuzione di funzioni ricorsive che si chiamano a vicenda.
if_zero_equals_one,

1
Non sono sicuro che ci sia una soluzione nel caso generale. Affinché Big-O abbia un senso, devi sapere se l'algoritmo si fermerà mai. Esistono alcuni algoritmi ricorsivi in ​​cui devi eseguire il calcolo prima di sapere quanto tempo impiegherà (ad es. Determinare se un punto appartiene o meno al set di Mandlebrot).
jimreed

Non sempre, achiama solo bse il numero passato è> = 0. Ma sì, c'è un ciclo infinito.
btilly

1
@btilly l'esempio è stato modificato dopo aver pubblicato la mia risposta.
jimreed

1
@jimreed: ed è stato nuovamente modificato. Eliminerei il mio commento se potessi.
btilly

4

Il metodo ovvio è eseguire la funzione e misurare il tempo impiegato. Questo ti dice solo quanto tempo impiega un input particolare, però. E se non sai in anticipo che la funzione termina, difficile: non esiste un modo meccanico per capire se la funzione termina: questo è il problema di arresto ed è indecidibile.

Trovare il tempo di esecuzione di una funzione è altrettanto indecidibile, secondo il teorema di Rice . In effetti, il teorema di Rice mostra che anche decidere se una funzione viene eseguita in O(f(n))tempo è indecidibile.

Quindi il meglio che puoi fare in generale è usare la tua intelligenza umana (che, per quanto ne sappiamo, non è vincolata dai limiti delle macchine di Turing) e provare a riconoscere un modello o inventarne uno. Un modo tipico di analizzare il tempo di esecuzione di una funzione è trasformare la definizione ricorsiva della funzione in un'equazione ricorsiva sul suo tempo di esecuzione (o un insieme di equazioni per funzioni reciprocamente ricorsive):

T_a(x) = if x ≤ 0 then 1 else T_b(x-1) + T_a(x-1)
T_b(x) = if x ≤ -5 then 1 else T_b(T_a(x-1))

Quale prossimo? Ora hai un problema di matematica: devi risolvere queste equazioni funzionali. Un approccio che spesso funziona è trasformare queste equazioni su funzioni intere in equazioni su funzioni analitiche e usare il calcolo per risolverle, interpretando le funzioni T_ae T_bcome funzioni generatrici .

Sulla generazione di funzioni e altri argomenti matematici discreti, consiglio il libro Concrete Mathematics , di Ronald Graham, Donald Knuth e Oren Patashnik.


1

Come altri hanno sottolineato, analizzare la ricorsione può diventare molto difficile molto velocemente. Ecco un altro esempio di questa cosa: http://rosettacode.org/wiki/Mutual_recursion http://en.wikipedia.org/wiki/Hofstadter_sequence#Hofstadter_Female_and_Male_sequences è difficile calcolare una risposta e un tempo di esecuzione per questi. Ciò è dovuto al fatto che queste funzioni reciprocamente ricorsive hanno una "forma difficile".

Comunque, diamo un'occhiata a questo semplice esempio:

http://pramode.net/clojure/2010/05/08/clojure-trampoline/

(declare funa funb)
(defn funa [n]
  (if (= n 0)
    0
    (funb (dec n))))
(defn funb [n]
  (if (= n 0)
    0
    (funa (dec n))))

Iniziamo provando a calcolare funa(m), m > 0:

funa(m) = funb(m - 1) = funa(m - 2) = ... funa(0) or funb(0) = 0 either way.

Il tempo di esecuzione è:

R(funa(m)) = 1 + R(funb(m - 1)) = 2 + R(funa(m - 2)) = ... m + R(funa(0)) or m + R(funb(0)) = m + 1 steps either way

Ora scegliamo un altro esempio leggermente più complicato:

Ispirato da http://planetmath.org/encyclopedia/MutualRecursion.html , che è una buona lettura da solo, diamo un'occhiata a: "" "I numeri di Fibonacci possono essere interpretati tramite ricorsione reciproca: F (0) = 1 e G (0 ) = 1, con F (n + 1) = F (n) + G (n) e G (n + 1) = F (n). "" "

Quindi, qual è il tempo di esecuzione di F? Andremo dall'altra parte.
Bene, R (F (0)) = 1 = F (0); R (G (0)) = 1 = G (0)
Ora R (F (1)) = R (F (0)) + R (G (0)) = F (0) + G (0) = F (1)
...
Non è difficile vedere che R (F (m)) = F (m) - ad es. Il numero di chiamate di funzione necessarie per calcolare un numero di Fibonacci all'indice i è uguale al valore di un numero di Fibonacci all'indice i. Ciò presupponeva che l'aggiunta di due numeri insieme fosse molto più veloce di una chiamata di funzione. Se così non fosse, allora sarebbe vero: R (F (1)) = R (F (0)) + 1 + R (G (0)), e l'analisi di questo sarebbe stata più complicata, possibilmente senza una soluzione di forma chiusa facile.

La forma chiusa per la sequenza di Fibonacci non è necessariamente facile da reinventare, per non parlare di alcuni esempi più complicati.


0

La prima cosa da fare è mostrare che le funzioni che hai definito terminano e per quali valori esattamente. Nell'esempio che hai definito

int a(int x){
  if (x < = 0)
    return 1010;
  else
    return b(x-1) + a(x-1);
}
int b(int y){
  if (y <= -5)
    return -2;
  else
    return b(a(y-1));
}

btermina solo y <= -5perché se si inserisce qualsiasi altro valore, si avrà un termine del modulo b(a(y-1)). Se fai un po 'più di espansione, vedrai che un termine del modulo b(a(y-1))alla fine porta al termine b(1010)che porta a un termine b(a(1009))che porta di nuovo al termine b(1010). Ciò significa che non è possibile inserire alcun valore ache non soddisfa x <= -4perché se lo si fa si finisce con un ciclo infinito in cui il valore da calcolare dipende dal valore da calcolare. Quindi essenzialmente questo esempio ha un tempo di esecuzione costante.

Quindi la semplice risposta è che non esiste un metodo generale per determinare il tempo di esecuzione delle funzioni ricorsive perché non esiste una procedura generale che determina se una funzione definita ricorsivamente termina.


-5

Runtime come in Big-O?

È facile: O (N) - supponendo che ci sia una condizione di terminazione.

La ricorsione è solo in loop e un loop semplice è O (N), indipendentemente da quante cose fai in quel loop (e chiamare un altro metodo è solo un altro passo nel loop).

Dove sarebbe interessante è se si dispone di un ciclo all'interno di uno o più dei metodi ricorsivi. In tal caso, si otterrebbe una sorta di prestazione esponenziale (moltiplicando per O (N) ad ogni passaggio attraverso il metodo).


2
Determinate le prestazioni Big-O prendendo l'ordine più alto di qualsiasi metodo chiamato e moltiplicandolo per l'ordine del metodo chiamante. Tuttavia, una volta che inizi a parlare di prestazioni esponenziali e fattoriali, puoi ignorare le prestazioni polinomiali. Io credo che lo stesso vale quando si confrontano esponenziali e fattoriali: vince fattoriale. Non ho mai dovuto analizzare un sistema che fosse sia esponenziale che fattoriale.
Anon,

5
Questo non è corretto Le forme ricorsive di calcolo dell'ennesimo numero di Fibonacci e quicksort sono O(2^n)e O(n*log(n)), rispettivamente.
unpythonic il

1
Senza fare alcune prove fantasiose vorrei indirizzarti su amazon.com/Introduction-Algorithms-Second-Thomas-Cormen/dp/… e provare a dare un'occhiata a questo sito SE cstheory.stackexchange.com .
Bryan Harrington,

4
Perché le persone hanno votato questa risposta orribilmente sbagliata? La chiamata di un metodo richiede tempo proporzionale al tempo impiegato dal metodo. In questo caso il metodo achiama be bchiama aquindi non puoi semplicemente supporre che entrambi i metodi richiedano tempo O(1).
btilly

2
@Anon - Il poster chiedeva una funzione arbitrariamente doppia ricorsiva, non solo quella mostrata sopra. Ho dato due esempi di semplice ricorsione che non corrispondono alla tua spiegazione. È banale convertire i vecchi standard in una forma "doppia ricorsiva", una che era esponenziale (adatta al tuo avvertimento) e una che non è (non coperta).
unpythonic il
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.