Todo lo que has aprendido en este curso cabe en una frase: elige bien tus datos y el código se escribe solo. Esta última lección te propone demostrártelo con tres proyectos integradores de dificultad creciente. No son ejercicios de una estructura: cada uno obliga a combinar varias, a justificar cada elección con su Big O y a verificar con timeit que las promesas se cumplen. Encontrarás enunciados detallados, requisitos por estructura, arquitectura sugerida, hitos incrementales y criterios de "hecho" — pero no código cerrado: el código, esta vez, es tuyo.

Contenido

  1. Cómo abordar los proyectos
  2. Proyecto 1: TaskFlow completo en consola
  3. Proyecto 2: Planificador de sprints
  4. Proyecto 3: Motor de búsqueda de tareas
  5. Cómo autoevaluarse con timeit
  6. Cierre del curso

Cómo abordar los proyectos

  • Orden recomendado: 1 → 2 → 3. El proyecto 1 integra piezas ya construidas; el 2 exige diseñar un algoritmo sobre ellas; el 3 pide diseñar estructura y algoritmo a la vez.
  • Antes de programar cada pieza, escribe una línea: "operación dominante → estructura elegida → coste". Es el método de 08-01 convertido en hábito.
  • Reutiliza tus clases de los módulos anteriores (la tabla-chuleta de 08-02 es tu inventario). Reescribir desde cero solo lo que el proyecto pida mejorar.
  • Hitos pequeños: cada proyecto trae hitos incrementales; no pases al siguiente sin que el anterior funcione con datos de prueba. Un proyecto a medias que funciona enseña más que uno completo que no arranca.
  • Criterio general de "hecho": funciona con los casos de prueba, cada estructura está justificada por escrito y las mediciones de timeit (sección 5) confirman los costes prometidos.

Proyecto 1: TaskFlow completo en consola

Enunciado

Integra las piezas construidas durante el curso en una única aplicación de consola coherente: un menú en bucle que permita gestionar tareas y proyectos usando, por debajo, las estructuras adecuadas para cada operación. Es un proyecto de integración: la dificultad no está en ninguna pieza, sino en que todas compartan un mismo almacén de tareas sin desincronizarse.

Requisitos por estructura

Requisito funcional Estructura exigida Origen
Alta, baja y consulta de tareas por id en O(1) dict id→tarea (almacén canónico) Módulo 5
Tablero con columnas (pendiente/en curso/hecha) y movimiento entre ellas ListaEnlazada (o deque) por columna Módulo 2
Deshacer/rehacer las últimas operaciones GestorDeshacerRehacer (dos pilas) Módulo 3
"Siguiente tarea urgente" en O(log n) BandejaUrgencias (heapq con (prioridad, contador, id)) Módulos 4 y 6
Búsqueda por etiqueta en O(1) medio Índice invertido etiqueta→set de ids Módulo 5
Proyectos con subtareas y coste agregado (presupuestar) NodoArbol (árbol general, postorden) Módulo 6
Dependencias entre tareas y "orden de trabajo" válido Grafo + orden_topologico, con hay_ciclo como validación Módulo 7
Registro de los últimos 20 eventos de la sesión deque(maxlen=20) o ColaCircular Módulo 4

Guía de solución orientativa

Arquitectura sugerida en tres capas, para que las estructuras no se mezclen con la interfaz:

flowchart TD
    UI[interfaz.py<br/>menú en bucle, input/print] --> N[nucleo.py<br/>clase TaskFlow: crear, mover,<br/>deshacer, urgente, buscar...]
    N --> E[estructuras.py<br/>tus clases de los módulos 2-7]
  • estructuras.py: copia aquí tus clases del curso tal cual (o impórtalas de donde las tengas).
  • nucleo.py: una clase TaskFlow que encapsula la coordinación. Parte del esqueleto NucleoTaskFlow de 08-01 y amplíalo. Regla de oro: la interfaz nunca toca una estructura directamente; toda modificación pasa por un método del núcleo, que actualiza todas las estructuras afectadas (dict, índice, montículo, grafo...) en la misma llamada.
  • Para el deshacer, guarda en la pila acciones invertibles: por ejemplo ("crear", id) se deshace borrando, ("mover", id, columna_origen, columna_destino) se deshace moviendo al revés. Empieza soportando deshacer solo para crear/borrar/mover; amplía después.
  • Para las dependencias, mantén la convención del módulo 7: arista A→B significa "B depende de A". Antes de añadir una dependencia, simúlala y comprueba hay_ciclo; si lo crea, recházala con un mensaje claro.

