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.
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.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
a)
ArrayList b) HashSet c) LinkedList d) TreeSetcontains). 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).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).•
ArrayList: duplicados=sí, orden=no automático, contains=O(n) → falla.•
HashSet: duplicados=no, orden=NINGUNO, contains=O(1) → falla el orden.•
LinkedList: duplicados=sí, orden=no, contains=O(n) → falla todo.•
TreeSet: duplicados=no, orden=sí, contains=O(log n) → ✔.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.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
containses 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
containses O(n). Falla en los tres requisitos.
Complejidad · Big-O2. Orden de complejidad de tres métodos
// (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);
}j*=2 → logarítmico).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ⁿ).(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.*=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).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).
(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
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.Deque<Character> pila = new ArrayDeque<>(). 2) Recorre la cadena carácter a carácter. 3) Si es apertura → push. 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).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
}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.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
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.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.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.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²)).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;
}if (vecino != null) antes de tocarlo, porque en los extremos uno de los vecinos es null y ahí se actualiza primero o ultimo.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
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.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.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;
}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.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
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.raiz.• 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.
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);
}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.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 80Verificado 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
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.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.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()).get(0) sobre vacío. El patrón de streams (filter → map → collect) 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.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());
}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.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)
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).interface) de una implementación concreta (class ... implements).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.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)).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++;
}<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.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
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.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.a[i] <= a[j]. Coste espacial O(n) por el array auxiliar.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
}<= en la mezcla (ante empate coge antes el de la izquierda). El caso base ini >= fin nunca falta.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 60c) 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
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.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²).sumaFilas visita cada una de las n² celdas una vez → O(n²), que es óptimo (hay que mirarlas todas).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;
}ini <= fin (con <=, no <), y actualizar medio+1/medio-1 (no medio, o bucle infinito).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
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.cima que apunta al tope; insertar y sacar se hacen siempre por la cabeza.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.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.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;
}invertir: apilar los caracteres y desapilarlos a un StringBuilder.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
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.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.mitad (la calcula una vez y la eleva al cuadrado), evitando recalcular. La versión ingenua (multiplicar exp veces) sería O(exp).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
}// 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)
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.equals antes de insertar).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.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).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;
}.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.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
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.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.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.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.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
}
}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».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
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.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²).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²).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.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
}i*=k o i/=k → O(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).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
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.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.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.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
}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».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
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).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.a%b decrece deprisa. Invertir es O(n) en tiempo (n/2 intercambios) y O(1) de espacio extra.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
}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.// 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 = 7mcd(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]. ✔