Cicli Innestati in C#

Concetti Chiave
  • Cicli annidati sono costrutti di programmazione che permettono di eseguire cicli all'interno di altri cicli.
  • Sono utili per risolvere problemi complessi che richiedono iterazioni multiple su strutture dati o per generare output formattati.
  • Un esempio comune è la stampa di un triangolo numerico, dove il ciclo esterno gestisce le righe e il ciclo interno gestisce i numeri in ciascuna riga.
  • I cicli annidati possono essere utilizzati per risolvere problemi come la ricerca di numeri primi in un intervallo o per generare numeri fortunati a quattro cifre.
  • È importante considerare l'efficienza quando si utilizzano cicli annidati, poiché possono aumentare significativamente il tempo di esecuzione, specialmente con più di due cicli annidati.

Cicli Annidati

I cicli annidati o cicli innestati sono costrutti di programmazione costituiti da diversi cicli posti uno dentro l'altro.

Il ciclo più interno viene eseguito più volte, e quello più esterno - meno volte. Vediamo come appaiono due cicli annidati:

for (inizializzazione, verifica, aggiornamento)
{
    for (inizializzazione, verifica, aggiornamento)
    {
        // codice eseguibile
    }
}

Dopo l'inizializzazione del primo ciclo for, inizierà l'esecuzione del suo corpo, che contiene il secondo ciclo (annidato).

La sua variabile verrà inizializzata, la sua condizione verrà controllata e il codice all'interno del suo corpo verrà eseguito, poi la variabile verrà aggiornata e l'esecuzione continuerà fino a quando la condizione restituirà false.

Dopo di che continuerà la seconda iterazione del primo ciclo for, la sua variabile verrà aggiornata e l'intero secondo ciclo verrà eseguito ancora una volta. Il ciclo interno verrà eseguito completamente tante volte quante sono le esecuzioni del corpo del ciclo esterno.

Consideriamo ora un esempio reale che dimostrerà quanto siano utili i cicli annidati.

Esempio: Stampa di un Triangolo

Risolviamo il seguente problema: per un dato numero n, stampare sulla console un triangolo con n numero di righe, che assomiglia a questo:

1
1 2
1 2 3
1 2 3 4
...
1 2 3 ... n

Risolveremo il problema con due cicli for.

Il loop esterno attraverserà le righe, e quello interno gli elementi in esse.

Quando siamo sulla prima riga, dobbiamo stampare "1" (1 elemento, 1 iterazione del loop interno). Sulla seconda riga dobbiamo stampare "1 2" (2 elementi, 2 iterazioni del loop interno). Vediamo che c'è una correlazione tra la riga su cui siamo e il numero degli elementi che stampiamo. Questo ci dice come organizzare la struttura del loop interno:

  • Inizializziamo la variabile del loop con 1 (il primo numero che stamperemo): col = 1;
  • La condizione di ripetizione dipende dalla riga su cui siamo: col <= riga (dove riga è la variabile del loop esterno);
  • Aumentiamo la variabile del loop di un'unità ad ogni iterazione del loop interno.

Fondamentalmente, dobbiamo implementare un ciclo for esterno da 1 a n (per le righe) e metterci dentro un altro ciclo for interno per i numeri sulla riga corrente, che dovrebbe andare da 1 al numero della riga corrente. Il loop esterno dovrebbe attraversare le righe mentre quello interno attraversa le colonne della riga corrente.

Infine otteniamo il seguente codice:

Console.Write("Inserisci un numero: ");
int n = int.Parse(Console.ReadLine());

for (int riga = 1; riga <= n; riga++)
{
    for (int col = 1; col <= riga; col++)
    {
        Console.Write(col + " ");
    }
    Console.WriteLine();
}

Se lo eseguiamo, ci assicureremo che funzioni correttamente. Ecco come appare il risultato per n=9:

Inserisci un numero: 9
1 
1 2 
1 2 3 
1 2 3 4 
1 2 3 4 5 
1 2 3 4 5 6 
1 2 3 4 5 6 7 
1 2 3 4 5 6 7 8 
1 2 3 4 5 6 7 8 9

Esempio: Numeri Primi in un Intervallo

Consideriamo un altro esempio di cicli annidati.

Ci poniamo l'obiettivo di stampare sulla console tutti i numeri primi nell'intervallo [n,m]. Limiteremo l'intervallo con un ciclo for e per controllare se un numero è primo useremo un ciclo while annidato:

Console.Write("n = ");
int n = int.Parse(Console.ReadLine());

Console.Write("m = ");
int m = int.Parse(Console.ReadLine());

for (int numero = n; numero <= m; numero++)
{
    bool primo = true;
    int divisore = 2;
    int divisoreMassimo = (int)Math.Sqrt(numero);
    while (divisore <= divisoreMassimo)
    {
        if (numero % divisore == 0)
        {
            primo = false;
            break;
        }
        divisore++;
    }
    if (primo)
    {
        Console.Write(" " + numero);
    }
}

Console.WriteLine();

Usando il ciclo for esterno controlliamo ognuno dei numeri n, n+1, \ldots, m se è primo. Ad ogni iterazione del ciclo for esterno controlliamo se la sua variabile di ciclo numero è un numero primo.

La logica con cui controlliamo se un numero è primo ci è già familiare. Prima inizializziamo la variabile primo con true. Con il ciclo while interno controlliamo per ognuno dei numeri [2…√numero] se è un divisore di numero e se è così, impostiamo primo a false. Dopo aver terminato il ciclo while la variabile booleana primo indica se il numero è primo o no. Se il numero è primo lo stampiamo sulla console.

Se eseguiamo l'esempio per n=3 e m=75, otterremo il seguente risultato:

n = 3
m = 75
 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73

Esempio: Numeri Fortunati

Consideriamo un altro esempio attraverso il quale mostreremo che possiamo mettere anche più di due loop l'uno dentro l'altro.

Il nostro scopo è trovare e stampare tutti i numeri a quattro cifre del tipo:

abcd

dove

a + b = c + d

e

a \neq 0

Questi numeri sono chiamati numeri fortunati. Ad esempio, il numero 1230 è fortunato perché 1 + 2 = 3 + 0.

Implementeremo questo programma con quattro cicli for: uno per ogni cifra.

Si noti che questo non è il modo più efficiente per risolvere il problema, ma è un buon esempio di cicli annidati.

Il ciclo più esterno definirà le migliaia, ossia la cifra a. Partirà da 1 mentre il resto dei cicli partiranno da 0. Essi determineranno le centinaia, le decine e le unità. Eseguiremo un controllo se il nostro numero corrente nel loop più interno è fortunato e se così, lo stamperemo sulla console. Ecco un esempio di implementazione:

for (int a = 1; a <= 9; a++) {
    for (int b = 0; b <= 9; b++) {
        for (int c = 0; c <= 9; c++) {
            for (int d = 0; d <= 9; d++) {
                if ((a + b) == (c + d))
                {
                    Console.WriteLine(
                        "> " + a + " " + b + " " + c + " " + d);
                }
            }
        }
    }
}

Ecco una parte del risultato stampato (l'intero risultato è troppo lungo):

> 1 0 0 1
> 1 0 1 0
> 1 1 0 2
> 1 1 1 1
> 1 1 2 0
> 1 2 0 3
> 1 2 1 2
> 1 2 2 1
> 1 2 3 0
> 1 3 0 4
> 1 3 1 3
> 1 3 2 2
> 1 3 3 1
> 1 3 4 0
> 1 4 0 5
> 1 4 1 4
> 1 4 2 3
...

Lasciamo come compito a casa per il lettore diligente di offrire una soluzione più efficiente allo stesso problema, usando solo tre loop annidati anziché quattro.