CodeGym /Cursos /Kotlin SELF /Arrays dentados: Array<IntArray>

Arrays dentados: Array<IntArray>

Kotlin SELF
Nivel 7, Lección 5
Disponible

1. Filas de distinta longitud

Cuando llegamos a tablas, horarios, matrices, «datos por dias», normalmente queremos pensar asi: hay filas y hay columnas. Pero el mundo real rara vez es ideal. Un estudiante tiene 5 notas, otro 3 (falto, estuvo enfermo, el gato arranco el Wi‑Fi), una semana tiene 7 dias, y la ultima semana del mes tiene 2–3 dias. Y ahi es cuando la «tabla rectangular» empieza a sufrir: hay que rellenar las celdas vacias con ceros o con algun tipo de marcador.

Un array dentado resuelve este problema: es literalmente una «estanteria con cajas», donde cada caja puede tener su propia longitud. Por fuera tenemos un array (por ejemplo, semanas), y dentro un array independiente para cada semana (por ejemplo, gastos por dia). Y cada semana puede tener su propio numero de dias/registros.

Para no confundirnos con la terminologia, quedemos en esto: el array externo es la tabla, sus elementos son las filas, y cada array interno es una fila (row). Si estas acostumbrado a las palabras «array bidimensional», piensa exactamente asi, pero con una salvedad importante: la «bidimensionalidad» aqui no tiene por que ser rectangular.

Creamos una estesctura dentada: filas de distinta longitud

La mayoria de las veces, en los primeros pasos es mas comodo crear un array dentado literalmente «a mano» mediante arrayOf(...) e intArrayOf(...). Es la forma mas legible y no requiere conocimientos adicionales.

Imaginemos que hacemos una miniaplicacion «Registro de estudio», donde cada tema tiene un numero distinto de ejercicios (a veces 2, a veces 5 — el profesor tambien es humano). Que cada fila sea un tema y que los numeros en la fila sean puntos por ejercicio.

fun main() {
    val scoresByTopic: Array<IntArray> = arrayOf(
        intArrayOf(10, 8, 9),        // tema 0: 3 ejercicios
        intArrayOf(7, 10),           // tema 1: 2 ejercicios
        intArrayOf(6, 6, 8, 10)      // tema 2: 4 ejercicios
    )

    println(scoresByTopic.size)      // 3
}

Aqui scoresByTopic.size es la cantidad de temas (filas). Pero la cantidad de ejercicios dentro de un tema ya es scoresByTopic[topicIndex].size. Y aqui aparece la primera dificultad: cada fila tiene sus propios limites.

Acceso table[row][col]: dos comprobaciones de limites

Cuando trabajas con un array unidimensional, tienes una comprobacion: el indice debe estar en a.indices. En un array dentado, las comprobaciones pasan a ser dos: primero comprobamos que existe la fila, y despues que dentro de la fila existe la columna.

El error mas frecuente de un principiante es: «he comprobado col con la longitud de la primera fila, asi que todo esta bien». No: la segunda fila puede ser mas corta. En un array dentado no hay “un ancho comun”.

Aqui tienes una plantilla cuidadosa de acceso seguro:

fun main() {
    val table = arrayOf(
        intArrayOf(1, 2, 3),
        intArrayOf(10, 20)
    )

    val row = 1
    val col = 1

    if (row in table.indices && col in table[row].indices) {
        println(table[row][col])     // 20
    } else {
        println("out of bounds")     // (no se imprimira)
    }
}

Fijate en el orden: primero row in table.indices, y solo despues table[row]. No es casualidad: asi no te arriesgas a acceder a una fila inexistente.

2. Trabajo con filas de distinta longitud

Bucles anidados: como recorrer una tabla si las filas tienen distinta longitud

Cuando tenemos un «array de filas», la logica de recorrido casi siempre es esta: el bucle externo va por las filas, el interno por los elementos de la fila. Pero la idea clave es ligeramente distinta: el bucle interno debe basarse en la fila actual, no en alguna «longitud comun imaginaria».

El recorrido mas simple y seguro del array es obtener todos los valores:

fun main() {
    val table = arrayOf(
        intArrayOf(1, 2, 3),
        intArrayOf(10, 20)
    )

    for (row in table) {
        for (x in row) {
            print("$x ")
        }
        println()
    }
    // 1 2 3
    // 10 20
}

Esta variante es buena porque no piensas en indices en absoluto, lo que casi elimina el riesgo de salirte de los limites del array.

