Iterazione enumerativa (for). Il costrutto iterativo for fornisce il
modo più semplice per eseguire ripetutamente una sequenza di
operazioni quando sia noto a priori il numero di ripetizioni da
eseguire (iterazione enumerativa).
for esamina il valore di una espressione di controllo o condizione,
e se la trova diversa da zero (o “vera”) esegue una o più istruzioni,
altrimenti esce dal ciclo.
Dopo ogni esecuzione delle istruzioni del ciclo, ne viene eseguita una
che modifica il valore dell’espressione, che viene quindi controllata
di nuovo.
Se ha ancora valore diverso da zero il ciclo ricomincia.
Le istruzioni vengono eseguite un numero prefissato di volte, dopo di
che si ha l’uscita dal ciclo.
Il diagramma di flusso è indicato in figura.
Ad esempio, per azzerare le componenti di un vettore a[k] di 100
componenti, si possono scrivere le istruzioni seguenti:
for (k = 1; k <= 100; k = k + 1)
a[k] = 0;
dove la parentesi che segue for contiene, nell’ordine,
• l’inizializzazione della variabile di controllo (k);
• la condizione che, se risulta vera, determina la ripetizione del ciclo;
• l’istruzione che fa variare la variabile di controllo.
Inizialmente l’istruzione for imposta il contatore k a 1, e dato che
questo valore rende vera (cioè diversa da zero) l’espressione di
controllo (k <= 100), viene eseguita l’(unica) istruzione, che azzera
il valore della componente a[1].
Quindi for incrementa di 1 il valore di k, e risultando k = 2 (e
quindi ancora k <= 100), viene ripetuta l’istruzione. Viene così
azzerato il valore di a[2], e così via.
Dopo che è stato azzerato il valore di a[100], risulta k = 101, il
che determina l’uscita dal ciclo e l’esecuzione della istruzione
successiva (se presente).
Il valore finale del contatore può essere:
• maggiore di quello iniziale, come in questo esempio, oppure
minore; in tale caso la variabile contatore va decrementata a ogni
passo di iterazione;
• una costante oppure una variabile (alla quale sia stato
naturalmente assegnato in precedenza un valore).
Esempio: Somma dei primi n numeri naturali. Le pseudo istruzioni
sono le seguenti:
leggi n;
somma = 0;
for (i=1; i<=n; i=i+1)
somma = somma + i;
Il corrispondente diagramma
di flusso è indicato in figura.
Osserviamo che l’algoritmo qui impiegato non è molto efficiente, dato
che la somma dei primi n numeri naturali è fornita direttamente dalla
formula di Gauss
n*(n+1)/2
ma l’esempio è solo indicativo del funzionamento di molti dei cicli che
si incontrano in pratica.
Iterazione con guardia all’inizio (while). Nel caso invece in cui non
sia noto a priori o non si voglia indicare il numero di ripetizioni da
eseguire (iterazione non enumerativa), il linguaggio C mette a
disposizione i due costrutti while e do-while, il primo con guardia
all’inizio (come for), il secondo con guardia alla fine.
Il costrutto while inizia esaminando il valore di una espressione
di controllo o condizione.
Se essa è diversa da zero (ossia è vera) le istruzioni del ciclo vengono
eseguite, altrimenti si ha l’uscita dal ciclo.
Dopo le istruzioni del ciclo ne viene eseguita una che altera il valore
dell’espressione di controllo, e quando questa risulta uguale a zero si
ha l’uscita del ciclo.
while realizza il diagramma di
flusso già visto
Se il ciclo è costituito da più istruzioni, esse vanno racchiuse tra
parentesi graffe, { }, secondo lo schema seguente:
while (condizione)
{
istruzione_1 ;
.........
;
istruzione_n ;
}
Esempio: Massimo Comun Divisore (2). Come esempio di iterazione
non enumerativa con guardia all’inizio, si consideri il seguente
algoritmo, derivato anch’esso dal metodo di Euclide per trovare il
Massimo Comun Divisore (MCD) di due interi positivi.
Per trovare il MCD di due interi positivi:
• si sottrae il minore dal maggiore
• se la differenza è uguale al minore, essa rappresenta
il MCD cercato
• altrimenti la differenza trovata sostituisce il maggiore
dei due numeri, e il procedimento ricomincia da capo.
Ecco le relative pseudo istruzioni
leggi X, Y;
fino a che (X!=Y)
{
se (X > Y) X=X-Y;
altrimenti Y=Y-X;
}
stampa X;
fine
e il corrispondente
diagramma di flusso.
Esempio: Stampa numero massimo. Come altro esempio di
iterazione non enumerativa con guardia all’inizio, si può considerare
il problema di:
trovare il massimo di un elenco di numeri immessi da tastiera.
In questo caso non conviene usare una iterazione enumerativa perché
potrebbe non essere noto a priori il numero di numeri che si
immetteranno (e quindi il numero di iterazioni da eseguire).
Conviene invece terminare l’elenco dei numeri con uno, detto
sentinella, che sicuramente non si presenterà nell’elenco, e ogni
volta che si legge un numero immesso controllare se esso sia
diverso dalla sentinella.
In caso affermativo il programma prosegue, in caso negativo termina.
Chiamiamo:
• dato la variabile nella quale sono copiati i valori di volta in volta letti
• max la variabile in cui viene copiato il valore di dato se risulta
dato>max
e poniamo la sentinella =0.
L’espressione dell’algoritmo in
pseudo istruzioni è la seguente:
poni max = 0;
leggi dato;
while (dato!=0)
{
if dato > max
max = dato;
leggi dato;
}
stampa max;
Il diagramma di flusso è indicato
in figura.
Esempio: Calcolo del fattoriale (1). Molte volte i cicli for e while
sono intercambiabili.
Si consideri, ad esempio, il fattoriale, n!, di un numero intero n,
definito come il prodotto degli interi 1 * 2 * 3 * ... * n
(definizione iterativa).
Quindi:
1! = 1
2! = 1 * 2 = 2
...
5! = 1 * 2 * 3 * 4 * 5 = 120
Questa definizione può dare luogo (per n  1) al semplice
diagramma di flusso seguente:
Esso può essere realizzato dal ciclo while seguente:
fatt = 1;
i = 0;
while (i < n)
{
i = i + 1;
fatt = fatt * i;
}
oppure dal ciclo for seguente:
fatt = 1;
for (i = 1; i <= n; ++i)
fatt = fatt * i;
Vedremo più avanti un approccio del tutto diverso per calcolare il
fattoriale.
Iterazione con guardia alla fine (do-while). Il ciclo do-while inizia
con la parola do, seguita dalla o dalle istruzioni da eseguire, e
termina con la parola while seguita dalla condizione che deve
essere verificata per la ripetizione del ciclo.
Esso realizza il diagramma di flusso già visto
Esempio: moltiplicazione tramite addizioni ripetute. Come primo
esempio di ciclo do-while, consideriamo le pseudo istruzioni che
traducono l’algoritmo della moltiplicazione tramite addizioni ripetute.
p = 0;
k = a;
do
{
p = p + b;
k = k - 1;
}
while (k == 0);
Naturalmente il costrutto do-while consente di eseguire anche un
ciclo nel quale sia noto a priori il numero di ripetizioni, cioè una
iterazione enumerativa.
Si consideri il seguente esempio.
Esempio: Somma di 5 numeri. Per scrivere un algoritmo che legga 5
numeri e ne stampi la somma usiamo le seguenti variabili:
• S per contenere la somma
• K per contare il numero dei valori letti (ossia quante volte è stato
eseguito il ciclo),
• X per indicare il numero di volta in volta letto.
Le pseudo istruzioni sono le seguenti:
poni S = 0;
poni K = 0;
do
{
leggi X;
poni K = K+1;
poni S = S+X;
}
while (K != 5);
stampa S;
Ecco il corrispondente
diagramma di flusso
La traduzione di queste istruzioni in un programma C è immediata,
come vedremo più avanti.
Esercizio. Una leggera modifica dei questo diagramma di flusso,
ponendo la guardia all’inizio, consente di risolvere il seguente
problema:
scrivere un algoritmo che legga 5 numeri e
stampi la somma di quelli maggiori di 100
come indica la figura seguente.
Calcolo della radice quadrata. Il calcolo della radice quadrata di un
numero x0 può essere eseguito con il metodo delle
approssimazioni successive. Questo consiste nel calcolare
dapprima il valore di prima approssimazione, x1, con la formula
x1 
x0
2
Da x1 si calcola il valore di seconda approssimazione, x2, con la
formula
x0
 x1
