Grado en Ingeniería Informática · UAX · Estructura de Datos y Algoritmos · Convocatoria extraordinaria

Guía interactiva de EDA «te llevo de la mano»

Los tres modelos de examen extraordinario (A, B y C), ejercicio a ejercicio: te digo cómo reconocer el tipo, qué estructura usar, cómo plantear el código y por qué. Tú picas el código y luego revelas la solución Java completa (compilada) para comprobar. Es un examen práctico de programación: abstracción, ordenación, complejidad y estructuras de datos.

Cómo usar esta guía

El examen de EDA es práctico: hay que picar código Java (más algún análisis de complejidad y de elección de estructura). Esta guía te lleva de la mano por los tres modelos de examen extraordinario (A, B y C) para que aprendas el método, no la respuesta de memoria.

Cada ejercicio sigue el mismo recorrido:

🔍 Cómo lo reconozco (qué señales del enunciado te dicen el tipo) → 🧭 Estrategia y pasos (el plan de código: firma, estructuras, invariantes, casos borde) → 💡 Por qué (justificación y coste en O()) → ✍️ Ahora pícalo tú (esqueleto con huecos / pseudocódigo) → 💊 Píldora de examen (patrón reutilizable) → y el botón ✅ Ver solución con el código Java completo.

Intenta escribir el código tapando la solución; solo la revelas para corregirte. Todo el código Java de las soluciones se ha compilado y ejecutado con OpenJDK 21 (javac + java) y produce exactamente las salidas indicadas.

🎯 Los 6 patrones que SIEMPRE caen (reconócelos y tienes medio examen): (1) Elegir estructura del framework → sin duplicados + orden + O(log n) = TreeSet; sin duplicados rápido sin orden = HashSet; con índice = ArrayList. (2) Complejidad Big-O → cuenta bucles: uno = O(n), anidados independientes = O(n²), un bucle que hace i*=2 o i/=2 = O(log n), recursión doble (Fibonacci/Hanoi) = O(2ⁿ), recursión que parte por la mitad = O(log n) o O(n log n). (3) Estructuras enlazadas → lista doble, pila (LIFO), cola (FIFO), cola con 2 pilas: gestiona siguiente/anterior, cima, frente/fin y los casos vacío/un nodo. (4) Ordenación → Quicksort (particiona por pivote, O(n log n) medio, O(n²) peor), Mergesort (parte por la mitad + mezcla, O(n log n) SIEMPRE, estable), Insertion Sort (inserta en la parte ordenada, O(n²), O(n) si ya ordenado). (5) Árboles / recursión → ABB (insertar/contiene/inorden/hojas), esquema recursivo: caso base + caso recursivo que se acerca al caso base. (6) TAD / abstracción → separa la interface (contrato) de la clase que la implements; programa contra la interfaz.
⚠️ Aviso de honestidad: la solución oficial del Ejercicio 2(a) del Examen 1 tiene una contradicción: en el título pone «m1 → O(n²)» pero en el propio texto se corrige a O(n·log n) (que es lo correcto). Lo he verificado contando operaciones: para m1 el número de pasos es exactamente n + n·log₂n, luego el orden es O(n·log n), NO O(n²). Lo dejo señalado dentro de ese ejercicio.

Examen 1 · Modelo A · 7 ejercicios

Colecciones, complejidad, pila, lista doble, quicksort, ABB y streams. El más completo: aquí ves todos los temas.

Abstracción · Colecciones1. Elegir la estructura del framework de Java

Un sistema debe almacenar matrículas de coche sin duplicados, poder recorrerlas en orden alfabético en cualquier momento, y con inserción y búsqueda en O(log n). ¿Qué colección eliges? Justifica y explica por qué las demás NO valen.
a) ArrayList   b) HashSet   c) LinkedList   d) TreeSet
🔍 Cómo lo reconozcoEs una pregunta de teoría razonada: te dan 3 requisitos y 4 colecciones. La clave es traducir cada palabra del enunciado a una propiedad: «sin duplicados» → un Set; «orden alfabético siempre» → estructura ordenada; «O(log n)» → árbol balanceado.
🧭 Estrategia y pasos a seguir1) Haz una tabla mental Colección × propiedad (duplicados / orden / coste de contains). 2) Descarta las que fallan algún requisito. 3) La que cumple los tres a la vez es la respuesta. 4) Para la máxima nota, explica por qué cada descartada falla (sin esto no dan todos los puntos).
💡 Por quéSolo TreeSet reúne las tres: es un Set (sin duplicados) sobre un árbol rojo-negro (balanceado) que mantiene el orden natural y da add/contains/remove en O(log n). HashSet es O(1) pero no ordena; ArrayList/LinkedList permiten duplicados y su contains es O(n).
✍️ Ahora pícalo túNo hay código: es argumentar. Rellena esta tabla y elige:
ArrayList: duplicados=, orden=no automático, contains=O(n) → falla.
HashSet: duplicados=no, orden=NINGUNO, contains=O(1) → falla el orden.
LinkedList: duplicados=, orden=no, contains=O(n) → falla todo.
TreeSet: duplicados=no, orden=, contains=O(log n) → ✔.
💊 Píldora de examenMemoriza el árbol de decisión de colecciones: ¿permite duplicados? No → Set; Sí → List. ¿necesito orden? Sí → Tree* (TreeSet/TreeMap, O(log n)); No → Hash* (O(1)). ¿acceso por índice?ArrayList. Con esta cadena resuelves cualquier pregunta de elección de estructura.
Solución comprobada (compilada con OpenJDK)Respuesta: d) TreeSet<String>.

Justificación: TreeSet es un conjunto (no admite duplicados) implementado sobre un árbol rojo-negro balanceado. Mantiene los elementos siempre ordenados por su orden natural y sus operaciones add, contains y remove son O(log n). Cumple los tres requisitos a la vez.

Por qué las demás NO valen:

  • a) ArrayList → permite duplicados (no es Set) y contains es O(n) (búsqueda secuencial), no O(log n).
  • b) HashSet → sí impide duplicados y es O(1) de media, pero NO mantiene ningún orden de iteración → incumple el recorrido alfabético.
  • c) LinkedList → permite duplicados y contains es O(n). Falla en los tres requisitos.

Complejidad · Big-O2. Orden de complejidad de tres métodos

Determina y justifica el orden de complejidad. Indica el tamaño del problema y explica las cuentas de cada bucle.
// (a) dos bloques seguidos
for (int i = 0; i < n; i++)  suma += a[i];        // bloque 1
for (int j = 1; j < n; j *= 2)                    // bloque 2 (externo)
    for (int k = 0; k < n; k++)  suma++;          // bloque 2 (interno)

// (b) el interno empieza en i
for (int i = 0; i < n; i++)
    for (int j = i; j < n; j++)  c++;

// (c) recursion doble
public static int m3(int n){
    if (n <= 1) return 1;
    return m3(n-1) + m3(n-2);
}
🔍 Cómo lo reconozcoPalabras clave: «orden de complejidad» + código con bucles o recursión. No se pide ejecutar nada, sino contar vueltas. Fíjate en 3 señales: (1) bucles seguidos (regla de la suma), (2) bucles anidados (regla del producto), (3) el contador que multiplica/divide (j*=2 → logarítmico).
🧭 Estrategia y pasos a seguirPara cada método: 1) di el tamaño del problema (n = longitud o valor). 2) Cuenta las vueltas de cada bucle por separado. 3) Bucles anidados → se multiplican; bucles seguidos → se suman y queda el dominante. 4) En recursión, mira cuántas llamadas genera cada llamada.
💡 Por qué(a) bloque1=O(n); bloque2: el externo con j*=2 da log₂n vueltas y el interno n → O(n log n); dominante = O(n log n). (b) suma 1+2+...+n = n(n+1)/2 → O(n²). (c) es Fibonacci con doble recursión: cada llamada genera dos → árbol que casi se duplica por nivel → O(2ⁿ).
✍️ Ahora pícalo túRazona tú antes de mirar:
(a) ¿cuántas vueltas da j si hace j*=2 hasta n? → ___  → multiplica por las n del interno.
(b) escribe la suma i=0→n, i=1→n-1, ... y simífica.
(c) dibuja el árbol de llamadas de m3(4): cuenta cuántas hojas hay.
💊 Píldora de examenChuleta Big-O: bucle simple = O(n); dos anidados a n = O(n²); contador *=2 o /=2 = O(log n); anidado log·n = O(n log n); suma aritmética 1+2+...+n = O(n²); recursión doble (dos llamadas) = O(2ⁿ); recursión simple que resta 1 = O(n); que divide entre 2 = O(log n).
Solución comprobada (compilada con OpenJDK)

Tamaño del problema: en (a) y (b) es n; en (c) es el valor de n.

(a) → O(n·log n). Dos bloques seguidos (regla de la suma, queda el dominante). Bloque 1: bucle simple → O(n). Bloque 2: el externo hace j*=2 → da log₂n vueltas; el interno da n vueltas independientes → se multiplican: n·log n. Total O(n) + O(n log n) = O(n log n).

