Saltar para o conteúdo
Topos Uranos

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

  1. Representar uma tabela como std::vector<std::vector<T>> ou como std::array de std::array, e escolher entre ambas conforme o tamanho seja ou não conhecido na compilação.
  2. Percorrer uma tabela por linhas e por colunas, e calcular os totais e as médias de cada linha e de cada coluna.
  3. 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.
  4. 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 mm linhas e nn colunas é uma coleção de m⋅nm \cdot n dados do mesmo tipo, cada um designado por dois índices: o da sua linha, ii, com 0≤i<m0 \leq i < m, e o da sua coluna, jj, com 0≤j<n0 \leq j < n. A matemática a chama matriz e escreve aija_{ij}; 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>>.

Programa em C++
#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);}
Saída
[[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.

ExemploPercorrer por linhas e por colunas

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

  1. A tabela, com os índices das linhas à esquerda e os das colunas em cima.

  2. Por linhas: com i=0i = 0, o laço interior percorre j=0,1,2,3j = 0, 1, 2, 3.

  3. Com i=1i = 1, o laço interior começa outra vez em j=0j = 0.

  4. Com i=2i = 2: a tabela foi percorrida como se lê um texto, uma linha após a outra.

  5. Por colunas: com j=0j = 0, o laço interior percorre i=0,1,2i = 0, 1, 2.

  6. Com j=1j = 1, a segunda coluna.

  7. Com j=2j = 2.

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

Programa em C++
#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));}
Saída
[[1], [1, 1], [1, 2, 1], [1, 3, 3, 1]] false[[1, 2, 3], [4, 5, 6]] true

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

Programa em 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 {}, {} bytes", magic[1][1], sizeof(magic));}
Saída
[[2, 7, 6], [9, 5, 1], [4, 3, 8]]Centro 5, 36 bytes

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

Programa em 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);}
Mensagens de erro do compilador
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.