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
- Cómo abordar los proyectos
- Proyecto 1: TaskFlow completo en consola
- Proyecto 2: Planificador de sprints
- Proyecto 3: Motor de búsqueda de tareas
- Cómo autoevaluarse con
timeit - 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 claseTaskFlowque encapsula la coordinación. Parte del esqueletoNucleoTaskFlowde 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.
- Valida el backlog: toda dependencia apunta a un id existente y
hay_ciclodevuelve falso. Si no, informa y detente — un plan sobre un grafo inválido no significa nada. - Calcula el grado de entrada de cada tarea. Las de grado 0 son elegibles: mételas en un montículo por
(prioridad, contador, id). - 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
dequede "no cupieron" que se reintenta antes del siguiente sprint es una solución intermedia elegante. - 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.
- Ruta crítica: con las
horascomo 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
setdel índice invertido. Guarda tambiénCounterde 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
setde 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 undictcará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
dictconsulta_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/printque 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
timeitposible. Escríbelas en el hito 1. - Sincronización a mano. Si la interfaz actualiza el
dictaquí 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
dequeyListaEnlazadapara 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:
- 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).
- De coste: las mediciones de la tabla de
timeitmuestran la forma de crecimiento prometida al multiplicar n por 10 y por 100. - 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.
Curso de Estructuras de Datos
Módulo 1: Introducción a las Estructuras de Datos
- ¿Qué son las Estructuras de Datos?
- Importancia de las Estructuras de Datos en la Programación
- Tipos de Estructuras de Datos
- Complejidad Algorítmica y Notación Big O
- Arrays y Memoria: la Base de las Estructuras de Datos
Módulo 2: Listas
- Introducción a las Listas
- Listas Enlazadas
- Listas Doblemente Enlazadas
- Listas Circulares
- Ejercicios con Listas
Módulo 3: Pilas
- Introducción a las Pilas
- Operaciones Básicas con Pilas
- Implementación de Pilas
- Aplicaciones de las Pilas
- Ejercicios con Pilas
Módulo 4: Colas
- Introducción a las Colas
- Operaciones Básicas con Colas
- Colas Circulares
- Colas de Prioridad
- Colas Dobles (Deques)
- Ejercicios con Colas
Módulo 5: Tablas Hash y Diccionarios
- Introducción a las Tablas Hash
- Funciones Hash y Resolución de Colisiones
- Diccionarios y Conjuntos en la Práctica
- Ejercicios con Tablas Hash
Módulo 6: Árboles
- Introducción a los Árboles
- Árboles Binarios
- Recorridos de Árboles
- Árboles Binarios de Búsqueda
- Árboles AVL
- Árboles B
- Montículos (Heaps)
- Ejercicios con Árboles
Módulo 7: Grafos
- Introducción a los Grafos
- Representación de Grafos
- Algoritmos de Búsqueda en Grafos
- Algoritmos de Caminos Mínimos
- Árboles de Expansión Mínima
- Aplicaciones de los Grafos
- Ejercicios con Grafos
