Programmazione 1
Transcript
Programmazione 1
19 3. Analisi degli Algoritmi Dato un problema, sia A un algoritmo che lo risolve. A può essere visto come una scatola in cui entrano gli input e da cui escono gli output. In certi casi non si ha interesse a sapere che cosa ci sia dentro la scatola, ma basta che l'uscita sia quella che ci si aspetta, noti i dati in ingresso. In questo caso, l'algoritmo è una black box, di cui interessa solo conoscere la coppia <Input, Output>. In altri casi, invece, occorre sapere che cosa ci sia dentro la scatola, per poter capire come le uscite siano ottenute dagli ingressi: l'algoritmo è dunque una glass box trasparente, di cui si può vedere l'interno. Gli aspetti di analisi degli algoritmi che ci interessano sono due: • Correttezza dell'algoritmo • Terminazione in un tempo finito • Generazione del risultato atteso • Complessità dell'algoritmo • Tempo (Numero di passi di esecuzione) • Spazio (Occupazione di memoria) Per definire in modo preciso la complessità di un algoritmo, in modo indipendente dal linguaggio di programmazione e dalla particolare macchina usata, si introduce un modello astratto di computazione. Noi useremo la Macchina di Turing, definita da Alan Turing negli anni '30. 3.1. Correttezza dei Programmi Dato un algoritmo o un programma π, come possiamo dimostrare che π è corretto ? Un modo empirico è quello di testare il programma su alcuni valori di ingresso e verificare che il valore di uscita è quello atteso. Siccome però non è possibile verificare il programma su tutti gli input (che in generale sono infiniti), si può simulare il programma su input significativi. Il testing si ferma se troviamo dei valori di input per cui il risultato non è corretto, nel qual caso il programma non è sicuramente corretto. Ma se non si trovano valori di input per cui il risultato non è corretto, non possiamo dire che il programma è corretto. In effetti il testing può al massimo rilevare la presenza di errori ma non la loro assenza. D’altra parte, prima di poter verificare che un programma è corretto, dobbiamo stabilire cosa il programma deve fare. Per fare questo, useremo (a volte in modo informale) il calcolo dei predicati. In particolare, esprimeremo quali limitazioni devono avere gli input (asserzione iniziale) e quale deve essere la relazione fra le variabili di input e quelle che vengono restituite come output (asserzione finale). Le asserzioni sono formule del calcolo dei predicati che esprimono le relazioni fra le variabili usate nell’algoritmo. Per verificare poi che il programma sia corretto, decoreremo il diagramma di flusso con asserzioni che ci permettano di dimostrare che l’asserzione finale può essere derivata da quella iniziale attraverso l'esecuzione delle istruzioni del programma. Per far questo introdurremo delle regole che ci permettono di propagare le asserzioni nel diagramma di flusso. Prima e dopo ogni istruzione si annotano le relazioni fra le variabili, e cioè le asserzioni, che valgono a quel punto del diagramma. Chiamiamo la condizione che precede una istruzione premessa e quella che segue conclusione. 20 premessa conclusione Le tre regole che seguono stabiliscono se il diagramma è decorato con asserzioni consistenti. Regola 1 : Se ci sono più flussi in ingresso in un blocco, deve esserci un'implicazione logica tra le conclusioni dei blocchi precedenti e la premessa del blocco. In altre parole, P, C1, C2, e C3 devono essere tali che la formula α ≡ (C1 → P ) ∨ ( C2 → P ) ∨ ( C3→ P ) sia vera. ❐ C1 C2 C3 P Regola 2 : Se vale l'asserzione P prima di un blocco di decisione controllato dall'espressione test, allora all’uscita "SI" deve valere sia P che test, mentre all’uscita "NO" deve valere sia P che ¬ test. ❐ P NO test SI P ∧ ¬test P ∧ test Regola 3 : Se in un blocco assegnamo l'espressione expr alla variabile v e vale l'asserzione P(expr) prima del blocco, allora, dopo l'assegnamento, vale l’asserzione P(v), e cioè l’asserzione P in cui alcune delle occorrenze della espressione expr sono sostituite da v. ❐ P(expr) v ← expr P(v) Vediamo alcuni semplici esempi delle tre regole. 21 Esempio 18 – Esempio di blocco con due ingressi: C1 ≡ - 10 ≤ x < 0 C2 ≡ 0 ≤ x < 10 Le asserzioni P1 ≡ - 10 ≤ x < 10 e P2 ≡ x ≥ -10 sono corrette, mentre l'asserzione P3 ≡ x ≥ 10 non lo è. ❐ Esempio 19 – Consideriamo il blocco di assegnamento che calcola una somma: x+y =0 z ← x + y z = 0 La premessa del blocco è P(x + y) ≡ x + y = 0. Sostituendo a (x + y) la variabile z otteniamo z = 0 (in questo caso ho sostituito tutte le occorrenze dell’espressione (x + y) con z. ❐ Esempio 20 – Consideriamo la seguente assegnazione: True z ← 3 z =3 Non occorre alcuna premessa per poter derivare l’asserzione z = 3 dopo l'assegnamento, poichè l’asserzione True = P(3) ≡ ( 3 = 3) è sempre vera. Possiamo quindi sostituire la prima occorrenza di 3 con z, ottenendo l’asserzione z = 3. Quando nel diagramma di flusso si ha un ciclo, l’istruzione iniziale del ciclo ha più di un ingresso. In particolare ne ha uno che arriva dalle istruzioni precedenti il ciclo e uno che ritorna dalla fine del ciclo. La Regola 1 ci dice che l’asserzione che deve valere all’inizio del ciclo deve essere implicata sia dall’asserzione alla fine delle istruzioni che precedono, sia da quella alla fine del ciclo. Chiamiamo tale asserzione l’invariante del ciclo, perchè l’asserzione deve rimanere vera a ogni iterazione. All’uscita del ciclo vale sia l’invariante che la condizione di terminazione. Esempio 21 – Consideriamo l'Esempio 15, relativo alla somma dei primi n numeri. Il diagramma di flusso decorato con le asserzioni è quello riportato sotto. L'invariante del ciclo è il seguente: (acc = 1 + 2 + ... + k) ∧ (1 ≤ k ≤ n) ∧ (n > 0) L’asserzione iniziale su n è n > 0. Questa asserzione, se è vera dopo aver letto n lo sarà sempre poiché n non viene mai modificato. Quando si entra nel ciclo la prima volta, si ha k = 1 e acc = 0, per cui l’invariante è soddisfatto. 22 n n>0 acc ← 0 k ← 1 (acc = 1+ 2 + ... + k) (1 k n) (n > 0) km == nn k <n acc←acc+k SI acc acc NO acc = 1+2+.. + n k ← k+1 (acc = 1+2+... + (k -1)) (1 k n) Dopo il test, se n non è uguale ad k, questo implica che n > k. Inoltre acc = 1 + 2 + ... + k implica acc = 1 + 2 + ... + (k+1), quando k viene incrementato di 1 e aggiunto ad acc. Di conseguenza, dopo l’assegnamento ad acc riotteniamo l’invariante. La condizione di terminazione k = n e l’invariante fanno sì che prima della scrittura di acc valga l'asserzione finale del programma acc = 1 + 2 + … + n. La costanza dell'invariante può essere provata per induzione matematica ❐ 3.2. Terminazione dei Programmi E’ importante notare che, in aggiunta all’invariante, per essere sicuri che un algoritmo o programma è conforme alle sue specifiche, esso deve terminare in un numero finito di passi. Determinare se un programma si fermerà su certi dati di ingresso non può essere fatto in modo uniforme. In altri termini, non è possibile costruire un algoritmo generale che, preso in ingresso un generico programma A e i suoi input, restituisce una risposta "SI" se A si fermerà e "NO" altrimenti. Di conseguenza, occorre usare metodi potenzialmente diversi per provare la terminazione di algoritmi/programmi diversi. Se un algoritmo non si ferma, è certo scorretto, ma se termina, non è detto che lo sia. Infatti può contenere errori sintattici, logici o semantici pur terminando in tempo finito. 23 3.3. Complessità La complessità di un algoritmo è una misura delle risorse da esso utilizzate. Le risorse fondamentali sono di due tipi: • lo spazio di memoria occupato • il tempo impiegato per l'esecuzione. Questi due tipi di risorse differiscono per la loro natura, in quanto lo spazio è una risorsa riutilizzabile, mentre il tempo non lo è. Il fatto che lo spazio sia riutilizzabile rende difficile calcolarne l’utilizzazione. In queste note ci occuperemo soprattutto (e in maniera molto introduttiva) della complessità in termini di tempo. La complessità temporale di un programma è una misura di quanto tempo il programma impiega a calcolare il suo risultato. Le domande che ci dobbiamo fare a questo proposito sono: 1. Come calcolare il tempo 2. Quali parametri utilizzare per il calcolo. Calcolare il tempo in secondi/minuti non è un buon metodo, perchè in questo caso il tempo dipenderebbe dalla macchina utilizzata e ovviamente dall’input che viene fornito al programma. Occorre invece un'unità di misura astratta, indipendente dagli aspetti di implementazione, e intrinseca all'algoritmo stesso. Il numero delle operazioni eseguite ci fornisce tale unità di misura. Contare il numero delle operazioni ci dà una valutazione a meno di un fattore costante, poichè l’assegnamento v← 3 è certemente più semplice di quello v← (a+b)/c, ma, con questa misura, il calcolo del tempo per entrambi gli assegnamenti avrebbero peso 1. Questo fatto, comunque, appare ragionevole, dato che macchine diverse hanno un diverso set di istruzioni che potrebbe rendere semplici calcoli apparentemente complessi. Inoltre, non siamo interessati alla valutazione precisa della complessità di un algoritmo (che, quasi sempre, risulta impossibile da calcolare), ma a un suo limite superiore. Quindi, per ogni operazione elementare, si può considerare la massima complessità della sua esecuzione. Come operazioni elementari, valutate 1 passo, consideriamo: • Le operazioni aritmetiche (+, –, *, /, Div, Mod) • Le operazioni logiche (NOT, AND, OR) • I confronti (=, ≠, >, ≥, <, ≤) • La lettura di un valore • La scrittura di un valore • L'assegnamento di un valore a una variabile. La complessità, inoltre, dipenderà dalla dimensione dei dati di ingresso. Indicando genericamente con n questa dimensione, ci interessa sapere come aumenta la complessità di un algoritmo al crescere di n. Per questo, occorre introdurre la nozione di ordine di grandezza. Date due funzioni f(n) e g(n) dal dominio degli interi N allo stesso dominio N: f:N→N g : N → N, Consideriamo il loro rapporto al crescere di n. Può accadere che: 24 lim n→∞ f(n) = ∞0 g(n) La funzione f(n) è di ordine di grandezza inferiore, rispetto a g(n), al crescere di n f(n) a ≠ 0, ∞ lim = ∞ n→∞ g(n) Le funzioni f(n) e g(n) sono dello stesso ordine di grandezza f(n) lim = ∞ n→∞ g(n) La funzione f(n) è di ordine di grandezza superiore, rispetto a g(n), al crescere di n. Introduciamo adesso la notazione O(.), e cioè "ordine di". Diremo che: f(n) lim = 0, a f(n) = O(g(n)) sse n→∞ g(n) Dire che f(n) è dell'ordine di g(n), non vuol dire che esse sono dello stesso ordine di grandezza, ma che la funzione f(n) è di un ordine di grandezza non superiore a quello di g(n). Formalmente: f(n) = O(g(n)) sse ∃n0 ∃(c > 0) [∀(n ≥ n0 ) [f(n) ≤ c g(n)]] (7) Nella Figura 11 la relazione tra f(n) e g(n) descritta dalla (7) è rappresentata graficamente. c g(n) f(n) 1 2 ………. n0 n Figura 11 – Significato della relazione f(n) = O(g(n)). Dopo un certo valore n0 la f(n) deve essere sempre inferiore alla g(n), eventualmente moltiplicata per una costante positiva (c può essere 1). Prima di n0 non ha importanza se la f(n) sia o no maggiore di c g(n). Vediamo alcuni esempi di ordini di grandezza: • f(n) = 50n2 +20n +3 = O(n2) • f(n) = n3+ 30n2+100 = O(n3), n n • f(n) = 3 + 100 n3 = O(3 ) Nel primo e secondo caso sarebbe corretto anche scrivere O(n4), anche se questo fornirebbe una stima per eccesso. Nel calcolo degli ordini di grandezza, dunque, si possono trascurare sia gli addendi di ordine inferiore che le costanti addittive e moltiplicative. Supponiamo di avere una macchina che svolge 1.000.000 di operazioni al secondo. Ecco come varia la durata di un programma in funzione dell'input e della complessità dell'algoritmo che esso implementa. Le righe corrispondono al tempo di esecuzione: 25 lineare il primo, quadratico il secondo ed esponenziale il terzo. Le colonne corrispondono alla dimensione dell’input (da cui dipende la complessità). f(n) 100n 10n2 2n n = 10 1 ms 1 ms 1 ms n = 30 3 ms 9 ms 18 min n = 60 6 ms 36 ms 366 secoli Come si vede, per n piccoli non c’è grande differenza. All’aumentare di n cresce la differenza fra i tempi di esecuzione. 3.4. Macchina di Turing Una Macchina di Turing è costituita da tre elementi: • Un nastro di lettura e scrittura, su cui si scrivono i dati di ingresso e si leggono i risultati. Il nastro è illimitato in lunghezza, con caselle numerate da – ∞ a +∞. • Una unità di controllo a stati finiti, che contiene il programma. • Una testina di lettura/scrittura, che legge e scrive sul nastro. Lo schema della macchina è rappresentato nella Figura 12. Le caselle del nastro contengono, all'inizio, un simbolo speciale , b , che indica "casella vuota". Definiamo: • Un alfabeto Σ con cui si scrivono i dati di ingresso. • Un alfabeto esteso Γ = Σ ∪ { b } che contiene tutti i simboli che si possono leggere o scrivere sul nastro. • Un insieme ∆ = {+, –, =}, che indica i possibili movimenti della testina (avanti di una casella, indietro di una casella, sta ferma). • Un insieme Q di stati. L'insieme Q è l'unione di tre insiemi: Q = {q0} ∪ {q1, …, qr} ∪ QF, dove q 0 è lo stato iniziale, Q F è l'insieme degli stati finali e {q 1, …, q r } è l'insieme degli stati intermedi. • Una funzione di transizione δ : (Q - QF ) × Γ → (Q - {q0}) × Γ × ∆. La funzione δ è il programma della Macchina di Turing. Possiamo anche scrivere δ nella seguente forma: (q',s',d) = δ(q,s). CSF R/W ..... b b b b b b b b b b b b ..... –1 0 1 2 3 4 5 Figura 12 – Schema della Macchina di Turing. La funzione δ specifica che se il controllo si trova nello stato q e nella casella su cui è posizionata la testina è scritto il simbolo s, allora il controllo passa nello stato s', la testina scrive nella stessa casella il simbolo s' ed esegue il movimento indicato da d. 26 Siamo adesso in grado di introdurre le nozioni che servono per definire la complessità. Innanzi tutto, i dati di ingresso vengono scritti sul nastro, a partire dalla casella 1 in avanti; sia σ la stringa scritta nell'alfabeto Σ che rappresenta i dati: σ = s1, …, sn La lunghezza n = |σ| di σ è la dimensione del problema considerato. Inoltre, un passo elementare è il trattamento di una casella (lettura e scrittura) del nastro. Quindi, il numero di passi di un algoritmo è precisamente il numero di caselle visitate durante l'esecuzione. Osserviamo che questo numero può essere molto maggiore di n, perchè la testina può visitare più volte la stessa casella, andando avanti e indietro. Dato un insieme ∏ di problemi e un algoritmo A che lo risolve, sia ∏n il sottoinsieme di ∏ che contiene problemi di dimensione n. Consideriamo un problema particolare π ∈ ∏n e facciamo eseguire il programma che risolve ∏ alla Macchina di Turing; sia TA(π,n) il numero di passi eseguiti dalla macchina. Chiaramente TA(π,n) è la complessità dell'algoritmo eseguito sul problema π di dimensione n. Questa definizione di complessità è però troppo dettagliata, ed è impossibile da valutare per ogni π. Usiamo quindi una definizione più generica: consideriamo come complessità dell'algoritmo A sui problemi di dimensione n il massimo, calcolato su tutti i problemi in ∏n, del numero di passi fatti da A. Quindi, la complessità non sarà funzione di π, ma solo di n : TA(n) = M a x TA(π,n) (8) π ∈∏n Esempio 22 – Consideriamo una Macchina di Turing e siano Σ = {0, 1} l'alfabeto di ingresso e Q = {q0, q1, q2, qY, qN} l'insieme degli stati. La funzione di transizione δ è data in forma tabulare come segue: 0 1 q0 (q0, 0, +) (q0, 1, +) q1 b , –) (qY, b , –) (qN, 1, =) q2 (q2, b (q1, b , –) (qN, b , =) (qN, b , =) (qN, 1, =) La stessa funzione può essere rappresentata graficamente, come in Figura 13. La funzione di transizione δ rappresenta un programma che decide se un numero m, scritto in binario, è divisibile per 4. Nella Figura 14 è riportata l'esecuzione del programma per m = 24. (0,0,+) ( b , b , =) q1 q0 ( b,b ,–) (1, 1, = ) (1,1,+) qN qY (0,b,–) q2 (1, 1, =) ( b , b ,= ) Figura 13 – Rappresentazione grafica della funzione di transizione δ, descritta dalla tabella precedente. 27 Il programma legge dapprima tutto il numero senza modificarlo, individuando la sua fine quando incontra il primo blank. A questo punto, si riposiziona sull'ultima cifra (binaria) del numero e controlla se essa è "0". Se non lo è, il numero non può essere divisibile per 4, perché è dispari (i numeri pari terminano per 0), e quindi il controllo passa nello stato finale qN, che corrisponde alla risposta "NO". Se l'ultima cifra è 0, allora si deve controllare la penultima: se questa è 0, la risposta è "SI" e il controllo passa nello stato finale qY, altrimenti la risposta è di nuovo "NO". b 1 b 0 1 0 0 0 b b b q 0 b b 1 0 1 0 0 0 b 1 0 1 0 0 0 b b q b b b b b b b 0 b q 1 b b 1 0 1 0 0 b q 1 b b 1 0 1 0 0 b q b b b b b b b 2 b b 1 0 1 0 b q Y Figura 14 – Esecuzione del programma che controlla se un numero m è divisibile per 4, eseguito per m = 24. Nel nastro si legge il quoziente della divisione. Osserviamo che la funzione δ fornisce il risultato NO quando m = 0 oppure quando sul nastro non è scritto alcun numero (tutti blank). Il programma considerato ha una dimensione n = lg2m (arrotondato all'intero superiore, se m non è una potenza di 2. Esso controlla al massimo (n + 1 + 2) caselle. Quindi, la sua complessità f(n) è data da: f(n) = O(n) = O(lg2m) ❐ La Macchina di Turing è un modello astratto che, pur nella sua semplicità, riesce a computare qualunque algoritmo si possa pensare: è una macchina universale. Questa asserzione costituisce la Tesi di Church e non è mai stata nè provata nè confutata. La potenza della macchina deriva dalla illimitatezza in lunghezza del suo nastro. 3.5. Universal Register Machine (URM) Ci sono altre macchine universali, che calcolano tutto quello che può calcolare la Macchina di Turing. Una di queste, più vicina al concetto di calcolatore usuale, è la URM (Universal Register Machine). La URM è una macchina che riconosce quattro istruzioni e opera su un numero illimitato di registri, denominati Rj (j ≥ 1) e ordinati 28 per indice crescente. Le istruzioni, che hanno per argomenti i registri, sono le seguenti: Z(Rj) S(Rj) T(Ri,Rj) J(Ri,Rj,h) Azzera il contenuto del registro Rj Incrementa di 1 il contenuto del registro Rj Copia il contenuto del registro Ri nel registro R j, lasciando inalterato il registro Ri Se il contenuto del registro Ri è uguale al contenuto del registro Rj, salta all'istruzione numero h del programma, altrimenti continua sequenzialmente l'esecuzione. Un programma sulla URM è una sequenza di istruzioni, numerate da 1. Esso viene denotato con Nome(a,b, ..,), dove Nome è il nome del programma, e a, b, … sono eventuali valori di inizializzazione, che vengono assegnati, per definizione, ai registri R1, R2, … in ordine. Il programma si ferma quando non ci sono più istruzioni da eseguire o quando salta a una istruzione che non è stata definita. Il risultato, di norma, si legge nel registro R1. Esempio 23 – Scrivere un programma Somma(x,y) che calcola la somma aritmetica di x e y. Dal nome del programma deduciamo che il numero x va scritto nel registro R1 e il numero y nel registro R2. Il metodo di calcolo è quello del successivo incremento del contenuto di R1 tante volte quante sono le unità di y. Somma(x,y) 1 Z(3) {Azzera R3} 2 J(2,3,6) {Se R2 = R3 salta all'istruzione 6 e termina} 3 S(1) {Incrementa R1} 4 S(3) {Incrementa R3} 5 J(1,1,2) {Ritorna all'istruzione 2} Il registro R 3 funge da contatore, che controlla quante unità vanno sommate a x in R1. Quindi, questo algoritmo esegue un numero di passi O(Max[x,y]). Osserviamo che l'istruzione J(1,1,2) è un salto incondizionato, perchè il test è sempre vero. ❐ Esempio 24 – Scrivere un programma Conf(x,y) che determina se x ≥ y. Dal nome del programma deduciamo che il numero x va scritto nel registro R 1 e il numero y nel registro R2. Il metodo per il confronto è quello di vedere se, incrementando R 3, si raggiunge prima x o y. Se si raggiunge prima y, allora è vero che x ≥ y e si pone 1 in R1, altrimenti R1 contiene 0 come risultato. Conf(x,y) 1 Z(3) 2 J(1,3,6) 3 J(2,3,6) 4 S(3) 5 J(1,1,2) 6 Z(1) 7 J(2,3,9) 8 J(1,1,10) 9 S(1) {Azzera R3} {Se R1 = R3 salta all'istruzione 6 } {Se R2 = R3 salta all'istruzione 6} {Incrementa R3} {Ritorna all'istruzione 2} {Azzera R1} {Se R2 = R3 salta all'istruzione 9} {Halt} {Incrementa R1} 29 Vediamo come viene eseguito il programma Conf(6,3). Istruzione eseguita – 1 2,3,4 5,2,3,4 5,2,3,4 5,2,3 6,7 9 R1 6 6 6 6 6 6 0 1 R2 3 3 3 3 3 3 3 3 R3 – 0 1 2 3 3 3 3 Il risultato (1 = True) si legge in R1. Anche questo algoritmo esegue un numero di passi O(Max(x,y)). ❐ 3.6. Invariante di Ciclo Dato un programma con un ciclo del tipo rappresentato in Figura 15(a), possiamo astrarlo come in Figura 15(b), in cui è evidenziata la variabile k, che conta il numero di esecuzioni del ciclo, e le variabili in ingresso e in uscita. n y=a yk-1 k=1 k y = f(y, k,...) k=k+1 NO k=n (a) SI yk y (b) Figura 15 – (a) Ciclo con numero fisso di esecuzioni. (b) Descrizione astratta del ciclo. Nella descrizione astratta del ciclo (Vedi Figura 15( b)), viene esplicitata la relazione tra le variabili di ingresso e di uscita. Il contatore k indica quante volte il ciclo è stato eseguito. Alla descrizione astratta occorre associare: • Lo stato iniziale, descritto dall'asserzione di inizio ciclo. Quando k = 1, y0 denota il valore di inizializzazione della variabile y, assegnato prima che il ciclo venga eseguito per la prima volta. 30 • La condizione di terminazione. In questo caso il ciclo finisce quando k = n, perchè si conosce il numero totale di esecuzioni. • La relazione input-output, desunta dal contenuto delle operazioni all'interno del ciclo stesso: yk = f(yk-1, k, …) La funzione f(yk-1, k, …) permette di esprimere l'invariante del ciclo e di provare la correttezza dell'algoritmo. Esempio 25 – Calcolare la potenza x n di un numero positivo x, essendo n un intero ≥ 1. Possiamo fare il calcolo mediante moltiplicazioni ripetute: n y=x =xxx…x Il diagramma di flusso è il seguente: n, x (n ≥ 1) ∧ (x > 0) y=1 (y = xk) ∧ (k < n) k=1 y=y*x NO k=k+1 (k = n) ∧ (y = xn) SI k=n y Il ciclo può essere sintetizzato come segue: yk-1 Asserzione iniziale: (n ≥ 1) ∧ (x > 0) Condizione iniziale: y0 = 1 Condizione di terminazione: k = n yk = yk-1 * x Invariante: y = xk k yk Terminazione: L'algoritmo termina perchè n è ≥ 1 e il ciclo viene eseguito esattamente n volte. Correttezza: Proviamo la correttezza mediante il principio di induzione matematica. L'invariante è vero per k = 1; infatti, y1 = x1 e questo è il passo base dell'induzione. Assumiamo ora che esso sia vero fino a (k-1); per ipotesi induttiva, sarà quindi yk-1 = xk-1. Ma abbiamo che yk = yk-1 * x, quindi si otterrà che y k = x k-1* x = xk. L'invariante allora è vero per ogni k ≥ 1. In particolare, sarà vero per k = n e dunque il risultato fornito sarà y = xn. 31 Complessità: L'algoritmo ha una parte lineare, comprendente 4 operazioni elementari prima del ciclo e 1 alla fine (scrittura di y). Il ciclo ha anch'esso 5 operazioni elementari ed è eseguito in modo completo (n - 1) volte. Quando k = n vengono eseguite solo 3 operazioni, perchè il ciclo si interrompe al test. Quindi, indicamdo con C(n) la complessità dell'algoritmo, avremo: C(n ) = 5 + 5 (n-1) + 3 = 5n + 3 = O(n) ❐ Esempio 26 – Calcolare la somma dei primi n numeri interi accumulandoli in modo discendente. Consideriamo il diagramma di flusso: n n>0 acc←0 acc←n m←0 m←n acc = n + (n-1) +...+ m ∧ 0 ≤ m ≤ ∧ n > 0 m >0 acc←acc+m m = 0 m←m+1 m←m-1 acc acc = 1+2+.. + n acc = 1+2+... +(m + 1) ∧ (0 < m ≤ n) Le considerazioni per provare la terminazione, la correttezza e per calcolare la complessità sono analoghe a quelle dell'Esempio 21. ❐