← CC2008

Arreglos Multidimensionales

Semestre 02, 2026

Contenido de la lección

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.
012
X
O
X
O
X
.
.
.
O

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.

[0]──▶
0
0
0
0
[1]──▶
0
0
0
0
[2]──▶
0
0
0
0

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.
012
0
0
0
0
0
0
45
0
0
0
0
0

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.
1
2
3
4
5
6
7
8
9
10
11
12
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.

1
4
7
10
2
5
8
11
3
6
9
12
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.

00
01
02
03
10
11
12
13
20
21
22
23
30
31
32
33
diagonal principal
00
01
02
03
10
11
12
13
20
21
22
23
30
31
32
33
diagonal inversa
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];
[0]──▶
0
[1]──▶
0
0
[2]──▶
0
0
0
[3]──▶
0
0
0
0
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.

1Recorrer las filas
  • El ciclo más externo baja por la imagen, una fila a la vez.
2Recorrer los píxeles de la fila
  • El ciclo intermedio avanza por las columnas y llega a un píxel concreto.
3Recorrer 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ónCasillas
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.

01234
cinco nodos, cinco conexiones
01234
0
0
1
0
1
0
1
1
0
1
0
0
2
0
1
0
1
0
3
1
0
1
0
1
4
0
0
0
1
0
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.

01234
el nodo 3 y sus vecinos
01234
0
0
1
0
1
0
1
1
0
1
0
0
2
0
1
0
1
0
3
1
0
1
0
1
4
0
0
0
1
0
fila 3 completa
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.

01234
0
0
1
0
1
0
1
1
0
1
0
0
2
0
1
0
1
0
3
1
0
1
0
1
4
0
0
0
1
0
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.

01234
¿el 0 se conecta con el 3?
01234
0
0
1
0
1
0
1
1
0
1
0
0
2
0
1
0
1
0
3
1
0
1
0
1
4
0
0
0
1
0
fila 0, columna 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.

01234
el nodo 3 y sus vecinos
01234
0
0
1
0
1
0
1
1
0
1
0
0
2
0
1
0
1
0
3
1
0
1
0
1
4
0
0
0
1
0
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.

01234
el nodo 3 es el más conectado
01234
0
0
1
0
1
0
2
1
1
0
1
0
0
2
2
0
1
0
1
0
2
3
1
0
1
0
1
3
4
0
0
0
1
0
1
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.

01234
el nodo 4 pierde su única conexión
01234
0
0
1
0
1
0
1
1
0
1
0
0
2
0
1
0
1
0
3
1
0
1
0
0
4
0
0
0
0
0
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.

01234
el 0 y el 2 comparten al 1 y al 3
01234
0
0
1
0
1
0
1
1
0
1
0
0
2
0
1
0
1
0
3
1
0
1
0
1
4
0
0
0
1
0
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.

01234
el camino 0 → 1 → 2
01234
0
0
1
0
1
0
1
1
0
1
0
0
2
0
1
0
1
0
3
1
0
1
0
1
4
0
0
0
1
0
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.

240
240
240
240
20
20
240
20
20
20
20
20
240
240
240
20
20
20
240
20
20
20
20
20
240
240
240
240
20
20
20
20
20
20
20
20
la matriz
la misma matriz, pintada

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.

original
invertida

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.

original
más brillante

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.

original
reflejada

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.

original
rotada

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.
[0]──▶
?
[1]──▶
?
[2]──▶
?

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();
}
80 75 90 88 92 79
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);
[0]──▶
Ana
Luis
[1]──▶
Carlos
Marta
Sofia
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

Criterioint[][]ArrayList de ArrayList
TamañoFijo al crearseCrece y se encoge
FilasExisten desde el inicioSe agregan con add()
Accesomatriz[i][j]matriz.get(i).get(j)
Tamaño de la filamatriz[i].lengthmatriz.get(i).size()
ContenidoPrimitivos u objetosSolo 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.