Hitos incrementales: (1) menú + crear/listar/consultar sobre el dict; (2) tablero por columnas y movimiento; (3) deshacer/rehacer; (4) bandeja de urgencias con borrado perezoso; (5) etiquetas e índice invertido; (6) proyectos jerárquicos con presupuestar; (7) dependencias + orden de trabajo; (8) registro de eventos y pulido.

Criterios de "hecho": ninguna operación del menú recorre el dict completo salvo "listar todo"; crear 10 000 tareas de prueba no degrada la consulta por id ni la extracción de urgentes; deshacer inmediatamente después de cualquier operación deja el sistema en el estado anterior (compruébalo comparando el dict antes y después); añadir una dependencia circular es imposible.

Proyecto 2: Planificador de sprints

Enunciado

Dado un backlog de tareas —cada una con id, titulo, prioridad, horas y una lista depende_de— y una capacidad de sprint en horas (p. ej. 40), genera una lista de sprints válidos: ninguna tarea aparece antes que sus dependencias, ningún sprint supera la capacidad y, a igualdad de condiciones, entran antes las de mayor prioridad. Además, el planificador debe calcular la ruta crítica del proyecto y avisar de los cuellos de botella: tareas que, si se retrasan, retrasan la entrega completa.

Requisitos por estructura

Pieza Estructura exigida Coste objetivo
Backlog indexado por id dict O(1) por consulta
Grafo de dependencias Grafo (lista de adyacencia) O(V+E) construcción
Validación previa (sin ciclos, dependencias existentes) hay_ciclo (colores) O(V+E)
Orden por niveles (qué puede hacerse "a la vez") Kahn por capas (variante por niveles del módulo 7) O(V+E)
Elegir dentro de un nivel por prioridad Montículo (prioridad, contador, id) O(log n) por extracción
Ruta crítica y holguras ruta_critica sobre el DAG (horas como pesos) O(V+E)

Guía de solución orientativa

El algoritmo central es el Kahn por niveles del planificador del módulo 7, con una vuelta de tuerca: dentro de cada nivel no da igual el orden, porque la capacidad es limitada.

  1. Valida el backlog: toda dependencia apunta a un id existente y hay_ciclo devuelve falso. Si no, informa y detente — un plan sobre un grafo inválido no significa nada.
  2. Calcula el grado de entrada de cada tarea. Las de grado 0 son elegibles: mételas en un montículo por (prioridad, contador, id).
  3. Para llenar un sprint: extrae elegibles del montículo mientras quepan en las horas restantes del sprint. Decisión de diseño que debes tomar y documentar: si la más prioritaria no cabe pero una menos prioritaria sí, ¿la saltas (aprovechas capacidad) o cierras el sprint (respetas prioridad estricta)? Ambas son defendibles; un deque de "no cupieron" que se reintenta antes del siguiente sprint es una solución intermedia elegante.
  4. Al "completar" un sprint, reduce el grado de entrada de las dependientes de sus tareas; las que lleguen a 0 entran al montículo de elegibles. Repite hasta vaciar el backlog.
  5. Ruta crítica: con las horas como peso, calcula para cada tarea el instante más temprano de fin (máximo sobre sus dependencias + sus horas, en orden topológico) y, hacia atrás, el más tardío. Las tareas con holgura 0 forman la ruta crítica: márcalas en la salida ("si migrar base de datos se retrasa, se retrasa todo").

Hitos incrementales: (1) carga y validación del backlog; (2) orden topológico plano; (3) niveles paralelos; (4) sprints con capacidad y prioridad; (5) ruta crítica y avisos; (6) salida legible (tabla por sprint con horas usadas/libres).

Criterios de "hecho": con un backlog artificial de 1 000 tareas y dependencias aleatorias sin ciclos, planifica en bien menos de un segundo; ninguna tarea aparece en un sprint anterior a alguna de sus dependencias (escribe un verificador automático: es un bucle con un set de completadas — O(V+E) — y es parte del proyecto); la suma de horas de cada sprint no supera la capacidad; la duración total estimada coincide con la longitud de la ruta crítica cuando la capacidad es "infinita".