x1
x2 
2
Se in questa formula si sostituisce a x1 il valore trovato di x2, si ottiene
x3
x0
 x2
x
x3  2
2
e così di seguito.
Il calcolo termina quando si è calcolato un valore che differisca dal
precedente meno di un numero scelto piccolo a piacere (eps).
Supponiamo, per esempio, di volere calcolare la radice quadrata di
x0 = 16. Avremmo:
x1
x2
x3
x4
x5
=
=
=
=
=
16/2 = 8
(16/8 + 8)/2 = 5
(16/5 + 5)/2 = 4,1
(16/4,1 + 4,1)/2 = 4,0012195121
(16/4,0012195 + 4,0012195)/2 = 4,0000001858
L’algoritmo si traduce nelle seguenti pseudo istruzioni:
leggi x0, eps;
x2 = x0/2;
do
{
x1 = x2;
x2=(x0/x1+x1)/2;
d = x1 - x2;
}
while (d > eps)
stampa x2;
e quindi nel diagramma
di flusso indicato a lato
Esercizio: stampa di numeri pari. Scrivere un algoritmo che stampi i
numeri pari di un elenco.
L’algoritmo può essere il seguente:
Si calcola il resto della divisione per 2 di ogni valore
dell’elenco, e si stampano i soli valori per i quali tale resto sia 0.
Si ripete l’operazione per tutti i valori dell’elenco, supponendo
noto il loro numero.
Se indichiamo con:
- n il numero degli elementi dell’elenco
- x l’elemento via via letto
- r il resto della divisione per 2 di ogni elemento (pertanto r = 0 se
l’elemento letto è pari)
- cont un contatore che memorizza il numero di elementi letti
(ossia di ripetizioni del ciclo)
il precedente algoritmo corrisponde
alle pseudo istruzioni seguenti:
leggi n;
poni cont = 0;
do
leggi x;
dividi x per 2 e chiama
r il resto;
se r == 0 stampa x;
poni cont = cont+1;
while cont != n
e quindi al diagramma di flusso
indicato a lato
Cicli nidificati. Anche i cicli do-while, come gli altri, si possono
trovare l’uno dentro l’altro, cioè essere nidificati.
Vediamo, a questo proposito, un algoritmo che stampa la tavola
pitagorica di dimensione n qualsiasi.
Ovviamente esso può essere realizzato sia con un ciclo do-while,
sia con uno for.
Confronto tra do-while e while. Si considerino i seguenti segmenti
di programmi:
cont = 10;
do
{
printf(“%d “, cont);
-- cont;
}
while (cont >= 5);
cont = 10;
while (cont >= 5)
{
printf(“%d “, cont);
-- cont;
}
Sebbene producano entrambi la stampa dei numeri 10, 9, 8, 7, 6, 5,
dal punto di vista concettuale essi non sono equivalenti.
Infatti nel ciclo do-while, nel quale la guardia è alla fine, le istruzioni
contenute all’interno sono eseguite almeno una volta, mentre nel
ciclo while, nel quale la guardia è all’inizio, le istruzioni potrebbero
anche non essere eseguite mai.
Pertanto lo schema do-while non consente di rappresentare
facilmente tutti i casi di iterazione che si possono incontrare in
pratica, e quindi la scelta tra i due tipi di cicli va effettuata con
attenzione.
Mostriamo ciò con il seguente esempio.
L’algoritmo della divisione con
sottrazioni ripetute, già visto
si traduce nel seguente
ciclo while
quoz = 0;
res = num;
while (res >= den)
{
res = res - den;
quoz = quoz + 1;
}
In questo algoritmo lo schema while esprime la ripetizione di una
sequenza di due azioni, controllata dalla condizione res >= den,
che specifica la condizione per rimanere dentro l’iterazione.
La condizione viene valutata prima di ogni esecuzione della sequenza
e, se è falsa, la ripetizione termina (o non ha inizio, se si è all’inizio
del processo).
Viceversa, se utilizzassimo il seguente ciclo do-while
quoz = 0;
res = num;
do
{
res = res - den;
quoz = quoz + 1;
}
while (res < den);
scriveremmo una sequenza non corretta, in quanto fornisce un
risultato errato se il dividendo è minore del divisore.
Ad esempio, se (num == 15) e (den == 20), otterremmo
quoz = 1 e res = –5
(al posto dei valori corretti quoz = 0 e res = 20).
Affinché la sequenza sia corretta occorre che le istruzioni del ciclo
non siano eseguite quando la condizione di ripetizione (res <
den) si verifica prima dell’inizio dell’iterazione.
Osserviamo che ciascuna delle tre strutture, sequenza, selezione,
iterazione, presenta un unico punto di entrata e un unico punto di
uscita.
Ciò comporta che esse siano “componibili” sequenzialmente, ovvero
che si possano collegare in successione.
Esse si possono anche nidificare l’una dentro l’altra, senza alcun
limite.
Iterazione e ricorsività
La potenza dei calcolatori è dovuta alla loro capacità di eseguire in
modo ripetitivo lo stesso compito o versioni diverse di esso.
Nell’informatica il concetto di iterazione s’incontra in molte forme,
tra le quali i modelli dei dati dove molti concetti, come le liste,
sono definiti in modo ripetitivo.
Per esempio:
una lista è vuota oppure è un elemento seguito
da un altro, seguito da un altro ancora e così via.
Alternativa all’iterazione è la ricorsività, una tecnica in cui un concetto
viene definito, direttamente o indirettamente, in termini di se stesso.
Per esempio una lista si può definire anche come:
una lista è vuota oppure è un elemento seguito da una lista
La ricorsività è prevista in molti linguaggi di programmazione tra i
quali il C, che consente di definire funzioni ricorsive, che possono
cioè invocare se stesse sia direttamente, all’interno del proprio
corpo, sia indirettamente, invocando un’altra funzione che ne invoca
un’altra, che a sua volta ne invoca un’altra ancora a così via, finché
qualche funzione in questa sequenza invoca quella di partenza.
Spesso i programmatori alle prime armi si sentono più sicuri quando
scrivono programmi iterativi piuttosto che programmi ricorsivi, ma è
buona abitudine abituarsi a pensare e programmare in modo
ricorsivo quando ciò risulti appropriato.
I programmi ricorsivi possono risultare più semplici da scrivere,
analizzare e comprendere.
La ricorsività trova il suo equivalente nel procedimento matematico
della induzione, utilizzato per dimostrare la verità di un asserto.
In una dimostrazione induttiva si dimostra che un asserto S(n),
funzione di una variabile n è vero, dimostrando dapprima una base,
ovvero l’asserto S(n) per un valore particolare di n.
Per esempio si pone n = 0 e si dimostra l’asserto S(0).
Successivamente si prova un passo induttivo, nel quale si dimostra
che l’asserto S, per un generico valore del suo argomento, deriva
dalla verità dell’asserto medesimo per valori precedenti del suo
argomento.
Nella forma più semplice di induzione, si dimostra che S(n) implica
S(n+1)qualunque sia n  0.
Per esempio, S(n) potrebbe essere l’asserto:
la somma degli interi da 1 a n è uguale a n*(n+1)/2
cioè S(n) potrebbe essere la nota formula
n
i
= n*(n+1)/2
1
La base sarebbe allora S(1), ovvero l’equazione precedente con 1 al
posto di n, che si riduce semplicemente all’uguaglianza
1 = 1 * 2/2 .
Il passo induttivo consiste nel dimostrare che, supposta vera S(n)
(cioè la formula precedente), risulta vera anche S(n+1), cioè
l’equazione
n 1
i
1
= (n+1)*(n+2)/2
Ora, che questa equazione sia vera è evidente, in quanto la formula
n
i
= n*(n+1)/2
1
è valida per qualsiasi valore di n, quindi anche per n+1, valore che
sostituito in essa fornisce appunto l’equazione
n 1
i
= (n+1)*(n+2)/2
1
L’iterazione e la ricorsività sono due concetti fondamentali che
compaiono in varie forme nei modelli dei dati, nelle strutture dati e
negli algoritmi.
Vediamo alcuni esempi di utilizzo del secondo concetto, riscrivendo
in modo ricorsivo la definizione di fattoriale, già vista nella sua
versione iterativa.
Definizione ricorsiva di fattoriale. La funzione fattoriale si può
definire in maniera ricorsiva come segue:
base:
induzione:
1! = 1
n! = n * (n – 1)!
Dato che la base ci dice che 1! = 1, possiamo utilizzare questo
fatto nel passo induttivo con n = 2 per determinare 2!,
scrivendo:
2! = 2 * 1! = 2 * 1 = 2.
Con n = 3, 4 e 5 otteniamo:
3! = 3 * 2! = 3 * 2 = 6
4! = 4 * 3! = 4 * 6 = 24
5! = 5 * 4! = 5 * 24 = 120
e così via.
Notiamo che, sebbene possa sembrare che il termine “fattoriale” sia
definito in termini di se stesso, in pratica possiamo ottenere il valore
di n!, per valori via via maggiori di n, nei termini del fattoriale di
valori più piccoli di n.
Abbiamo dunque una definizione “sensata” di fattoriale.
La trascrizione di questa definizione ricorsiva di n! è costituita dal
frammento di programma seguente:
if (n == 0)
return(1);
return(n * fatt(n - 1));
Per esempio, se effettuiamo la chiamata fatt(4), il risultato è una
chiamata a fatt(3), che chiama fatt(2), che a sua volta
chiama fatt(1).
A questo punto, fatt(1) applica la regola base, poiché n<=1, e
restituisce il valore 1 a fatt(2).
La chiamata fatt(2) termina l’esecuzione della penultima linea,
restituendo 2 a fatt(3).
A sua volta, fatt(3) restituisce 6 a fatt(4), che completa
l’esecuzione della linea restituendo 24 come risposta.
La figura mostra la struttura delle chiamate e dei risultati.
Definizione ricorsiva di potenza. Anche la potenza può essere
definita in modo ricorsivo. Consideriamo la funzione pot, che ha
come argomenti
• un numero reale positivo, base, elevato a
• un numero intero, esp.
Risulta che pot(base, esp) può essere definita in maniera ricorsiva
come segue:
base:
induzione:
pot(base, 0) = 1
pot(base, esp) = base * pot(base, esp-1).
Ciò dà luogo al seguente segmento di programma, che calcola la
potenza in modo ricorsivo:
if (esp == 0)
pot(base, esp) = 1
else
pot(base, esp) = base*pot(base, esp-1)
Scarica

Fonda6