Saltar al contenido
Topos Uranos

Resumen

Muchos datos no forman una fila, sino un cuadro: las notas de un curso por estudiante y por evaluación, un tablero, una matriz. Esta clase presenta la tabla, la estructura de dos dimensiones, en sus dos formas de C++: el vector de vectores, de tamaño decidido al ejecutar, y el arreglo de arreglos, de tamaño conocido al compilar. Siguen los recorridos por filas y por columnas; la traspuesta y el producto de matrices, de costo cúbico; los vecinos de una casilla, que obligan a cuidar los bordes; y la tabla guardada en un solo vector con el índice i * columns + j, que es como la memoria la ve.

Objetivos de aprendizaje

  1. Representar una tabla como std::vector<std::vector<T>> o como std::array de std::array, y elegir entre ambas según se conozca o no el tamaño al compilar.
  2. Recorrer una tabla por filas y por columnas, y calcular los totales y las medias de cada fila y de cada columna.
  3. Implementar operaciones de tablas (la traspuesta, el producto de matrices, el recuento de los vecinos de una casilla) cuidando que ningún índice salga de los bordes.
  4. Representar una tabla en un vector plano con el índice i * columns + j, y explicar con un ejemplo la correspondencia entre la casilla y su posición.

Tablas

Filas y columnas

Una tabla de mm filas y nn columnas es un conjunto de m⋅nm \cdot n datos del mismo tipo, cada uno designado por dos índices: el de su fila, ii, con 0≤i<m0 \leq i < m, y el de su columna, jj, con 0≤j<n0 \leq j < n. La matemática la llama matriz y escribe aija_{ij}; el curso Álgebra y Geometría II estudia las matrices y sus operaciones, y esta clase escribe algunas como programas. El orden de los índices no admite excepciones: primero la fila, después la columna. Confundirlos es el error más frecuente con tablas, y el compilador no puede advertirlo, porque ambos índices son del mismo tipo.

La clase Vectores presentó la secuencia de tamaño variable. Si esto es así, una tabla es una secuencia de filas, y cada fila es a su vez una secuencia: un vector cuyos elementos son vectores, std::vector<std::vector<int>>.

Programa en C++
#include <print>#include <vector>​int main(){    std::vector<std::vector<int>> table{{1, 2, 3}, {4, 5, 6}};    std::println("{}", table);    std::println("Filas {}, columnas {}", table.size(), table[0].size());    std::println("Fila 1: {}; fila 1, columna 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);}
Salida
[[1, 2, 3], [4, 5, 6]]Filas 2, columnas 3Fila 1: [4, 5, 6]; fila 1, columna 2: 6   1  20   3   4   5   6[[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]

La línea 6 escribe la tabla como una lista de filas, cada una entre sus llaves, y el formato de rangos la imprime igual, con corchetes. table.size() es el número de filas, y table[0].size(), la longitud de la primera, que en una tabla bien formada es el número de columnas. table[1] es la fila 1 entera, un std::vector<int>, y table[1][2], el elemento 2 de esa fila: el doble índice no es una operación nueva, sino dos accesos sucesivos, el primero elige la fila y el segundo, dentro de ella, la columna. Las líneas 11 a 16 escriben la tabla como cuadro con dos for por rango anidados; la fila se toma por referencia constante para no copiarla en cada vuelta. La línea 17 crea una tabla de 3 por 4 llena de ceros: con paréntesis, como en la clase anterior, se piden 3 copias de una fila de cuatro ceros.

EjemploRecorrer por filas y por columnas

Una tabla de 3 por 4, recorrida fila por fila (i en el bucle exterior) y después columna por columna (los bucles intercambiados). Cada casilla muestra su número de orden de visita.

Demostración

  1. La tabla, con los índices de las filas a la izquierda y los de las columnas arriba.

  2. Por filas: con i=0i = 0, el bucle interior recorre j=0,1,2,3j = 0, 1, 2, 3.

  3. Con i=1i = 1, el bucle interior empieza otra vez en j=0j = 0.

  4. Con i=2i = 2: la tabla se recorrió como se lee un texto, renglón tras renglón.

  5. Por columnas: con j=0j = 0, el bucle interior recorre i=0,1,2i = 0, 1, 2.

  6. Con j=1j = 1, la segunda columna.

  7. Con j=2j = 2.

  8. Con j=3j = 3. Cada casilla se visitó otra vez exactamente una vez: intercambiar los bucles no cambia qué se visita, sino cuándo.

Filas de longitudes distintas

Nada obliga a las filas de un vector de vectores a tener la misma longitud: cada una es un vector independiente. A veces es lo que se quiere, como en el triángulo de Pascal; pero en una tabla, rectangular por definición, una fila más corta es un error de los datos, y el programa que usa table[0].size() como número de columnas lee fuera de ella. Una tabla que llega de fuera debe, por tanto, comprobarse.

Programa en C++
#include <print>#include <vector>​// Precondición: ninguna.// Poscondición: devuelve true si table tiene al menos una fila y todas sus// filas tienen la longitud de la primera.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));}
Salida
[[1], [1, 1], [1, 2, 1], [1, 3, 3, 1]] false[[1, 2, 3], [4, 5, 6]] true

La función excluye la tabla sin filas, en la que table[0] no existe. Las funciones de esta clase que leen table[0].size() exigen en su precondición una tabla rectangular y no vacía, y isRectangular permite asegurarla.

El arreglo de arreglos

Cuando las dimensiones se conocen al compilar (un tablero de ajedrez, un cuadrado mágico), la tabla puede ser un std::array de std::array: std::array<std::array<int, 3>, 3> se lee de adentro hacia afuera, «3 filas de 3 enteros». Sirve de ejemplo el cuadrado mágico de orden 3, cuyas filas, columnas y diagonales suman 15.

Programa en 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));}
Salida
[[2, 7, 6], [9, 5, 1], [4, 3, 8]]Centro 5, 36 bytes

Las llaves son tres en cada extremo. std::array tiene un solo miembro, un arreglo de C, de modo que la llave exterior es la de std::array, la segunda la de ese arreglo interior, y recién la tercera la de cada fila. Con dos llaves, la primera fila se toma como inicializador del arreglo interior, y la segunda ya no tiene qué inicializar.

Programa en 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);}
Mensajes de error del 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 la lista {4, 5, 6}. La lista plana, {1, 2, 3, 4, 5, 6}, también compila y llena las filas en orden, pero oculta la forma de la tabla; el curso escribe las tres llaves.

Los 36 bytes del sizeof son los de nueve enteros, sin nada más: el arreglo de arreglos guarda las filas una tras otra en un solo bloque. El vector de vectores, en cambio, pide un bloque por fila, además del de las filas, y esos bloques no tienen por qué estar contiguos. La elección sigue, por tanto, la regla de la clase anterior.

  • Si ambas dimensiones se conocen al compilar, el arreglo de arreglos: un solo bloque, la forma rectangular garantizada por el tipo y la posibilidad de constexpr.
  • Si alguna se conoce solo al ejecutar, el vector de vectores, con la obligación de comprobar que las filas tengan la misma longitud, o el vector plano de la última sección.