glripetizioni

Matrici e array bidimensionali in C e Python

Array bidimensionali spiegati con C e Python: indici riga e colonna, cicli annidati, somme, diagonale, trasposta e gli errori che costano punti in verifica.

di Gaetano Livornese

  • #informatica
  • #terza superiore
  • #tecnico
  • #array bidimensionali
  • #matrici
  • #c

In queste settimane, nelle terze degli istituti tecnici a indirizzo Informatica, il programma è arrivato agli array: prima quelli a una dimensione, poi le matrici. La verifica, in genere pratica al PC più uno scritto, cade fra sei e otto settimane. Il professore quasi sempre la costruisce attorno a un esercizio con una tabella di numeri: riempirla, stamparla, sommare righe e colonne, trovare un massimo. Questo post serve a chi ha appena incontrato il doppio indice m[i][j] e vuole arrivare alla verifica senza confondere righe e colonne.

Se ti manca la base sui cicli e sugli array monodimensionali, parti da cicli annidati e array in terza informatica: qui diamo per scontato che un for semplice non ti faccia paura.

Che cos’è una matrice, in pratica

Un array bidimensionale è una tabella: righe e colonne. In C lo dichiari con due dimensioni, prima il numero di righe, poi quello di colonne:

int m[3][4];

Significa 3 righe e 4 colonne, quindi 12 elementi, tutti dello stesso tipo. Si accede a un elemento con due indici: m[i][j] è l’elemento della riga i e della colonna j. Gli indici partono da 0, quindi la prima riga è la numero 0 e l’ultima è la numero 2.

Puoi inizializzarla già in fase di dichiarazione, una riga per volta fra graffe:

int m[3][4] = {
    {4, 7, 1, 9},
    {2, 8, 6, 3},
    {5, 0, 10, 12}
};

Su questa matrice, m[0][0] vale 4, m[1][2] vale 6 e m[2][3] vale 12. Ci lavoreremo per tutto il post, quindi vale la pena copiarla su un foglio come griglia con le righe numerate 0, 1, 2 e le colonne numerate 0, 1, 2, 3.

Una cosa che a volte il docente chiede nello scritto: in memoria gli elementi sono contigui e, in C, ordinati per riga. Prima tutta la riga 0, poi la riga 1, poi la 2. Non serve per scrivere i cicli, ma spiega perché, quando passi una matrice a una funzione, il compilatore vuole conoscere il numero di colonne.

Il ciclo annidato: la regola che regge tutto

Per visitare tutti gli elementi servono due cicli, uno dentro l’altro. Il ciclo esterno scorre le righe, quello interno le colonne:

for (int i = 0; i < 3; i++) {
    for (int j = 0; j < 4; j++) {
        printf("%3d", m[i][j]);
    }
    printf("\n");
}

L’output è:

  4  7  1  9
  2  8  6  3
  5  0 10 12

Il ciclo interno gira per intero a ogni passo di quello esterno: in totale il corpo viene eseguito 3 × 4 = 12 volte. Il printf("\n") sta dopo il ciclo interno ma dentro quello esterno, ed è la riga che manda a capo alla fine di ogni riga della tabella. Spostarlo di una graffa, dentro il ciclo interno o fuori da entrambi, è uno degli errori più comuni: la matrice esce tutta su una riga o con un a capo dopo ogni numero.

Il consiglio che do sempre: prima di scrivere il ciclo, scrivi sul foglio chi sono i e j. Io uso sempre i per le righe e j per le colonne, e non cambio per tutto l’esercizio.

Python: la stessa idea, sintassi diversa

In Python una matrice è una lista di liste. Ogni lista interna è una riga:

m = [
    [4, 7, 1, 9],
    [2, 8, 6, 3],
    [5, 0, 10, 12],
]

for i in range(3):
    for j in range(4):
        print(f"{m[i][j]:3d}", end="")
    print()

L’output è identico a quello del programma C. Gli indici funzionano allo stesso modo: m[1][2] vale 6. Le differenze che ti interessano sono due. Le dimensioni non si dichiarano, quindi se ti servono le ricavi con len(m) per le righe e len(m[0]) per le colonne. E si può ciclare direttamente sugli elementi, senza indici, quando non ti serve sapere la posizione:

for riga in m:
    for x in riga:
        print(f"{x:3d}", end="")
    print()

Gli indici restano necessari quando la posizione conta, come nella diagonale o nella trasposta.

Somma per righe e per colonne

Somma di ogni riga: per ogni i azzeri un accumulatore, poi scorri le colonne.

for (int i = 0; i < 3; i++) {
    int somma = 0;
    for (int j = 0; j < 4; j++) {
        somma += m[i][j];
    }
    printf("riga %d: %d\n", i, somma);
}

Con la nostra matrice stampa riga 0: 21, riga 1: 19, riga 2: 27. Controllo a mano la prima: 4 + 7 + 1 + 9 = 21.

Per le colonne si invertono i ruoli: il ciclo esterno scorre j, quello interno scorre i, e l’indice che cambia nel ciclo interno è quello della riga.

for (int j = 0; j < 4; j++) {
    int somma = 0;
    for (int i = 0; i < 3; i++) {
        somma += m[i][j];
    }
    printf("colonna %d: %d\n", j, somma);
}

Il risultato: colonna 0: 11, colonna 1: 15, colonna 2: 17, colonna 3: 24. Verifica: 4 + 2 + 5 = 11 per la prima colonna. Come controllo finale, la somma delle somme di riga (21 + 19 + 27 = 67) deve coincidere con quella delle somme di colonna (11 + 15 + 17 + 24 = 67). In verifica è un controllo che costa trenta secondi.

La versione Python è breve:

for i in range(len(m)):
    print(f"riga {i}: {sum(m[i])}")

for j in range(len(m[0])):
    somma = 0
    for i in range(len(m)):
        somma += m[i][j]
    print(f"colonna {j}: {somma}")

Diagonale principale e trasposta

La diagonale principale esiste in una matrice quadrata, cioè con tante righe quante colonne: sono gli elementi con indice di riga uguale a quello di colonna, m[0][0], m[1][1], m[2][2]. Qui basta un ciclo solo, perché i due indici sono uguali:

