Cicli For Innestati in MATLAB

In questa lezione esploreremo i cicli for innestati in MATLAB, una tecnica fondamentale per gestire operazioni ripetitive su matrici e array multidimensionali.

Impareremo a creare cicli for all'interno di altri cicli for, comprendendo come iterare su righe e colonne per eseguire calcoli complessi. Scopriremo anche come combinare cicli for con istruzioni if per manipolare e analizzare dati in modo efficiente.

Cicli for innestati in MATLAB

Il corpo di un ciclo for in MATLAB può consistere in qualunque istruzione (o insieme di istruzioni) valida.

Quando il corpo di un ciclo for è a sua volta un ciclo for, si parla di ciclo for innestato o ciclo for annidato.

La forma generale di un ciclo for innestato è la seguente:

for varloop1 = intervallo1    % <-- ciclo esterno
    % corpo1, che include il ciclo interno
    for varloop2 = intervallo2  % <-- ciclo interno
        corpo2
    end
end

Il primo ciclo for è chiamato ciclo esterno; il secondo for è il ciclo interno. L'azione del ciclo esterno consiste (in parte; potrebbero esserci altre istruzioni) nell'intero ciclo interno.

Come esempio, mostriamo un ciclo annidato in uno script che stamperà un rettangolo di asterischi (*). Le variabili nello script specificano quante righe e quante colonne stampare. Per esempio, se la variabile righe è impostata a 3 e colonne a 5, verrà stampato un rettangolo di dimensioni 3×5.

Poiché la stampa su diverse righe avviene tramite il carattere di newline, l'algoritmo di base è il seguente:

Per ogni riga di output:

  • Stampare il numero richiesto di asterischi
  • Spostare il cursore sulla riga successiva (stampando \n)
% stampa_rettangolo.m
% Stampa un rettangolo di asterischi
% Il numero di righe e colonne è specificato
% dalle variabili righe e colonne

righe = 3;
colonne = 5;
% ciclo sulle righe
for i = 1:righe
    % per ogni riga, un ciclo per stampare
    % gli asterischi e poi andare a capo
    for j = 1:colonne
        fprintf('*')
    end
    fprintf('\n')
end

Esempio di esecuzione e relativo output:

>> stampa_rettangolo
*****
*****
*****

La variabile righe specifica quante righe stampare, mentre la variabile colonne specifica quanti asterischi stampare in ogni riga.

Sono presenti due variabili di ciclo: i itera sulle righe e j itera sulle colonne. Poiché il numero di righe (righe) e di colonne (colonne) è noto, vengono utilizzati due cicli for. Uno serve a iterare sulle righe e l'altro a stampare il numero richiesto di asterischi per ogni riga.

I valori delle variabili di ciclo non vengono effettivamente usati nelle istruzioni, ma servono semplicemente a determinare quante volte ripetere l'azione.

Il primo ciclo for stabilisce che l'azione si ripeterà un numero di volte pari a righe. L'azione di questo ciclo è di stampare gli asterischi e poi il carattere di newline. In particolare, tale azione consiste in un ulteriore ciclo che stampa colonne asterischi su una riga; solo dopo questi colonne asterischi viene stampato il carattere di ritorno a capo.

In questo caso, il ciclo esterno itera sulle righe, mentre il ciclo interno itera sulle colonne. Il ciclo esterno deve riguardare le righe poiché lo script deve stampare un certo numero di righe di output. Per ogni riga, è necessario un ciclo dedicato a stampare il numero richiesto di asterischi, e questo è il ciclo interno.

Durante l'esecuzione, per prima cosa la variabile del ciclo esterno i è inizializzata a 1, e poi si passa al suo corpo.

Il corpo include l'esecuzione del ciclo interno e in seguito la stampa del carattere di newline. Mentre i=1, la variabile del ciclo interno j itera su tutti i suoi valori. Avendo colonne=5, il ciclo interno stamperà un singolo asterisco per cinque volte. In seguito verrà stampato il carattere di newline, quindi i sarà incrementato a 2 e così via, finché l'azione del ciclo esterno non sarà stata eseguita righe volte.