Pero a veces los indices son necesarios: por ejemplo, para una salida bonita con coordenadas ([row][col]) o para modificar el array. Entonces usamos indices y recordamos que pueden ser diferentes en cada fila:

fun main() {
    val table = arrayOf(
        intArrayOf(1, 2, 3),
        intArrayOf(10, 20)
    )

    for (r in table.indices) {
        for (c in table[r].indices) {
            print("[$r,$c]=${table[r][c]} ")
        }
        println()
    }
    // [0,0]=1 [0,1]=2 [0,2]=3
    // [1,0]=10 [1,1]=20
}

¿Por que esto es correcto? Porque table[r].indices describe los indices de columna validos precisamente para la fila r, y no «para toda la tabla». Este enfoque sigue directamente la idea de indices como rango seguro de indices.

Limites de las filas: por que no se puede «medir a todos por la primera fila»

Ahora viene un punto importante que ahorra horas de depuracion.

Imagina esta tabla:

  • fila 0: longitud 3
  • fila 1: longitud 2
  • fila 2: longitud 4

Si haces el bucle interno como for (c in 0..2) porque en la primera fila hay 3 elementos, entonces en la segunda fila tarde o temprano intentaras acceder a table[1][2], que no existe.

Precisamente por eso hay que tener en mente que un array no siempre es un rectangulo. Es mejor pensarlo como un conjunto de filas, donde cada fila es un array independiente con su propio size.

Recordemos una pequena tabla:

Modelo Como se ve en codigo Idea de ancho
Tabla «rectangular» aprox. Array<Array<Int>>, donde todas las filas son iguales el ancho es comun
Tabla dentada Array<IntArray> cada fila tiene su propio ancho

3. Practica: «Registro de ejercicios por temas»

Vamos a montar un proyecto pequeno que se desarrolla paso a paso. La idea es esta: tenemos temas, y en cada tema hay una lista de puntos por ejercicios. Queremos imprimir la tabla, calcular la suma por cada tema y encontrar el «mejor ejercicio» (maximo) dentro de cada tema.

Imprimimos la tabla

Primero haremos una salida bonita: numero de tema y puntos.

fun main() {
    val scoresByTopic = arrayOf(
        intArrayOf(10, 8, 9),
        intArrayOf(7, 10),
        intArrayOf(6, 6, 8, 10)
    )

    for (t in scoresByTopic.indices) {
        print("Topic $t: ")
        for (score in scoresByTopic[t]) {
            print("$score ")
        }
        println()
    }
    // Topic 0: 10 8 9
    // Topic 1: 7 10
    // Topic 2: 6 6 8 10
}

Aqui el bucle externo va por los temas (filas) y el interno por los valores dentro de la fila. Es la forma mas segura: no usas indices donde no hacen falta.

Suma por cada fila: un patron viejo con un giro nuevo

Ahora aplicamos el patron conocido de «suma de un array», pero por separado para cada fila. Importante: la variable sum debe crearse de nuevo para cada tema; si no, por accidente obtendras la «suma total de todo el array».

fun main() {
    val scoresByTopic = arrayOf(
        intArrayOf(10, 8, 9),
        intArrayOf(7, 10),
        intArrayOf(6, 6, 8, 10)
    )

    for (row in scoresByTopic) {
        var sum = 0					// sum esta dentro del bucle externo
        for (x in row) sum += x
        println("row sum = $sum")    // row sum = ...
    }
    // row sum = 27
    // row sum = 17
    // row sum = 30
}

Maximo en cada fila: empezamos con cuidado desde el primer elemento

El maximo es ese caso en el que no puedes escribir «que el maximo sea 0» (si no, los valores negativos rompen la logica). Tomamos el primer elemento de la fila como valor inicial. Pero este enfoque exige una comprobacion obligatoria: la fila no debe estar vacia.

fun main() {
    val scoresByTopic = arrayOf(
        intArrayOf(10, 8, 9),
        intArrayOf(7, 10),
        intArrayOf(6, 6, 8, 10)
    )

    for (topic in scoresByTopic.indices) {
        val row = scoresByTopic[topic]
        var best = row[0]
        for (x in row) 
        	if (x > best) best = x
        println("topic $topic best = $best")
    }
    // topic 0 best = 10
    // topic 1 best = 10
    // topic 2 best = 10
}

Modificacion de un elemento por coordenadas

