Vai al contenuto
Topos Uranos

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

  1. Rappresentare una tabella come std::vector<std::vector<T>> o come std::array di std::array, e scegliere tra le due secondo che la dimensione sia nota o no in compilazione.
  2. Scorrere una tabella per righe e per colonne, e calcolare i totali e le medie di ogni riga e di ogni colonna.
  3. Implementare operazioni sulle tabelle (la trasposta, il prodotto di matrici, il conteggio delle vicine di una cella) badando che nessun indice esca dai bordi.
  4. 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 mm righe e nn colonne è una collezione di m⋅nm \cdot n dati dello stesso tipo, ciascuno designato da due indici: quello della sua riga, ii, con 0≤i<m0 \leq i < m, e quello della sua colonna, jj, con 0≤j<n0 \leq j < n. La matematica la chiama matrice e scrive aija_{ij}; 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>>.

Programma in C++
#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);}
Output
[[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.

EsempioScorrere per righe e per colonne

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

  1. La tabella, con gli indici delle righe a sinistra e quelli delle colonne in alto.

  2. Per righe: con i=0i = 0, il ciclo interno scorre j=0,1,2,3j = 0, 1, 2, 3.

  3. Con i=1i = 1, il ciclo interno ricomincia da j=0j = 0.

  4. Con i=2i = 2: la tabella è stata scorsa come si legge un testo, una riga dopo l’altra.

  5. Per colonne: con j=0j = 0, il ciclo interno scorre i=0,1,2i = 0, 1, 2.

  6. Con j=1j = 1, la seconda colonna.

  7. Con j=2j = 2.

  8. Con j=3j = 3. 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.

Programma in C++
#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));}
Output
[[1], [1, 1], [1, 2, 1], [1, 3, 3, 1]] false[[1, 2, 3], [4, 5, 6]] true

La 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.

Programma in C++
#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));}
Output
[[2, 7, 6], [9, 5, 1], [4, 3, 8]]Centro 5, 36 byte

Le 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.

Programma in C++
#include <array>#include <print>​int main(){    const std::array<std::array<int, 3>, 2> table{{1, 2, 3}, {4, 5, 6}};    std::println("{}", table);}
Messaggi di errore del compilatore
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.