REPASO EXPRÉS — ESTRUCTURA DE DATOS (Java) LECTURA: 30 MIN

1º Ing. Informática UAX · Examen escrito a mano · 5 ejercicios: encapsulación · complejidad · ordenación · estructuras · código · Imprimir

1 · PLANTILLA DE CLASE ENCAPSULADA (la que siempre cae)

public class Alumno {                              // 1. clase publica
    private String nombre;                         // 2. atributos SIEMPRE private
    private double nota;                           //    (nadie los toca desde fuera)

    public Alumno(String nombre, double nota) {    // 3. constructor = nombre de la clase, sin tipo de retorno
        this.nombre = nombre;                      //    this.atributo = parametro (mismo nombre)
        setNota(nota);                             //    reuso el setter: tambien valida al construir
    }

    public String getNombre() { return nombre; }   // 4. getters: lectura controlada
    public double getNota()   { return nota;   }

    public void setNota(double nota) {             // 5. setter CON validacion (esto da puntos)
        if (nota >= 0 && nota <= 10) {             //    solo acepta valores legales
            this.nota = nota;                      //    si no, se ignora (o lanzar excepcion)
        }
    }

    @Override                                      // 6. redefine el toString de Object
    public String toString() {
        return nombre + " (" + nota + ")";         //    lo que imprime System.out.println(alumno)
    }
}

Checklist del corrector (los 5 puntos que mira)

  1. Atributos private — todos, sin excepción.
  2. Constructor que inicializa todos los atributos con this.
  3. Getters públicos para cada atributo que deba leerse.
  4. Setters con validación (un if que rechaza valores ilegales).
  5. toString() con @Override devolviendo un String.

2 · PLANTILLA DE JERARQUÍA: abstracta + hijas + polimorfismo

public abstract class Figura {                     // abstract: NO se puede hacer new Figura()
    protected String nombre;                       // protected: las hijas SI lo ven

    public Figura(String nombre) {                 // las hijas lo llaman con super(...)
        this.nombre = nombre;
    }

    public abstract double area();                 // SIN cuerpo: cada hija DEBE implementarlo

    public String describir() {                    // metodo normal: se hereda tal cual
        return nombre + ": area = " + area();      // llama al area() de la hija real
    }
}

class Circulo extends Figura {
    private double radio;
    public Circulo(double radio) {
        super("Circulo");                          // PRIMERA linea: constructor del padre
        this.radio = radio;
    }
    @Override public double area() { return Math.PI * radio * radio; }
}

class Rectangulo extends Figura {
    private double base, altura;
    public Rectangulo(double base, double altura) {
        super("Rectangulo");
        this.base = base; this.altura = altura;
    }
    @Override public double area() { return base * altura; }
}

Uso polimórfico (esto es lo que demuestra que entiendes el tema):

Figura[] figuras = { new Circulo(2), new Rectangulo(3, 4) };  // array del tipo PADRE
for (Figura f : figuras) {
    System.out.println(f.describir());   // ejecuta el area() de CADA hija (enlace dinamico)
}
Clase abstractaInterfaz
¿Qué aporta?Atributos + código común + métodos abstractosSolo el contrato: métodos que hay que cumplir
¿Cómo se usa?extends — solo UNAimplements — VARIAS a la vez
¿Estado?Sí: atributos y constructorNo tiene atributos de instancia ni constructor
¿Cuándo?Jerarquía "ES-UN" con código compartido (Figura→Círculo)Capacidad "SABE-HACER" (Comparable, Serializable)
Sobrescribir (@Override): misma firma en la hija, cambia el comportamiento heredado.
Sobrecargar: mismo nombre con distintos parámetros en la misma clase (se decide en compilación).

3 · COMPLEJIDAD EN 4 PASOS (receta)

  1. Un bucle que recorre n elementos → O(n).
  2. Bucles anidados independientes → se multiplican: n·n = O(n²). Bucles seguidos (no anidados) → se suman: n+n → sigue siendo O(n).
  3. Índice que se duplica o divide (i*=2, i/=2, mitades) → O(log n). Bucle de n con interno logarítmico → O(n log n).
  4. Quédate con el término dominante y tira las constantes: 3n² + 5n + 7 → O(n²).
Patrón de códigoOrdenPor qué
for (int i = 0; i < n; i++) x++;O(n)n vueltas de trabajo constante
for (int i = 0; i < n; i++) for (int j = 0; j < n; j++) x++;O(n²)anidados independientes: n·n
for (int i = 0; i < n; i++) x++; for (int j = 0; j < n; j++) x++;O(n)seguidos se suman: 2n → O(n)
for (int i = 1; i < n; i *= 2) x++;O(log n)i = 1,2,4,8... llega a n en log₂n pasos
for (int i = 0; i < n; i++) for (int j = 1; j < n; j *= 2) x++;O(n log n)n vueltas × log n internas
for (int i = 0; i < n; i++) for (int j = 0; j < i; j++) x++;O(n²)dependiente: 0+1+…+(n−1) = n(n−1)/2
int v = a[0] + a[n - 1]; x = v * 2;O(1)nº fijo de operaciones, no depende de n
static void f(int ini, int fin) { if (ini >= fin) return; int m = (ini + fin) / 2; f(ini, m); f(m + 1, fin); // + mezcla O(n) }O(n log n)T(n) = 2T(n/2) + n → mergesort

Las 2 trampas que ponen SIEMPRE

for (int i = 0; i < n; i++) for (int j = 0; j <= i; j++) x++; Suma aritmética: 1+2+…+n = n(n+1)/2 → O(n²), NO O(n log n).
for (int tam = 1; tam <= n; tam *= 2) for (int j = 0; j < tam; j++) x++; Suma geométrica: 1+2+4+…+n ≈ 2n → O(n), NO O(n log n).
Orden (de MEJOR a PEOR)NombreEjemplon = 1000
O(1)constanteacceso a[i], push/pop, get de HashMap1
O(log n)logarítmicobúsqueda binaria, ABB equilibrado~10
O(n)linealrecorrer un array, búsqueda secuencial1 000
O(n log n)casilinealmergesort, heapsort, quicksort (medio)~10 000
O(n²)cuadráticoburbuja, selección, inserción1 000 000
O(2ⁿ)exponencialfuerza bruta, Fibonacci recursivo puroastronómico

4 · ORDENACIÓN — los 6 códigos que hay que saber picar

4.1 Burbuja mejorada

static void burbujaMejorada(int[] a) {
    boolean cambio = true;                         // bandera: optimizacion 1
    for (int i = 0; i < a.length - 1 && cambio; i++) {
        cambio = false;                            // supongo que ya esta ordenado
        for (int j = 0; j < a.length - 1 - i; j++) {   // -i: el final ya esta ordenado (opt. 2)
            if (a[j] > a[j + 1]) {                 // vecinos desordenados...
                int t = a[j]; a[j] = a[j + 1]; a[j + 1] = t;   // ...se intercambian
                cambio = true;
            }
        }
    }
}

Compara vecinos y los intercambia si están al revés: en cada pasada el mayor "flota" hasta el final. Si una pasada entera no cambia nada, la bandera corta el algoritmo (mejor caso O(n)).

Pasada 1 de [5,2,4,1]: 5↔2→[2,5,4,1] · 5↔4→[2,4,5,1] · 5↔1→[2,4,1,5] (el 5 ya está colocado) Mejor O(n) (ya ordenado, 1 pasada) · Medio/Peor O(n²) · Estable · In situ

4.2 Selección

static void seleccion(int[] a) {
    for (int i = 0; i < a.length - 1; i++) {
        int min = i;                               // posicion del minimo de lo no ordenado
        for (int j = i + 1; j < a.length; j++) {
            if (a[j] < a[min]) min = j;            // solo APUNTO donde esta (no intercambio aun)
        }
        int t = a[i]; a[i] = a[min]; a[min] = t;   // UN solo intercambio por pasada
    }
}

Busca el mínimo de la zona sin ordenar y lo coloca al principio con un único intercambio por pasada. Hace siempre las mismas comparaciones, esté como esté el array (no tiene mejor caso).

Pasada 1 de [5,2,4,1]: mínimo = 1 (pos 3) → intercambio 5↔1 → [1,2,4,5] (el 1 ya está colocado) Mejor/Medio/Peor O(n²) siempre · Estable NO · Mínimo nº de intercambios (n−1)

4.3 Inserción

static void insercion(int[] a) {
    for (int i = 1; i < a.length; i++) {           // a[0] ya es una "mano" ordenada
        int v = a[i];                              // carta que voy a colocar
        int j = i - 1;
        while (j >= 0 && a[j] > v) {               // mientras haya mayores a la izquierda...
            a[j + 1] = a[j];                       // ...los desplazo un hueco a la derecha
            j--;
        }
        a[j + 1] = v;                              // inserto la carta en su hueco
    }
}

Toma cada elemento y lo desliza hacia la izquierda hasta su hueco, como ordenar cartas en la mano. La zona izquierda del array está siempre ordenada.

i=1 en [5,2,4,1]: v=2, el 5 se desplaza → [2,5,4,1] · (i=2: el 4 entra entre 2 y 5 → [2,4,5,1]) Mejor O(n) (casi ordenado: el mejor de los simples) · Medio/Peor O(n²) · Estable

4.4 Mergesort (divide y vencerás)

static void mergeSort(int[] a, int ini, int fin) {
    if (ini >= fin) return;                        // CASO BASE: 0 o 1 elementos
    int mid = (ini + fin) / 2;
    mergeSort(a, ini, mid);                        // 1) ordeno mitad izquierda
    mergeSort(a, mid + 1, fin);                    // 2) ordeno mitad derecha
    merge(a, ini, mid, fin);                       // 3) mezclo las dos mitades ordenadas
}

static void merge(int[] a, int ini, int mid, int fin) {
    int[] aux = new int[fin - ini + 1];            // array auxiliar (memoria extra O(n))
    int i = ini, j = mid + 1, k = 0;
    while (i <= mid && j <= fin) {                 // comparo cabezas y copio la menor
        aux[k++] = (a[i] <= a[j]) ? a[i++] : a[j++];   // <= mantiene la estabilidad
    }
    while (i <= mid) aux[k++] = a[i++];            // restos de la izquierda
    while (j <= fin) aux[k++] = a[j++];            // restos de la derecha
    for (k = 0; k < aux.length; k++) a[ini + k] = aux[k];  // vuelco aux al original
}

Divide el array en mitades hasta trozos de 1 elemento (ya ordenados) y luego los mezcla por parejas comparando cabezas. Rendimiento garantizado, pero gasta un array auxiliar.

[5,2 | 4,1] → ordena mitades: [2,5] y [1,4] → mezcla cabezas: 1,2,4,5 → [1,2,4,5] Mejor/Medio/Peor O(n log n) SIEMPRE · Estable · Memoria extra O(n)

4.5 Quicksort

static void quickSort(int[] a, int ini, int fin) {
    if (ini >= fin) return;                        // CASO BASE: 0 o 1 elementos
    int p = particionar(a, ini, fin);              // coloca el pivote en su sitio definitivo
    quickSort(a, ini, p - 1);                      // ordeno los menores
    quickSort(a, p + 1, fin);                      // ordeno los mayores
}

static int particionar(int[] a, int ini, int fin) {
    int pivote = a[ini];                           // pivote = primer elemento
    int i = ini;                                   // frontera de los menores
    for (int j = ini + 1; j <= fin; j++) {
        if (a[j] < pivote) {                       // menor que el pivote:
            i++;                                   // amplio la zona de menores...
            int t = a[i]; a[i] = a[j]; a[j] = t;   // ...y lo meto en ella
        }
    }
    int t = a[ini]; a[ini] = a[i]; a[i] = t;       // pivote a la frontera (su sitio final)
    return i;                                      // posicion definitiva del pivote
}

Elige un pivote y particiona: menores a su izquierda, mayores a su derecha; el pivote queda ya en su posición definitiva. Repite recursivamente en cada lado.

[5,2,4,1], pivote=5: 2,4,1 son menores → swap final 5↔1 → [1,2,4,5], pivote fijo en pos 3 Mejor/Medio O(n log n) · Peor O(n²) (array YA ordenado con pivote = primero) · Estable NO · In situ

4.6 Heapsort (montículo)

static void heapSort(int[] a) {
    int n = a.length;
    for (int i = n / 2 - 1; i >= 0; i--) {         // FASE 1: construir MAX-HEAP
        hundir(a, n, i);                           //   desde el ultimo padre hacia la raiz
    }
    for (int fin = n - 1; fin > 0; fin--) {        // FASE 2: extraer el maximo n-1 veces
        int t = a[0]; a[0] = a[fin]; a[fin] = t;   //   raiz (maximo) ↔ ultimo del heap
        hundir(a, fin, 0);                         //   re-hundir la nueva raiz (heap mas corto)
    }
}

static void hundir(int[] a, int n, int i) {        // baja a[i] hasta cumplir padre >= hijos
    int mayor = i, izq = 2 * i + 1, der = 2 * i + 2;   // hijos de i en el array
    if (izq < n && a[izq] > a[mayor]) mayor = izq;
    if (der < n && a[der] > a[mayor]) mayor = der;
    if (mayor != i) {                              // algun hijo es mayor que el padre:
        int t = a[i]; a[i] = a[mayor]; a[mayor] = t;
        hundir(a, n, mayor);                       // sigo hundiendo por esa rama
    }
}

Construye un max-heap sobre el propio array (todo padre ≥ sus hijos, máximo en a[0]). Luego, n−1 veces: intercambia raíz↔último, acorta el heap y "hunde" la nueva raíz.

[5,2,4,1] ya es max-heap → 5↔1 → [1,2,4 | 5] → hundir raíz → [4,2,1 | 5] Mejor/Medio/Peor O(n log n) garantizado · Estable NO · In situ (memoria O(1))

TABLA RESUMEN — vuélcala tal cual en el examen

AlgoritmoMejorMedioPeorEstableMemoriaApunte clave
Burbuja mejoradaO(n)O(n²)O(n²)O(1)bandera → para si no hay cambios
SelecciónO(n²)O(n²)O(n²)NoO(1)siempre igual; solo n−1 intercambios
InserciónO(n)O(n²)O(n²)O(1)el mejor con arrays casi ordenados
MergesortO(n log n)O(n log n)O(n log n)O(n)garantizado, pero array auxiliar
QuicksortO(n log n)O(n log n)O(n²)NoO(log n)peor caso: ya ordenado, pivote 1º
HeapsortO(n log n)O(n log n)O(n log n)NoO(1)garantizado e in situ

5 · BÚSQUEDA BINARIA (solo sobre array ORDENADO)

static int busquedaBinaria(int[] a, int x) {
    int low = 0, high = a.length - 1;
    while (low <= high) {                          // OJO: <=  (con < falla, ver abajo)
        int mid = (low + high) / 2;                // miro el centro
        if (a[mid] == x) return mid;               // encontrado
        if (a[mid] < x) low  = mid + 1;            // esta a la derecha: descarto mitad izq.
        else            high = mid - 1;            // esta a la izquierda: descarto mitad der.
    }
    return -1;                                     // no esta
}
El fallo típico: escribir while (low < high). Cuando el intervalo se reduce a UN elemento (low == high) el bucle no entra y ese elemento no se comprueba. Siempre low <= high.

Cada vuelta descarta la mitad → O(log n) (peor caso). Mejor caso O(1): acierta al centro a la primera.

6 · ESTRUCTURAS DE DATOS EXPRÉS

EstructuraIdea en 5 palabrasOperaciones y costeClase JavaElígela si piden…
Arraycasillas contiguas con índice directoacceso O(1) · insertar/borrar en medio O(n)int[], ArrayListacceso por posición i
Lista enlazadanodos encadenados por referenciasinsertar/borrar en extremos O(1) · acceso O(n)LinkedListmuchas altas/bajas, sin índices
Pila (LIFO)el último en entrar salepush / pop / peek O(1)ArrayDeque (push/pop)deshacer, paréntesis, llamadas recursivas
Cola (FIFO)el primero en entrar saleoffer / poll / peek O(1)ArrayDeque (offer/poll)turnos, cola de impresión, BFS
ABB (árbol bin. búsqueda)izquierda < raíz < derechabuscar/insertar/borrar O(log n) equilibrado (O(n) degenerado)TreeSet, TreeMapdatos SIEMPRE ordenados, rangos, mín/máx
Tabla hashla clave calcula su posiciónput / get / remove O(1) medioHashMap, HashSetacceso rápido por clave, contar, duplicados
Regla de decisión en el examen: piden ordenTree* · piden velocidad por claveHash* · piden FIFO (turnos) → cola · piden LIFO (deshacer) → pila · piden posición i → array/ArrayList · piden insertar/borrar constanteLinkedList.

Pila en 5 líneas

Deque<Integer> pila = new ArrayDeque<>();   // pila LIFO
pila.push(1); pila.push(2); pila.push(3);   // apilar: la cima es 3
System.out.println(pila.pop());             // 3 (sale el ULTIMO que entro)
System.out.println(pila.peek());            // 2 (mira la cima SIN sacarla)
System.out.println(pila.isEmpty());         // false

HashMap en 5 líneas

Map<String, Integer> notas = new HashMap<>();  // clave -> valor
notas.put("Ana", 7);  notas.put("Luis", 5);    // insertar O(1)
notas.put("Ana", 9);                           // clave repetida: SOBRESCRIBE
System.out.println(notas.get("Ana"));          // 9 (acceso O(1) por clave)
System.out.println(notas.containsKey("Eva"));  // false

Imports: import java.util.*; cubre Deque, ArrayDeque, Map, HashMap, List, ArrayList…

7 · RECURSIÓN EXPRÉS

Plantilla mental (2 piezas, en este orden):CASO BASE: un if que devuelve un valor directo SIN llamarse a sí mismo (para la recursión). ② CASO RECURSIVO: se llama a sí misma con un problema más pequeño que se acerca SIEMPRE a la base.
static long factorial(int n) {
    if (n <= 1) return 1;              // CASO BASE: 0! = 1! = 1
    return n * factorial(n - 1);       // CASO RECURSIVO: n! = n * (n-1)!
}

Traza: factorial(4) = 4·factorial(3) = 4·3·factorial(2) = 4·3·2·factorial(1) = 4·3·2·1 = 24. Sin caso base → StackOverflowError.

8 · ERRORES QUE TE QUITAN PUNTOS

  1. Olvidar private en los atributos → adiós encapsulación (medio ejercicio).
  2. Comparar Strings con ==: compara referencias. Usa equals(): s1.equals(s2).
  3. Intercambiar sin variable temporal: a[i]=a[j]; a[j]=a[i]; pierde el valor. Siempre: int t=a[i]; a[i]=a[j]; a[j]=t;
  4. Olvidar el caso base en recursión (o no acercarse a él) → StackOverflowError.
  5. Búsqueda binaria con low < high en vez de low <= high → se salta el último candidato.
  6. Decir que quicksort es "O(n log n) siempre": su peor caso es O(n²). Los garantizados son mergesort y heapsort.
  7. Confundir el mejor caso con el peor: burbuja mejorada e inserción son O(n) SOLO si ya está (casi) ordenado.
  8. Índices fuera de rango: si comparas a[j+1], el bucle debe llegar solo hasta j < n-1 (en burbuja, n-1-i).
  9. No implementar el método abstracto en la hija u olvidar @Override / extends → no compila.
  10. Hacer new de una clase abstracta o saltarse super(...) como primera línea del constructor de la hija.

Repaso Exprés EDA · UAX 1º Ing. Informática · Si sabes reproducir a mano las plantillas 1, 2 y los 6 códigos del punto 4, el examen está hecho.