Osserviamo che l'azione del ciclo esterno comprende due istruzioni (il ciclo interno for e la chiamata fprintf('\n')), mentre l'azione del ciclo interno è una singola chiamata fprintf('*').

Si noti bene che se avessimo usato un'unica istruzione nel ciclo interno del tipo fprintf('*\n'), avremmo ottenuto una singola colonna di 15 asterischi (nel caso 3×5), anziché un rettangolo di 3 righe e 5 colonne.

Proviamo a modificate lo script stampa_rettangolo per stampare un triangolo di asterischi anziché un rettangolo. Ad esempio, se righe è impostato a 3, lo script dovrebbe stampare:

*
**
***

In questo caso, il numero di asterischi da stampare in ogni riga è uguale al numero della riga. Il ciclo interno non deve iterare fino a colonne, ma fino al valore della variabile di ciclo del ciclo esterno; quindi la variabile colonne non serve.

stampa_triangolo.m
% Stampa un triangolo di asterischi
% Il numero di righe sarà specificato dalla variabile "righe"

righe = 3;
for i = 1:righe
    % il ciclo interno si ferma a i
    for j = 1:i
        fprintf('*')
    end
    fprintf('\n')
end

Nei precedenti esempi, le variabili di ciclo servivano esclusivamente a specificare quante volte ripetere l'azione. Nel prossimo esempio, invece, verranno stampati effettivamente i valori delle variabili di loop.

stampa_variabili_loop.m
% Mostra i valori delle variabili di loop

for i = 1:3
    for j = 1:2
        fprintf('i=%d, j=%d\n', i, j)
    end
    fprintf('\n')
end

Eseguendo questo script, si otterrà la stampa dei valori di i e j su una riga ogni volta che si esegue l'azione del ciclo interno. L'azione del ciclo esterno consiste nell'eseguire il ciclo interno e poi stampare una riga vuota (\n), separando così le iterazioni del ciclo esterno:

>> stampa_variabili_loop
i=1, j=1
i=1, j=2

i=2, j=1
i=2, j=2

i=3, j=1
i=3, j=2

Ora, invece di limitarci a stampare i valori delle variabili di loop, possiamo usarli per generare una tabella di moltiplicazione, moltiplicando i valori delle variabili dei due cicli.

Lo script seguente, chiamato tabellamolt, calcola e restituisce una matrice che rappresenta una tabella di moltiplicazione. Vengono passati due argomenti alla funzione, che indicano il numero di righe e di colonne della matrice desiderata.

tabellamolt.m
function matout = tabellamolt(righe, colonne)
% tabellamolt restituisce una matrice che rappresenta
% una tabella di moltiplicazione
% Formato: tabellamolt(nRighe, nColonne)

% Preallocazione della matrice
matout = zeros(righe, colonne);
for i = 1:righe
    for j = 1:colonne
        matout(i, j) = i * j;
    end
end
end

Chiamando la funzione con:

>> tabellamolt(3,5)

si ottiene una matrice con 3 righe e 5 colonne:

ans =
     1     2     3     4     5
     2     4     6     8    10
     3     6     9    12    15

Si tratta di una funzione che restituisce una matrice.

Prealloca la matrice a zeri, quindi sostituisce ogni elemento con il prodotto i * j. Poiché si conoscono il numero di righe e di colonne, si usano cicli for: quello esterno per le righe e quello interno per le colonne. L'azione del ciclo annidato calcola i * j per tutti i valori di i e j. Come avviene con i vettori, è di nuovo importante notare che le variabili di loop vengono utilizzate come indici della matrice.

  • Quando i=1, j itera da 1 a 5, calcolando 1*1, 1*2, 1*3, 1*4, 1*5, che diventano gli elementi della prima riga.
  • Quando i=2, gli elementi della seconda riga sono 2*1, 2*2, 2*3, 2*4, 2*5.
  • Quando i=3, gli elementi della terza riga sono 3*1, 3*2, 3*3, 3*4, 3*5.

