Arreglos y Arreglos Multidimensional

Codigo 1 - esTriángulo usando arreglos
boolean esTriángulo(double a, double b, double c) {
  var lados = new double[]{a, b, c};
  Arrays.sort(lados);
  return (lados[0] + lados[1]) > lados[2];
} 

En el Código 1 implementamos esTriángulo() pero usando un arreglo. En otro capítulo hablamos de cómo tres longitudes sólo pueden formar un triángulo si la mayor de ellas es menor que la suma de las otras dos. Al implementar esa función el problema era cómo encontrar el máximo de tres variables y la dificultad de hacer escalar ese proceso: ¿qué pasa si en lugar de 3 variables, hay 20 o 100? La respuesta es los arreglos.

Un arreglo es una variable que guarda una secuencia de valores homogéneos. Al igual que los Strings, los arreglos también usan índices para hacer referencia a estos valores, el primer índice es 0 y el último índice es la longitud del arreglo menos 1. Para acceder a un elemento de una matriz, se utiliza el operador de llaves cuadradas con el índice al que se desea acceder.

La línea 02 crea un arreglo de variables de tipo double, utilizando los parámetros como valores. En cierto sentido, tomamos los 3 parámetros y los empaquetamos juntos en un arreglo. Al estar en un arreglo es más fácil realizar análisis sobre esa información que con las variables sueltas.

Para inicializar un arreglo hay dos maneras: inicializar conociendo los valores, como hicimos en el Código 01 o crear el arreglo primero y luego dar los valores como en el siguiente código:

Código 2 creación de arreglo con inicialización después
boolean esTriángulo(double a, double b, double c) {
  var lados = new double[3];
  lados[0] = a;
  lados[1] = b;
  lados[2] = c;
  Arrays.sort(lados);
  return lados[0] + lados[1] > lados[2];
} 

En el Código 01 el compilador sabe que el arreglo tiene 3 valores, dado que la lista de valores separados por comas tiene 3 valores. En cambio, en el Código 02 el tamaño se hace explícito en la línea 02.

Java viene con varios métodos útiles para usar con arreglos, uno de ellos es Arreglos.sort() que ordena los elementos dentro del arreglo por su orden natural, para variables de tipo double es de menor a mayor. Al correr la función sabemos que la variable menor va a estar en el índice 0 y la mayor en el último índice.

Una vez ordenados, podemos sumar los elementos en los índices 0 y 1, los lados menores, y revisar que sean exclusivamente mayores que el elemento en el índice 2, que es el lado mayor.

Arreglos como Parámetros

Código 3 promedio de un arreglo
double promedio(double[] datos) {
  var suma = 0.0;
  for (var i = 0; i < datos.length; ++i) {
    suma += datos[i];
  } 
  var promedio = suma / datos.length;
  return promedio;
} 
Input
> promedio(new double[] {1.0, 2.0, 3.0})
Output
2.0

Los arreglos son como cualquier variable así que podemos usarlos como parámetros igualmente. El tipo de dato de un arreglo de elementos double es «double[]», el de un String es «String[]», el de int es «int[]», etc

El código 03 calcula el promedio de un arreglo de double. Primero, recorremos el arreglo para sumar todos sus elementos y luego calculamos el promedio dividiendo la suma por el número total de valores usando la propiedad de «length».

Los ciclos son los mejores amigos de los arreglos. Al igual que con los String, podemos recorrer todos los elementos de un arreglo usando un ciclo que se ve muy parecido al que usábamos con Strings. La única diferencia entre un ciclo para un String y para un arreglo, es que para conocer la longitud de un arreglo no usamos un método, sino una propiedad que se llama igualmente «length» pero al no ser un método no necesita paréntesis

Al igual que con Strings podemos usar diferentes tipos de ciclos, según lo que necesitemos.

Código 4 - un ciclo que empieza en el índice 1
boolean elementosCrecientes(double[] datos) {
  for (var i = 1; i < datos.length; ++i) {
    if (datos[i - 1] >= datos[i]) {
      return false;
    }
  } 
  return true;
 } 

El código 4 implementa elementosCrecientes(), una función que verifica si los valores en un arreglo están ordenados de menor a mayor y sin repetidos. Toma una arreglo y verifica que cada elemento sea menor que el siguiente. Dado que la operación depende de la posición de cada elemento, necesitamos utilizar un ciclo tradicional, recorriendo sus índices aunque empezamos en el segundo elemento para en cada iteración comparar el elemento actual con el anterior, el elemento en el índice «i- 1».

En la línea 03 verificamos la condición opuesta que queremos verificar, es decir que el elemento anterior sea mayor o igual al elemento actual, en caso de que un solo elemento la incumpla esta condición hace innecesario continuar todo el ciclo por lo que podemos tener un return temprano. Si el código llega a la línea 07, significa que no se encontró ningún elemento que rompa nuestra expectativa, por lo que podemos retornar «true».

Mutabilidad en los arreglos

Código 5 - modificando el parámetro
double max(double[] datos) {
  Arrays.sort(datos);
  return datos[datos.length - 1];
} 

¿Qué podría estar mal con max()? Realicemos un experimento después de implementar la función:

Input
> var elementos = new double[] {5.0, 4.0, 3.0};
> max(elementos);
> elementos;
Output
double[3] { 5.0, 4.0, 3.0 }
5.0
double[3] { 3.0, 4.0, 5.0 }

Como puedes ver después de llamar a max(), el orden de los elementos fue modificado pero el nombre de la función max() no indica que el orden del arreglo se iba a modificar, parecía que lo único que haría el método era devolver el elemento máximo en el arreglo. Cuando utilices un arreglo como parámetro, recuerda que si modificas un elemento dentro del arreglo, este cambio será visible por fuera de la función. Esto rara vez es una buena idea.

Parámetros de salida

Código 6 - Parámetros de salida
void duplicar(double[] data) {
  for (var i = 0; i < data.length; ++i) {
     data[i] *= 2;
  }
} 
Input
> var elementos = new double[] {5.0, 4.0, 3.0};
> doubleIt(elements);
> elements;
Output
double[3] { 5.0, 4.0, 3.0 }
double[3] { 10.0, 8.0, 6.0 }

Ya hemos dicho que cuando usas un arreglo como parámetro puedes realizarle cambios dentro de una función. Cuando llamas a una función para cambiar uno de los parámetros, un arreglo o un objeto mutable, generalmente se denomina como parámetro de salida. Como no produce un valor, generalmente se hace que la función sea «void», lo que significa que no devolverá ningún valor y por lo tanto puedes omitir la sentencia «return». En este capítulo ya habíamos usado una función «void»: Arrays.sort()

Las funciones que tienen parámetros de salida deberían ser la excepción para las funciones públicas y no la regla. Y ten especial cuidado con funciones que tengan muchos parámetros de salida, definitivamente es mejor retornar un objeto, tema que veremos más adelante. Dicho esto, funciones privadas que sean auxiliares a funciones públicas podrían usar parámetros de salida dado que esas funciones se llaman controladamente por el código que tu escribes.

Arreglos como valores de retorno

Código 7 - usando arreglos como valores de retorno
double[]duplicar(double[] datos) {
    var copiaDatos = Arrays.copyOf(datos, datos.length);
    for (var i = 0; i < datos.length; ++i) {
      copiaDatos[i] *= 2;
    } 
    return copiaDatos;
} 

Los arreglos como variables no tienen nada especial, así que no debería extrañarnos que se les puede usar como valores de retorno, cómo vemos en la línea 06.

Antes habíamos visto que podemos usar arreglos como parámetros de salida. ¿Cuál es la alternativa? Hacer una copia del parámetro y hacer las modificaciones sobre esa copia. En la línea 02 hacemos la copia, y luego en la línea 04 hacemos el duplicado sobre esa copia.

Los valores de retorno no tienen que ser copias de otros parámetros, también pueden ser valores nuevos creados en la función. Veremos una de las razones para crearlos a continuación.

Código 8 - arreglos como una forma de devolver más de un valor
double discriminanteCuadrática(double a, double b, double c) {
  return b * b - 4 * a * c;
} 

double[] cuadrática(double a, double b, double c) {
  double discriminante = discriminanteCuadrática(a, b, c);
  if (discriminante < 0) {
    return new double[0];
  } else if (discriminante == 0) {
    var valor = new double[] { -b / 2 * a };
    return valor;
  } else {
    var raízCuadrada = Math.sqrt(discriminante);
    var valor1 = (-b + raízCuadrada) / (2 * a);
    var valor2 = (-b - raízCuadrada) / (2 * a);
    var valores = new double[] { valor1, valor2 };
    return valores;
  }
}

La ecuación cuadrática tiene 0, 1 o 2 respuestas reales. ¿Cómo podemos representar eso en una función? Una opción es utilizar un arreglo ya que puede tener 0 o más elementos

¿Qué determina cuántas soluciones tiene? Lo hace un valor llamado el discriminante que se calcula como se muestra en la línea 02. Una vez que lo calculamos en la línea 06, escribimos un condicional para calcular los valores a retornar según el valor del discriminante.

En la línea 08 devolvemos un arreglo vacío, en las líneas 10-11 se crea y devuelve un arreglo de un solo elemento y finalmente en las líneas 13-17 creamos un arreglo de dos valores.

Esta solución de regresar un arreglo para poder devolver 0, 1 o 2 valores, es efectiva pero no es ideal. Nuevamente los objetos pueden ser una mejor alternativa, sin embargo es útil saber que puedes usar arreglos para algo simple.

Valores predeterminados

Si un arreglo es un contenedor, ¿qué sucede cuándo creamos un contenedor con un tamaño pero sin ningún valor aún? Hagamos un experimento:

Input
> new int[1]
> new double[1]
> new boolean[1]
> new char[1]
> new String[1]
Output
int[1] { 0 }
double[1] { 0.0 }
boolean[1] { false }
char[1] { '\000' }
String[1] { null }

En programación no hay una manera simple de representar valores vacíos para muchos tipos de datos, pero la mayoría de ellos tienen un valor cero que es el que obtienes como valor por defecto en un arreglo. Hay 2 valores cero sobre los que quizás valga la pena ahondar: en efecto existe un carácter Unicode con el valor 0, este pertenece al rango de caracteres que no son imprimibles así que imprimirlo no hace mucho. Por otro lado, para Strings vemos el valor null. Tendremos mucho que decir sobre null en el futuro cercano, pero por ahora, solo recuerda que un arreglo de String no tiene por defecto Strings vacíos. Este valor predeterminado también se utiliza para cualquier tipo de de arreglo de tipos de datos no primitivos.

Arreglos Multidimensionales

Código 9 - matrices
int[][] matrizNúmeros(int tamaño) {
  var matriz = new int[tamaño][tamaño];
  var n = 1;
  for (var i = 0; i < matriz.length; ++i) {
    for (var j = 0; j < matriz[i].length; ++j) {
      matriz[i][j] = n;
      ++n;
    }
  } 
  return matriz;
}
Input
> matrizNúmeros(3)

              
Output
double[3][] { int[3] { 1, 2, 3 },
int[3] { 4, 5, 6 },
int[3] { 7, 8, 9 } }

Los arreglos pueden contener cualquier tipo de valor, incluso pueden contener arreglos. A un arreglo de arreglos se le suele llamar una matriz. La línea 02 declara una matriz cuadrada. Pero puedes tener una matriz rectangular e incluso puedes tener un arreglo que contenga arreglos de diferentes tamaños. Para hacer referencia a un valor en una matriz sólo tienes que proporcionar dos índices en lugar de uno, como puedes ver en la línea 06. matrizNúmeros() pone un número creciente, controlado por la variable «n», en cada celda de izquierda a derecha, arriba hacia abajo.

Copiar una matriz

Hemos visto Arrays.copy() para hacer una copia de un arreglo. ¿Funciona igual con las matrices? No totalmente. Tenemos que usar la función más de una vez, primero para la estructura general y luego por cada hilera. Por ejemplo una manera de copiar una matriz de ints sería:

int[][] copiar(int[][] inicial) {
  var copia = Arrays.copyOf(inicial, inicial.length);
  for (var i = 0; i < copia.length; ++i) {
    copia[i] = Arrays.copyOf(inicial[i], inicial[i].length);
  }
  return copia;
}

Si tuviéramos una matriz de double, entonces tendríamos que hacer una función idéntica, solo cambiando el tipo de datos. Esta es una limitación de Java con los valores primitivos, no hay una manera de generar funciones que solo cambian en su tipo.

Iteraciones

Código 10 - Usando el for mejorado
double promedio(double[] data) {
  var suma = 0.0;
  for (var n : data) {
    suma += n;
  }
  var promedio = suma / data.length;
  return promedio;
}

Hemos visto que podemos realizar un ciclo usando una variable para obtener todos los índices válidos de un arreglo. Esto es propenso a errores porque podrías cometer un error en el valor inicial de la variable o en la condición del ciclo. Java ofrece una forma de ciclo para iterar sobre colecciones, como los arreglos, que puedes ver en la línea 03, llamada «for mejorado». En lugar de trabajar con índices, crea una variable, denominada «n» en el código, y Java colocará el elemento que está en la iteración actual en esa variable. La sintaxis «for (var n: datos)» debe entenderse como: para cada elemento, «n», en el arreglo «datos». Entonces, en la línea 04, puedes usar «n» para actualizar la variable «suma».

Veamos el código al lado del funcionalmente equivalente usando índices:

double promedio(double[] data) {
  var suma = 0.0;
  for (var n : data) {
    suma += n;
  } 
  var promedio = suma / data.length;
  return promedio;
}

double promedio(double[] data) {
  var suma = 0.0;
  for (var i = 0; i < data.length; ++i) {
    var n = data[i];
    suma += n;
  } 
  var promedio = suma / data.length;
  return promedio;
}

Esta sintaxis funcionará incluso para matrices. Veamos el código que cuenta el número de veces que 0 está en una matriz, usando ambas sintaxis una al lado de la otra:

int contarUnos(int[][] data) {
  var conteo = 0;
  for (var i = 0; i < data.length; ++i) {
    for (var j = 0; j < data[i].length; ++j) {
      if (data[i][j] == 1) {
        ++conteo;
      } 
    }
  }
  return conteo;
}              
int contarUnos(int[][] data) {
  var conteo = 0;
  for (var fila : data) {
    for (var n : fila) {
      if (n == 1) {
        ++conteo;
      }
    }
  }
  return conteo;
}

La única limitación de esta sintaxis es que no puedes usarla para modificar los elementos en los arreglos, solo consultarlos. Además, a veces es necesario recorrer el arreglo, pero el índice es tan importante como el elemento, por lo que tendrías que utilizar el recorrido con índices, por lo tanto el for mejorado no reemplaza la otra manera de recorrer colecciones, solo lo complementa. Sin embargo, si es posible, utiliza la versión mejorada, ya que ofrece mejor legibilidad y es menos propensa a errores.

Ejercicios

E8.1

Escribe una función másLargo(), que reciba un arreglo de String y devuelva el índice del String con mayor cantidad de caracteres. En caso de haber más de uno con la misma cantidad de caracteres devuelve el que tenga el menor índice.

assertEquals(1, masLargo(new String[] {"abc", "casa", "cc"}));
assertEquals(0, masLargo(new String[] {"a", "b", "c"}));

E8.2

Escribe una función contiene() que dado un arreglo de String y un String, retorne verdadero si ese String está en el arreglo y falso en caso contrario.

assertTrue(contiene(new String[] {"a", "b", "c"}, "b"));
assertFalse(contiene(new String[] {"a", "b", "c"}, "1"));

E8.3

Escribe una función buscar() que dado un arreglo de ints, retorne un int que sea el índice del último elemento del arreglo que sea mayor que 0 y menor que 10. Si no hay un elemento que cumpla esta condición retorne -1.

assertEquals(2, buscar(new int[] {5, 10, 3, 100}));
assertEquals(-1, buscar(new int[] {10, 10, 10}));

E8.4

Escribe una función operar() reciba un arreglo de ints y retorne un nuevo arreglo de ints con los mismos elementos del arreglo original después de multiplicarlos por 3 y sumarles 1. Por ejemplo si en el arreglo original está el elemento 2, en el arreglo retornado en esa posición debe aparecer el elemento 7.

assertArrayEquals(new int[]{4, 7, 1}, operar(new int[] {1, 2, 0}));
assertArrayEquals(new int[]{10, 13, 16}, operar(new int[] {3, 4, 5}));

E8.5

Escribe una función que retorne «true» si todos los elementos de un arreglo de ints son únicos (no hay elementos repetidos) o «false» en caso contrario.

assertTrue(únicos(new int[] {5, 1, 3}));
assertFalse(únicos(new int[] {-1, 7, -1, 3}));

E8.6

Escribe una función segundoMayor(), que reciba un arreglo de más de un número entero y devuelva el segundo elemento mayor.

assertEquals(3, segundoMayor(new int[] {5, 1, 3}));
assertEquals(7, segundoMayor(new int[] {-1, 7, 10, 3}));

E8.7

Escribe una función moda() que dado un arreglo de números enteros retorne el elemento que más se repite. Para esta función, haz una copia del arreglo dado y ordénalo antes de hacer el análisis. En caso de haber empates, devuelve el elemento menor.

assertEquals(2, moda(new int[] {3, 2, 4, 2, 1}));
assertEquals(1, moda(new int[] {1, 2, 4, 2, 1}));

E8.8

Escribe una función que reciba un arreglo de enteros positivos y retorne un histograma representándolo. El histograma debe ser un String con igual número de filas que elementos en el arreglo. Cada fila solo usa el caracter '*' y este se repite la cantidad de veces que indique el número correspondiente. Por ejemplo, si uso el arreglo [5, 1, 3], significa que la primera fila tendrá 5 caracteres, la segunda fila tendrá 1 y la tercera y última fila tendrá 3.

assertEquals("""
             *****
             *
             ***
             """, histograma(new int[] {5, 1, 3}));
assertEquals("""
             ***

             ***
             """, histograma(new int[] {3, 0, 3}));

E8.9

Dado un arreglo de números enteros entre 0 y 5, escribe una función que devuelva un nuevo arreglo de exactamente 6 posiciones. Cada posición en el arreglo resultante debe representar la cantidad de veces que aparece el número correspondiente en el arreglo original, es decir el número en el índice 0 representa cuántas veces estaba el número 0 en el arreglo original, el número en el índice 1 representa cuántas veces estaba el número 1 en el arreglo original, etc.

Por ejemplo para el arreglo [ 3 ], el valor de retorno sería [0, 0, 0, 1, 0, 0] porque en el arreglo original había cero 0s, cero 1s, cero 2s, un 3, cero 4s y cero 5s, mientras que el arreglo [ 2, 2 ] el valor de retorno sería [0, 0, 2, 0, 0, 0].

assertArrayEquals(new int[] {0, 0, 0, 1, 0, 0}, stats(new int[] {3}));
assertArrayEquals(new int[] {0, 0, 2, 0, 0, 0}, stats(new int[] {2, 2}));

E8.10

Crea una función que dadas las dimensiones de una matriz cuadrada, devuelva una matriz con 0 en todas las celdas, excepto en las celdas de la diagonal principal en donde debe haber un 1. La diagonal principal es una celda que tiene el mismo índice para su fila y columna. Por ejemplo, en una matriz cuadrada m de tamaño 3, entonces hay un 1 en m[0][0], m[1][1] y m[2][2].

assertArrayEquals(new int[][] {{1, 0, 0},
                               {0, 1, 0},
                               {0, 0, 1}}, m(3));

E8.11

Crea una función que dadas las dimensiones de una matriz cuadrada, devuelva una matriz con 0 en todas las celdas, excepto en las celdas de la diagonal contraria a la diagonal principal en debe haber un 1.

assertArrayEquals(new int[][] {{0, 0, 1},
                               {0, 1, 0},
                               {1, 0, 0}}, m2(3));

E8.12

Escribe una función ventana() que reciba un arreglo de Strings y un número entero y devuelva una matriz donde se agrupen los elementos de esta manera:

  • El número de hileras es el techo del tamaño del arreglo dividido por el número dado
  • El número de columnas es el número dado
  • Cada elemento del arreglo original se va almacenando en el primer índice disponible de la primera columna que no esté llena
  • Si hay menos elementos que posiciones en la matriz, es correcta que estos elementos sean null

Por ejemplo si el arreglo es {"1", "2", "3", "4", "5", "6", "7", "8", "9", "10"} y el número es 3 entonces la matriz tiene 4 hileras y 3 columnas.

{{"1", "2", "3"},
 {"4", "5", "6"},
 {"7", "8", "9"},
 {"10", null, null}}

Si el número dado hubiera sido 5 entonces el resultado sería:

{{"1", "2", "3", "4", "5"},
 {"6", "7", "8", "9", "10"}}
var e = new String[]{"1", "2", "3", "4", "5", "6", "7", "8", "9", "10"};
assertArrayEquals(new String[][] {{"1", "2", "3"},
                                  {"4", "5", "6"},
                                  {"7", "8", "9"},
                                  {"10", null, null}}, ventana(e, 3));

assertArrayEquals(new String[][] {{"1", "2", "3", "4", "5"},
                                  {"6", "7", "8", "9", "10"}}, ventana(e, 5));

E8.13

Crea una función que reciba una matriz con solo 2 caracteres ' ' y '*' y retorne una nueva matriz que tenga ' ', '*' y '.' de esta manera:

  • una celda de la matriz nueva tiene un '*' si en la matriz inicial había un '*'
  • una celda que en la matriz inicial tenía un ' ', se vuelve un '.' en la matriz nueva si esa celda comparte una fila o columna donde había un '*' en la matriz inicial
  • una celda en la matriz nueva tiene un ' ' en los demás casos

Por ejemplo para una matriz como:

{{' ', ' ', ' '},
 {' ', '*', ' '},
 {' ', ' ', ' '}}

Debería devolver una matriz como esta:

{{' ', '.', ' '},
 {'.', '*', '.'},
 {' ', '.', ' '}}

En la matriz original hay un solo asterisco en la posición (1, 1). Entonces la matriz que se devuelve, tiene '.' a través de toda la hilera con índice 1, salvo donde había un '*' y en toda la columna con el índice 1, salvo donde había un '*'

char[][] inicial = {{' ', ' ', ' '},
                    {' ', '*', ' '},
                    {' ', ' ', ' '}};

char[][] esperado = {{' ', '.', ' '},
                     {'.', '*', '.'},
                     {' ', '.', ' '}};

assertArrayEquals(esperado, Arreglos.expandir(inicial));
char[][] inicial2 = {{'*', ' ', '*'},
                     {' ', ' ', ' '},
                     {'*', ' ', '*'}};

char[][] esperado2 = {{'*', '.', '*'},
                      {'.', ' ', '.'},
                      {'*', '.', '*'}};

assertArrayEquals(esperado2, Arreglos.expandir(inicial2));

© 2022 Carlos Obregón. This work is licensed under Creative Commons Attribution 4.0 International (CC BY 4.0)

Scroll to Top