Ahora imaginemos que el profesor decidio: «Si el ejercicio se resolvio con pista, menos 2 puntos». Supongamos que es el tema 2, ejercicio 1. Es decir, coordenadas (row=2, col=1).

Cambiemos el valor en el array (con comprobaciones de limites):

fun main() {
    val scoresByTopic = arrayOf(
        intArrayOf(10, 8, 9),
        intArrayOf(7, 10),
        intArrayOf(6, 6, 8, 10)
    )

    val row = 2
    val col = 1

    if (row in scoresByTopic.indices && col in scoresByTopic[row].indices) {
        scoresByTopic[row][col] -= 2
    }

    println(scoresByTopic[2][1])    // 4
}

El punto clave: la segunda comprobacion es precisamente col in scoresByTopic[row].indices. No «en 0..something», no «como en la primera fila», sino especificamente en la fila a la que estas accediendo.

4. Como imaginar una tabla dentada en la cabeza y en el codigo

A veces conviene reforzar lo aprendido no con codigo, sino con una imagen. Asi se puede representar Array<IntArray>:

flowchart TD
    T["table: Array⟨IntArray⟩"] --> R0["table[0]: IntArray (size=3)"]
    T --> R1["table[1]: IntArray (size=2)"]
    T --> R2["table[2]: IntArray (size=4)"]

    R0 --> A00["[0][0]"]
    R0 --> A01["[0][1]"]
    R0 --> A02["[0][2]"]

    R1 --> A10["[1][0]"]
    R1 --> A11["[1][1]"]

    R2 --> A20["[2][0]"]
    R2 --> A21["[2][1]"]
    R2 --> A22["[2][2]"]
    R2 --> A23["[2][3]"]

Fijate: table[1] no tiene [1][2]. Las tablas rectangulares se quedaron muy atras. Asi es la realidad del programador.

5. Errores tipicos al trabajar con arrays dentados

Error n.º 1: creer que la tabla tiene “un ancho comun”.
Es la trampa mas popular: tomas table[0].size y lo usas como ancho para todas las filas. En filas cortas esto termina en un acceso fuera de limites. En una estesctura dentada, el ancho es una propiedad de cada fila, asi que en el bucle interno los limites deben ser table[r].indices, y no “algo unico para todos”.

Error n.º 2: confundir el orden de los indices y acabar con una confusion logica.
Cuando se escribe table[col][row] en lugar de table[row][col], el programa puede incluso no fallar (si los tamanos lo permiten), pero empezara a hacer «otra cosa». Un buen habito es llamar a los indices con nombres claros r y c (row/col) y mantener el mismo convenio en todas partes.

Error n.º 3: comprobar solo un nivel de limites.
A veces los principiantes compeseban row in table.indices, pero se olvidan de col in table[row].indices. Esto es especialmente traicionero: parece que «¡ya lo comprobe!», pero aun asi se cae. Para acceder a table[row][col] se necesitan dos comprobaciones, porque son dos accesos [] seguidos.

Error n.º 4: hacer el bucle interno con el rango 0..row.size en vez de usar indices correctos.
Aqui esta el clasico off-by-one: el rango 0..row.size incluye row.size, y el ultimo indice permitido es row.size - 1. El resultado es un acceso fuera de limites en la ultima iteracion. Si quieres un bucle por indices, usa row.indices — ya es el rango correcto.

Error n.º 5: olvidar que una “fila” tambien es un array con vida propia.
A veces la gente escribe codigo como si table[r] fuera solo «una parte de la tabla», y no un array completo. Pero table[r] tiene su propio size, se puede recorrer, se puede buscar el maximo, la suma, etc. En cuanto empiezas a percibir una fila como un IntArray normal, los bucles anidados se vuelven logicos y predecibles.

1
Tarea
Kotlin SELF, nivel 7, lección 5
Bloqueada
Plantas del ascensor
Plantas del ascensor
1
Tarea
Kotlin SELF, nivel 7, lección 5
Bloqueada
Lanzamiento par
Lanzamiento par
1
Tarea
Kotlin SELF, nivel 7, lección 5
Bloqueada
Suma por ranuras
Suma por ranuras
1
Tarea
Kotlin SELF, nivel 7, lección 5
Bloqueada
Logs al revés
Logs al revés
1
Cuestionario/control
Arrays en Kotlin, nivel 7, lección 5
No disponible
Arrays en Kotlin
Arrays en Kotlin
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION