Arrays en Java — ejercicios con invertir, rotar, quitar duplicados y fusionar (y un error que se esconde donde menos lo esperas)

Cuando hice mi primer ejercicio con matrices, lo probé con una matriz cuadrada de 3×3 y funcionó a la primera. Contentísimo. Después lo probé con una de 2 filas y 4 columnas y el programa petó: había usado tabla.length donde tocaba tabla[0].length. Con una matriz cuadrada, las dos cosas valen lo mismo, así que el fallo se quedó escondido hasta que probé otra forma. Desde entonces pruebo siempre con más de un caso, y verás que en estos ejercicios hay varios casos «incómodos» a propósito.

Aquí tienes tres ejercicios de arrays resueltos y comentados, y al final una sección de «¿Qué falla aquí?» con tres fragmentos con error para que los cacemos. Si todavía no has visto la teoría o la práctica, conviene pasar antes por ellas.

Cómo sacarle partido: lee el enunciado, intenta resolverlo tú en tu editor y solo entonces mira la solución. Aunque no te salga, el intento hace que la solución se entienda diez veces mejor. Y cuando termines, prueba tu código con casos raros: array vacío, un solo elemento, valores repetidos.

Ejercicio 1: invertir y rotar un array (sin tocar el original)

Enunciado. Escribe dos métodos:

  • invertir(int[] a): devuelve un array nuevo con los mismos elementos en orden inverso.
  • rotar(int[] a, int k): devuelve un array nuevo con los elementos desplazados k posiciones a la derecha. Los que salen por la derecha vuelven a entrar por la izquierda. Si k es negativo, se rota a la izquierda.

En los dos casos, el array original no debe cambiar.

Ejemplo. Con {1, 2, 3, 4, 5}: invertido es {5, 4, 3, 2, 1}; rotado 2 posiciones a la derecha es {4, 5, 1, 2, 3}.

Solución comentada

static int[] invertir(int[] a) {
    int[] r = new int[a.length];                 // array NUEVO: el original no se toca
    for (int i = 0; i < a.length; i++) {
        r[i] = a[a.length - 1 - i];              // el primero de r es el último de a, etc.
    }
    return r;
}

// Rota k posiciones a la derecha (k negativo = a la izquierda)
static int[] rotar(int[] a, int k) {
    int n = a.length;
    int[] r = new int[n];
    if (n == 0) {
        return r;                                // evita dividir entre cero con %
    }
    int desplazamiento = ((k % n) + n) % n;      // lo deja siempre entre 0 y n-1
    for (int i = 0; i < n; i++) {
        r[(i + desplazamiento) % n] = a[i];      // el que sale por la derecha entra por la izquierda
    }
    return r;
}

Por qué funciona

En invertir: la clave es la posición simétrica. En un array de longitud n, el elemento de la posición i tiene su «opuesto» en n - 1 - i. Con 5 elementos: la 0 se empareja con la 4, la 1 con la 3 y la 2 consigo misma. Y como escribimos en r, que es un array nuevo, el original queda intacto.

En rotar: cada elemento de la posición i pasa a la posición i + desplazamiento. Si esa suma se sale del array, el operador % (resto) la devuelve al principio: es lo que hace que lo que sale por la derecha entre por la izquierda. Probemos con {1, 2, 3, 4, 5} y desplazamiento 2: el 1 (posición 0) va a la 2, el 4 (posición 3) va a (3 + 2) % 5 = 0, y el 5 va a (4 + 2) % 5 = 1.

Casos de prueba

int[] notas = {1, 2, 3, 4, 5};
System.out.println("Original:        " + Arrays.toString(notas));
System.out.println("Invertido:       " + Arrays.toString(invertir(notas)));
System.out.println("Rotado 2 (der.): " + Arrays.toString(rotar(notas, 2)));
System.out.println("Rotado -1 (izq.):" + Arrays.toString(rotar(notas, -1)));
System.out.println("Rotado 7:        " + Arrays.toString(rotar(notas, 7)));
System.out.println("Original:        " + Arrays.toString(notas));
System.out.println("Vacío invertido: " + Arrays.toString(invertir(new int[0])));
Original:        [1, 2, 3, 4, 5]
Invertido:       [5, 4, 3, 2, 1]
Rotado 2 (der.): [4, 5, 1, 2, 3]
Rotado -1 (izq.):[2, 3, 4, 5, 1]
Rotado 7:        [4, 5, 1, 2, 3]
Original:        [1, 2, 3, 4, 5]
Vacío invertido: []

Fíjate en tres cosas: rotar 7 da lo mismo que rotar 2 (porque 7 % 5 = 2), rotar -1 lleva el primero al final, y el original sale igual al principio y al final.

Preguntas que te puedes hacer

¿Por qué ((k % n) + n) % n y no simplemente k % n? Porque en Java el resto de un número negativo es negativo: -1 % 5 da -1, no 4, y una posición negativa daría ArrayIndexOutOfBoundsException. Sumar n y volver a hacer % n lo convierte siempre en un valor entre 0 y n - 1.

¿Por qué comprobamos n == 0 primero? Porque k % 0 es una división entre cero y lanza ArithmeticException. Un array vacío es un caso límite que siempre conviene contemplar.

¿No se podría invertir «en el sitio», intercambiando elementos? Sí, y es más eficiente en memoria, pero modifica el original. Para este enunciado (que pide no tocarlo), la copia es lo correcto. Más abajo verás qué pasa cuando no se hace así.

Escribir int[] r = a; pensando que copia el array. Solo copia la dirección: r y a serían el mismo array y al invertir «en r» estarías modificando a. Para una copia de verdad, new int[...] o Arrays.copyOf.

Ejercicio 2: eliminar duplicados

Enunciado. Escribe sinDuplicados(int[] a), que devuelve un array nuevo con los valores de a sin repeticiones, respetando el orden de la primera aparición de cada uno.

Ejemplo. Con {4, 7, 4, 0, 7, 7, 9, 0} devuelve {4, 7, 0, 9}.

Un array no puede cambiar de tamaño, y no sabes cuántos valores distintos habrá hasta recorrerlo. Piensa en crear primero un array «temporal» lo bastante grande y recortarlo al final con Arrays.copyOf.

Solución comentada

// ¿Está valor entre las primeras 'hasta' posiciones de a?
static boolean contiene(int[] a, int hasta, int valor) {
    for (int i = 0; i < hasta; i++) {
        if (a[i] == valor) {
            return true;
        }
    }
    return false;
}

static int[] sinDuplicados(int[] a) {
    int[] temporal = new int[a.length];          // como mucho caben todos
    int cuantos = 0;                             // cuántos valores distintos llevamos
    for (int n : a) {
        if (!contiene(temporal, cuantos, n)) {   // solo miramos lo ya rellenado
            temporal[cuantos] = n;
            cuantos++;
        }
    }
    return Arrays.copyOf(temporal, cuantos);     // recortamos al tamaño real
}

Por qué funciona

Recorremos a y, para cada valor, miramos si ya lo hemos guardado en temporal. Si no está, lo añadimos en la siguiente posición libre (cuantos) y avanzamos el contador. Al terminar, temporal tiene los valores distintos al principio y ceros de relleno al final, así que Arrays.copyOf(temporal, cuantos) se queda solo con la parte buena.

La clave de todo el ejercicio está en el parámetro hasta. Fíjate en que contiene no recorre todo temporal, solo las primeras cuantos posiciones. ¿Por qué? Porque el resto del array todavía no se ha rellenado y contiene ceros por defecto.

Casos de prueba

int[] repetidos = {4, 7, 4, 0, 7, 7, 9, 0};
System.out.println("Con duplicados:  " + Arrays.toString(repetidos));
System.out.println("Sin duplicados:  " + Arrays.toString(sinDuplicados(repetidos)));
System.out.println("Sin duplicados (con ceros): " + Arrays.toString(sinDuplicados(new int[]{0, 0, 0})));
Con duplicados:  [4, 7, 4, 0, 7, 7, 9, 0]
Sin duplicados:  [4, 7, 0, 9]
Sin duplicados (con ceros): [0]

Preguntas que te puedes hacer

¿Qué pasaría si contiene recorriera todo temporal? Que los ceros «de relleno» se tomarían por valores ya guardados. Con {0, 0, 0} el programa creería que el 0 ya está desde el principio y devolvería un array vacío en vez de [0]. Por eso el caso de prueba con ceros es importante: es justo donde el fallo se destapa.

¿Por qué no usar Arrays.sort y quitar los consecutivos? Se puede, y es más eficiente, pero perdería el orden de aparición que pide el enunciado (y además modificaría el original si no se trabaja con una copia).

¿Qué coste tiene esta solución? Para cada elemento recorremos los ya guardados, así que en el peor caso (todos distintos) hace del orden de n² comparaciones. Para los tamaños de estos ejercicios no importa, pero con millones de datos habría que usar otras estructuras (más adelante verás HashSet).

Este patrón de «array temporal grande + copyOf al final» es la forma estándar de construir resultados cuyo tamaño no conoces de antemano, hasta que llegues a ArrayList.

Ejercicio 3: fusionar dos arrays ordenados

Enunciado. Escribe fusionar(int[] a, int[] b), que recibe dos arrays ya ordenados de menor a mayor y devuelve un array nuevo con todos sus elementos, también ordenado. No se puede volver a ordenar todo desde cero: hay que aprovechar que ya vienen ordenados.

Ejemplo. Con {1, 4, 6, 9} y {2, 3, 7, 8, 10, 12} devuelve {1, 2, 3, 4, 6, 7, 8, 9, 10, 12}.

Imagina dos montones de cartas ordenadas boca arriba. Siempre coges la menor de las dos que están encima. Para saber «cuál es la de encima» de cada montón necesitas una posición en cada array: i para a y j para b. ¿Y qué haces cuando un montón se acaba?

Solución comentada

static int[] fusionar(int[] a, int[] b) {
    int[] r = new int[a.length + b.length];
    int i = 0;                                   // posición en a
    int j = 0;                                   // posición en b
    int k = 0;                                   // posición en r
    while (i < a.length && j < b.length) {       // mientras queden de los dos
        if (a[i] <= b[j]) {
            r[k] = a[i];
            i++;
        } else {
            r[k] = b[j];
            j++;
        }
        k++;
    }
    while (i < a.length) {                       // sobras de a (si las hay)
        r[k] = a[i];
        i++;
        k++;
    }
    while (j < b.length) {                       // sobras de b (si las hay)
        r[k] = b[j];
        j++;
        k++;
    }
    return r;
}

Por qué funciona

Con tres posiciones (i, j, k) avanzamos por los tres arrays a la vez. En cada vuelta comparamos los dos elementos de «arriba» (a[i] y b[j]), copiamos el menor a r y avanzamos solo en el array del que lo sacamos. Cuando uno de los dos se agota, el primer bucle termina; uno de los dos bucles siguientes copia lo que sobra del otro (y el otro no hace nada, porque su condición ya es falsa).

Por qué las «sobras» se copian tal cual sin comparar: como el array que queda ya está ordenado y todo lo que contiene es mayor o igual que lo ya copiado, puede pegarse directamente al final.

Casos de prueba

int[] a = {1, 4, 6, 9};
int[] b = {2, 3, 7, 8, 10, 12};
System.out.println("Fusión:          " + Arrays.toString(fusionar(a, b)));
System.out.println("Con vacío:       " + Arrays.toString(fusionar(a, new int[0])));
System.out.println("Con repetidos:   " + Arrays.toString(fusionar(new int[]{1, 3, 3}, new int[]{3, 4})));
Fusión:          [1, 2, 3, 4, 6, 7, 8, 9, 10, 12]
Con vacío:       [1, 4, 6, 9]
Con repetidos:   [1, 3, 3, 3, 4]

Preguntas que te puedes hacer

¿Por qué <= y no < al comparar? Con <=, si hay un empate gana a, y el resultado sale igual de ordenado. Con < también funcionaría; cambia solo de qué array sale primero un valor repetido. Lo importante es que la condición cubra el caso de igualdad en uno de los dos lados.

¿Qué pasa si uno de los arrays está vacío? El primer bucle no se ejecuta ni una vez (porque a.length o b.length es 0) y el bucle de sobras copia el otro entero. Por eso el caso fusionar(a, new int[0]) funciona sin código especial.

¿Por qué no concatenar y ordenar con Arrays.sort? Daría el mismo resultado, pero el enunciado pide aprovechar que ya vienen ordenados. Esta fusión recorre cada array una sola vez, mientras que ordenar desde cero es más costoso. Es, además, el corazón de un algoritmo famoso (merge sort) que verás más adelante.

Cuando un algoritmo tiene varias «posiciones» (aquí i, j, k), ponles nombres que indiquen de qué array son, o un comentario al declararlas, como en la solución. Casi todos los errores de este tipo de ejercicios vienen de avanzar la posición equivocada.

¿Qué falla aquí?

Tres fragmentos de código con un error de arrays cada uno. Antes de leer la explicación, intenta encontrar el fallo.

Fragmento 1: la suma de columnas

static int[] sumaColumnas(int[][] tabla) {
    int[] sumas = new int[tabla.length];
    for (int f = 0; f < tabla.length; f++) {
        for (int c = 0; c < tabla[f].length; c++) {
            sumas += tabla[f];
        }
    }
    return sumas;
}

Con la tabla {{1,2,3},{4,5,6},{7,8,9}} imprime [12, 15, 18], perfecto. Pero con {{1,2,3,4},{5,6,7,8}} aparece:

java.lang.ArrayIndexOutOfBoundsException: Index 2 out of bounds for length 2

¿Qué falla? El array sumas se crea con tabla.length (el número de filas), pero necesita una posición por columna. En una matriz cuadrada coinciden y el error se esconde; con 2 filas y 4 columnas, sumas tiene 2 posiciones y al llegar a la columna 2 se sale.

Arreglo: int[] sumas = new int[tabla[0].length];. Con eso, la segunda tabla da [6, 8, 10, 12]. Es exactamente mi anécdota del principio.

Fragmento 2: el invertir que fastidia el original

static int[] invertir(int[] a) {
    for (int i = 0; i < a.length / 2; i++) {
        int tmp = a[i];
        a[i] = a[a.length - 1 - i];
        a[a.length - 1 - i] = tmp;
    }
    return a;
}

// En main:
int[] notas = {1, 2, 3, 4};
int[] inv = invertir(notas);
System.out.println(Arrays.toString(inv) + " " + Arrays.toString(notas));

Imprime [4, 3, 2, 1] [4, 3, 2, 1]: los dos están invertidos.

¿Qué falla? El método intercambia los elementos directamente en el array que recibe (a) y lo devuelve. Como a es la dirección del array original, inv y notas son el mismo array. El método «funciona», pero estropea los datos de quien lo llama.

Arreglo: trabajar sobre una copia, por ejemplo int[] r = Arrays.copyOf(a, a.length);, hacer los intercambios en r y devolver r. Con eso, el resultado es [4, 3, 2, 1] [1, 2, 3, 4].

Fragmento 3: la comparación que nunca acierta

int[] esperado = {1, 2, 3, 4};
int[] obtenido = fusionar(new int[]{1, 3}, new int[]{2, 4});

if (obtenido == esperado) {
    System.out.println("Correcto");
} else {
    System.out.println("Incorrecto");
}

Imprime Incorrecto, aunque obtenido tiene exactamente [1, 2, 3, 4].

¿Qué falla? == compara direcciones, no contenidos. esperado y obtenido son dos arrays distintos en memoria, aunque contengan lo mismo.

Arreglo: if (Arrays.equals(obtenido, esperado)). Con eso imprime Correcto.

Errores típicos en estos ejercicios

  • Escribir int[] r = a; creyendo que copia. Solo copia la dirección.
  • Usar tabla.length donde toca tabla[0].length (o al revés). En matrices cuadradas no se nota.
  • Olvidar los casos límite: array vacío, un solo elemento, todos los valores iguales, ceros.
  • Recorrer todo un array temporal en vez de solo la parte ya rellenada.
  • Comparar arrays con == en lugar de Arrays.equals.

Chuletario: arrays en Java

Lo esencial en 30 segundos

  • Un array tiene tamaño fijo y un solo tipo. Los índices van de 0 a length - 1.
  • length en arrays no lleva paréntesis (en String sí: length()).
  • Un array es un objeto: la variable guarda una referencia, no los datos.
  • Valores por defecto al crear: 0 (números), false (boolean), '\u0000' (char), null (objetos).

Crear y recorrer

Quiero…Código
Crear vacíoint[] a = new int[5];
Crear con valoresint[] a = {5, 2, 9};
Tamañoa.length
Último elementoa[a.length - 1]
Recorrer con índicefor (int i = 0; i < a.length; i++) { … a[i] … }
Recorrer sin índicefor (int x : a) { … x … }
RellenarArrays.fill(a, 4); → [4, 4, 4]

i <= a.length. El último índice válido es a.length - 1: si no, ArrayIndexOutOfBoundsException.

Copiar y comparar

Quiero…CódigoResultado
❌ «Copiar»int[] m = n;Dos nombres para el mismo array: m[0] = 1 cambia también n[0]
✅ Copia realArrays.copyOf(n, n.length)Array nuevo e independiente
Copia más largaArrays.copyOf(n, 5)[5, 2, 9, 0, 0] (rellena con ceros)
Copiar un tramoArrays.copyOfRange(n, 1, 3)[2, 9] (el final no se incluye)
❌ Comparara == bCompara referencias: dos copias iguales dan false
✅ Comparar contenidoArrays.equals(a, b)true si tienen los mismos valores en el mismo orden

modificar el array que recibes en un método (invertir, ordenar…) y descubrir que has cambiado el original. Si quieres devolver otro, haz Arrays.copyOf primero.

Ordenar y buscar (import java.util.Arrays;)

Quiero…Código
Ordenar (modifica el array)Arrays.sort(a);
Buscar (solo si está ordenado)Arrays.binarySearch(a, valor)
ImprimirSystem.out.println(Arrays.toString(a));
Imprimir matrizArrays.deepToString(m)
  • binarySearch en [1, 2, 9]: buscar 2 → 1 (la posición). Buscar 5 → -3, es decir -(punto de inserción) - 1: negativo = no está.
  • Imprimir un array con println(a) muestra algo como [I@1b6d3586, no los datos.

binarySearch sobre un array sin ordenar no da error, da un resultado sin sentido. Ordena antes.

Arrays 2D (matrices)

int[][] t = {{1, 2, 3, 4},
             {5, 6, 7, 8}};

t.length        // 2  → número de FILAS
t[0].length     // 4  → número de COLUMNAS (de la fila 0)
t[1][2]         // 7  → fila 1, columna 2

// recorrido completo
for (int f = 0; f < t.length; f++) {
    for (int c = 0; c < t[f].length; c++) {
        System.out.print(t[f] + " ");
    }
    System.out.println();
}
  • Una matriz es un array de arrays: las filas pueden tener longitudes distintas (new int[2][] y luego ir[0] = new int[1]; ir[1] = new int[3];).
  • Para crear una matriz del tamaño de otra: filas = t.length, columnas = t[0].length.

new int[tabla.length] cuando necesitas un resultado por columna. Con una tabla 2×4 eso crea 2 posiciones, y al escribir en la columna 2 salta ArrayIndexOutOfBoundsException. Lo correcto: new int[tabla[0].length].

Arrays como parámetro y retorno

static int[] doble(int[] a) {          // recibe la REFERENCIA
    int[] r = new int[a.length];        // resultado nuevo → no tocamos el original
    for (int i = 0; i < a.length; i++) r[i] = a[i] * 2;
    return r;
}
  • Lo que hagas dentro con a[i] = … sí afecta al array de fuera. Reasignar a = new int[3] dentro no.

Plantillas de los ejercicios

Invertir (a otro array):

int[] r = new int[a.length];
for (int i = 0; i < a.length; i++) r[i] = a[a.length - 1 - i];

[1,2,3,4,5] → [5,4,3,2,1]

Rotar k posiciones a la derecha (k puede ser negativo o mayor que n):

int n = a.length;
int[] r = new int[n];
int d = ((k % n) + n) % n;              // normaliza k a 0..n-1
for (int i = 0; i < n; i++) r[(i + d) % n] = a[i];

rotar(2) → [4,5,1,2,3] · rotar(-1) → [2,3,4,5,1] · rotar(7) = rotar(2)

Quitar duplicados (patrón array + contador):

int[] temp = new int[a.length];
int count = 0;
for (int x : a) {
    boolean esta = false;
    for (int j = 0; j < count; j++)     // solo mira hasta count, no hasta temp.length
        if (temp[j] == x) esta = true;
    if (!esta) temp[count++] = x;
}
int[] r = Arrays.copyOf(temp, count);   // recorta al tamaño real

{4,7,4,0,7,7,9,0} → [4,7,0,9] · {0,0,0} → [0]

mirar temp hasta temp.length en vez de hasta count. Las posiciones sin usar valen 0 y «encuentran» un 0 que nunca añadiste.

Fusionar dos arrays ordenados (dos punteros):

int[] r = new int[a.length + b.length];
int i = 0, j = 0, k = 0;
while (i < a.length && j < b.length)
    r[k++] = (a[i] <= b[j]) ? a[i++] : b[j++];
while (i < a.length) r[k++] = a[i++];   // lo que sobre de a
while (j < b.length) r[k++] = b[j++];   // lo que sobre de b

{1,4,6,9} + {2,3,7,8,10,12} → [1,2,3,4,6,7,8,9,10,12]

Errores que más se repiten

SíntomaCausa probable
ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 5<= en lugar de <, o índice calculado sin comprobar
El array original cambió «solo»Asignaste m = n o modificaste el parámetro en sitio
a == b da false con datos idénticosCompara referencias → Arrays.equals
Sale [I@… por pantallaFalta Arrays.toString
Ceros «fantasma» en el resultadoArray más grande de lo necesario sin recortar con copyOf(…, count)
binarySearch devuelve algo absurdoEl array no estaba ordenado
Fallo solo con matrices no cuadradasConfundir t.length con t[0].length

Antes de entregar, pregúntate

  1. ¿Mi bucle llega hasta length - 1 y no más?
  2. ¿Estoy modificando el original sin querer?
  3. ¿Qué pasa con un array vacío? ¿Y de un elemento?
  4. ¿Funciona en una matriz que no es cuadrada?

Resumen

  • Para no tocar el original, trabaja con un array nuevo (new o Arrays.copyOf).
  • El operador % permite «dar la vuelta» a un array; con negativos, usa ((k % n) + n) % n.
  • Para resultados de tamaño desconocido: array temporal grande, contador y Arrays.copyOf al final.
  • Dos arrays ordenados se fusionan con una posición por array y copiando las sobras al final.
  • Prueba siempre con casos distintos: vacío, un elemento, repetidos, matrices no cuadradas.

Checklist: ¿lo has conseguido?

  • ¿Has intentado resolver cada ejercicio antes de mirar la solución?
  • ¿Sabes explicar por qué invertir devuelve un array nuevo?
  • ¿Entiendes por qué contiene recibe el parámetro hasta?
  • ¿Sabes qué ocurre con las «sobras» al fusionar?
  • ¿Has encontrado los tres fallos de «¿Qué falla aquí?» sin leer la explicación?

Con esto cierras el bloque de arrays. El siguiente paso en Java son los Strings, que se parecen mucho más a los arrays de lo que parece. ¡Allí te espero!

Sigue aprendiendoTodos los artículos de Java

¿Te ha servido este artículo? Así puedes apoyar SergioLearns →

Sigue aprendiendo

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *