El dato que necesita dos coordenadas
Creación, índices y recorridos anidados
Caso de uso: redes y matriz de adyacencia
Caso de uso: imágenes y píxeles
Arreglos irregulares, tres dimensiones y más
Matrices dinámicas con ArrayList anidado
Errores comunes y buenas prácticas
El dato que tiene dos coordenadas
Un arreglo de una dimensión resuelve bien las listas: cada elemento se localiza con un solo número.
Un tablero, una pantalla o una hoja de cálculo no son listas: cada casilla necesita dos números.
La fila dice a qué altura está, la columna a qué distancia horizontal.
La alternativa de aplanar
El tablero se podría guardar en un arreglo de nueve casillas, calculando la posición a mano.
tablero[ fila * 3 + columna ]
La fórmula funciona, pero hay que repetirla en cada acceso.
Cada lectura y cada escritura arrastran la multiplicación, y basta equivocarse una vez para corromper el tablero.
El código deja de parecerse al problema: se lee aritmética donde debería leerse una fila y una columna.
El arreglo multidimensional hace esa traducción por nosotros.
Un arreglo cuyas casillas son arreglos
En Java no existe una estructura especial de matriz. Lo que existe es un arreglo cuyos elementos son, a su vez, arreglos.
int[][] matriz = new int[3][4];
Cuatro objetos
Esa línea crea un arreglo externo de tres casillas y tres arreglos internos de cuatro casillas cada uno.
Las casillas externas guardan referencias
El arreglo externo no guarda números: guarda la dirección de cada arreglo interno.
Es el mismo mecanismo de los arreglos de objetos que ya vimos, solo que aquí los objetos apuntados son arreglos.
Cada fila es un arreglo completo e independiente, con su propia longitud.
Al pasar la matriz a un método se copia solo la referencia externa, así que el método trabaja sobre la misma estructura.
Creación con tamaño conocido
int[][] matriz = new int[3][4];
Reserva la estructura completa: el arreglo externo y todas sus filas de una sola vez.
Todo queda con el valor por defecto del tipo: cero para los numéricos, false para boolean, null para las referencias.
El primer número es la cantidad de filas y el segundo la cantidad de columnas.
Creación con valores literales
int[][] notas = {
{80, 75, 90},
{88, 92, 79},
{65, 70, 72}
};
Sin new
El compilador deduce que son tres filas de tres columnas y reserva la memoria por su cuenta.
Es la forma más clara cuando los datos se conocen al escribir el código.
Cada grupo de llaves internas es una fila completa.
Creación por partes
int[][] matriz = new int[3][];
matriz[0] = new int[4];
matriz[1] = new int[2];
matriz[2] = new int[7];
Segundo corchete vacío
Solo se reserva el arreglo externo. Cada fila se crea después, y puede tener el largo que se quiera.
Entre las dos instrucciones las filas valen null: usarlas antes de crearlas lanza una excepción de puntero nulo.
Los dos índices
matriz[ 1 ][ 2 ] = 45;
El primero elige la fila, el segundo la columna.
El primer índice elige cuál de los arreglos internos se va a usar.
El segundo elige la posición dentro de ese arreglo.
Leer la expresión en dos tiempos
Los dos corchetes no son una unidad: se pueden separar, porque una fila es un arreglo por derecho propio.
int[] segundaFila = matriz[1];
int valor = segundaFila[2];
La primera línea guarda la referencia a la fila uno en una variable de tipo arreglo.
La segunda accede a su casilla dos, que es exactamente lo que significa matriz[1][2].
Como es una referencia y no una copia, modificar segundaFila modifica la matriz original.
El tamaño de cada dimensión
matriz.length // cantidad de filas
matriz[0].length // columnas de la primera fila
matriz[2].length // columnas de la tercera fila
El length del arreglo externo da el número de filas, porque cada casilla externa es una fila.
Cada fila es un arreglo independiente, así que cada una tiene su propio length.
Usar matriz[0].length para todas las filas solo es correcto si la matriz es rectangular.
El orden del recorrido
Recorrer una matriz completa pide dos ciclos. Los números indican en qué orden se visita cada casilla.
El ciclo externo avanza por las filas y decide en cuál se está trabajando.
El interno recorre todas las columnas de esa fila antes de que el externo avance.
El resultado es el orden de lectura de un texto: se termina una fila completa antes de bajar a la siguiente.
orden de visita, fila por fila
Cuántas veces se ejecuta el cuerpo
Filas por columnas
El cuerpo del ciclo interno corre una vez por cada casilla. En una matriz de 3 por 4 son 12 ejecuciones.
El costo crece rápido
Duplicar filas y columnas cuadruplica el trabajo. Una imagen de 1000 por 1000 son un millón de visitas por cada filtro.
El ciclo interno se reinicia en cada vuelta del externo: vuelve a empezar en la columna cero para la fila siguiente.
El límite del ciclo interno se consulta sobre la fila actual, no sobre la primera, para que funcione también con filas de largos distintos.
Recorrido por columnas
Intercambiar el papel de los dos ciclos cambia el orden de visita sin cambiar la estructura ni un solo dato.
orden de visita, columna por columna
Ahora el externo avanza por columnas y el interno por filas: la columna queda fija mientras se baja por ella.
Sirve para agregar por columna: el promedio de cada examen en vez del promedio de cada alumno.
Este recorrido asume que la matriz es rectangular, porque supone que todas las filas llegan a la misma columna.
Los patrones de índices
No todo recorrido usa dos ciclos. Cuando los índices siguen un patrón, basta con una sola variable.
Fila: el primer índice queda fijo y el segundo avanza.
Columna: el segundo queda fijo y el primero avanza.
Diagonal principal: los dos avanzan juntos, así que siempre coinciden.
Diagonal inversa: uno crece mientras el otro decrece, y suman siempre lo mismo.
El recorrido define la operación
Idea clave
La matriz siempre es la misma. Lo que cambia entre calcular un promedio por alumno, uno por examen o revisar una diagonal es únicamente el patrón con que se mueven los dos índices.
Antes de escribir un ciclo conviene dibujar qué casillas hay que tocar y en qué orden.
Reconocido el patrón, escribir el ciclo es mecánico: es la parte que vamos a construir en clase.
Arreglos irregulares
Nada obliga a que todas las filas midan lo mismo. Cuando difieren, el arreglo se llama irregular o dentado.
int[][] triangulo = new int[4][];
triangulo[0] = new int[1];
triangulo[1] = new int[2];
triangulo[2] = new int[3];
triangulo[3] = new int[4];
Es posible porque cada fila es un objeto independiente: el arreglo externo solo guarda referencias, y no le importa a qué tamaño apuntan.
Irregulares con valores literales
int[][] pascal = {
{1},
{1, 1},
{1, 2, 1},
{1, 3, 3, 1}
};
Triángulo de Pascal
Cada fila tiene un elemento más que la anterior, así que la estructura irregular es la que describe el dato con exactitud.
Una matriz rectangular de cuatro por cuatro desperdiciaría seis casillas en ceros que nunca se usan.
La regla obligatoria del recorrido
Incorrecto
for (int j = 0;
j < matriz[0].length; j++)
Correcto
for (int j = 0;
j < matriz[i].length; j++)
Usar el length de la primera fila provoca un error de índice fuera de rango en cuanto una fila posterior sea más corta.
Consultar el length de la fila actual funciona igual en matrices rectangulares, así que conviene escribirlo siempre así.
Cuándo conviene un arreglo irregular
Estructuras naturalmente triangulares, como el triángulo de Pascal o una tabla de distancias entre ciudades.
Listas de longitud variable por categoría: los alumnos inscritos en cada sección de un curso.
Ahorro de memoria cuando la mayoría de las filas son mucho más cortas que la más larga.
Tres dimensiones
Agregar un índice agrega una dimensión. Una imagen a color es el ejemplo típico: cada píxel ya no es un número sino tres, uno por canal.
int[][][] imagen = new int[alto][ancho][3];
imagen[i][j][0] = 255; // canal rojo
imagen[i][j][1] = 128; // canal verde
imagen[i][j][2] = 0; // canal azul
De afuera hacia adentro
Un arreglo de filas, donde cada fila es un arreglo de píxeles, donde cada píxel es un arreglo de tres canales.
Recorrer tres dimensiones
Cada dimensión agrega un ciclo. Invertir una imagen a color es el mismo recorrido de antes, con un nivel más adentro.
1 Recorrer las filas
El ciclo más externo baja por la imagen, una fila a la vez.
2 Recorrer los píxeles de la fila
El ciclo intermedio avanza por las columnas y llega a un píxel concreto.
3 Recorrer los canales del píxel
El ciclo interno visita el rojo, el verde y el azul, y aplica la operación a cada uno.
El caso general de n dimensiones
La sintaxis no tiene un límite práctico: cada par de corchetes agrega una dimensión.
int[][][][] tensor = new int[2][3][4][5];
El número de casillas es el producto de todas las dimensiones, así que el consumo crece muy rápido.
Declaración Casillas
new int[100][100] 10 mil
new int[100][100][100] 1 millón
new int[100][100][100][100] 100 millones, unos 400 MB
El límite práctico
Rara vez más de tres
Cuando el problema parece pedir cuatro o más dimensiones, casi siempre conviene modelarlo con clases.
La alternativa
Un arreglo de objetos, donde cada objeto tiene sus propios atributos con nombre.
En datos[2][7][3][1] ningún índice dice qué significa, y hay que recordar el orden de memoria.
En sucursales[2].ventas[7] cada nivel tiene nombre, y el compilador ayuda si se escribe mal.
Matriz de adyacencia
Una red de conexiones se guarda en una matriz cuadrada: tantas filas y columnas como nodos tenga la red.
0 1 2 3 4
cinco nodos, cinco conexiones
0 1 2 3 4
la misma red como matriz
El dibujo y la matriz contienen exactamente la misma información: un uno significa que los dos nodos están conectados.
Una fila es un nodo
La fila tres reúne todas las conexiones del nodo tres: se lee de corrido, sin recorrer el resto de la matriz.
0 1 2 3 4
el nodo 3 y sus vecinos
Sumar la fila da el número de conexiones del nodo, que en un grafo se llama su grado.
La columna tres dice lo mismo desde el otro lado: quién apunta hacia ese nodo.
La simetría de la matriz
Si la amistad es mutua, cada conexión aparece dos veces: en la casilla y en su reflejo respecto a la diagonal.
0 1 2 3 4
la casilla y su reflejo
La casilla de la fila cero y columna tres, y la de la fila tres y columna cero, guardan el mismo dato.
La diagonal marca las casillas donde un nodo se compara consigo mismo, y normalmente vale cero.
Cuando la relación no es mutua, como seguir a alguien en una red social, la matriz deja de ser simétrica.
Preguntas que responde la matriz
Cada pregunta sobre la red se convierte en un recorrido distinto sobre la misma matriz. Cambia el patrón de índices, no los datos.
Una casilla: la conexión entre dos nodos.
Una fila: el grado de un nodo.
Todas las filas: el nodo más conectado.
Una fila de ceros: un nodo aislado.
Dos filas comparadas: los amigos en común.
Dos casillas encadenadas: llegar en dos pasos.
Conexión directa entre dos nodos
La pregunta se responde leyendo una sola casilla, sin recorrer nada.
0 1 2 3 4
¿el 0 se conecta con el 3?
La casilla de la fila cero y la columna tres vale uno, así que la conexión existe.
La casilla de la fila cero y la columna dos vale cero: entre el 0 y el 2 no hay conexión directa.
Es la operación más barata de la matriz: una sola lectura, sin importar cuántos nodos tenga la red.
Grado de un nodo
El grado es cuántas conexiones tiene un nodo. Se obtiene sumando toda su fila.
0 1 2 3 4
el nodo 3 y sus vecinos
0 1 2 3 4
fila 3: 1 + 0 + 1 + 0 + 1 = 3
Sumar los cinco valores de la fila da tres, que es el número de líneas que salen del nodo en el dibujo.
Es un recorrido de una sola dimensión: se fija la fila y se avanza por las columnas.
El nodo más conectado
Calcular el grado de todos y quedarse con el mayor. Ahora sí hacen falta los dos índices.
0 1 2 3 4
el nodo 3 es el más conectado
0 1 2 3 4
la suma de cada fila, a la derecha
El ciclo externo recorre las filas y el interno suma cada una: el recorrido completo de la matriz.
Los nodos 0, 1 y 2 tienen grado dos y el nodo 4 solo uno, así que el nodo 3 gana con tres.
En una red social esto identifica a la cuenta más influyente.
Un nodo aislado
Un nodo sin ninguna conexión deja su fila entera en ceros. Si se elimina la conexión entre el 3 y el 4, el nodo 4 queda suelto.
0 1 2 3 4
el nodo 4 pierde su única conexión
0 1 2 3 4
fila 4: puros ceros
Basta recorrer la fila y verificar que no aparezca ningún uno.
El nodo 4 tenía grado uno, así que era el más frágil de la red: perder una sola conexión lo desconecta por completo.
Amigos en común
Comparar dos filas y contar en qué columnas ambas tienen un uno.
0 1 2 3 4
el 0 y el 2 comparten al 1 y al 3
0 1 2 3 4
filas 0 y 2, columnas que coinciden
Las dos filas coinciden en las columnas uno y tres: esos son los amigos que comparten.
El recorrido avanza por las columnas comparando dos filas a la vez, no una sola.
Llegar en dos pasos
El 0 y el 2 no están conectados directamente, pero se alcanzan pasando por un nodo intermedio.
0 1 2 3 4
el camino 0 → 1 → 2
0 1 2 3 4
dos casillas encadenadas
La casilla de la fila cero y la columna dos vale cero: no hay conexión directa.
Pero la fila cero tiene un uno en la columna uno, y la fila uno tiene un uno en la columna dos.
Así funciona la sugerencia de a quién seguir: gente conectada con tus conexiones, pero no contigo.
Dónde se usa esta estructura
Redes sociales: la sugerencia de a quién seguir sale de buscar conexiones a dos pasos.
Mapas y rutas: en lugar de unos y ceros, la casilla guarda la distancia entre dos ciudades.
Recomendaciones: filas de usuarios y columnas de productos, con la compra marcada en la casilla.
Dependencias entre módulos de un programa, que es como se detectan las referencias circulares.
Una imagen es una matriz
Cada casilla guarda la intensidad de un píxel, de 0 para negro a 255 para blanco. Los números de la izquierda producen la figura de la derecha.
for (int i = 0; i < imagen.length; i++) {
for (int j = 0; j < imagen[i].length; j++) {
pintarPixel(j, i, imagen[i][j]);
}
}
Invertir los colores
Se visita cada casilla y se reemplaza su valor por el complemento: lo que valía 240 pasa a valer 15.
for (int i = 0; i < imagen.length; i++) {
for (int j = 0; j < imagen[i].length; j++) {
salida[i][j] = 255 - imagen[i][j];
}
}
Subir el brillo
Sumar una constante a cada casilla aclara la imagen completa. El resultado se recorta en 255, porque no existe un blanco más blanco.
for (int i = 0; i < imagen.length; i++) {
for (int j = 0; j < imagen[i].length; j++) {
salida[i][j] = Math.min(255, imagen[i][j] + 70);
}
}
Espejo horizontal
Aquí no cambia ningún valor: cambia a qué casilla va cada uno. La fila se mantiene y la columna se lee al revés.
int ancho = imagen[0].length;
for (int i = 0; i < imagen.length; i++) {
for (int j = 0; j < ancho; j++) {
salida[i][j] = imagen[i][ancho - 1 - j];
}
}
Rotar noventa grados
Rotar es intercambiar el papel de los dos índices: lo que era fila pasa a ser columna, y la nueva columna se cuenta desde el otro extremo.
int alto = imagen.length;
for (int i = 0; i < alto; i++) {
for (int j = 0; j < imagen[i].length; j++) {
salida[j][alto - 1 - i] = imagen[i][j];
}
}
El límite del arreglo nativo
El tamaño de una matriz nativa se fija al crearla, y hay problemas donde ese número no se conoce hasta que el programa ya está corriendo.
int[][] secciones = new int[?][?];
No se sabe cuántas secciones tiene el curso ni cuántos alumnos hay en cada una hasta leer el archivo.
Un alumno se puede inscribir a mitad de semestre, y la fila tendría que crecer.
Es el mismo problema que resolvió el ArrayList para una dimensión: aquí se necesita en dos.
Un ArrayList dentro de otro ArrayList
La solución es la misma idea del arreglo de arreglos: cada elemento del ArrayList externo es, a su vez, un ArrayList.
ArrayList<ArrayList<Integer>> matriz = new ArrayList<>();
De afuera hacia adentro
Un ArrayList cuyos elementos son ArrayList de enteros. El tipo interno va dentro de los diamantes del externo.
Nace completamente vacío: sin filas y sin elementos, a diferencia del arreglo nativo que se crea con su tamaño ya reservado.
Agregar filas con add()
Cada fila es un ArrayList independiente que hay que crear, llenar y después agregar al externo.
ArrayList<ArrayList<Integer>> matriz = new ArrayList<>();
ArrayList<Integer> fila = new ArrayList<>();
fila.add(80);
fila.add(75);
fila.add(90);
matriz.add(fila);
El add() del ArrayList interno agrega un número a la fila.
El add() del externo agrega la fila completa a la matriz.
Olvidar el último add() es el error más común: la fila se llena pero nunca se conecta, y la matriz queda vacía.
Acceder a elementos con get()
El acceso baja un nivel a la vez, igual que los dos corchetes del arreglo nativo.
matriz.get(1 ).get(2 )
el primer get entrega la fila, el segundo el elemento
Arreglo nativo
int valor = matriz[1][2];
ArrayList anidado
int valor = matriz.get(1).get(2);
matriz.get(1) devuelve un ArrayList completo, así que se le puede aplicar cualquier método de ArrayList.
Para modificar se usa set() en el nivel interno: matriz.get(1).set(2, 95).
Recorrer un ArrayList de ArrayList
El for-each externo entrega una fila completa y el interno cada uno de sus elementos.
for (ArrayList<Integer> fila : matriz) {
for (int nota : fila) {
System.out.print(nota + " ");
}
System.out.println();
}
El tipo del ciclo externo es ArrayList<Integer>, no Integer, porque cada elemento de la matriz es una fila entera.
Con índices se usa size() en lugar de length: matriz.size() da las filas y matriz.get(i).size() las columnas de esa fila.
Filas de distinto largo
Cada sección de un curso tiene un número distinto de alumnos, y ese número cambia durante la inscripción.
ArrayList<ArrayList<String>> secciones = new ArrayList<>();
ArrayList<String> seccionA = new ArrayList<>();
seccionA.add("Ana");
seccionA.add("Luis");
ArrayList<String> seccionB = new ArrayList<>();
seccionB.add("Carlos");
seccionB.add("Marta");
seccionB.add("Sofia");
secciones.add(seccionA);
secciones.add(seccionB);
secciones.get(0).size() es 2 y get(1).size() es 3
La estructura irregular sale gratis: nadie obliga a que las filas midan lo mismo.
Si llega un alumno nuevo, secciones.get(0).add("Pedro") hace crecer esa fila sin tocar las demás.
Arreglo nativo contra ArrayList anidado
Criterio int[][] ArrayList de ArrayList
Tamaño Fijo al crearse Crece y se encoge
Filas Existen desde el inicio Se agregan con add()
Acceso matriz[i][j] matriz.get(i).get(j)
Tamaño de la fila matriz[i].length matriz.get(i).size()
Contenido Primitivos u objetos Solo objetos
Arreglo nativo cuando el tamaño se conoce y no cambia: tableros, imágenes, matrices de tamaño fijo.
ArrayList anidado cuando las filas se agregan durante la ejecución o la estructura cambia.