int q[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
int diag = 0;
for (int i = 0; i < 3; i++) {
    diag += q[i][i];
}
printf("diagonale: %d\n", diag);

L’output è diagonale: 15, perché 1 + 5 + 9 = 15. Un errore tipico è usare due cicli annidati con un if (i == j) dentro: funziona, ma fa 9 passi invece di 3 e il professore lo nota.

La trasposta scambia righe e colonne: l’elemento in posizione [i][j] finisce in [j][i]. Se la matrice originale è 2 righe per 3 colonne, la trasposta è 3 righe per 2 colonne, quindi serve una matrice nuova con le dimensioni scambiate:

int a[2][3] = {{1, 2, 3}, {4, 5, 6}};
int b[3][2];
for (int i = 0; i < 2; i++) {
    for (int j = 0; j < 3; j++) {
        b[j][i] = a[i][j];
    }
}

Stampando b riga per riga si ottiene:

  1  4
  2  5
  3  6

In Python la stessa operazione si può scrivere con [list(r) for r in zip(*a)], ma se non l’hai vista a lezione usa i due cicli: in verifica conta far vedere che hai capito lo scambio degli indici.

Ricerca del massimo, con la posizione

Per trovare il massimo parti dal primo elemento, non da zero: se tutti i numeri fossero negativi, partire da 0 darebbe un risultato sbagliato. Poi scorri e aggiorni, ricordando anche dove l’hai trovato:

int max = m[0][0];
int mi = 0, mj = 0;
for (int i = 0; i < 3; i++) {
    for (int j = 0; j < 4; j++) {
        if (m[i][j] > max) {
            max = m[i][j];
            mi = i;
            mj = j;
        }
    }
}
printf("massimo %d in [%d][%d]\n", max, mi, mj);

Con la nostra matrice stampa massimo 12 in [2][3]. In Python il codice è lo stesso con range. A questo punto è utile una funzione: in C la matrice passata come parametro non viene copiata, e va indicato il numero di colonne, ad esempio void stampa(int m[][4], int righe). Il numero di righe lo passi a parte, quello delle colonne deve comparire nel tipo. Se hai dubbi su cosa significhi questo passaggio, c’è il post su passaggio di parametri per valore e per riferimento.

Gli errori che fanno perdere punti

  1. Indici scambiati. Scrivere m[j][i] al posto di m[i][j] compila e gira. Se la matrice è quadrata il risultato è solo trasposto e non te ne accorgi subito; se è rettangolare ti ritrovi fuori dai limiti.
  2. Fuori dai limiti. Su int m[3][4] l’indice di riga valido va da 0 a 2 e quello di colonna da 0 a 3. Scrivere i <= 3 nel ciclo delle righe fa leggere m[3][...], che non esiste. In C non c’è nessun controllo: il comportamento è indefinito, a volte il programma sembra funzionare, a volte esce un numero strano. In Python, invece, ottieni subito un IndexError.
  3. Accumulatore non azzerato. Nella somma per righe, int somma = 0; deve stare dentro il ciclo esterno. Messo fuori, la somma della seconda riga include anche la prima.
  4. Dimensioni scritte a mano ovunque. Se dichiari #define RIGHE 3 e #define COLONNE 4, un solo cambiamento aggiorna tutto il programma. Sparso fra dichiarazione e cicli, il numero 4 ti tradisce al primo cambio di traccia.
  5. Solo Python: creare la matrice con [[0] * 3] * 3. Sembra giusto, ma replica tre volte la stessa lista. Se poi scrivi g[0][0] = 1, ottieni [[1, 0, 0], [1, 0, 0], [1, 0, 0]]. Il modo corretto è [[0] * 3 for _ in range(3)], che con la stessa assegnazione dà [[1, 0, 0], [0, 0, 0], [0, 0, 0]].

Esercizi svolti

Esercizio 1. Data la matrice m di 3 righe e 4 colonne vista sopra, calcola la somma degli elementi sulla riga 1 e la media di tutti gli elementi.

Riga 1 è {2, 8, 6, 3}: 2 + 8 + 6 + 3 = 19. La somma di tutti gli elementi è 21 + 19 + 27 = 67 e gli elementi sono 12, quindi la media è 67 / 12, circa 5,58. In C, ricordati che 67 / 12 fra interi dà 5: serve (double) somma / 12.

Esercizio 2. Nella matrice quadrata q = {{1,2,3},{4,5,6},{7,8,9}}, calcola la somma della diagonale secondaria, cioè gli elementi q[0][2], q[1][1], q[2][0].

Qui la somma degli indici è sempre 2, cioè i + j = N - 1 con N = 3. Quindi basta un ciclo con q[i][2 - i]: 3 + 5 + 7 = 15. Qui coincide con la diagonale principale (anch’essa 15) solo perché i numeri crescono regolarmente da 1 a 9: su una matrice qualsiasi i due valori sono diversi.

Esercizio 3. Scrivi il frammento che conta quanti elementi pari ci sono in m e stampa il risultato. Aspettati un numero e verificalo a mano.

int pari = 0;
for (int i = 0; i < 3; i++) {
    for (int j = 0; j < 4; j++) {
        if (m[i][j] % 2 == 0) {
            pari++;
        }
    }
}
printf("pari: %d\n", pari);

Riga 0: 4 è pari, 7 no, 1 no, 9 no, quindi 1. Riga 1: 2, 8, 6 sono pari, 3 no, quindi 3. Riga 2: 0, 10, 12 sono pari, 5 no, quindi 3. Totale 7: l’output è pari: 7. Nota che 0 conta come pari.

Come prepararti nelle prossime settimane

Su informatica, per ora, non c’è ancora una simulazione dedicata: la prova pratica si allena scrivendo codice. Il mio metodo è fare un esercizio al giorno, prima a mano sul foglio con la griglia e i valori di i e j, poi al computer, e confrontare l’output con quello atteso. Ogni passaggio visto qui, somme, diagonale, trasposta, massimo, lo puoi riscrivere in C e in Python senza guardare. Altro materiale sulla materia è nella pagina informatica. Per le stringhe e i file vedi stringhe e file in Python, e per capire che cosa c’è sotto gli indici in C, i puntatori spiegati bene.

Se vuoi capire dove sei, scrivimi dalla pagina contatti e facciamo una diagnosi gratuita: guardiamo insieme un tuo esercizio e capiamo se l’intoppo è negli indici, nei cicli o nella lettura della traccia.

Continua a leggere

Vuoi applicare quello che hai letto?

Parliamone 30 minuti gratis: guardiamo insieme dove sei bloccato e ti dico onestamente come posso aiutarti.

Oppure scrivimi su WhatsApp.