En la lección anterior definimos qué es una estructura de datos y dejamos una pregunta abierta: si varias estructuras pueden guardar los mismos datos, ¿de verdad importa cuál elijas? En esta lección vas a comprobar que sí, y no con teoría, sino con el cronómetro en la mano: veremos cómo la misma operación de TaskFlow —buscar una tarea— puede tardar microsegundos o segundos según la estructura elegida. También veremos que la importancia va más allá del rendimiento: afecta a la escalabilidad, a la legibilidad del código y hasta a tu carrera profesional.

Contenido

  1. Elegir estructura es una decisión de diseño, no un detalle
  2. Rendimiento: el experimento de la búsqueda en TaskFlow
  3. Escalabilidad: lo que funciona con 10 no funciona con un millón
  4. Legibilidad y mantenibilidad: código que se explica solo
  5. Impacto profesional: entrevistas técnicas y trabajo real

Elegir estructura es una decisión de diseño, no un detalle

Cuando un programa va lento o se vuelve difícil de mantener, la intuición del principiante es buscar el problema en los algoritmos o en el lenguaje ("Python es lento"). Pero con muchísima frecuencia el problema está antes: en cómo se organizaron los datos. Una frase célebre de Linus Torvalds, creador de Linux, lo resume así: los buenos programadores se preocupan por las estructuras de datos y sus relaciones más que por el código en sí.

La elección de estructura condiciona cuatro aspectos de tu software:

  • Rendimiento: cuánto tarda cada operación.
  • Escalabilidad: si ese tiempo sigue siendo aceptable cuando los datos crecen.
  • Legibilidad: si el código expresa con claridad la intención.
  • Mantenibilidad: si mañana se puede cambiar sin romper todo.

Vamos a ver cada uno con TaskFlow como banco de pruebas.

Rendimiento: el experimento de la búsqueda en TaskFlow

TaskFlow necesita una operación aparentemente trivial: dado el identificador de una tarea, recuperarla. Es la operación más frecuente de la aplicación: cada vez que el usuario abre una tarea, la edita o la marca como hecha, primero hay que encontrarla.

Primera aproximación: guardamos las tareas en una lista y buscamos recorriéndola.

def crear_tareas(n):
    """Genera n tareas de ejemplo para TaskFlow."""
    return [
        {"id": i, "titulo": f"Tarea {i}", "estado": "pendiente"}
        for i in range(n)
    ]

def buscar_en_lista(tareas, id_buscado):
    """Recorre la lista hasta dar con la tarea (búsqueda secuencial)."""
    for tarea in tareas:
        if tarea["id"] == id_buscado:
            return tarea
    return None

Expliquemos el código:

  • crear_tareas usa una comprensión de lista para fabricar n diccionarios de tarea, con ids 0, 1, 2, .... Nos sirve para simular tableros de distintos tamaños.
  • buscar_en_lista examina las tareas una a una, en orden, hasta encontrar la que tiene el id buscado. Si el id está al final —o no existe—, habrá mirado todas.

Segunda aproximación: además de la lista, mantenemos un índice: un diccionario que asocia cada id con su tarea.

def crear_indice(tareas):
    """Construye un diccionario id -> tarea."""
    return {tarea["id"]: tarea for tarea in tareas}

def buscar_en_indice(indice, id_buscado):
    """Pide la tarea directamente por su clave."""
    return indice.get(id_buscado)

Aquí crear_indice recorre las tareas una única vez y monta el diccionario; a partir de ese momento, indice.get(id) devuelve la tarea sin recorrer nada (cómo consigue esto el diccionario lo desvelaremos en el módulo 5; por ahora, trátalo como magia bien documentada).

Ahora midamos. Python trae el módulo timeit, pensado justamente para cronometrar fragmentos de código repitiéndolos muchas veces y dando un resultado fiable:

import timeit

tareas = crear_tareas(1_000_000)      # un millón de tareas
indice = crear_indice(tareas)
peor_id = 999_999                     # la última: el peor caso para la lista

t_lista = timeit.timeit(
    lambda: buscar_en_lista(tareas, peor_id), number=10
)
t_indice = timeit.timeit(
    lambda: buscar_en_indice(indice, peor_id), number=10
)

print(f"Lista:  {t_lista:.4f} s en 10 búsquedas")
print(f"Índice: {t_indice:.6f} s en 10 búsquedas")

Detalles del experimento:

  • number=10 indica a timeit que ejecute la búsqueda 10 veces y sume los tiempos.
  • Buscamos el id 999_999 a propósito: al estar al final, obliga a la lista a recorrerlo todo. (Los guiones bajos en 1_000_000 son solo separadores visuales de Python; no cambian el número.)
  • La función lambda envuelve la llamada para que timeit pueda ejecutarla repetidamente.

Resultados típicos en un portátil corriente (los tuyos variarán en cifras, no en la conclusión):

Nº de tareas Búsqueda en lista (peor caso) Búsqueda con índice Diferencia aproximada
10 0,0000005 s 0,00000005 s ~10×
10.000 0,0004 s 0,00000005 s ~8.000×
1.000.000 0,04 s 0,00000005 s ~800.000×

Lee la tabla con calma, porque contiene la lección entera:

  • Con 10 tareas, ambas soluciones son instantáneas. Cualquier estructura vale.
  • Con un millón, la lista tarda unas 800.000 veces más que el índice en cada búsqueda. Y una aplicación real busca constantemente: si TaskFlow atiende 100 búsquedas por segundo, la versión con lista sencillamente no puede.
  • El índice ni se inmuta al crecer los datos: tarda prácticamente lo mismo con 10 que con un millón.

No necesitas todavía vocabulario formal para nombrar este fenómeno (ese vocabulario, la notación Big O, es el tema de la lección 01-04). La intuición es suficiente: una estructura obliga a mirar todo; la otra va directa.

Escalabilidad: lo que funciona con 10 no funciona con un millón

El experimento anterior ilustra el error más traicionero del desarrollo de software: el código que funciona perfectamente en pruebas y se derrumba en producción. Es traicionero porque no hay ningún aviso: no hay error de sintaxis, no hay excepción, los tests pasan. Solo que con datos reales, todo se arrastra.

graph LR
    A[Desarrollo: 10 tareas de prueba] -->|todo va bien| B[Demo: 500 tareas]
    B -->|todo va bien| C[Producción: 1.000.000 de tareas]
    C -->|la app se arrastra| D[¿Reescribir? ¿Más servidores?]
    D -->|la causa era| E[Una estructura mal elegida el primer día]

Pensar en escalabilidad significa preguntarse, ante cada estructura que eliges: ¿qué pasará con esta operación cuando los datos se multipliquen por mil? A veces la respuesta es "nada grave, esta colección nunca crecerá" (los tres estados de una tarea de TaskFlow siempre serán tres), y una estructura simple es la elección correcta. Otras veces la respuesta obliga a cambiar el diseño. Lo importante es hacerse la pregunta a tiempo: cambiar una estructura el primer día cuesta minutos; cambiarla con la aplicación en producción puede costar semanas.

Un matiz honesto: la estructura indexada tampoco es gratis. Construir el índice lleva tiempo y ocupa memoria adicional (¡estamos guardando referencias a cada tarea dos veces!). En nuestro caso compensa de sobra, porque se construye una vez y se consulta millones de veces. Este tipo de balance —pagar algo de memoria o de tiempo de preparación a cambio de operaciones rápidas— aparecerá una y otra vez durante el curso.

Legibilidad y mantenibilidad: código que se explica solo

El rendimiento no es el único motivo para elegir bien. Una estructura adecuada hace que el código diga lo que hace. Compara estas dos formas de gestionar los estados que puede tener una tarea en TaskFlow:

# Opción A: estados "a mano" con variables sueltas
pendientes = ["Diseñar logo", "Enviar factura"]
en_curso = ["Escribir informe"]
hechas = []

def mover_a_en_curso(titulo):
    if titulo in pendientes:
        pendientes.remove(titulo)
        en_curso.append(titulo)
# Opción B: un diccionario de estado -> tareas
tablero = {
    "pendiente": ["Diseñar logo", "Enviar factura"],
    "en curso": ["Escribir informe"],
    "hecha": [],
}

def mover(titulo, origen, destino):
    if titulo in tablero[origen]:
        tablero[origen].remove(titulo)
        tablero[destino].append(titulo)

Ambas funcionan, pero fíjate en las diferencias:

  • En la opción A, añadir un cuarto estado ("bloqueada", por ejemplo) implica crear otra variable y escribir nuevas funciones de movimiento para cada combinación. En la B, basta con añadir una clave al diccionario: mover ya sirve para cualquier par de estados.
  • La opción B expresa la relación real del dominio: "cada estado tiene su lista de tareas". Quien lea el código entiende el modelo de datos de un vistazo.
  • La opción A reparte el estado del programa entre variables sueltas, lo que multiplica los sitios donde puede haber inconsistencias.

La regla general: cuando la estructura de datos refleja la estructura del problema, el código se simplifica solo. Muchos condicionales enrevesados y funciones duplicadas son el síntoma de una estructura que no encaja con el dominio.

Impacto profesional: entrevistas técnicas y trabajo real

Conviene ser claros sobre esto, porque afecta directamente a tu carrera como desarrollador junior:

  • Entrevistas técnicas. Las estructuras de datos son el corazón de los procesos de selección técnicos en la mayoría de empresas, desde startups hasta las grandes tecnológicas. Es habitual que te pidan resolver un problema y justificar qué estructura usas y por qué; plataformas de práctica como LeetCode o HackerRank están organizadas, literalmente, por estructura de datos. No se trata de memorizar soluciones, sino de demostrar el razonamiento que estás aprendiendo en este curso: identificar las relaciones y operaciones del problema, y elegir en consecuencia.
  • Revisiones de código. En equipos profesionales, comentarios como "esto es una búsqueda lineal dentro de un bucle, usa un set" son el pan de cada día. Entenderlos —y poder hacerlos tú— marca la diferencia entre ejecutar tareas y participar en el diseño.
  • Depuración de rendimiento. Gran parte de los problemas de lentitud en aplicaciones reales se resuelven sin tocar algoritmos sofisticados: solo cambiando una estructura mal elegida, exactamente como en nuestro experimento del índice.
  • Vocabulario común. Cuando un compañero dice "esto es una cola de prioridad" o "modelémoslo como un grafo", está comprimiendo horas de explicación en una frase. Las estructuras de datos son el idioma compartido de la profesión.

Y un apunte para la era actual: aunque los asistentes de IA generen código por ti, decidir si ese código organiza bien los datos sigue siendo trabajo tuyo. De hecho, revisar código ajeno (humano o generado) exige más criterio sobre estructuras, no menos.

Errores Comunes y Consejos

  • "Con pocos datos da igual" convertido en hábito. Es cierto que con 10 elementos cualquier estructura vale, y no hay que sobre-optimizar. El error es no dejar anotado el supuesto. Un comentario como # OJO: búsqueda lineal, válida mientras haya pocas decenas de tareas vale oro cuando la aplicación crece.
  • Optimizar sin medir. El reflejo opuesto también es un error: reescribir estructuras "porque seguro que va lento" sin haberlo cronometrado. Acostúmbrate desde ya a usar timeit: las corazonadas sobre rendimiento fallan muchísimo.
  • Medir mal. Cuidado al cronometrar: ejecuta las operaciones muchas veces (number=...), no midas una sola ejecución (el sistema operativo mete ruido), y no incluyas en la medición el coste de preparar los datos.
  • Confundir "estructura más rápida" con "estructura mejor". El índice de nuestro experimento gasta memoria extra y hay que mantenerlo actualizado cuando se añaden o borran tareas. Casi toda elección de estructura es un balance; tu trabajo es conocer los términos del intercambio.

Ejercicios

Ejercicio 1: reproducir el experimento

Copia las funciones crear_tareas, buscar_en_lista, crear_indice y buscar_en_indice de esta lección y mide con timeit la búsqueda del peor caso para tamaños 100, 10.000 y 1.000.000. Construye tu propia tabla de resultados. ¿A partir de qué tamaño empieza a ser perceptible la diferencia en tu máquina?

Ejercicio 2: detectar el punto débil

Este código comprueba qué usuarios de una lista de invitados ya están registrados en TaskFlow:

def invitados_registrados(invitados, usuarios_registrados):
    resultado = []
    for invitado in invitados:
        if invitado in usuarios_registrados:   # usuarios_registrados es una lista
            resultado.append(invitado)
    return resultado

Sabiendo que invitado in usuarios_registrados sobre una lista recorre la lista entera en el peor caso: (a) explica con tus palabras por qué este código escalará mal si ambas colecciones crecen; (b) propón una mejora usando set (los conjuntos de Python responden a la pregunta "¿está este elemento?" casi al instante, como el diccionario del experimento); (c) compruébala con timeit para 10.000 invitados y 10.000 registrados.

Ejercicio 3: argumentar como en una revisión de código

Un compañero propone guardar el historial de acciones de TaskFlow (para poder deshacer) en un diccionario {numero_de_accion: accion} y llevar aparte un contador. Escribe, en 3-5 frases y sin código, qué le preguntarías sobre las operaciones que necesita el historial y qué riesgos de mantenibilidad ves en llevar el contador separado de los datos. (No hace falta que propongas la estructura ideal: eso llegará en el módulo 3.)

Soluciones

Solución 1:

Un programa de medición completo:

import timeit

for n in (100, 10_000, 1_000_000):
    tareas = crear_tareas(n)
    indice = crear_indice(tareas)
    peor = n - 1
    t_lista = timeit.timeit(lambda: buscar_en_lista(tareas, peor), number=10)
    t_dict = timeit.timeit(lambda: buscar_en_indice(indice, peor), number=10)
    print(f"n={n:>9}: lista {t_lista:.6f} s | índice {t_dict:.6f} s")

Resultados orientativos: con n=100 ambas están por debajo del milisegundo (diferencia imperceptible); con n=10_000 la lista ya tarda del orden de milisegundos; con n=1_000_000 la diferencia es de varios órdenes de magnitud. La cifra exacta depende de tu máquina; la tendencia no.

Solución 2:

(a) Por cada invitado se recorre potencialmente toda la lista de registrados: si hay 10.000 invitados y 10.000 registrados, se hacen hasta 100 millones de comparaciones. Al duplicar ambas colecciones, el trabajo se multiplica por cuatro, no por dos: el crecimiento se dispara.

(b) Basta convertir los registrados a conjunto una sola vez:

def invitados_registrados_v2(invitados, usuarios_registrados):
    registrados = set(usuarios_registrados)   # conversión única
    return [inv for inv in invitados if inv in registrados]

inv in registrados sobre un set no recorre nada: responde casi al instante (el porqué, en el módulo 5).

(c) Medición:

import timeit
invitados = [f"user{i}" for i in range(10_000)]
registrados = [f"user{i}" for i in range(5_000, 15_000)]

t1 = timeit.timeit(lambda: invitados_registrados(invitados, registrados), number=3)
t2 = timeit.timeit(lambda: invitados_registrados_v2(invitados, registrados), number=3)
print(t1, t2)   # típico: ~2-4 s frente a ~0,003 s

Solución 3:

Una buena respuesta debería incluir preguntas como: ¿qué operaciones hará el historial (añadir la última acción, recuperar la última, quizá las N últimas)? ¿Necesitamos acceder a acciones antiguas por número, o solo a la más reciente? Riesgos del contador separado: es estado duplicado, de modo que si el diccionario y el contador se desincronizan (por un borrado, un error o un olvido), el historial queda corrupto sin avisar; además, quien lea el código tiene que descubrir por su cuenta que ambos van juntos. La conclusión razonable: el patrón de uso "lo último que entra es lo primero que sale" pide una estructura que lo garantice por contrato, que es justo lo que veremos en el módulo 3.

Conclusión

En esta lección has visto, con mediciones reales, que la elección de estructura de datos importa: la búsqueda de tareas en TaskFlow pasó de tardar décimas de segundo a ser instantánea al cambiar una lista por un índice, y la diferencia crece brutalmente con el tamaño de los datos. También has visto que la importancia no es solo de rendimiento: una estructura que refleja el dominio hace el código más legible y mantenible, y dominar este razonamiento es de lo más valorado en entrevistas técnicas y en el trabajo diario.

Ahora bien: para elegir estructura hay que conocer el catálogo. ¿Qué familias de estructuras existen y para qué sirve cada una? Ese panorama —lineales y no lineales, estáticas y dinámicas, y el mapa completo de lo que veremos en el curso— es el tema de la próxima lección.

© Copyright 2026. Todos los derechos reservados