Resumo
Muitos dados não formam uma fila, mas uma grade: as notas de uma turma por estudante e por prova, um tabuleiro, uma matriz. Esta aula apresenta a tabela, a estrutura bidimensional, nas suas duas formas de C++: o vetor de vetores, de tamanho decidido na execução, e o arranjo de arranjos, de tamanho conhecido na compilação. Seguem-se os percursos por linhas e por colunas; a transposta e o produto de matrizes, de custo cúbico; as vizinhas de uma célula, que obrigam a cuidar das bordas; e a tabela guardada em um só vetor com o índice i * columns + j, que é como a memória a vê.
Objetivos de aprendizagem
- Representar uma tabela como
std::vector<std::vector<T>>ou comostd::arraydestd::array, e escolher entre ambas conforme o tamanho seja ou não conhecido na compilação. - Percorrer uma tabela por linhas e por colunas, e calcular os totais e as médias de cada linha e de cada coluna.
- Implementar operações de tabelas (a transposta, o produto de matrizes, a contagem das vizinhas de uma célula) cuidando que nenhum índice saia das bordas.
- Representar uma tabela em um vetor plano com o índice
i * columns + j, e explicar com um exemplo a correspondência entre a célula e a sua posição.
Tabelas
Linhas e colunas
Uma tabela de linhas e colunas é uma coleção de dados do mesmo tipo, cada um designado por dois índices: o da sua linha, , com , e o da sua coluna, , com . A matemática a chama matriz e escreve ; o curso Álgebra e Geometria II estuda as matrizes e as suas operações, e esta aula escreve algumas como programas. A ordem dos índices não admite exceções: primeiro a linha, depois a coluna. Confundi-los é o erro mais frequente com tabelas, e o compilador não pode avisar, porque ambos os índices são do mesmo tipo.
A aula Vetores apresentou a sequência de tamanho variável. Se isso é assim, uma tabela é uma sequência de linhas, e cada linha é por sua vez uma sequência: um vetor cujos elementos são vetores, 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("Linhas {}, colunas {}", table.size(), table[0].size()); std::println("Linha 1: {}; linha 1, coluna 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]]Linhas 2, colunas 3Linha 1: [4, 5, 6]; linha 1, coluna 2: 6 1 20 3 4 5 6[[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]A linha 6 escreve a tabela como uma lista de linhas, cada uma entre as suas chaves, e a formatação de intervalos a imprime igual, com colchetes. table.size() é o número de linhas, e table[0].size(), o comprimento da primeira, que em uma tabela bem formada é o número de colunas. table[1] é a linha 1 inteira, um std::vector<int>, e table[1][2], o elemento 2 dessa linha: o índice duplo não é uma operação nova, mas dois acessos sucessivos; o primeiro escolhe a linha e o segundo, dentro dela, a coluna. As linhas 11 a 16 escrevem a tabela em forma de grade com dois for baseados em intervalo aninhados; a linha da tabela é tomada por referência constante para não copiá-la a cada iteração. A linha 17 cria uma tabela de 3 por 4 cheia de zeros: com parênteses, como na aula Vetores, pedem-se 3 cópias de uma linha de quatro zeros.
Uma tabela de 3 por 4, percorrida linha por linha (i no laço exterior) e depois coluna por coluna (os laços trocados). Cada célula mostra o seu número de ordem de visita.
Demonstração
A tabela, com os índices das linhas à esquerda e os das colunas em cima.
Por linhas: com , o laço interior percorre .
Com , o laço interior começa outra vez em .
Com : a tabela foi percorrida como se lê um texto, uma linha após a outra.
Por colunas: com , o laço interior percorre .
Com , a segunda coluna.
Com .
Com . Cada célula foi visitada outra vez exatamente uma vez: trocar os laços não muda o que se visita, mas quando.
Linhas de comprimentos diferentes
Nada obriga as linhas de um vetor de vetores a ter o mesmo comprimento: cada uma é um vetor independente. Às vezes é o que se quer, como no triângulo de Pascal; mas em uma tabela, retangular por definição, uma linha mais curta é um erro dos dados, e o programa que usa table[0].size() como número de colunas lê fora dela. Uma tabela que chega de fora deve, portanto, ser verificada.
#include <print>#include <vector>// Pré-condição: nenhuma.// Pós-condição: devolve true se table tem pelo menos uma linha e todas as// suas linhas têm o comprimento da primeira.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]] trueA função exclui a tabela sem linhas, na qual table[0] não existe. As funções desta aula que leem table[0].size() exigem na sua pré-condição uma tabela retangular e não vazia, e isRectangular permite assegurá-la.
O arranjo de arranjos
Quando as dimensões são conhecidas na compilação (um tabuleiro de xadrez, um quadrado mágico), a tabela pode ser um std::array de std::array: std::array<std::array<int, 3>, 3> se lê de dentro para fora, “3 linhas de 3 inteiros”. Serve de exemplo o quadrado mágico de ordem 3, cujas linhas, colunas e diagonais somam 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 {}, {} bytes", magic[1][1], sizeof(magic));}[[2, 7, 6], [9, 5, 1], [4, 3, 8]]Centro 5, 36 bytesAs chaves são três em cada extremo. std::array tem um só membro, um arranjo de C, de modo que a chave exterior é a de std::array, a segunda a desse arranjo interior, e só a terceira a de cada linha. Com duas chaves, a primeira linha é tomada como inicializador do arranjo interior, e a segunda já não tem o que inicializar.
#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: sobra a lista {4, 5, 6}. A lista plana, {1, 2, 3, 4, 5, 6}, também compila e preenche as linhas em ordem, mas oculta a forma da tabela; o curso escreve as três chaves.
Os 36 bytes do sizeof são os de nove int de 4 bytes (o tamanho habitual, que o padrão não garante), sem mais nada: o arranjo de arranjos guarda as linhas uma após a outra em um só bloco. O vetor de vetores, por sua vez, pede um bloco por linha, além do das linhas, e esses blocos não têm por que estar contíguos. A escolha segue, portanto, a regra das aulas Arranjos e Vetores.
- Se ambas as dimensões são conhecidas na compilação, o arranjo de arranjos: um só bloco, a forma retangular garantida pelo tipo e a possibilidade de
constexpr. - Se alguma só é conhecida na execução, o vetor de vetores, com a obrigação de verificar que as linhas tenham o mesmo comprimento, ou o vetor plano da última seção.
Carregando o conteúdo…