Mi chiedevo se un ciclo while fosse intrinsecamente una ricorsione?
Penso che sia perché un ciclo while può essere visto come una funzione che si chiama alla fine. Se non è ricorsione, allora qual è la differenza?
Mi chiedevo se un ciclo while fosse intrinsecamente una ricorsione?
Penso che sia perché un ciclo while può essere visto come una funzione che si chiama alla fine. Se non è ricorsione, allora qual è la differenza?
Risposte:
I loop sono molto non ricorsione. In realtà, sono il primo esempio del meccanismo opposto : iterazione .
Il punto di ricorsione è che un elemento dell'elaborazione chiama un'altra istanza di se stesso. La macchina di controllo ad anello semplicemente salta di nuovo al punto in cui è iniziato.
Saltare nel codice e chiamare un altro blocco di codice sono operazioni diverse. Ad esempio, quando si salta all'inizio del loop, la variabile di controllo del loop ha ancora lo stesso valore che aveva prima del salto. Ma se chiami un'altra istanza della routine in cui ti trovi, allora la nuova istanza ha copie nuove e non correlate di tutte le sue variabili. In effetti, una variabile può avere un valore al primo livello di elaborazione e un altro valore a un livello inferiore.
Questa funzionalità è cruciale per il funzionamento di molti algoritmi ricorsivi ed è per questo che non è possibile emulare la ricorsione tramite iterazione senza gestire anche una pila di frame chiamati che tiene traccia di tutti quei valori.
Dire che X è intrinsecamente Y ha senso solo se hai in mente un sistema (formale) in cui stai esprimendo X. Se definisci la semantica while
in termini di calcolo lambda, potresti menzionare la ricorsione *; se lo definisci in termini di macchina del registro, probabilmente non lo farai.
In entrambi i casi, probabilmente le persone non ti capiranno se chiami una funzione ricorsiva solo perché contiene un ciclo while.
* Anche se forse solo indirettamente, ad esempio se lo definisci in termini di fold
.
while
costrutto la ricorsività è generalmente una proprietà delle funzioni, non riesco proprio a pensare a nient'altro da descrivere come "ricorsivo" in questo contesto.
Questo dipende dal tuo punto di vista.
Se osservi la teoria della calcolabilità , l'iterazione e la ricorsione sono ugualmente espressive . Ciò significa che puoi scrivere una funzione che calcola qualcosa e non importa se lo fai in modo ricorsivo o iterativo, sarai in grado di scegliere entrambi gli approcci. Non c'è nulla che tu possa calcolare ricorsivamente che non puoi calcolare iterativamente e viceversa (sebbene il funzionamento interno del programma potrebbe essere diverso).
Molti linguaggi di programmazione non trattano la ricorsione e l'iterazione allo stesso modo, e per buoni motivi. Di solito , la ricorsione significa che il linguaggio / compilatore gestisce lo stack di chiamate e l'iterazione indica che potrebbe essere necessario eseguire autonomamente la gestione dello stack.
Tuttavia, ci sono lingue - in particolare linguaggi funzionali - in cui cose come i loop (per, mentre) sono davvero solo zucchero sintattico per la ricorsione e implementate dietro le quinte in quel modo. Questo è spesso desiderabile nei linguaggi funzionali, perché di solito non hanno il concetto di looping altrimenti, e aggiungerlo renderebbe il loro calcolo più complesso, per ragioni pratiche.
Quindi no, non sono intrinsecamente uguali . Sono ugualmente espressivi , il che significa che non puoi calcolare qualcosa in modo iterativo, non puoi calcolare ricorsivamente e viceversa, ma questo è tutto, nel caso generale (secondo la tesi di Church-Turing).
Si noti che stiamo parlando di programmi ricorsivi qui. Esistono altre forme di ricorsione, ad esempio nelle strutture di dati (ad esempio alberi).
Se lo guardi da un punto di vista dell'implementazione , la ricorsione e l'iterazione non sono praticamente la stessa cosa. La ricorsione crea un nuovo frame di stack per ogni chiamata. Ogni fase della ricorsione è autonoma, ottenendo gli argomenti per il calcolo dalla chiamata (stessa).
I loop invece non creano cornici di chiamata. Per loro, il contesto non viene conservato su ogni passaggio. Per il loop, il programma torna semplicemente all'inizio del loop fino a quando la condizione del loop fallisce.
Questo è abbastanza importante da sapere, dal momento che può fare differenze abbastanza radicali nel mondo reale. Per la ricorsione, l'intero contesto deve essere salvato su ogni chiamata. Per l'iterazione, hai un controllo preciso su quali variabili sono in memoria e su cosa viene salvato dove.
Se lo guardi in questo modo, vedi rapidamente che per la maggior parte delle lingue, iterazione e ricorsione sono fondamentalmente diverse e hanno proprietà diverse. A seconda della situazione, alcune delle proprietà sono più desiderabili di altre.
Ricorsione può rendere i programmi più semplice e più facile da testare e la prova . La conversione di una ricorsione in iterazione solitamente rende il codice più complesso, aumentando la probabilità di errore. D'altro canto, la conversione in iterazione e la riduzione della quantità di frame dello stack di chiamate possono far risparmiare molta memoria necessaria.
La differenza è lo stack implicito e semantico.
Un ciclo while che "si chiama alla fine" non ha stack di cui eseguire il backup. L'ultima iterazione imposta quale sarà lo stato al termine.
Tuttavia, la ricorsione non può essere eseguita senza questo stack implicito che ricorda lo stato del lavoro svolto in precedenza.
È vero che è possibile risolvere qualsiasi problema di ricorsione con iterazione se si dà accesso a uno stack in modo esplicito. Ma farlo in questo modo non è lo stesso.
La differenza semantica ha a che fare con il fatto che guardare il codice ricorsivo trasmette un'idea in un modo completamente diverso dal codice iterativo. Il codice iterativo fa le cose un passo alla volta. Accetta qualsiasi stato proveniente da prima e funziona solo per creare lo stato successivo.
Il codice ricorsivo suddivide un problema in frattali. Questa piccola parte assomiglia a quella grande parte in modo che possiamo fare solo un po 'di esso e quel po' allo stesso modo. È un modo diverso di pensare ai problemi. È molto potente e ci vuole l'abitudine. Si può dire molto in poche righe. Non puoi tirarlo fuori da un ciclo while anche se ha accesso a uno stack.
Tutto dipende dal tuo uso intrinseco del termine . A livello di linguaggio di programmazione, sono sintatticamente e semanticamente diversi e hanno prestazioni e uso della memoria piuttosto diversi. Ma se si scava abbastanza in profondità nella teoria, possono essere definiti l'uno nell'altro, ed è quindi "lo stesso" in un certo senso teorico.
La vera domanda è: quando ha senso distinguere tra iterazione (loop) e ricorsione, e quando è utile pensarla come le stesse cose? La risposta è che quando si esegue effettivamente la programmazione (anziché scrivere prove matematiche) è importante distinguere tra iterazione e ricorsione.
La ricorsione crea un nuovo frame di stack, ovvero un nuovo set di variabili locali per ogni chiamata. Questo ha un sovraccarico e occupa spazio nello stack, il che significa che una ricorsione abbastanza profonda può traboccare lo stack causando l'arresto anomalo del programma. L'iterazione, d'altra parte, modifica solo le variabili esistenti, quindi è generalmente più veloce e occupa solo una quantità costante di memoria. Quindi questa è una distinzione molto importante per uno sviluppatore!
Nelle lingue con ricorsione di coda (in genere linguaggi funzionali), il compilatore potrebbe essere in grado di ottimizzare le chiamate ricorsive in modo tale da occupare solo una quantità costante di memoria. In queste lingue l'importante distinzione non è iterazione vs ricorsione, ma versione non-coda-ricorsione coda-chiamata-ricorsione e iterazione.
In conclusione: devi essere in grado di dire la differenza, altrimenti il tuo programma si arresterà in modo anomalo.
while
i loop sono una forma di ricorsione, vedi ad esempio la risposta accettata a questa domanda . Corrispondono all'operatore μ nella teoria della calcolabilità (vedere ad esempio qui ).
Tutte le varianti di for
loop che ripetono su un intervallo di numeri, una raccolta finita, una matrice e così via, corrispondono alla ricorsione primitiva, vedi ad esempio qui e qui . Si noti che i for
loop di C, C ++, Java e così via sono in realtà zucchero sintattico per un while
loop, e quindi non corrispondono alla ricorsione primitiva. Il for
ciclo di Pascal è un esempio di ricorsione primitiva.
Una differenza importante è che la ricorsione primitiva termina sempre, mentre la ricorsione generalizzata ( while
loop) potrebbe non terminare.
MODIFICARE
Alcuni chiarimenti riguardanti commenti e altre risposte. "La ricorsione si verifica quando una cosa è definita in termini di se stessa o del suo tipo." (vedi Wikipedia ). Così,
Un ciclo while è intrinsecamente una ricorsione?
Dal momento che è possibile definire un while
ciclo in termini di se stesso
while p do c := if p then (c; while p do c))
quindi, sì , un while
ciclo è una forma di ricorsione. Le funzioni ricorsive sono un'altra forma di ricorsione (un altro esempio di definizione ricorsiva). Elenchi e alberi sono altre forme di ricorsione.
Un'altra domanda implicitamente assunta da molte risposte e commenti è
I loop while e le funzioni ricorsive sono equivalenti?
La risposta a questa domanda è no : un while
loop corrisponde a una funzione ricorsiva della coda, dove le variabili a cui accede il loop corrispondono agli argomenti della funzione ricorsiva implicita, ma, come altri hanno sottolineato, funzioni non ricorsive della coda non può essere modellato da un while
loop senza usare uno stack aggiuntivo.
Quindi, il fatto che "un while
ciclo sia una forma di ricorsione" non contraddice il fatto che "alcune funzioni ricorsive non possono essere espresse da un while
ciclo".
FOR
ciclo può calcolare esattamente tutte le funzioni ricorsive primitive e una lingua con solo un WHILE
ciclo può calcolare esattamente tutte le funzioni ricorsive µ (e si scopre che le funzioni ricorsive µ sono esattamente quelle funzioni che una macchina di Turing può calcolare). Oppure, per farla breve: ricorsione primitiva e ricorsione µ sono termini tecnici della teoria matematica / calcolabilità.
Una chiamata di coda (o chiamata ricorsiva di coda) è esattamente implementata come un "goto con argomenti" (senza spingere alcun frame di chiamata aggiuntivo nello stack di chiamate ) e in alcuni linguaggi funzionali (in particolare Ocaml) è il solito modo di eseguire il loop.
Quindi un ciclo while (nelle lingue che li hanno) può essere visto come terminando con una chiamata di coda al suo corpo (o al suo test di testa).
Allo stesso modo, le chiamate ricorsive ordinarie (non di coda) possono essere simulate mediante loop (usando un po 'di stack).
Leggi anche le continuazioni e lo stile del passaggio di continuazione .
Quindi "ricorsione" e "iterazione" sono profondamente equivalenti.
È vero che sia la ricorsione che i loop continui illimitati sono equivalenti in termini di espressività computazionale. Cioè, qualsiasi programma scritto in modo ricorsivo può essere riscritto in un programma equivalente usando invece i loop e viceversa. Entrambi gli approcci sono completi di turing , che possono essere utilizzati per calcolare qualsiasi funzione calcolabile.
La differenza fondamentale in termini di programmazione è che la ricorsione consente di utilizzare i dati che vengono memorizzati nello stack di chiamate. Per illustrare ciò, si supponga di voler stampare un elemento di un elenco collegato singolarmente utilizzando un ciclo o una ricorsione. Userò C per il codice di esempio:
typedef struct List List;
struct List
{
List* next;
int element;
};
void print_list_loop(List* l)
{
List* it = l;
while(it != NULL)
{
printf("Element: %d\n", it->element);
it = it->next;
}
}
void print_list_rec(List* l)
{
if(l == NULL) return;
printf("Element: %d\n", l->element);
print_list_rec(l->next);
}
Semplice vero? Ora facciamo una leggera modifica: stampa l'elenco in ordine inverso.
Per la variante ricorsiva, questa è una modifica quasi banale alla funzione originale:
void print_list_reverse_rec(List* l)
{
if (l == NULL) return;
print_list_reverse_rec(l->next);
printf("Element: %d\n", l->element);
}
Per la funzione loop, tuttavia, abbiamo un problema. La nostra lista è collegata singolarmente e quindi può essere percorsa solo in avanti. Ma poiché stiamo stampando al contrario, dobbiamo iniziare a stampare l'ultimo elemento. Una volta raggiunto l'ultimo elemento, non possiamo più tornare al penultimo elemento.
Quindi, o dobbiamo effettuare molti re-traversing, oppure dobbiamo costruire una struttura di dati ausiliari che tenga traccia degli elementi visitati e dai quali possiamo quindi stampare in modo efficiente.
Perché non abbiamo questo problema con la ricorsione? Perché in ricorsione abbiamo già in atto una struttura di dati ausiliari: lo stack di chiamate di funzione.
Poiché la ricorsione ci consente di tornare alla precedente invocazione della chiamata ricorsiva, con tutte le variabili locali e lo stato per quella chiamata ancora intatti, otteniamo una certa flessibilità che sarebbe noioso da modellare nel caso iterativo.
I loop sono una forma speciale di ricorsione per raggiungere un compito specifico (principalmente iterazione). È possibile implementare un loop in uno stile ricorsivo con le stesse prestazioni [1] in diverse lingue. e nel SICP [2], puoi vedere che i loop sono descritti come "zucchero sintetico". Nella maggior parte dei linguaggi di programmazione imperativi, per e mentre i blocchi utilizzano lo stesso ambito della loro funzione padre. Nondimeno, nella maggior parte dei linguaggi di programmazione funzionale non esiste né per né mentre esistono loop perché non ce n'è bisogno.
Il motivo per cui le lingue imperative hanno per / mentre i cicli è che stanno gestendo gli stati mutandoli. Ma in realtà, se guardi da una prospettiva diversa, se pensi a un blocco while come una funzione stessa, prendendo il parametro, elaborandolo e restituendo un nuovo stato - che potrebbe anche essere il richiamo della stessa funzione con parametri diversi - tu può pensare al loop come a una ricorsione.
Il mondo potrebbe anche essere definito mutabile o immutabile. se definiamo il mondo come un insieme di regole e chiamiamo una funzione ultima che prende tutte le regole e lo stato corrente come parametri e restituiamo il nuovo stato in base a questi parametri che ha la stessa funzionalità (genera lo stato successivo nello stesso modo), potremmo anche dire che è una ricorsione e un ciclo.
nel seguente esempio, la vita è la funzione prende due parametri "regole" e "stato", e il nuovo stato verrà costruito nel prossimo tick.
life rules state = life rules new_state
where new_state = construct_state_in_time rules state
[1]: l'ottimizzazione delle chiamate di coda è un'ottimizzazione comune nei linguaggi di programmazione funzionale per utilizzare lo stack di funzioni esistente nelle chiamate ricorsive anziché crearne uno nuovo.
[2]: Struttura e interpretazione dei programmi per computer, MIT. https://mitpress.mit.edu/books/structure-and-interpretation-computer-programs
Un ciclo while è diverso dalla ricorsione.
Quando viene chiamata una funzione, si verifica quanto segue:
Un frame dello stack viene aggiunto allo stack.
Il puntatore del codice si sposta all'inizio della funzione.
Alla fine di un ciclo while si verifica quanto segue:
Una condizione chiede se qualcosa è vero.
In tal caso, il codice passa a un punto.
In generale, il ciclo while è simile al seguente pseudocodice:
if (x)
{
Jump_to(y);
}
Soprattutto, ricorsione e loop hanno diverse rappresentazioni del codice assembly e rappresentazioni del codice macchina. Ciò significa che non sono gli stessi. Potrebbero avere gli stessi risultati, ma il diverso codice macchina dimostra che non sono la stessa cosa al 100%.
Solo l' iterazione è insufficiente per essere generalmente equivalente alla ricorsione, ma l' iterazione con uno stack è generalmente equivalente. Qualsiasi funzione ricorsiva può essere riprogrammata come un ciclo iterativo con uno stack e viceversa. Ciò non significa che sia pratico, tuttavia, e in qualsiasi situazione particolare l'una o l'altra forma può avere chiari vantaggi rispetto all'altra versione.
Non sono sicuro del perché questo sia controverso. La ricorsione e l'iterazione con uno stack sono lo stesso processo computazionale. Sono lo stesso "fenomeno", per così dire.
L'unica cosa a cui riesco a pensare è che quando si considerano questi come "strumenti di programmazione", sono d'accordo che non dovresti pensare a loro come alla stessa cosa. Sono equivalenti "matematicamente" o "computazionalmente" (di nuovo iterazione con una pila , non iterazione in generale), ma ciò non significa che dovresti affrontare i problemi con il pensiero che uno dei due farà. Da un punto di vista dell'implementazione / risoluzione dei problemi, alcuni problemi potrebbero funzionare meglio in un modo o nell'altro, e il tuo compito come programmatore è di decidere correttamente quale è più adatto.
Per chiarire, la risposta alla domanda è un ciclo while intrinsecamente una ricorsione? è un no definito , o almeno "a meno che tu non abbia anche lo stack".