Questa funzione potrebbe essere utilizzata in uno script che chiede all'utente il numero di righe e di colonne, richiama la funzione per generare la tabella di moltiplicazione e infine scrive la matrice risultante in un file:

creatabellamolt.m
% Richiede all'utente il numero di righe e colonne
% e crea una tabella di moltiplicazione da salvare
% nel file "miatabellamolt.dat"

num_righe = input('Inserire il numero di righe: ');
num_colonne = input('Inserire il numero di colonne: ');
moltmatrice = tabellamolt(num_righe, num_colonne);
save miatabellamolt.dat moltmatrice -ascii

Un esempio di esecuzione:

>> creatabellamolt
Inserire il numero di righe: 6
Inserire il numero di colonne: 4

>> load miatabellamolt.dat
>> miatabellamolt

miatabellamolt =
     1     2     3     4
     2     4     6     8
     3     6     9    12
     4     8    12    16
     5    10    15    20
     6    12    18    24

Esercizio Pratico

Per chiarire il concetto di cicli innestati, proviamo a risolvere i seguenti esercizi.

Per ognuno dei seguenti blocchi di codice (che sono separati), determinare cosa verrà stampato.

mat = [7 11 3; 3 5];
[r, c] = size(mat);
for i = 1:r
    fprintf('La somma è %d\n', sum(mat(i,:)))
end
for i = 1:2
    fprintf('%d: ', i)
    for j = 1:4
        fprintf('%d ', j)
    end
    fprintf('\n')
end

Nel primo caso, la matrice mat è definita come mat = [7 11 3; 3 5], quindi essa sarà una matrice 2×3:

 7    11    3
 3     5    0

Il ciclo esterno itera sulle righe della matrice, quindi i varrà 1 e 2. Per i=1, sum(mat(1,:)) calcola la somma degli elementi della prima riga, cioè 7+11+3=21. Per i=2, sum(mat(2,:)) calcola la somma degli elementi della seconda riga, cioè 3+5+0=8.

Quindi l'output sarà:

La somma è 21
La somma è 8

Nel secondo caso, il ciclo esterno itera su i=1 e i=2. Per ogni valore di i, il ciclo interno itera su j=1, j=2, j=3, j=4. Quindi, per i=1, verrà stampato 1 2 3 4, e per i=2, verrà stampato 1 2 3 4.

Quindi l'output sarà:

1: 1 2 3 4
2: 1 2 3 4

Combinare cicli for annidati e istruzioni if

Le istruzioni all'interno di un ciclo annidato possono essere una qualunque istruzione valida, incluse le istruzioni di selezione. Ad esempio, è possibile usare un'istruzione if o if-else come azione (o parte dell'azione) in un ciclo.

Come esempio, si supponga di avere un file chiamato datavalori.dat contenente i risultati di un esperimento.

Alcuni valori sono stati registrati in modo errato: tutti i numeri dovrebbero essere positivi, ma non lo sono. Lo script seguente legge da questo file, memorizza i dati in una matrice e stampa la somma solo dei numeri positivi per ogni riga.

Si assume che il file contenga numeri interi; non si assume però quante righe ci siano nel file né quanti numeri per riga (anche se si suppone che su ogni riga ci sia lo stesso numero di interi).

sumsolopositivi.m
% Somma solo i numeri positivi da un file
% Legge il file in una matrice e poi calcola e stampa
% la somma solo dei numeri positivi da ogni riga

load datavalori.dat
[r, c] = size(datavalori);

for riga = 1:r
    sommaCorrente = 0;
    for colonna = 1:c
        if datavalori(riga,colonna) >= 0
            sommaCorrente = sommaCorrente + datavalori(riga,colonna);
        end
    end
    fprintf('La somma per la riga %d è %d\n', riga, sommaCorrente)
end

Per esempio, se il file datavalori.dat contenesse:

33 -11 2
4 5 9
22 5 -7
2 11 3

l'output del programma sarebbe:

>> sumsolopositivi
La somma per la riga 1 è 35
La somma per la riga 2 è 18
La somma per la riga 3 è 27
La somma per la riga 4 è 16

In questo caso, il file viene caricato e i dati sono memorizzati in una variabile di tipo matrice.

Lo script trova le dimensioni di tale matrice e poi itera su tutti gli elementi usando un ciclo annidato: il ciclo esterno scorre le righe, mentre quello interno scorre le colonne. Questo è importante perché, volendo eseguire un'azione per ogni riga, il ciclo esterno dev'essere quello che itera sulle righe.

Per ogni elemento, un'istruzione if determina se l'elemento è positivo o no. Solo i valori positivi vengono aggiunti alla somma di quella riga. Poiché la somma si deve calcolare per ciascuna riga, la variabile sommaCorrente viene inizializzata a 0 all'inizio di ogni iterazione del ciclo esterno.

Domanda: Farebbe differenza invertire l'ordine dei cicli in questo esempio, in modo che il ciclo esterno iteri sulle colonne e quello interno sulle righe?

Risposta: Sì, perché vogliamo calcolare una somma per ogni riga. Di conseguenza, il ciclo esterno deve riguardare le righe.

Domanda: Cosa sarebbe necessario cambiare per calcolare e stampare la somma dei soli numeri positivi per ciascuna colonna invece che per ogni riga?

Risposta: Basterebbe invertire i due cicli e sostituire la frase con "La somma per la colonna...". Tutto il resto rimarrebbe invariato: gli elementi della matrice si referenziano comunque come datavalori(riga,colonna), perché l'indice di riga si indica sempre per primo.

Esercizio Pratico

Scrivere una funzione mioMatMin che trovi il valore minimo in ogni colonna di una matrice passata come argomento e restituisca un vettore di tali minimi. Si utilizzi il metodo di programmazione basato su cicli. Ecco un esempio di chiamata:

>> mat = randi([1 20], 3, 4)
mat =
    15    19    17     5
     6    14    13    13
     9     5     3    13

>> mioMatMin(mat)
ans =
     6     5     3     5

Una possibile soluzione è la seguente:

mioMatMin.m
function minimi = mioMatMin(matrice)
% Restituisce il valore minimo di ogni colonna di una matrice

    [righe, colonne] = size(matrice);
    minimi = zeros(1, colonne);

    for col = 1:colonne
        minimoCorrente = matrice(1, col);
        for riga = 2:righe
            if matrice(riga, col) < minimoCorrente
                minimoCorrente = matrice(riga, col);
            end
        end
        minimi(col) = minimoCorrente;
    end
end

Domanda: La funzione mioMatMin dell'esempio funzionerebbe anche per un vettore come argomento?

Risposta: Sì. Un vettore non è altro che un sottoinsieme di una matrice. In questo caso, una delle due dimensioni (righe o colonne) comporterebbe l'esecuzione di un solo ciclo.

In Sintesi

In questa lezione abbiamo approfondito il concetto di cicli for innestati, ovvero cicli for all'interno di altri cicli for.

I concetti chiave che abbiamo imparato sono:

  • Un ciclo for innestato è un ciclo for all'interno di un altro ciclo for.
  • Il ciclo esterno è quello che itera sulle righe, mentre il ciclo interno itera sulle colonne o viceversa.
  • Le variabili di loop vengono utilizzate come indici per accedere agli elementi di una matrice.
  • Le istruzioni all'interno di un ciclo annidato possono essere qualsiasi istruzione valida, inclusi cicli for e istruzioni di selezione come if e if-else.

Abbiamo anche visto come combinare cicli for annidati con istruzioni if per eseguire azioni complesse su matrici, come calcolare la somma dei soli numeri positivi per ogni riga di una matrice.

Infine, abbiamo risolto un esercizio pratico che richiedeva di scrivere una funzione che trovasse il valore minimo in ogni colonna di una matrice.

Nella prossima lezione, invece, studieremo un altro tipo di ciclo: il ciclo while.