Proyecto 3: Motor de búsqueda de tareas

Enunciado

Construye el buscador de TaskFlow: dado un corpus de tareas, debe responder consultas de texto libre ("informe mensual cliente") devolviendo las k tareas más relevantes, en tiempo interactivo aunque haya decenas de miles de tareas. Opcionalmente, autocompletar mientras se escribe. Es el proyecto más abierto: hay decisiones de diseño sin respuesta única, y documentarlas es parte del trabajo.

Requisitos por estructura

Pieza Estructura exigida Por qué
Índice invertido palabra→ids dict de set (o defaultdict(set)) Buscar sin recorrer el corpus: O(1) medio por palabra
Frecuencias para el ranking Counter por documento Contar es su oficio
Top-k resultados Montículo (heapq.nlargest o Monticulo propio) O(n log k), sin ordenar todo
Consultas repetidas Memoización con dict (caché consulta→resultado) Módulo 5; invalídala al indexar tareas nuevas
(Opcional) Autocompletar dict de prefijos, lista ordenada + bisect, o un trie (08-03) Comparar las tres es el ejercicio

Guía de solución orientativa

  • Indexación. Normaliza cada título/descripcion (minúsculas, sin tildes, troceado por espacios y signos — cuidado: "año"→"ano" es aceptable aquí; documenta la decisión). Para cada palabra, añade el id al set del índice invertido. Guarda también Counter de palabras por tarea para el ranking. Indexar debe ser O(total de palabras).
  • Consulta. Trocea la consulta igual que el corpus (¡misma normalización o nada casará!). Recupera los set de ids de cada palabra. Decisión de diseño: ¿intersección (todas las palabras: preciso, pocos resultados) o unión (alguna palabra: flexible, muchos)? Sugerencia: unión, y que el ranking premie a quien tiene más palabras.
  • Ranking. Puntuación simple y suficiente: suma, para cada palabra de la consulta, de su frecuencia en la tarea, con un plus por palabra distinta cubierta; desempata por prioridad de la tarea (¡menor número primero!). Extrae el top-k con montículo, nunca ordenando la lista completa de candidatos.
  • Autocompletar (opcional). Implementa al menos la versión de 08-01 (lista ordenada + bisect). Si te atreves con el trie: un nodo es un dict carácter→hijo más una marca de fin de palabra; insertar y buscar prefijo son O(longitud). Compara memoria y velocidad de ambas con tu corpus y escribe dos párrafos con la conclusión — ese pequeño informe vale más que el código.
  • Caché. Un dict consulta_normalizada→resultados. Al indexar una tarea nueva, vacíala (o invalida solo las consultas afectadas si quieres un reto extra usando el propio índice invertido).

Hitos incrementales: (1) normalizador + índice invertido con búsqueda de una palabra; (2) consultas multi-palabra con unión; (3) ranking + top-k con montículo; (4) caché con invalidación; (5) autocompletar; (6) medición comparativa (sección siguiente).

Criterios de "hecho": con 20 000 tareas sintéticas (genera títulos combinando un vocabulario de ~200 palabras), la consulta responde en milisegundos; buscar una palabra inexistente devuelve vacío sin error; añadir una tarea la hace encontrable inmediatamente; la búsqueda con caché caliente es medible y claramente más rápida que en frío.

Cómo autoevaluarse con timeit

La promesa de cada estructura es un Big O; tu última tarea del curso es auditar tus propias promesas, como hicimos en el módulo 1. El método: mide la misma operación con n, 10n y 100n elementos y comprueba la forma del crecimiento — O(1) apenas se mueve, O(log n) suma una cantidad fija por salto, O(n) multiplica por 10, O(n²) por 100.

import timeit

def medir(fn, repeticiones=1000):
    """Tiempo medio (en microsegundos) de una llamada a fn."""
    total = timeit.timeit(fn, number=repeticiones)
    return total / repeticiones * 1_000_000

