Riepilogo
Molti dati non formano una fila, ma un prospetto: i voti di un corso per studente e per prova, una plancia di gioco, una matrice. Questa lezione presenta la tabella, la struttura bidimensionale, nelle sue due forme in C++: il vettore di vettori, di dimensione decisa in esecuzione, e l’array di array, di dimensione nota in compilazione. Seguono le scansioni per righe e per colonne; la trasposta e il prodotto di matrici, di costo cubico; le vicine di una cella, che obbligano a badare ai bordi; e la tabella conservata in un solo vettore con l’indice i * columns + j, che è il modo in cui la memoria la vede.
Obiettivi di apprendimento
- Rappresentare una tabella come
std::vector<std::vector<T>>o comestd::arraydistd::array, e scegliere tra le due secondo che la dimensione sia nota o no in compilazione. - Scorrere una tabella per righe e per colonne, e calcolare i totali e le medie di ogni riga e di ogni colonna.
- Implementare operazioni sulle tabelle (la trasposta, il prodotto di matrici, il conteggio delle vicine di una cella) badando che nessun indice esca dai bordi.
- Rappresentare una tabella in un vettore piatto con l’indice
i * columns + j, e spiegare con un esempio la corrispondenza tra la cella e la sua posizione.
Tabelle
Righe e colonne
Una tabella di righe e colonne è una collezione di dati dello stesso tipo, ciascuno designato da due indici: quello della sua riga, , con , e quello della sua colonna, , con . La matematica la chiama matrice e scrive ; il corso Algebra e Geometria II studia le matrici e le loro operazioni, e questa lezione ne scrive alcune come programmi. L’ordine degli indici non ammette eccezioni: prima la riga, poi la colonna. Confonderli è l’errore più frequente con le tabelle, e il compilatore non può segnalarlo, perché i due indici sono dello stesso tipo.
La lezione Vettori ha presentato la sequenza di dimensione variabile. Se così è, una tabella è una sequenza di righe, e ogni riga è a sua volta una sequenza: un vettore i cui elementi sono vettori, std::vector<std::vector<int>>.
#include <print>#include <vector>int main(){ std::vector<std::vector<int>> table{{1, 2, 3}, {4, 5, 6}}; std::println("{}", table); std::println("Righe {}, colonne {}", table.size(), table[0].size()); std::println("Riga 1: {}; riga 1, colonna 2: {}", table[1], table[1][2]); table[0][1] = 20; for (const std::vector<int>& row : table) { for (const int value : row) { std::print("{:>4}", value); } std::println(""); } const std::vector<std::vector<int>> zeros(3, std::vector<int>(4)); std::println("{}", zeros);}[[1, 2, 3], [4, 5, 6]]Righe 2, colonne 3Riga 1: [4, 5, 6]; riga 1, colonna 2: 6 1 20 3 4 5 6[[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]La riga 6 del programma scrive la tabella come una lista di righe, ciascuna tra le sue graffe, e la formattazione dei range la stampa allo stesso modo, tra parentesi quadre. table.size() è il numero di righe, e table[0].size(), la lunghezza della prima, che in una tabella ben formata è il numero di colonne. table[1] è l’intera riga 1, un std::vector<int>, e table[1][2], l’elemento 2 di quella riga: il doppio indice non è un’operazione nuova, ma due accessi successivi; il primo sceglie la riga e il secondo, al suo interno, la colonna. Le righe da 11 a 16 del programma scrivono la tabella come prospetto con due for basati su intervallo annidati; la riga della tabella si prende per riferimento costante per non copiarla a ogni iterazione. La riga 17 crea una tabella di 3 per 4 piena di zeri: con le parentesi tonde, come nella lezione Vettori, si chiedono 3 copie di una riga di quattro zeri.
Una tabella di 3 per 4, scorsa riga per riga (i nel ciclo esterno) e poi colonna per colonna (i cicli scambiati). Ogni cella mostra il suo numero d’ordine di visita.
Dimostrazione
La tabella, con gli indici delle righe a sinistra e quelli delle colonne in alto.
Per righe: con , il ciclo interno scorre .
Con , il ciclo interno ricomincia da .
Con : la tabella è stata scorsa come si legge un testo, una riga dopo l’altra.
Per colonne: con , il ciclo interno scorre .
Con , la seconda colonna.
Con .
Con . Ogni cella è stata visitata di nuovo esattamente una volta: scambiare i cicli non cambia che cosa si visita, ma quando.
Righe di lunghezze diverse
Nulla obbliga le righe di un vettore di vettori ad avere la stessa lunghezza: ciascuna è un vettore indipendente. A volte è ciò che si vuole, come nel triangolo di Tartaglia (o di Pascal); ma in una tabella, rettangolare per definizione, una riga più corta è un errore dei dati, e il programma che usa table[0].size() come numero di colonne legge fuori da essa. Una tabella che arriva dall’esterno deve, pertanto, essere controllata.
#include <print>#include <vector>// Precondizione: nessuna.// Postcondizione: restituisce true se table ha almeno una riga e tutte le// sue righe hanno la lunghezza della prima.bool isRectangular(const std::vector<std::vector<int>>& table){ if (table.empty()) { return false; } for (const std::vector<int>& row : table) { if (row.size() != table[0].size()) { return false; } } return true;}int main(){ const std::vector<std::vector<int>> pascal{{1}, {1, 1}, {1, 2, 1}, {1, 3, 3, 1}}; const std::vector<std::vector<int>> grid{{1, 2, 3}, {4, 5, 6}}; std::println("{} {}", pascal, isRectangular(pascal)); std::println("{} {}", grid, isRectangular(grid));}[[1], [1, 1], [1, 2, 1], [1, 3, 3, 1]] false[[1, 2, 3], [4, 5, 6]] trueLa funzione esclude la tabella senza righe, nella quale table[0] non esiste. Le funzioni di questa lezione che leggono table[0].size() esigono nella loro precondizione una tabella rettangolare e non vuota, e isRectangular permette di assicurarla.
L’array di array
Quando le dimensioni sono note in compilazione (una scacchiera, un quadrato magico), la tabella può essere uno std::array di std::array: std::array<std::array<int, 3>, 3> si legge dall’interno verso l’esterno, «3 righe di 3 interi». Serve da esempio il quadrato magico di ordine 3, le cui righe, colonne e diagonali hanno somma 15.
#include <array>#include <print>int main(){ constexpr std::array<std::array<int, 3>, 3> magic{{ {2, 7, 6}, {9, 5, 1}, {4, 3, 8}, }}; std::println("{}", magic); std::println("Centro {}, {} byte", magic[1][1], sizeof(magic));}[[2, 7, 6], [9, 5, 1], [4, 3, 8]]Centro 5, 36 byteLe graffe sono tre a ogni estremità. std::array ha un solo membro, un array C, sicché la graffa esterna è quella di std::array, la seconda quella di questo array interno, e solo la terza quella di ogni riga. Con due graffe, la prima riga viene presa come inizializzatore dell’array interno, e la seconda non ha più nulla da inizializzare.
#include <array>#include <print>int main(){ const std::array<std::array<int, 3>, 2> table{{1, 2, 3}, {4, 5, 6}}; std::println("{}", table);}main.cpp: In function ‘int main()’:main.cpp:6:71: error: too many initializers for ‘const std::array<std::array<int, 3>, 2>’ 6 | const std::array<std::array<int, 3>, 2> table{{1, 2, 3}, {4, 5, 6}}; | ^Too many initializers: la lista {4, 5, 6} è di troppo. La lista piatta, {1, 2, 3, 4, 5, 6}, compila anch’essa e riempie le righe in ordine, ma nasconde la forma della tabella; il corso scrive le tre graffe.
I 36 byte del sizeof sono quelli di nove int da 4 byte (la dimensione abituale, che lo standard non garantisce), senza nient’altro: l’array di array conserva le righe una dopo l’altra in un solo blocco. Il vettore di vettori, invece, chiede un blocco per riga, oltre a quello delle righe, e questi blocchi non sono necessariamente contigui. La scelta segue, pertanto, la regola delle lezioni Array e Vettori.
- Se entrambe le dimensioni sono note in compilazione, l’array di array: un solo blocco, la forma rettangolare garantita dal tipo e la possibilità di
constexpr. - Se una delle due è nota solo in esecuzione, il vettore di vettori, con l’obbligo di controllare che le righe abbiano la stessa lunghezza, o il vettore piatto dell’ultima sezione.
Caricamento del contenuto…