⚠️ La solución oficial tenía un desliz: su titular dice «m1 → O(n²)» pero el propio texto lo corrige a O(n·log n). Lo he comprobado contando operaciones con OpenJDK: para n=16→80 pasos, n=64→448, n=256→2304, n=1024→11264, que es exactamente n + n·log₂n (no n²). El resultado correcto es O(n·log n).

(b) → O(n²). El interno empieza en j = i, así que para cada i da n−i vueltas: n + (n-1) + ... + 1 = n(n+1)/2 ≈ n²/2 → O(n²).

(c) → O(2ⁿ). Es la sucesión de Fibonacci con doble recursión ingenua: cada llamada genera otras dos (m3(n-1) y m3(n-2)), formando un árbol que casi se duplica por nivel → crecimiento exponencial O(2ⁿ) (más finamente O(φⁿ) con φ≈1,618). Lo eficiente sería resolverlo iterativo en O(n). Verificado: m3(0..7) = 1 1 2 3 5 8 13 21 (Fibonacci).

Pila · Código Java3. Comprobador de paréntesis equilibrados

Escribe public static boolean equilibrada(String expr) que use una pila para comprobar si ( ) [ ] { } están bien equilibrados y anidados; cualquier otro carácter se ignora. Ejemplos: {[a+(b-c)]*d}→true, ([)]→false, (((→false, a)→false. Di la complejidad.
🔍 Cómo lo reconozcoPalabra clave: «equilibrados/anidados» con símbolos que abren y cierran. Es el problema clásico de pila: lo último que abre es lo primero que debe cerrar (LIFO).
🧭 Estrategia y pasos a seguir1) Crea Deque<Character> pila = new ArrayDeque<>(). 2) Recorre la cadena carácter a carácter. 3) Si es aperturapush. 4) Si es cierre → si la pila está vacía → false (cierre sin apertura); si no, pop y comprueba que casa con ese cierre. 5) Otros caracteres: ignorar. 6) Al final: return pila.isEmpty() (true solo si no quedaron aperturas).
💡 Por quéLa pila modela el anidamiento LIFO exactamente: en su cima siempre está el último abierto, que es justo el que debe casar con el próximo cierre. Complejidad O(n) en tiempo (una pasada, push/pop son O(1)) y O(n) de memoria en el peor caso.
✍️ Ahora pícalo túRellena el esqueleto:
public static boolean equilibrada(String expr){
    Deque<Character> pila = new ArrayDeque<>();
    for (int i = 0; i < expr.length(); i++){
        char c = expr.charAt(i);
        if (c=='(' || c=='[' || c=='{')  pila.____(c);   // apilar
        else if (c==')' || c==']' || c=='}'){
            if (pila.isEmpty()) return _____;             // cierre sin apertura
            char tope = pila.____();                      // desapilar
            if ( (c==')' && tope!='(') || ... ) return false; // no casan
        }
    }
    return pila._______();   // vacia = equilibrada
}
💊 Píldora de examenPatrón «emparejar»: cuando veas abrir/cerrar, anidamiento, deshacerpila. Truco Java: usa Deque<Character> con ArrayDeque (más moderno que Stack), y push/pop/isEmpty. No olvides el return final con isEmpty(): sin él, ((( daría true por error.
Solución comprobada (compilada con OpenJDK)
import java.util.Deque;
import java.util.ArrayDeque;

public static boolean equilibrada(String expr) {
    Deque<Character> pila = new ArrayDeque<>();
    for (int i = 0; i < expr.length(); i++) {
        char c = expr.charAt(i);
        if (c == '(' || c == '[' || c == '{') {
            pila.push(c);                          // apertura -> apilar
        } else if (c == ')' || c == ']' || c == '}') {
            if (pila.isEmpty()) return false;      // cierre sin apertura
            char tope = pila.pop();
            if ((c == ')' && tope != '(') ||
                (c == ']' && tope != '[') ||
                (c == '}' && tope != '{')) {
                return false;                      // no casan (cruzados)
            }
        }
        // cualquier otro caracter se ignora
    }
    return pila.isEmpty();   // true solo si NO quedan aperturas
}

Por qué una pila: el último símbolo abierto debe ser el primero en cerrarse (anidamiento LIFO); la pila guarda ese «último abierto» en su cima. Complejidad O(n) (una pasada; push/pop O(1)).

Verificado con OpenJDK: {[a+(b-c)]*d}true, ([)]false, (((false, a)false. ✔

Lista doble · Código Java4. Lista doblemente enlazada

Gestiona un historial como lista doblemente enlazada de enteros (cada Nodo tiene dato, siguiente y anterior). Implementa en ListaDoble (con primero y ultimo): a) insertarFinal(int), b) insertarPrincipio(int), c) eliminar(int) (devuelve boolean), d) imprimirAlReves() y explica por qué aquí es eficiente.
🔍 Cómo lo reconozcoPalabras clave: «doblemente enlazada», siguiente y anterior. Todo consiste en recolocar punteros con cuidado y tratar los casos borde: lista vacía, un solo nodo, borrar el primero o el último.
🧭 Estrategia y pasos a seguirRegla de oro: en una lista doble, cada inserción/borrado toca dos punteros por nodo afectado (siguiente y anterior) y hay que mantener primero y ultimo.
insertarFinal: si vacía → primero=ultimo=nuevo; si no → enlaza nuevo↔ultimo y actualiza ultimo.
insertarPrincipio: simétrico con primero.
eliminar: busca el nodo; reconecta su anterior y su siguiente (si alguno es null, actualiza primero/ultimo).
imprimirAlReves: parte de ultimo y avanza con anterior.
💡 Por quéLa ventaja de la doble enlazada es el puntero anterior: recorrer hacia atrás es O(n) directo desde ultimo. En una lista simple no existe anterior, así que imprimir al revés obligaría a pila/recursión auxiliar o a recorrerla n veces (O(n²)).
✍️ Ahora pícalo túPiensa los enlaces dibujando flechas. Esqueleto de eliminar (el más delicado):
public boolean eliminar(int valor){
    Nodo actual = primero;
    while (actual != null){
        if (actual.dato == valor){
            if (actual.anterior != null) actual.anterior.siguiente = ________;
            else primero = ________;              // era el primero
            if (actual.siguiente != null) actual.siguiente.anterior = ________;
            else ultimo = ________;               // era el ultimo
            return true;
        }
        actual = actual.siguiente;
    }
    return false;
}
💊 Píldora de examenPatrón punteros: al insertar, enlaza primero el nodo nuevo hacia sus vecinos y luego los vecinos hacia el nuevo (si lo haces al revés puedes perder una referencia). Al borrar, siempre pregunta if (vecino != null) antes de tocarlo, porque en los extremos uno de los vecinos es null y ahí se actualiza primero o ultimo.
Solución comprobada (compilada con OpenJDK)
public class ListaDoble {
    private Nodo primero, ultimo;

    // a) insertar al final
    public void insertarFinal(int valor){
        Nodo nuevo = new Nodo(valor);
        if (primero == null){ primero = nuevo; ultimo = nuevo; return; }
        nuevo.anterior = ultimo;
        ultimo.siguiente = nuevo;
        ultimo = nuevo;
    }
    // b) insertar al principio
    public void insertarPrincipio(int valor){
        Nodo nuevo = new Nodo(valor);
        if (primero == null){ primero = nuevo; ultimo = nuevo; return; }
        nuevo.siguiente = primero;
        primero.anterior = nuevo;
        primero = nuevo;
    }
    // c) eliminar el primero cuyo dato coincida
    public boolean eliminar(int valor){
        Nodo actual = primero;
        while (actual != null){
            if (actual.dato == valor){
                if (actual.anterior != null) actual.anterior.siguiente = actual.siguiente;
                else primero = actual.siguiente;          // era el primero
                if (actual.siguiente != null) actual.siguiente.anterior = actual.anterior;
                else ultimo = actual.anterior;            // era el ultimo
                return true;
            }
            actual = actual.siguiente;
        }
        return false;
    }
    // d) imprimir del ultimo al primero
    public void imprimirAlReves(){
        Nodo actual = ultimo;
        while (actual != null){
            System.out.print(actual.dato + " ");
            actual = actual.anterior;     // retrocedo gracias a 'anterior'
        }
        System.out.println();
    }
}

d) Por qué es eficiente: como cada nodo guarda anterior y tenemos ultimo, recorrer hacia atrás es O(n) directo. En una lista simple habría que usar recursión/pila o recorrerla varias veces (O(n²)). Esa es la ventaja principal de la doble enlazada.

Verificado con OpenJDK (insertar 10,20,30 + principio 5; reverso da 30 20 10 5; borrar 20, borrar primero 5 y último 30 recolocan bien primero/ultimo). ✔

Ordenación · Quicksort5. Quicksort: implementación + traza

a) Implementa quickSort(int[] a, int ini, int fin) y particionar(int[] a, int ini, int fin) tomando como pivote el primer elemento. b) Traza sobre 40 15 55 10 30 60 20. c) Complejidad en caso medio y peor, y qué provoca el peor caso con este pivote.
🔍 Cómo lo reconozcoPalabra clave: Quicksort + «pivote = primer elemento» + traza. Es ordenación divide y vencerás recursiva: particionar deja menores a la izquierda y mayores a la derecha, y luego se ordena cada mitad.
🧭 Estrategia y pasos a seguir1) quickSort: caso base if (ini >= fin) return; (0 o 1 elemento). Llama a particionar, que devuelve la posición p del pivote, y recursiona en [ini,p-1] y [p+1,fin]. 2) particionar: pivote=a[ini]; con un índice i que marca la frontera de menores, recorre j de ini+1 a fin; cada vez que a[j] < pivote, incrementa i e intercambia. Al final intercambia pivote con a[i] y devuelve i.
💡 Por quéEn el caso medio cada partición divide en dos mitades similares → log n niveles × O(n) de trabajo = O(n log n). En el peor caso con pivote=primero, si el array ya está ordenado (o al revés) el pivote es siempre el mínimo/máximo: una partición queda vacía y la otra con n−1 → n niveles → O(n²).
✍️ Ahora pícalo túEsqueleto de particionar (lo más difícil):
public static int particionar(int[] a, int ini, int fin){
    int pivote = a[ini];
    int i = ini;                       // frontera de la zona de menores
    for (int j = ini + 1; j <= fin; j++){
        if (a[j] < pivote){            // a[j] es menor -> a la zona de menores
            i++;
            intercambiar(a, i, j);
        }
    }
    intercambiar(a, ini, i);           // pivote a su sitio definitivo
    return i;
}
💊 Píldora de examenPatrón Quicksort: recuerda el caso base ini >= fin (si lo olvidas, recursión infinita) y que particionar devuelve la posición final del pivote. Peligro clásico en examen: preguntan el peor caso → responde «array ya ordenado con pivote primer elemento → O(n²)». Mergesort es O(n log n) siempre; Quicksort no.
Solución comprobada (compilada con OpenJDK)
public static void quickSort(int[] a, int ini, int fin){
    if (ini >= fin) return;                 // CASO BASE
    int p = particionar(a, ini, fin);       // pivote a su sitio
    quickSort(a, ini, p - 1);               // ordena menores
    quickSort(a, p + 1, fin);               // ordena mayores
}
public static int particionar(int[] a, int ini, int fin){
    int pivote = a[ini];
    int i = ini;
    for (int j = ini + 1; j <= fin; j++)
        if (a[j] < pivote){ i++; intercambiar(a, i, j); }
    intercambiar(a, ini, i);
    return i;
}
private static void intercambiar(int[] a, int x, int y){
    int tmp = a[x]; a[x] = a[y]; a[y] = tmp;
}

b) Traza sobre [40,15,55,10,30,60,20] (pivote = primer elemento de cada trozo):

[40 15 55 10 30 60 20]  pivote=40 -> [20 15 10 30] 40 [60 55]
  [20 15 10 30]         pivote=20 -> [10 15] 20 [30]
    [10 15]             pivote=10 -> 10 [15]         ([15] base)
    [30]  base
  [60 55]               pivote=60 -> [55] 60          ([55] base)

RESULTADO: [10 15 20 30 40 55 60]

c) Complejidad: caso medio O(n log n); peor caso O(n²). El peor caso ocurre cuando el array ya está ordenado (o en orden inverso): el pivote es siempre el mínimo/máximo, una partición queda vacía y la otra con n−1 elementos → n niveles → 1+2+...+n = O(n²).

Verificado con OpenJDK: el array queda [10, 15, 20, 30, 40, 55, 60]. ✔

Árbol ABB · Recursividad6. Árbol Binario de Búsqueda

Con Nodo (dato, izquierdo, derecho) y ArbolABB (raiz): a) insertar(int) recursivo (duplicados se ignoran), b) contiene(int) recursivo aprovechando el orden (complejidad equilibrado/degenerado), c) inorden() y qué propiedad tiene su salida, d) contarHojas(), e) inserta 50 30 70 20 40 60 80 y da inorden y preorden.
🔍 Cómo lo reconozcoPalabra clave: ABB / recursividad. Propiedad clave del ABB: menores a la izquierda, mayores a la derecha. Casi todo se resuelve con el esquema recursivo estándar: método público que llama a uno privado con el nodo raiz.
🧭 Estrategia y pasos a seguirPatrón «wrapper público + recursivo privado»:
insertar: recursivo que devuelve Nodo; si el nodo es null crea uno nuevo; si valor<dato baja a la izquierda, si valor>dato a la derecha, si igual no hace nada (duplicado).
contiene: si null→false; si igual→true; si menor→solo rama izquierda; si mayor→solo derecha (no recorre las dos).
inorden: izquierda → procesar → derecha.
contarHojas: null→0; sin hijos→1; si no → suma de las dos ramas.
💡 Por quécontiene descarta media estructura en cada paso → O(log n) si el árbol está equilibrado, pero O(n) si está degenerado (insertado en orden, forma de lista). El inorden de un ABB devuelve los valores ordenados de menor a mayor: por definición, a la izquierda está todo lo menor y a la derecha lo mayor.
✍️ Ahora pícalo túRellena el esquema recursivo de insertar y contiene:
private Nodo insertar(Nodo actual, int valor){
    if (actual == null) return ______________;   // hueco -> nuevo nodo
    if (valor < actual.dato)  actual.izquierdo = insertar(actual.izquierdo, valor);
    else if (valor > actual.dato) actual.derecho = insertar(________, valor);
    return actual;                                 // igual = duplicado, no inserta
}
private boolean contiene(Nodo actual, int valor){
    if (actual == null) return ______;             // caso base: no esta
    if (valor == actual.dato) return true;
    return valor < actual.dato ? contiene(actual.izquierdo, valor)
                               : contiene(________, valor);
}
💊 Píldora de examenPatrón recursivo en árboles: caso base = nodo null, y en el caso recursivo reasigna (actual.izquierdo = insertar(actual.izquierdo, v)) para que los enlaces se conserven. Recorridos: inorden (Izq-Raíz-Der) → ordenado; preorden (Raíz-Izq-Der) → sirve para copiar el árbol; postorden (Izq-Der-Raíz) → para liberar/borrar.
Solución comprobada (compilada con OpenJDK)
public class ArbolABB {
    private Nodo raiz;
    public void insertar(int valor){ raiz = insertar(raiz, valor); }
    private Nodo insertar(Nodo actual, int valor){
        if (actual == null) return new Nodo(valor);
        if (valor < actual.dato)      actual.izquierdo = insertar(actual.izquierdo, valor);
        else if (valor > actual.dato) actual.derecho   = insertar(actual.derecho, valor);
        return actual;                     // valor == dato -> duplicado, se ignora
    }
    public boolean contiene(int valor){ return contiene(raiz, valor); }
    private boolean contiene(Nodo actual, int valor){
        if (actual == null) return false;
        if (valor == actual.dato) return true;
        if (valor < actual.dato) return contiene(actual.izquierdo, valor);
        else                     return contiene(actual.derecho, valor);
    }
    public void inorden(){ inorden(raiz); System.out.println(); }
    private void inorden(Nodo actual){
        if (actual == null) return;
        inorden(actual.izquierdo);            // 1) izquierda
        System.out.print(actual.dato + " ");  // 2) raiz (EN MEDIO)
        inorden(actual.derecho);              // 3) derecha
    }
    public int contarHojas(){ return contarHojas(raiz); }
    private int contarHojas(Nodo actual){
        if (actual == null) return 0;
        if (actual.izquierdo == null && actual.derecho == null) return 1;
        return contarHojas(actual.izquierdo) + contarHojas(actual.derecho);
    }
}

b) Complejidad de contiene: equilibrado O(log n); degenerado (forma de lista) O(n). c) Propiedad del inorden: devuelve los valores ordenados de menor a mayor.

e) Insertando 50 30 70 20 40 60 80:

          50
       /      \
     30        70
    /  \      /  \
   20   40   60   80

Inorden (Izq-Raiz-Der):  20 30 40 50 60 70 80
Preorden (Raiz-Izq-Der): 50 30 20 40 70 60 80

Verificado con OpenJDK: inorden = 20 30 40 50 60 70 80, preorden = 50 30 20 40 70 60 80, hojas = 4 (20,40,60,80). ✔

Streams · Código Java7. Procesar una lista de objetos

Con Producto (nombre:String, precio:double, con getters) y un ArrayList<Producto> inventario: a) con bucles, precio medio (protege lista vacía); b) con bucles, el Producto más caro (devuelve el objeto); c) con streams, nombres de productos que cuestan >20 €; d) un main que cree 3 productos y pruebe los métodos.
🔍 Cómo lo reconozcoEs un ejercicio de recorrido de colecciones: parte con for-each (bucles) y parte con la API de streams. Señales: «precio medio»/«más caro» → acumular/comparar; «lista de nombres que cumplen X» → filter+map+collect.
🧭 Estrategia y pasos a seguir1) precioMedio: si inv.isEmpty() devuelve 0; si no, acumula precios en un for y divide por inv.size(). 2) masCaro: si vacía devuelve null; empieza con inv.get(0) y compara precios, devolviendo el objeto (no el precio). 3) nombresCaros: inv.stream().filter(p->p.getPrecio()>20).map(Producto::getNombre).collect(Collectors.toList()).
💡 Por quéProteger la lista vacía evita dividir por 0 o hacer get(0) sobre vacío. El patrón de streams (filtermapcollect) es declarativo: filtras por condición, transformas (objeto→nombre) y recoges en una lista. Devolver el objeto Producto (no su precio) es lo que pide el enunciado.
✍️ Ahora pícalo túEsqueleto de los tres métodos:
public static double precioMedio(List<Producto> inv){
    if (inv._________()) return 0;             // proteccion
    double suma = 0;
    for (Producto p : inv) suma += p.getPrecio();
    return suma / inv.______();
}
public static Producto masCaro(List<Producto> inv){
    if (inv.isEmpty()) return null;
    Producto mejor = inv.get(0);
    for (Producto p : inv)
        if (p.getPrecio() > mejor.getPrecio()) mejor = ___;
    return mejor;                              // devuelve el OBJETO
}
public static List<String> nombresCaros(List<Producto> inv){
    return inv.stream()
              .filter(p -> p.getPrecio() > 20)
              .map(Producto::getNombre)
              .collect(Collectors.toList());
}
💊 Píldora de examenPatrón streams: stream()filter(condición)map(transformar)collect(Collectors.toList()). Errores típicos: olvidar proteger la lista vacía, devolver el precio en vez del objeto en masCaro, y confundir filter (quita elementos) con map (transforma). Referencia a método: Producto::getNombre.
Solución comprobada (compilada con OpenJDK)
import java.util.*;
import java.util.stream.Collectors;

class Producto {
    private String nombre; private double precio;
    public Producto(String nombre, double precio){ this.nombre=nombre; this.precio=precio; }
    public String getNombre(){ return nombre; }
    public double getPrecio(){ return precio; }
    public String toString(){ return nombre + " (" + precio + "€)"; }
}
public class GestionInventario {
    public static double precioMedio(List<Producto> inv){
        if (inv.isEmpty()) return 0;
        double suma = 0;
        for (Producto p : inv) suma += p.getPrecio();
        return suma / inv.size();
    }
    public static Producto masCaro(List<Producto> inv){
        if (inv.isEmpty()) return null;
        Producto mejor = inv.get(0);
        for (Producto p : inv)
            if (p.getPrecio() > mejor.getPrecio()) mejor = p;
        return mejor;                      // devuelve el OBJETO
    }
    public static List<String> nombresCaros(List<Producto> inv){
        return inv.stream()
                  .filter(p -> p.getPrecio() > 20)
                  .map(Producto::getNombre)
                  .collect(Collectors.toList());
    }
    public static void main(String[] args){
        List<Producto> inv = new ArrayList<>();
        inv.add(new Producto("Teclado", 18.0));
        inv.add(new Producto("Monitor", 120.0));
        inv.add(new Producto("Raton", 25.5));
        System.out.println("Precio medio: " + precioMedio(inv));  // 54.5
        System.out.println("Mas caro: " + masCaro(inv));          // Monitor (120.0EUR)
        System.out.println("Caros (>20): " + nombresCaros(inv));  // [Monitor, Raton]
    }
}

Salida verificada con OpenJDK: Precio medio: 54.5 · Mas caro: Monitor (120.0€) · Caros (>20): [Monitor, Raton]. (Media = (18+120+25.5)/3 = 163.5/3 = 54.5.) ✔

Examen 2 · Modelo B · 5 ejercicios

TAD Cola (interfaz + array circular), mergesort, complejidad + búsqueda binaria, pila enlazada y recursividad.

TAD · Abstracción1. Diseño de un TAD Cola (interfaz + implementación)

Modela el TAD Cola (FIFO). a) Interfaz genérica Cola<T> con encolar, desencolar, frente, estaVacia, tamano. b) Clase ColaArray<T> con array circular (índices frente/fin/n) que redimensiona al llenarse. c) desencolar/frente lanzan NoSuchElementException si vacía. d) main declarando Cola<String> c = new ColaArray<>(3), encolando 4 (fuerza redimensión) y probando FIFO. e) Ventaja de programar contra la interfaz y por qué el array circular da O(1).
🔍 Cómo lo reconozcoPalabras clave: TAD, interfaz + implementación, FIFO, array circular. Es el ejercicio de abstracción: separar el contrato (interface) de una implementación concreta (class ... implements).
🧭 Estrategia y pasos a seguir1) Define la interface Cola<T> con las 5 firmas. 2) ColaArray<T>: campos Object[] datos, frente, fin, n. 3) Circular: al avanzar usa (indice+1) % datos.length. 4) encolar: si n == length redimensiona (×2) recolocando en orden lógico; luego coloca en fin y avanza. 5) desencolar/frente: si vacía → throw new NoSuchElementException. 6) En main declara la variable con el tipo de la interfaz.
💡 Por quéProgramar contra la interfaz desacopla al cliente de la implementación: mañana cambias ColaArray por ColaEnlazada sin tocar el main (polimorfismo, testeo, mantenimiento). El array circular da O(1) amortizado: encolar/desencolar solo mueven un índice; una cola sobre array «normal» que saque por la posición 0 desplazaría todo (O(n)).
✍️ Ahora pícalo túLo más importante: la interfaz y el avance circular.
public interface Cola<T> {
    void    encolar(T e);
    T       desencolar();
    T       frente();
    boolean estaVacia();
    int     tamano();
}
// dentro de ColaArray: avance CIRCULAR
public void encolar(T e){
    if (n == datos.length) redimensionar(datos.length * 2);
    datos[fin] = e;
    fin = (fin + 1) % datos.length;   // <-- clave circular
    n++;
}
💊 Píldora de examenPatrón TAD: siempre interfaz genérica (<T>) + clase que la implements, y declara las variables con el tipo de la interfaz. Truco del array circular: usa el módulo % length en vez de ++, así reutilizas los huecos liberados sin desplazar. Excepción de colecciones vacías: NoSuchElementException.
Solución comprobada (compilada con OpenJDK)
public interface Cola<T> {
    void encolar(T e);
    T desencolar();
    T frente();
    boolean estaVacia();
    int tamano();
}

import java.util.NoSuchElementException;
public class ColaArray<T> implements Cola<T> {
    private Object[] datos;
    private int frente, fin, n;
    public ColaArray(int capacidad){
        if (capacidad <= 0) capacidad = 8;
        datos = new Object[capacidad]; frente = 0; fin = 0; n = 0;
    }
    public void encolar(T e){
        if (n == datos.length) redimensionar(datos.length * 2);
        datos[fin] = e;
        fin = (fin + 1) % datos.length;      // avance circular
        n++;
    }
    @SuppressWarnings("unchecked")
    public T desencolar(){
        if (estaVacia()) throw new NoSuchElementException("Cola vacia");
        T e = (T) datos[frente];
        datos[frente] = null;                // ayuda al recolector
        frente = (frente + 1) % datos.length;
        n--;
        return e;
    }
    @SuppressWarnings("unchecked")
    public T frente(){
        if (estaVacia()) throw new NoSuchElementException("Cola vacia");
        return (T) datos[frente];
    }
    public boolean estaVacia(){ return n == 0; }
    public int tamano(){ return n; }
    private void redimensionar(int nueva){
        Object[] nuevo = new Object[nueva];
        for (int i = 0; i < n; i++)
            nuevo[i] = datos[(frente + i) % datos.length];   // recoloca en orden logico
        datos = nuevo; frente = 0; fin = n;
    }
    public static void main(String[] args){
        Cola<String> c = new ColaArray<>(3);      // tipo = interfaz
        c.encolar("A"); c.encolar("B");
        c.encolar("C"); c.encolar("D");           // el 4o fuerza redimension
        System.out.println("frente = " + c.frente() + ", tamano = " + c.tamano());
        System.out.print("Saliendo (FIFO): ");
        while (!c.estaVacia()) System.out.print(c.desencolar() + " ");
        System.out.println();
    }
}

Salida verificada con OpenJDK: frente = A, tamano = 4 y Saliendo (FIFO): A B C D (sale primero la A: FIFO correcto, la redimensión conservó el orden). Además comprobado que desencolar sobre vacía lanza NoSuchElementException. ✔

e) Programar contra la interfaz → bajo acoplamiento y polimorfismo (cambiar la implementación sin tocar al cliente). Array circular → O(1) amortizado (solo se mueve un índice; la redimensión O(n) ocasional se reparte).

Ordenación · Mergesort2. Mergesort: implementación + traza

a) Implementa mergeSort(int[] a, int ini, int fin) y mezclar(int[] a, int ini, int medio, int fin). b) Traza sobre 40 15 55 10 30 60 20 (árbol de divisiones + mezclas de abajo a arriba). c) Complejidad en mejor/medio/peor + espacial; por qué es O(n log n) SIEMPRE y qué significa que sea estable.
🔍 Cómo lo reconozcoPalabra clave: Mergesort (ordenación por mezcla). Es divide y vencerás: parte por la mitad, ordena cada mitad recursivamente y mezcla las dos mitades ya ordenadas usando un array auxiliar.
🧭 Estrategia y pasos a seguir1) mergeSort: caso base if (ini >= fin) return;. Calcula medio=(ini+fin)/2, recursiona en [ini,medio] y [medio+1,fin], y llama a mezclar. 2) mezclar: crea aux de tamaño fin-ini+1; con dos punteros i (mitad izq) y j (mitad der) copia el menor a aux; vacía los restos; vuelca aux al array original.
💡 Por quéMergesort siempre parte exactamente por la mitad, así que la altura del árbol de recursión es siempre log n y en cada nivel la mezcla recorre n elementos → O(n log n) en mejor, medio y peor caso (a diferencia de Quicksort, que degenera a O(n²)). Es estable (mantiene el orden relativo de iguales) gracias a la condición a[i] <= a[j]. Coste espacial O(n) por el array auxiliar.
✍️ Ahora pícalo túRellena la mezcla (el corazón del algoritmo):
public static void mezclar(int[] a, int ini, int medio, int fin){
    int[] aux = new int[fin - ini + 1];
    int i = ini, j = medio + 1, k = 0;
    while (i <= medio && j <= fin){
        if (a[i] <= a[j]) aux[k++] = a[i++];   // "<=" -> ESTABLE
        else              aux[k++] = a[j++];
    }
    while (i <= medio) aux[k++] = a[i++];      // resto izquierda
    while (j <= fin)   aux[k++] = a[j++];      // resto derecha
    for (int p = 0; p < aux.length; p++)
        a[ini + p] = aux[p];                   // vuelca al original
}
💊 Píldora de examenQuicksort vs Mergesort en examen: Mergesort = O(n log n) siempre, estable, pero usa O(n) de memoria extra (no in-place). Quicksort = O(n log n) medio pero O(n²) peor, in-place. La estabilidad se garantiza con <= en la mezcla (ante empate coge antes el de la izquierda). El caso base ini >= fin nunca falta.
Solución comprobada (compilada con OpenJDK)
public static void mergeSort(int[] a, int ini, int fin){
    if (ini >= fin) return;               // CASO BASE: 0 o 1 elemento
    int medio = (ini + fin) / 2;
    mergeSort(a, ini, medio);             // mitad izquierda
    mergeSort(a, medio + 1, fin);         // mitad derecha
    mezclar(a, ini, medio, fin);          // fusiona las dos mitades
}
public static void mezclar(int[] a, int ini, int medio, int fin){
    int[] aux = new int[fin - ini + 1];
    int i = ini, j = medio + 1, k = 0;
    while (i <= medio && j <= fin){
        if (a[i] <= a[j]) aux[k++] = a[i++];   // "<=" hace el algoritmo ESTABLE
        else              aux[k++] = a[j++];
    }
    while (i <= medio) aux[k++] = a[i++];
    while (j <= fin)   aux[k++] = a[j++];
    for (int p = 0; p < aux.length; p++)
        a[ini + p] = aux[p];
}

b) Traza sobre [40,15,55,10,30,60,20]. División (medio=(0+6)/2=3):

[40 15 55 10 30 60 20]  -> [40 15 55 10] y [30 60 20]
 [40 15 55 10] -> [40 15] y [55 10]  -> [40][15] [55][10]
 [30 60 20]    -> [30 60] y [20]     -> [30][60] [20]

Mezclas (de abajo a arriba), estado del array completo:
 [0..1] 40,15 -> [15,40]           15 40 55 10 30 60 20
 [2..3] 55,10 -> [10,55]           15 40 10 55 30 60 20
 [0..3] mezcla -> [10,15,40,55]    10 15 40 55 30 60 20
 [4..5] 30,60 -> [30,60]           10 15 40 55 30 60 20
 [4..6] mezcla -> [20,30,60]       10 15 40 55 20 30 60
 [0..6] mezcla -> [10..60]         10 15 20 30 40 55 60

c) Complejidad: O(n log n) en mejor, medio y peor caso; espacial O(n) (array auxiliar). Siempre O(n log n) porque parte exactamente por la mitad (altura log n independiente de los datos). Estable: iguales conservan su orden relativo, garantizado por a[i] <= a[j].

Verificado con OpenJDK: resultado [10, 15, 20, 30, 40, 55, 60]. ✔

Complejidad + código3. Big-O + completar búsqueda binaria + sumaFilas

a) Justifica el Big-O de tres métodos (dos bucles seguidos; un bucle i=i/2; doble bucle con j<i). b) Completa los huecos de busquedaBinaria sobre array ordenado y di su complejidad. c) Escribe int[] sumaFilas(int[][] m) para matriz n×n con su complejidad.
🔍 Cómo lo reconozcoMezcla de análisis Big-O y completar/escribir código. En (a) cuenta bucles; en (b) reconoce la búsqueda binaria (mitad cada vez → O(log n)); en (c) una matriz n×n con doble bucle es O(n²).
🧭 Estrategia y pasos a seguir(a) f1: bucle simple O(n) + doble anidado O(n²) → O(n²). f2: i=i/2 desde n → log₂n vueltas → O(log n). f3: interno j<i → suma 0+1+...+(n-1)=n(n-1)/2 → O(n²). (b) huecos: ini <= fin / return medio / ini = medio+1 / fin = medio-1. (c) doble bucle sobre las n×n celdas → O(n²).
💡 Por quéLa búsqueda binaria descarta la mitad del rango en cada iteración: de n elementos, tras k pasos quedan n/2ⁿ, y termina cuando n/2ⁿ≈1, o sea k≈log₂n → O(log n) (requiere array ordenado). sumaFilas visita cada una de las n² celdas una vez → O(n²), que es óptimo (hay que mirarlas todas).
✍️ Ahora pícalo túCompleta la búsqueda binaria:
public static int busquedaBinaria(int[] a, int clave){
    int ini = 0, fin = a.length - 1;
    while (ini ___ fin){                    // (1)
        int medio = ini + (fin - ini) / 2;
        if (a[medio] == clave) return ___;      // (2)
        else if (a[medio] < clave) ini = ___;   // (3)  descarta mitad izquierda
        else fin = ___;                         // (4)  descarta mitad derecha
    }
    return -1;
}
💊 Píldora de examenChuleta: búsqueda binaria = O(log n) (mitad cada vez, array ordenado); búsqueda lineal = O(n) (sin ordenar). Recorrer una matriz n×n siempre es O(n²). Huecos típicos de la binaria: condición ini <= fin (con <=, no <), y actualizar medio+1/medio-1 (no medio, o bucle infinito).
Solución comprobada (compilada con OpenJDK)

a) f1 → O(n²) (O(n) + doble bucle n·n). f2 → O(log n) (i=i/2 desde n hasta 0 → log₂n vueltas). f3 → O(n²) (interno j<i → 0+1+...+(n-1)=n(n-1)/2).

b) Búsqueda binaria completada → O(log n):

public static int busquedaBinaria(int[] a, int clave){
    int ini = 0, fin = a.length - 1;
    while (ini <= fin){                        // (1) <=
        int medio = ini + (fin - ini) / 2;
        if (a[medio] == clave) return medio;       // (2) medio
        else if (a[medio] < clave) ini = medio + 1;// (3) medio + 1
        else fin = medio - 1;                      // (4) medio - 1
    }
    return -1;
}

c) sumaFilas → O(n²):

public static int[] sumaFilas(int[][] m){
    int n = m.length;
    int[] r = new int[n];
    for (int i = 0; i < n; i++){        // n filas
        int s = 0;
        for (int j = 0; j < n; j++)     // n columnas por fila
            s += m[i][j];
        r[i] = s;
    }
    return r;
}

Verificado con OpenJDK: sobre [2,5,8,12,16,23,38,56,72,91], busquedaBinaria(a,23)=5 y busquedaBinaria(a,17)=-1; sumaFilas({{1,2,3},{4,5,6},{7,8,9}})=[6,15,24]. ✔

Pila enlazada · LIFO4. Pila con lista enlazada simple

Implementa una Pila (LIFO) con lista enlazada simple. Con NodoP (dato, siguiente) y PilaEnlazada (cima, n): a) apilar(int) por la cabeza (O(1)), b) desapilar() (lanza excepción si vacía), c) cima(), d) estaVacia() y tamano(), e) static String invertir(String) usando la pila (aplica a «Java»). f) Por qué es O(1) y por qué la pila sirve para deshacer/invertir.
🔍 Cómo lo reconozcoPalabra clave: Pila (LIFO) con lista enlazada (no array). Todo gira en torno a una única referencia cima que apunta al tope; insertar y sacar se hacen siempre por la cabeza.
🧭 Estrategia y pasos a seguir1) apilar: crea nodo, nuevo.siguiente = cima, cima = nuevo, n++ (insertar por la cabeza = O(1)). 2) desapilar: si cima==null lanza excepción; guarda cima.dato, avanza cima = cima.siguiente, n--. 3) cima: igual pero sin mover nada. 4) invertir: apila todos los char de la cadena y luego los desapila → salen al revés.
💡 Por quéInsertar/sacar por la cabeza es O(1) porque solo se crea/desconecta un nodo y se mueve la referencia cima: no hay que recorrer nada. La pila es natural para deshacer (undo) e invertir porque su orden LIFO devuelve los elementos en orden inverso al de inserción: lo último que entra es lo primero que sale.
✍️ Ahora pícalo túEsqueleto de apilar/desapilar:
public void apilar(int v){
    NodoP nuevo = new NodoP(v);
    nuevo.siguiente = ____;     // apunta al antiguo tope
    cima = ____;                // el nuevo es la cima
    n++;
}
public int desapilar(){
    if (cima == null) throw new RuntimeException("Pila vacia");
    int v = cima.dato;
    cima = cima._________;      // la cima pasa al siguiente
    n--;
    return v;
}
💊 Píldora de examenPila = LIFO: apila/desapila SIEMPRE por la cabeza → O(1). Casos de examen para pila: comprobar paréntesis, invertir una secuencia, deshacer (undo), evaluar expresiones, recorrer un árbol sin recursión. Truco de invertir: apilar los caracteres y desapilarlos a un StringBuilder.
Solución comprobada (compilada con OpenJDK)
class NodoP {
    int dato; NodoP siguiente;
    NodoP(int dato){ this.dato = dato; }
}
public class PilaEnlazada {
    private NodoP cima;   // tope
    private int n;
    public void apilar(int v){            // a) inserta por la CABEZA -> O(1)
        NodoP nuevo = new NodoP(v);
        nuevo.siguiente = cima;
        cima = nuevo;
        n++;
    }
    public int desapilar(){               // b)
        if (cima == null) throw new RuntimeException("Pila vacia");
        int v = cima.dato;
        cima = cima.siguiente;
        n--;
        return v;
    }
    public int cima(){                    // c)
        if (cima == null) throw new RuntimeException("Pila vacia");
        return cima.dato;
    }
    public boolean estaVacia(){ return cima == null; }   // d)
    public int tamano(){ return n; }                      // d)
    public static String invertir(String s){              // e)
        PilaEnlazada p = new PilaEnlazada();
        for (int i = 0; i < s.length(); i++) p.apilar(s.charAt(i));
        StringBuilder sb = new StringBuilder();
        while (!p.estaVacia()) sb.append((char) p.desapilar());
        return sb.toString();
    }
    public static void main(String[] args){
        PilaEnlazada p = new PilaEnlazada();
        p.apilar(1); p.apilar(2); p.apilar(3);
        System.out.print("LIFO: cima=" + p.cima() + " -> ");
        while (!p.estaVacia()) System.out.print(p.desapilar() + " ");
        System.out.println("
invertir("Java") = " + invertir("Java"));
    }
}

Salida verificada con OpenJDK: LIFO: cima=3 -> 3 2 1 (sale primero el último apilado) y invertir("Java") = avaJ. ✔

f) apilar/desapilar tocan solo la cabeza → O(1). La pila es natural para deshacer/invertir por su orden LIFO: lo último en entrar es lo primero en salir.

Recursividad5. Exponenciación rápida, suma de dígitos y palíndromo

Con recursividad (caso base + caso recursivo): a) long potencia(long base, int exp) por exponenciación rápida (O(log exp)); b) int sumaDigitos(int n); c) boolean esPalindromo(String s, int ini, int fin). d) Traza de potencia(2,10) y comprueba que da 1024.
🔍 Cómo lo reconozcoPalabra clave: recursividad. Cada apartado necesita un caso base (que corta) y un caso recursivo que se acerca a él. La exponenciación rápida es la clave: divide el exponente entre 2 en cada llamada.
🧭 Estrategia y pasos a seguira) potencia: caso base exp==0 → return 1; calcula mitad = potencia(base, exp/2) (una sola llamada) y devuelve mitad*mitad si exp es par o base*mitad*mitad si impar. b) sumaDigitos: caso base n<10 → return n; si no, n%10 + sumaDigitos(n/10). c) esPalindromo: caso base ini>=fin → true; si s.charAt(ini)!=s.charAt(fin) → false; si no, recursiona con ini+1, fin-1.
💡 Por quéLa exponenciación es O(log exp) porque en cada llamada el exponente se divide entre 2: solo hay log₂(exp) llamadas, no exp. Además reutiliza mitad (la calcula una vez y la eleva al cuadrado), evitando recalcular. La versión ingenua (multiplicar exp veces) sería O(exp).
✍️ Ahora pícalo túRellena la exponenciación rápida:
public static long potencia(long base, int exp){
    if (exp == 0) return ____;                    // CASO BASE
    long mitad = potencia(base, exp / ____);      // una sola llamada
    if (exp % 2 == 0) return mitad * mitad;       // exp par
    else              return base * mitad * mitad;// exp impar
}
💊 Píldora de examenEsquema recursivo universal: 1) caso base (lo más pequeño, sin recursión) + 2) caso recursivo que se acerca al base. Si divides el problema entre 2 → O(log n); si reduces de 1 en 1 → O(n); si haces dos llamadas → O(2ⁿ). Truco palíndromo: comparar extremos (ini, fin) y avanzar hacia el centro.
Solución comprobada (compilada con OpenJDK)
// a) exponenciacion rapida -> O(log exp)
public static long potencia(long base, int exp){
    if (exp == 0) return 1;                 // CASO BASE
    long mitad = potencia(base, exp / 2);   // una sola llamada recursiva
    if (exp % 2 == 0) return mitad * mitad;         // par
    else              return base * mitad * mitad;  // impar
}
// b) suma de digitos
public static int sumaDigitos(int n){
    n = Math.abs(n);
    if (n < 10) return n;                   // CASO BASE: un digito
    return n % 10 + sumaDigitos(n / 10);    // ultimo digito + resto
}
// c) palindromo por los extremos
public static boolean esPalindromo(String s, int ini, int fin){
    if (ini >= fin) return true;                  // CASO BASE: se cruzaron
    if (s.charAt(ini) != s.charAt(fin)) return false;
    return esPalindromo(s, ini + 1, fin - 1);     // avanza al centro
}
// llamada: esPalindromo("reconocer", 0, "reconocer".length()-1)

d) Traza de potencia(2,10) = 1024 (solo 5 llamadas, no 10):

potencia(2,10) par   -> m*m    con m=potencia(2,5)
potencia(2,5)  impar -> 2*m*m  con m=potencia(2,2)
potencia(2,2)  par   -> m*m    con m=potencia(2,1)
potencia(2,1)  impar -> 2*m*m  con m=potencia(2,0)
potencia(2,0)        -> 1      (caso base)

Vuelta:
 potencia(2,0)=1
 potencia(2,1)=2*1*1 = 2
 potencia(2,2)=2*2   = 4
 potencia(2,5)=2*4*4 = 32
 potencia(2,10)=32*32= 1024  <-

Verificado con OpenJDK: potencia(2,10)=1024, potencia(3,4)=81; sumaDigitos(9876)=30; esPalindromo("reconocer")=true, esPalindromo("java")=false. ✔

Examen 3 · Modelo C · 5 ejercicios

TAD Conjunto (lista enlazada), insertion sort, complejidad + búsqueda lineal, cola con dos pilas y recursividad (Hanói, Euclides, invertir).

TAD · Abstracción1. Diseño de un TAD Conjunto (interfaz + lista enlazada)

Modela el TAD Conjunto (sin duplicados, sin orden). a) Interfaz Conjunto<T>: anadir (true si NO estaba), contiene, eliminar, tamano, estaVacio. b) Clase ConjuntoLista<T> con lista enlazada simple y contador n. c) anadir no admite duplicados; comparar con equals (no ==); cuidar el caso de eliminar el primero. d) main contra la interfaz. e) Ventaja de la interfaz y complejidad de contiene/anadir.
🔍 Cómo lo reconozcoPalabras clave: TAD Conjunto, interfaz + implementación, lista enlazada, sin duplicados. Igual que la Cola del Examen 2 pero la operación clave es evitar duplicados (comprobar con equals antes de insertar).
🧭 Estrategia y pasos a seguir1) interface Conjunto<T> con las 5 firmas. 2) NodoC<T> (dato, siguiente) y ConjuntoLista con primero y n. 3) anadir: si contiene(e) devuelve false; si no, inserta por la cabeza (O(1)) y n++. 4) contiene: recorre comparando con .equals(). 5) eliminar: lleva ant y act; si el borrado es el primero (ant==null), actualiza primero.
💡 Por quéProgramar contra la interfaz Conjunto<String> c = new ConjuntoLista<>() desacopla al cliente: mañana cambias a ConjuntoHash sin tocar nada. En esta implementación con lista, contiene recorre hasta hallar o acabar → O(n); anadir llama a contiene (O(n)) + inserta (O(1)) → O(n); eliminar también O(n). Un HashSet daría O(1).
✍️ Ahora pícalo túRellena anadir y eliminar (ojo con equals y el primer nodo):
public boolean anadir(T e){
    if (________(e)) return false;      // ya estaba -> no duplicar
    NodoC<T> nuevo = new NodoC<>(e);
    nuevo.siguiente = primero;          // insercion por la cabeza
    primero = nuevo; n++;
    return true;
}
public boolean eliminar(T e){
    NodoC<T> act = primero, ant = null;
    while (act != null){
        if (act.dato.______(e)){        // comparar con equals
            if (ant == null) primero = act.siguiente;   // era el PRIMERO
            else ant.siguiente = act.siguiente;
            n--; return true;
        }
        ant = act; act = act.siguiente;
    }
    return false;
}
💊 Píldora de examenRegla: para comparar objetos (String, etc.) usa SIEMPRE .equals(), nunca == (que compara referencias). Patrón «borrar en lista simple»: lleva un puntero anterior además del actual, y trata aparte el caso de borrar la cabeza. Un Set se distingue de una List porque anadir comprueba duplicados primero.
Solución comprobada (compilada con OpenJDK)
public interface Conjunto<T> {
    boolean anadir(T e);     // true si NO estaba
    boolean contiene(T e);
    boolean eliminar(T e);   // true si estaba y se quito
    int     tamano();
    boolean estaVacio();
}

class NodoC<T> { T dato; NodoC<T> siguiente; NodoC(T dato){ this.dato = dato; } }

public class ConjuntoLista<T> implements Conjunto<T> {
    private NodoC<T> primero;
    private int n;
    public boolean anadir(T e){
        if (contiene(e)) return false;          // sin duplicados
        NodoC<T> nuevo = new NodoC<>(e);
        nuevo.siguiente = primero;              // insercion por la cabeza -> O(1)
        primero = nuevo; n++;
        return true;
    }
    public boolean contiene(T e){
        NodoC<T> act = primero;
        while (act != null){
            if (act.dato.equals(e)) return true;   // equals, no ==
            act = act.siguiente;
        }
        return false;
    }
    public boolean eliminar(T e){
        NodoC<T> act = primero, ant = null;
        while (act != null){
            if (act.dato.equals(e)){
                if (ant == null) primero = act.siguiente;   // era el PRIMERO
                else ant.siguiente = act.siguiente;
                n--; return true;
            }
            ant = act; act = act.siguiente;
        }
        return false;
    }
    public int tamano(){ return n; }
    public boolean estaVacio(){ return n == 0; }

    public static void main(String[] args){
        Conjunto<String> c = new ConjuntoLista<>();   // tipo = interfaz
        System.out.println(c.anadir("rojo"));    // true
        System.out.println(c.anadir("verde"));   // true
        System.out.println(c.anadir("rojo"));    // false (duplicado)
        System.out.println(c.contiene("verde")); // true
        System.out.println(c.tamano());          // 2
        System.out.println(c.eliminar("rojo"));  // true
        System.out.println(c.eliminar("azul"));  // false
        System.out.println(c.tamano());          // 1
        System.out.println(c.estaVacio());       // false
    }
}

Salida verificada con OpenJDK: true, true, false, true, 2, true, false, 1, false. ✔

e) La interfaz permite cambiar la implementación (p.ej. a HashSet) sin tocar al cliente. Complejidad con lista: contiene, anadir y eliminar son O(n) (hay que recorrer); un HashSet las haría O(1).

Ordenación · Insertion Sort2. Insertion Sort: implementación + traza

a) Implementa public static void insertionSort(int[] a) (ordena de menor a mayor). b) Traza sobre 29 10 14 37 13 25 (estado tras cada i de 1 a 5). c) Complejidad en mejor (ya ordenado), peor (al revés) y medio, complejidad espacial, y por qué es estable e in situ.
🔍 Cómo lo reconozcoPalabra clave: Insertion Sort (ordenación por inserción). Idea: la parte izquierda a[0..i-1] siempre está ya ordenada (invariante); tomas a[i] como clave y la insertas en su sitio corriendo a la derecha los mayores.
🧭 Estrategia y pasos a seguir1) Bucle externo i de 1 a a.length-1. 2) clave = a[i], j = i-1. 3) Mientras j>=0 && a[j]>clave: desplaza a[j+1]=a[j] y j--. 4) Coloca a[j+1]=clave. El invariante tras cada vuelta del externo: a[0..i] queda ordenado.
💡 Por quéMejor caso (ya ordenado): el while no entra nunca → 1 comparación por i → O(n). Peor caso (al revés): cada clave baja hasta el principio → 1+2+...+(n-1)=n(n-1)/2 → O(n²). Medio: O(n²). Espacial O(1) (ordena in situ). Es estable porque la condición es a[j] > clave (estrictamente): un igual no se desplaza y mantiene su orden.
✍️ Ahora pícalo túRellena el núcleo:
public static void insertionSort(int[] a){
    for (int i = 1; i < a.length; i++){
        int clave = a[i];
        int j = i - 1;
        while (j >= 0 && a[j] ___ clave){   // mayores que la clave
            a[j + 1] = a[j];                // desplaza a la derecha
            j--;
        }
        a[j + 1] = ______;                  // inserta la clave en su hueco
    }
}
💊 Píldora de examenInsertion Sort en examen: es O(n) en el mejor caso (array casi ordenado) → muy bueno para datos casi ordenados; O(n²) en el peor. Es estable e in-place (O(1) extra). Truco de la traza: en cada i, si la clave ya es mayor que todo lo ordenado, el while no hace nada (no se desplaza). El invariante es «a[0..i] ordenado».
Solución comprobada (compilada con OpenJDK)
public static void insertionSort(int[] a){
    for (int i = 1; i < a.length; i++){
        int clave = a[i];             // elemento a insertar
        int j = i - 1;
        while (j >= 0 && a[j] > clave){   // desplaza los mayores
            a[j + 1] = a[j];
            j--;
        }
        a[j + 1] = clave;             // hueco donde entra la clave
    }
}

b) Traza sobre [29,10,14,37,13,25] (estado al final de cada i):

inicial       -> [29, 10, 14, 37, 13, 25]
i=1 clave=10  -> [10, 29, 14, 37, 13, 25]
i=2 clave=14  -> [10, 14, 29, 37, 13, 25]
i=3 clave=37  -> [10, 14, 29, 37, 13, 25]   (37 ya es mayor, el while no entra)
i=4 clave=13  -> [10, 13, 14, 29, 37, 25]   (13 baja hasta la posicion 1)
i=5 clave=25  -> [10, 13, 14, 25, 29, 37]
final         -> [10, 13, 14, 25, 29, 37]

c) Complejidad: mejor (ya ordenado) O(n); peor (al revés) O(n²); medio O(n²); espacial O(1) (in situ). Estable por la condición a[j] > clave (los iguales no se mueven).

Traza verificada con OpenJDK (coincide exactamente con la tabla). ✔

Complejidad + código3. Big-O + completar búsqueda lineal + maxPorColumna

a) Justifica el Big-O de tres métodos (bucle i=i*3 con interno n; factorial recursivo; dos bloques con interno j=j/2). b) Completa busquedaLineal y di su complejidad en mejor/peor caso. c) Escribe int[] maxPorColumna(int[][] m) para matriz n×n con su complejidad.
🔍 Cómo lo reconozcoMezcla de Big-O y código. Señales: i=i*3 → log₃n vueltas; factorial recursivo simple → O(n); j=j/2 → log₂n; búsqueda lineal → O(n); recorrer matriz → O(n²).
🧭 Estrategia y pasos a seguir(a) f1: externo i*3 da log₃n vueltas × interno n → O(n log n). f2: factorial n*f2(n-1), n llamadas → O(n). f3: bloque1 O(n) + bloque2 (externo n × interno j/2 = log₂n) → O(n log n) dominante. (b) huecos: i < a.length / a[i] == clave / return i / return -1. (c) doble bucle columnas×filas → O(n²).
💡 Por quéUn bucle que multiplica (i*=3) o divide (j/=2) la variable da O(log n) (la base del log no cambia el orden). El factorial recursivo hace una llamada por nivel → O(n). La búsqueda lineal es O(1) en el mejor caso (está al principio) y O(n) en el peor (al final o no está), y NO necesita array ordenado. maxPorColumna mira las n² celdas → O(n²), óptimo.
✍️ Ahora pícalo túCompleta la búsqueda lineal:
public static int busquedaLineal(int[] a, int clave){
    for (int i = 0; i ___ a.length; i++){    // (1)
        if (a[i] ___ clave) return ___;      // (2) y (3)
    }
    return ___;                              // (4) no encontrado
}
💊 Píldora de examenChuleta: i*=k o i/=kO(log n); recursión simple que resta 1 (factorial) → O(n); búsqueda lineal → O(n) (sin ordenar) vs binaria → O(log n) (ordenado); matriz n×n → O(n²). En búsqueda lineal el mejor caso es O(1) (primer elemento).
Solución comprobada (compilada con OpenJDK)

a) f1 → O(n·log n) (externo i*3 → log₃n vueltas × interno n). f2 → O(n) (es el factorial: una llamada por nivel hasta n≤1). f3 → O(n·log n) (bloque1 O(n) + bloque2 n×log₂n; domina el segundo).

b) Búsqueda lineal completada:

public static int busquedaLineal(int[] a, int clave){
    for (int i = 0; i < a.length; i++){    // (1) <
        if (a[i] == clave) return i;       // (2) ==   (3) i
    }
    return -1;                             // (4) -1
}

Complejidad: mejor caso O(1) (clave en posición 0); peor caso O(n) (al final o no está). No requiere array ordenado.

c) maxPorColumna → O(n²):

public static int[] maxPorColumna(int[][] m){
    int n = m.length;
    int[] res = new int[n];
    for (int col = 0; col < n; col++){
        int max = m[0][col];               // primer elemento de la columna
        for (int fila = 1; fila < n; fila++)
            if (m[fila][col] > max) max = m[fila][col];
        res[col] = max;
    }
    return res;
}

Verificado con OpenJDK: con {{1,9,4},{7,2,8},{3,5,6}} devuelve [7, 9, 8]; f1(27)=81=3·27 y f1(81)=324=4·81 (confirma n·log₃n); fact(5)=120. ✔

Cola con 2 pilas · FIFO4. Cola implementada con dos pilas

Construye una cola (FIFO) usando dos pilas (ArrayDeque como pila: push/pop/peek/isEmpty). Con campos entrada y salida: a) encolar(T), b) desencolar() (lanza NoSuchElementException si vacía), c) frente(), d) estaVacia()/tamano(). Pista: el trasvase vuelca entrada a salida solo si salida está vacía. e) main que compruebe orden 1,2,3,4. f) Por qué es O(1) amortizado.
🔍 Cómo lo reconozcoPalabra clave: cola con dos pilas (FIFO a partir de LIFO). El truco es el trasvase: al pasar elementos de una pila a otra, el orden se invierte, y dos inversiones dan el orden FIFO.
🧭 Estrategia y pasos a seguir1) encolar: entrada.push(e). 2) trasvasar (privado): if (salida.isEmpty()) while(!entrada.isEmpty()) salida.push(entrada.pop()); — solo si salida está vacía. 3) desencolar: trasvasar(); si salida vacía → excepción; return salida.pop(). 4) frente: igual con peek(). 5) tamano = suma de tamaños.
💡 Por quéSi encolamos 1,2,3 quedan en entrada con el 3 arriba (LIFO). Al trasvasar (pop de entrada, push en salida) el orden se invierte: en salida el 1 queda arriba, y un pop devuelve el 1 (el más antiguo) → FIFO. La condición if (salida.isEmpty()) es esencial: si trasvasas con salida no vacía, rompes el orden. Amortizado O(1): cada elemento se mueve un número constante de veces.
✍️ Ahora pícalo túRellena el trasvase y desencolar:
private void trasvasar(){
    if (salida.isEmpty()){                    // SOLO si salida esta vacia
        while (!entrada.isEmpty())
            salida.push(entrada.____());      // vuelca invirtiendo el orden
    }
}
public T desencolar(){
    ___________();                            // asegura salida cargada
    if (salida.isEmpty()) throw new NoSuchElementException("cola vacia");
    return salida.____();                     // saca el mas antiguo
}
💊 Píldora de examenPatrón «FIFO con 2 pilas»: encolar siempre en entrada, desencolar/frente siempre desde salida, y trasvasar solo cuando salida está vacía. Doble inversión (push a entrada + trasvase) = orden original. Complejidad amortizada O(1) aunque un desencolar puntual sea O(n). Es una pregunta clásica de «construye X con Y».
Solución comprobada (compilada con OpenJDK)
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.NoSuchElementException;

public class ColaDosPilas<T> {
    private Deque<T> entrada = new ArrayDeque<>();   // se apila al encolar
    private Deque<T> salida  = new ArrayDeque<>();   // se saca al desencolar

    public void encolar(T e){ entrada.push(e); }     // a)

    private void trasvasar(){                          // SOLO si salida vacia
        if (salida.isEmpty()){
            while (!entrada.isEmpty())
                salida.push(entrada.pop());            // el mas antiguo queda arriba
        }
    }
    public T desencolar(){                             // b)
        trasvasar();
        if (salida.isEmpty()) throw new NoSuchElementException("cola vacia");
        return salida.pop();
    }
    public T frente(){                                 // c)
        trasvasar();
        if (salida.isEmpty()) throw new NoSuchElementException("cola vacia");
        return salida.peek();
    }
    public boolean estaVacia(){ return entrada.isEmpty() && salida.isEmpty(); }  // d)
    public int tamano(){ return entrada.size() + salida.size(); }               // d)

    public static void main(String[] args){            // e)
        ColaDosPilas<Integer> c = new ColaDosPilas<>();
        c.encolar(1); c.encolar(2); c.encolar(3);
        System.out.println(c.frente());     // 1
        System.out.println(c.desencolar()); // 1
        c.encolar(4);
        System.out.println(c.desencolar()); // 2
        System.out.println(c.desencolar()); // 3
        System.out.println(c.desencolar()); // 4
        System.out.println(c.estaVacia());  // true
    }
}

Salida verificada con OpenJDK: 1, 1, 2, 3, 4, true. El caso interesante: encolar 4 después de trasvasar 2,3 a salida → el 4 se queda en entrada y no se mezcla; sale respetando 1,2,3,4. ✔

f) Un desencolar puntual puede costar O(n) (trasvase completo), pero cada elemento se apila/desapila un número constante de veces en toda su vida (máx. 4 operaciones O(1)) → coste amortizado O(1).

Recursividad5. Torres de Hanói, Euclides (mcd) e inversión in-place

Con recursividad: a) void hanoi(int n, char origen, char aux, char destino) (imprime cada movimiento; di cuántos hace y su complejidad); b) int mcd(int a, int b) por Euclides (mcd(a,b)=mcd(b, a%b), base b==0); c) void invertir(int[] a, int ini, int fin) in situ. d) Traza de hanoi(3,'A','B','C') (7 movimientos), mcd(48,36) a mano y {1,2,3,4,5} tras invertir(a,0,4).
🔍 Cómo lo reconozcoPalabra clave: recursividad con tres clásicos: Hanói (recursión doble), Euclides (recursión simple con módulo) e inversión por extremos. Cada uno = caso base + caso recursivo.
🧭 Estrategia y pasos a seguira) hanoi: base n==1 → mueve disco 1 origen→destino. Recursivo: mueve n-1 de origen a aux (usando destino), mueve el disco n de origen a destino, mueve n-1 de aux a destino (usando origen). b) mcd: base b==0 → return a; recursivo return mcd(b, a%b). c) invertir: base ini>=fin; intercambia extremos y recursiona con ini+1, fin-1.
💡 Por quéHanói hace 2ⁿ−1 movimientos: cada disco extra duplica el trabajo (M(n)=2·M(n-1)+1) → para n=3 son 7, complejidad O(2ⁿ) (recursión doble). Euclides es muy rápido (aprox. O(log(min(a,b)))) porque el resto a%b decrece deprisa. Invertir es O(n) en tiempo (n/2 intercambios) y O(1) de espacio extra.
✍️ Ahora pícalo túRellena Hanói (fija bien el orden de las varillas):
public static void hanoi(int n, char origen, char aux, char destino){
    if (n == 1){                                    // CASO BASE
        System.out.println("mover disco 1 de " + origen + " a " + destino);
        return;
    }
    hanoi(n - 1, origen, ________, ________);        // n-1 a la varilla auxiliar
    System.out.println("mover disco " + n + " de " + origen + " a " + destino);
    hanoi(n - 1, ________, origen, destino);         // n-1 encima del destino
}
💊 Píldora de examenLos tres esquemas para memorizar: Hanói = doble recursión, 2ⁿ−1 movimientos, O(2ⁿ) (el orden de varillas: primero a aux, luego a destino). Euclides = mcd(b, a%b) con base b==0 (elegísimo). Invertir/recorrer por extremos = intercambia (ini,fin) y avanza al centro, base ini>=fin. Todos: caso base primero.
Solución comprobada (compilada con OpenJDK)
// a) Torres de Hanoi
public static void hanoi(int n, char origen, char aux, char destino){
    if (n == 1){                                    // CASO BASE
        System.out.println("mover disco 1 de " + origen + " a " + destino);
        return;
    }
    hanoi(n - 1, origen, destino, aux);             // 1) subtorre n-1 a auxiliar
    System.out.println("mover disco " + n + " de " + origen + " a " + destino);
    hanoi(n - 1, aux, origen, destino);             // 3) subtorre n-1 al destino
}
// b) Maximo comun divisor (Euclides)
public static int mcd(int a, int b){
    if (b == 0) return a;          // CASO BASE
    return mcd(b, a % b);          // CASO RECURSIVO
}
// c) Inversion in-place recursiva
public static void invertir(int[] a, int ini, int fin){
    if (ini >= fin) return;        // CASO BASE
    int t = a[ini]; a[ini] = a[fin]; a[fin] = t;   // intercambia extremos
    invertir(a, ini + 1, fin - 1); // avanza al centro
}

d) Traza de hanoi(3,'A','B','C') — 7 movimientos:

1: mover disco 1 de A a C
2: mover disco 2 de A a B
3: mover disco 1 de C a B
4: mover disco 3 de A a C
5: mover disco 1 de B a A
6: mover disco 2 de B a C
7: mover disco 1 de A a C   -> total = 2^3 - 1 = 7

mcd(48,36) a mano: mcd(48,36) → mcd(36,12) → mcd(12,0) → 12. invertir({1,2,3,4,5}, 0, 4)[5,4,3,2,1] (intercambios 1↔5, 2↔4, el 3 queda fijo).

Verificado con OpenJDK: Hanói imprime esos 7 movimientos exactos; mcd(48,36)=12 y mcd(1071,462)=21; invertir da [5, 4, 3, 2, 1]. ✔