# Ejemplo: auditar la consulta por id del Proyecto 1 con tres tamaños
for n in (1_000, 10_000, 100_000):
    app = construir_taskflow_con(n)                  # tu generador de datos de prueba
    us = medir(lambda: app.consultar(n // 2))
    print(f"n={n:>7}: {us:8.2f} µs por consulta")

timeit ejecuta la operación muchas veces y promedia, eliminando el ruido de una medición suelta; medimos con la tarea "central" para no favorecer casos límite. Casos de prueba sugeridos, uno por promesa clave:

Proyecto Operación auditada Promesa Señal de fallo
1 consultar(id) con n = 10³/10⁴/10⁵ O(1) El tiempo crece con n → estás recorriendo una lista en alguna parte
1 siguiente_urgente() tras muchos cambios de prioridad O(log n) Crecimiento lineal → el borrado perezoso no purga, o reordenas la lista
2 Planificar backlog de 10²/10³/10⁴ tareas O(V+E) Crecimiento cuadrático → buscas dependientes recorriendo todo el backlog
3 Consulta en frío con corpus de 10³/10⁴ ~O(candidatos) Crece con el corpus total y no con los candidatos → no usas el índice
3 Consulta repetida (caché caliente) O(1) Igual que en frío → la caché no intercepta (¿normalizas antes de cachear?)

Si una medición contradice la teoría, enhorabuena: acabas de encontrar el mejor ejercicio del curso. Persigue la causa con la tabla de síntomas de 08-01.

Errores Comunes y Consejos

Adaptamos la sección a consejos de enfoque de proyectos:

  • Empezar por la interfaz. El menú bonito es la trampa clásica: son horas de input/print que no ejercitan nada. Núcleo primero, probado desde un script; interfaz al final.
  • No escribir datos de prueba generables. Necesitas funciones que fabriquen 10 000 tareas sintéticas en un segundo; sin ellas no hay hito verificable ni timeit posible. Escríbelas en el hito 1.
  • Sincronización a mano. Si la interfaz actualiza el dict aquí y el índice allá, la desincronización es cuestión de tiempo. Toda escritura pasa por el núcleo; es la lección del patrón de 08-01.
  • Perfeccionismo de estructura. ¿Duda entre deque y ListaEnlazada para una columna del tablero? Elige una, anota por qué, sigue. Cambiarla después costará poco precisamente si respetaste el TDA (concepto transversal 1).
  • Saltarse la justificación escrita. La línea "operación → estructura → coste" por pieza convierte el proyecto en material de portfolio y de entrevista. Sin ella, es solo código que funciona hoy.
  • Compararse con bibliotecas reales. Tu motor de búsqueda no compite con Elasticsearch. El objetivo es que tus promesas de coste se cumplan y sepas explicarlas.

Ejercicios

En esta lección, los proyectos son los ejercicios: elige al menos uno (idealmente los tres, en orden) y llévalo hasta sus criterios de "hecho". No hay solución cerrada que copiar — las guías orientativas de cada proyecto son tu mapa, y las mediciones de timeit tu corrector automático.

Soluciones

La "solución" de cada proyecto es una autoevaluación en tres comprobaciones, comunes a los tres:

  1. Funcional: los criterios de "hecho" del proyecto se cumplen con tus datos de prueba generados (incluido el verificador automático, en el caso del planificador).
  2. De coste: las mediciones de la tabla de timeit muestran la forma de crecimiento prometida al multiplicar n por 10 y por 100.
  3. De criterio: puedes recorrer tu código y, pieza por pieza, recitar "operación dominante → estructura → coste → por qué no las alternativas". Si alguna pieza no supera este interrogatorio, revisa 08-01; si lo superan todas, el curso ha cumplido su objetivo contigo.

Conclusión

Enhorabuena: has llegado al final del curso de Estructuras de Datos. Empezaste con un dict suelto llamado tarea y terminas siendo capaz de diseñar, justificar y medir un sistema completo — tablero, deshacer, urgencias, índices, jerarquías, dependencias y búsqueda — eligiendo en cada pieza la estructura que abarata la operación que de verdad importa. Esa es la filosofía que querríamos que te llevaras grabada: primero los datos, luego el código. Los algoritmos se olvidan y se vuelven a consultar; el criterio, una vez adquirido, se queda. Practícalo en cada revisión de código, en cada diseño y en cada entrevista: pregunta siempre qué operaciones dominan y qué estructura las sirve mejor. TaskFlow es tuyo, tu criterio también. Ha sido un placer construir contigo. Hasta aquí el curso — y hasta donde tú quieras llegar a partir de ahora.

© Copyright 2026. Todos los derechos reservados