Al cerrar el módulo 5 dijimos que el éxito de TareaFácil traía consigo el mejor tipo de problema: cuando la agenda de Estudio Alba tenga doscientas tareas guardadas en tareas.json, «buscar la del cliente Vidal» dejará de ser trivial. Hasta ahora hemos usado in, .index() y la pertenencia en conjuntos como cajas negras: funcionan, y no hemos preguntado cómo. Esta lección abre la primera de esas cajas.
Buscar es, con diferencia, la operación más frecuente de cualquier programa que maneje datos. Aquí aprenderás las tres familias de técnicas que existen —búsqueda lineal, búsqueda binaria y búsqueda por clave—, qué exige cada una, cuánto cuesta y cuándo elegirla. Las implementaremos a mano para entenderlas por dentro, con la misma moraleja honesta de siempre: en producción se usa la herramienta del lenguaje.
Contenido
- Qué significa buscar y qué se devuelve
- Búsqueda lineal: el algoritmo de referencia
- Trazado paso a paso de la búsqueda lineal
- Búsqueda con criterio: el callback vuelve
- Buscar todas las coincidencias
- Búsqueda binaria: la idea de descartar la mitad
- Búsqueda binaria: implementación y trazado
- Lineal frente a binaria
- Búsqueda por clave: diccionarios y conjuntos
- Índice invertido: preparar la búsqueda
- TareaFácil v0.12: tres formas de localizar una tarea
- Errores comunes y consejos
- Ejercicios
- Conclusión
- Qué significa buscar y qué se devuelve
«Buscar» parece una sola cosa y en realidad son tres preguntas distintas, y confundirlas es el primer error del principiante:
| Pregunta | Qué se devuelve | Herramienta de Python |
|---|---|---|
| ¿Existe? | True o False |
x in coleccion |
| ¿Dónde está? | Una posición (índice) | lista.index(x) |
| ¿Cuál es? | El elemento completo | Un bucle, o next(...) |
En una lista de números las tres coinciden casi siempre. En nuestra agenda —una lista de diccionarios— la diferencia es enorme: saber que existe una tarea de Nuria no me sirve para cambiarle la prioridad; para eso necesito el diccionario, o al menos su posición.
Y hay una cuarta decisión: qué pasa cuando no se encuentra nada. Los tres convenios habituales son devolver -1 (tradición heredada de C y Java), devolver None (lo idiomático en Python cuando se devuelve un elemento) o lanzar un error, que es lo que hace .index() con su ValueError. Usaremos -1 para posiciones y None para elementos. Sea cual sea el convenio, documéntalo en el docstring y sé coherente en todo el programa.
- Búsqueda lineal: el algoritmo de referencia
La búsqueda lineal (o secuencial) es el algoritmo más simple que existe: mirar los elementos uno por uno, desde el principio, hasta encontrar el que buscas o agotar la colección. No exige absolutamente nada de los datos: sirven ordenados o desordenados, en una lista, en un fichero o en una cinta.
graph TD
A["Empezar en la posicion 0"] --> B{"Quedan elementos?"}
B -->|No| C["Devolver -1: no esta"]
B -->|Si| D{"Es el que busco?"}
D -->|Si| E["Devolver la posicion"]
D -->|No| B
Traducido a Python sobre nuestra agenda, con enumerate de 05-01 para tener a la vez posición y elemento:
def buscar_posicion(agenda, titulo):
"""Devuelve la posicion de la tarea con ese titulo exacto, o -1 si no esta."""
for i, tarea in enumerate(agenda):
if tarea["titulo"] == titulo:
return i # salida temprana: ya no hace falta seguir
return -1 # el bucle termino sin encontrar nada
def buscar_tarea(agenda, titulo):
"""Devuelve el diccionario de la tarea con ese titulo, o None si no esta."""
for tarea in agenda:
if tarea["titulo"] == titulo:
return tarea # el diccionario, no una copia: se puede modificar
return NoneTres detalles hacen bueno a este código. La salida temprana con return: en cuanto encuentra la tarea, la función termina; si guardáramos el resultado en una variable y siguiéramos recorriendo, en una agenda de 200 tareas miraríamos 199 de más. El return -1 está fuera del bucle, alineado con el for: solo se llega ahí cuando el recorrido ha terminado sin encontrar nada; puesto dentro, la función devolvería -1 en la primera vuelta que no coincidiera. Y la comparación es ==, no in: exige coincidencia exacta, y ya veremos la variante «contiene» en la sección 4.
Las dos funciones son idénticas salvo en lo que devuelven, la posición o el elemento. Y recuerda el aliasing de 05-01: lo que devuelve la segunda es el mismo diccionario que está en la lista, así que buscar_tarea(agenda, "Cartel feria")["prioridad"] = "baja" modifica la agenda de verdad. Es el mecanismo en el que ya se apoyaba elegir_tarea desde la v0.10.
- Trazado paso a paso de la búsqueda lineal
Hagamos la prueba de escritorio de 01-05 sobre esta agenda de Estudio Alba:
| Posición | Título | Responsable | Días |
|---|---|---|---|
| 0 | Cartel feria del libro | Marta | 3 |
| 1 | Menú Panadería Solé | Luis | 5 |
| 2 | Logotipo cliente Vidal | Nuria | 2 |
| 3 | Presupuesto marzo | Marta | 1 |
Trazamos dos casos a la vez: A, buscar "Logotipo cliente Vidal", que sí está; y B, buscar "Flyer verano", que no existe.
| Vuelta | i |
tarea["titulo"] |
¿Coincide en A? | ¿En B? |
|---|---|---|---|---|
| 1 | 0 | Cartel feria del libro | No | No |
| 2 | 1 | Menú Panadería Solé | No | No |
| 3 | 2 | Logotipo cliente Vidal | Sí → return 2 |
No |
| 4 | 3 | Presupuesto marzo | (no se mira) | No → fin del bucle |
| — | — | — | — | return -1 |
El caso A termina en tres comparaciones y la posición 3 no se llega a mirar: ese es el efecto de la salida temprana. El caso B necesita cuatro, es decir, todas. Y ahí está la primera lección de coste del módulo, que se lee directamente de la tabla. Mejor caso: el elemento está el primero, una comparación. Peor caso: está el último o no está, tantas comparaciones como elementos. Caso medio: la mitad de los elementos. Retén el detalle importante: el peor caso de la búsqueda lineal es no encontrar nada, así que un programa que busca a menudo cosas inexistentes está en el peor escenario posible para este algoritmo.
- Búsqueda con criterio: el callback vuelve
Las funciones anteriores solo saben buscar por título exacto. Si Marta quiere buscar por responsable, por prioridad o por «títulos que contengan la palabra feria», ¿escribimos una función por caso? No: aplicamos el patrón callback de 04-05, y en vez del valor a comparar, la función recibe la comprobación en forma de función.
def buscar_si(agenda, criterio):
"""Devuelve la primera tarea que cumple el criterio, o None.
criterio: funcion que recibe una tarea (dict) y devuelve True o False.
"""
for tarea in agenda:
if criterio(tarea):
return tarea
return None
# La misma funcion resuelve los tres casos, cambiando solo el criterio:
buscar_si(agenda, lambda t: t["titulo"] == "Presupuesto marzo")
buscar_si(agenda, lambda t: t["responsable"] == "Nuria")
buscar_si(agenda, lambda t: "feria" in t["titulo"].lower())
buscar_si(agenda, lambda t: t["prioridad"] == "alta" and not t["completada"])El esqueleto del recorrido lo pone buscar_si; la decisión de qué cuenta como «encontrado» la pone quien llama. La tercera línea usa el in de cadenas de 05-02 —«contiene»— y .lower() para no distinguir mayúsculas, que es lo que espera cualquier usuario; la cuarta combina dos condiciones sin que buscar_si se entere. Python trae esta idea incorporada en next con una expresión generadora:
Se lee «el siguiente elemento de la agenda cuyo responsable sea Nuria, o None si no hay ninguno». Sin ese segundo argumento, next lanza un error cuando no hay coincidencias. Es la forma idiomática y perezosa —deja de recorrer en cuanto encuentra, igual que nuestro return— y la que usarías en un proyecto real. buscar_si existe para que veas que por dentro no hay magia: es un for con un if.
- Buscar todas las coincidencias
A veces no quieres la primera, quieres todas: «todas las tareas de Luis», «todas las de prioridad alta». El algoritmo cambia en un punto esencial: no hay salida temprana, porque hay que examinar la colección entera.
def buscar_todas(agenda, criterio):
"""Devuelve una lista con todas las tareas que cumplen el criterio."""
encontradas = []
for tarea in agenda:
if criterio(tarea):
encontradas.append(tarea) # se acumula y se sigue
return encontradas
# Es el patron acumulador de 03-02. En Python se escribe en una linea:
de_luis = [t for t in agenda if t["responsable"] == "Luis"] # comprension (05-01)
altas = list(filter(lambda t: t["prioridad"] == "alta", agenda)) # filter (04-05)
cuantas = sum(1 for t in agenda if not t["completada"]) # contar sin construir la listaLa comprensión es la opción preferente por legibilidad. Y ojo a la última línea: si solo necesitas cuántas hay, cuenta directamente en vez de construir una lista que ibas a tirar.
- Búsqueda binaria: la idea de descartar la mitad
Si los datos están ordenados, se puede hacer algo mucho mejor que mirar uno por uno. Piensa en cómo buscas una palabra en un diccionario de papel: no empiezas por la A, lo abres por la mitad, ves en qué letra estás y descartas media obra de un golpe. Ese es el algoritmo de búsqueda binaria, y su requisito es innegociable: la colección debe estar ordenada según el mismo criterio por el que buscas. Si no lo está, la búsqueda binaria no da error, da respuestas incorrectas, que es mucho peor.
Sobre esta lista ordenada de códigos de encargo de Estudio Alba —[102, 118, 134, 156, 170, 189, 203, 240], posiciones 0 a 7— buscamos el 170:
graph TD
A["Trozo 0..7 - medio=3 vale 156"] -->|"156 menor que 170: sobra la mitad izquierda"| B["Trozo 4..7 - medio=5 vale 189"]
B -->|"189 mayor que 170: sobra la mitad derecha"| C["Trozo 4..4 - medio=4 vale 170"]
C --> D["Encontrado en la posicion 4"]
Tres comparaciones para ocho elementos, frente a las cinco de la lineal. La diferencia parece modesta aquí; con un millón es abismal, y lo veremos numéricamente en la sección 8.
- Búsqueda binaria: implementación y trazado
El algoritmo mantiene tres variables: izquierda y derecha delimitan el trozo donde el valor todavía podría estar, y medio es la posición que se examina en cada vuelta.
def busqueda_binaria(valores, buscado):
"""Devuelve la posicion de buscado en la lista ORDENADA valores, o -1.
Requisito: valores debe estar ordenada de menor a mayor.
"""
izquierda = 0
derecha = len(valores) - 1
while izquierda <= derecha:
medio = (izquierda + derecha) // 2
if valores[medio] == buscado:
return medio
elif valores[medio] < buscado:
izquierda = medio + 1 # descartamos el medio y todo lo anterior
else:
derecha = medio - 1 # descartamos el medio y todo lo posterior
return -1Cuatro puntos donde se rompe este algoritmo, y hay que entenderlos con precisión:
while izquierda <= derecha, con el igual. Cuando ambos coinciden queda un elemento por mirar, y hay que mirarlo. Con<a secas fallarían justo los valores que quedan solos al final del trozo.(izquierda + derecha) // 2usa la división entera de 02-02: si la suma es impar se redondea hacia abajo. Da igual hacia qué lado, siempre que el trozo se reduzca cada vuelta.medio + 1ymedio - 1, nuncamedioa secas. Ya hemos comprobado quevalores[medio]no es el buscado, así que se descarta también él. Conizquierda = medio, el trozo deja de encoger cuando quedan dos elementos y el programa se queda colgado en un bucle infinito.- El trozo se reduce a la mitad en cada vuelta, y por eso el bucle siempre termina: o encuentra el valor, o
izquierdaacaba pasando aderecha.
Trazamos dos búsquedas sobre la lista de códigos, una fila por vuelta del while: el 170, que está, y el 150, que no.
| Buscado | Vuelta | izquierda |
derecha |
medio |
valores[medio] |
Comparación | Acción |
|---|---|---|---|---|---|---|---|
| 170 | 1 | 0 | 7 | 3 | 156 | 156 < 170 | izquierda = 4 |
| 170 | 2 | 4 | 7 | 5 | 189 | 189 > 170 | derecha = 4 |
| 170 | 3 | 4 | 4 | 4 | 170 | igual | return 4 |
| 150 | 1 | 0 | 7 | 3 | 156 | 156 > 150 | derecha = 2 |
| 150 | 2 | 0 | 2 | 1 | 118 | 118 < 150 | izquierda = 2 |
| 150 | 3 | 2 | 2 | 2 | 134 | 134 < 150 | izquierda = 3 |
| 150 | — | 3 | 2 | — | — | 3 <= 2 es falso |
return -1 |
Fíjate en las dos terceras vueltas: en ambas izquierda y derecha valen lo mismo, el bucle sí entra y examina el último candidato. Solo después izquierda supera a derecha y el bucle termina. Es la mejor demostración de que el <= no es un detalle cosmético.
Existe también una versión recursiva de este algoritmo, más corta y para muchos más elegante, que escribiremos en Recursión cuando tengamos la herramienta. Y, como siempre, Python ya trae esto hecho: el módulo bisect de la biblioteca estándar implementa la búsqueda binaria en C, y su función bisect.insort(lista, valor) inserta manteniendo el orden. En producción se usa bisect; esta implementación es para entender qué hace por dentro.
- Lineal frente a binaria
| Búsqueda lineal | Búsqueda binaria | |
|---|---|---|
| ¿Exige datos ordenados? | No | Sí, siempre |
| Comparaciones en 10 elementos | hasta 10 | hasta 4 |
| Comparaciones en 1.000 | hasta 1.000 | hasta 10 |
| Comparaciones en 1.000.000 | hasta 1.000.000 | hasta 20 |
| ¿Sirve para «contiene»? | Sí | No: solo igualdad y orden |
| Coste de mantener el requisito | Ninguno | Hay que ordenar antes |
| Herramienta de Python | in, .index(), next |
módulo bisect |
Los números salen de una regla sencilla: cada vuelta de la binaria divide entre dos el trozo pendiente, así que doblar los datos le añade una sola comparación. Pasar de mil a un millón de elementos multiplica por mil el trabajo de la lineal y le suma diez comparaciones a la binaria; el nombre formal de esa diferencia lo pondremos en Eficiencia y notación Big-O. Pero la tabla también avisa de la letra pequeña: ordenar cuesta. Si vas a buscar una sola vez en una lista desordenada, ordenarla para poder usar la binaria sale más caro que recorrerla entera. La binaria compensa cuando la colección ya está ordenada o cuando vas a buscar muchas veces.
- Búsqueda por clave: diccionarios y conjuntos
Queda la técnica más rápida de las tres, la que ya llevas usando desde 05-03 sin saber cómo funciona. Preguntar "Marta" in equipo o leer ficha["rol"] no recorre nada y no compara nada: encuentra el dato de un salto, tenga el diccionario diez claves o diez millones. El mecanismo se llama tabla hash, y la idea intuitiva, sin entrar en el detalle, es esta: Python aplica a la clave una función matemática —la función hash— que la convierte siempre en el mismo número; ese número indica en qué casilla de una tabla interna vive el par clave-valor; y para buscar se repite el cálculo y se va directamente a esa casilla, sin recorrer nada.
graph LR
A["clave: Marta"] --> B["funcion hash"] --> C["numero grande"]
C --> D["casilla 4 de la tabla"] --> E["valor guardado"]
Esa es también la explicación del requisito de 05-03: las claves de un diccionario y los elementos de un conjunto deben ser inmutables. Si usaras una lista como clave y luego la modificaras, su hash cambiaría, la casilla calculada sería otra y el dato quedaría guardado en un sitio donde nadie va a mirar. El resumen práctico es contundente:
| Colección | Cómo se busca | Trabajo con 1.000.000 de elementos |
|---|---|---|
| Lista desordenada | Recorriendo | hasta 1.000.000 de comparaciones |
| Lista ordenada | Descartando mitades | hasta 20 comparaciones |
| Conjunto o diccionario | Calculando la casilla | 1 cálculo, sin comparar apenas |
La conclusión operativa la anticipamos en 05-03 y ahora ya sabes por qué: si tu programa hace muchas comprobaciones de pertenencia sobre una colección, esa colección debería ser un conjunto o un diccionario, no una lista. El precio es memoria extra y la pérdida del orden de inserción como criterio de búsqueda.
- Índice invertido: preparar la búsqueda
Aquí llega la idea que convierte todo lo anterior en una decisión de diseño. Si Marta va a preguntar quince veces al día «¿qué tiene Luis?», recorrer la agenda entera quince veces es absurdo: mejor recorrerla una vez y construir un diccionario que responda al instante. Esa estructura se llama índice invertido: en vez de ir de la tarea a su responsable, va del responsable a sus tareas.
def indice_por_responsable(agenda):
"""Construye un diccionario responsable -> lista de tareas."""
indice = {}
for tarea in agenda:
nombre = tarea["responsable"]
if nombre not in indice:
indice[nombre] = [] # primera tarea de esa persona
indice[nombre].append(tarea)
return indice
indice = indice_por_responsable(agenda) # se recorre UNA vez
for t in indice.get("Luis", []): # despues, acceso instantaneo
print(t["titulo"])El patrón if nombre not in indice: indice[nombre] = [] es el mismo «primera vez» de 05-03, y también se escribe en una línea con indice.setdefault(nombre, []).append(tarea). indice.get("Luis", []) devuelve una lista vacía si esa persona no tiene nada, evitando el KeyError.
Esto es un intercambio: gastamos memoria y un recorrido previo para que las consultas posteriores sean inmediatas. Compensa cuando se consulta mucho y se modifica poco; no compensa cuando la agenda cambia sin parar, porque el índice queda obsoleto en cuanto se registra una tarea nueva. Es la primera aparición del intercambio tiempo-memoria, que estudiaremos con nombre propio en 06-04.
- TareaFácil v0.12: tres formas de localizar una tarea
Añadimos al programa una opción de búsqueda que usa las tres técnicas según el caso: por título exacto cuando Marta sabe qué busca, por texto contenido cuando solo recuerda una palabra, y por responsable con índice cuando quiere el reparto. El menú pasa a ocho opciones.
# tareafacil.py - Estudio Alba / Version 0.12: buscar en la agenda
OPCIONES = ("1", "2", "3", "4", "5", "6", "7", "8")
# --- Resto de constantes y funciones: sin cambios respecto a la v0.11 ---
def buscar_si(agenda, criterio):
"""Devuelve la primera tarea que cumple el criterio, o None si no hay ninguna."""
for tarea in agenda:
if criterio(tarea):
return tarea
return None
def buscar_todas(agenda, criterio):
"""Devuelve la lista de todas las tareas que cumplen el criterio."""
return [tarea for tarea in agenda if criterio(tarea)]
def indice_por_responsable(agenda):
"""Construye un diccionario responsable -> lista de tareas."""
indice = {}
for tarea in agenda:
indice.setdefault(tarea["responsable"], []).append(tarea)
return indice
def menu_buscar(agenda):
"""Localiza tareas por titulo exacto, por texto contenido o por responsable."""
if not agenda:
print("La agenda esta vacia: no hay nada que buscar.")
return
print("1) Titulo exacto 2) Texto contenido 3) Responsable")
modo = pedir_opcion("Como quieres buscar? (1-3): ", ("1", "2", "3"))
if modo == "1":
titulo = pedir_texto("Titulo exacto: ")
hallada = buscar_si(agenda, lambda t: t["titulo"] == titulo)
halladas = [] if hallada is None else [hallada]
elif modo == "2":
texto = pedir_texto("Texto a buscar: ").lower()
halladas = buscar_todas(agenda, lambda t: texto in t["titulo"].lower())
else:
nombre = pedir_opcion("De quien? ", EQUIPO)
halladas = indice_por_responsable(agenda).get(nombre, [])
print(f"Coincidencias: {len(halladas)}")
for tarea in halladas:
mostrar_ficha(tarea)
# En main(): la opcion 7 llama a menu_buscar(agenda) y la salida pasa a ser la 8.Decisiones de diseño que merece la pena señalar:
buscar_siybuscar_todasno saben nada de tareas. Reciben el criterio como callback, así que sirven para cualquier colección de diccionarios; toda la lógica específica vive en laslambdademenu_buscar.menu_buscares la única que habla con el usuario, cumpliendo la separación entre entrada/salida y lógica de 04-04. Las funciones de búsqueda no imprimen nada, y las tres ramas confluyen en una única listahalladasque se muestra igual en los tres casos. Además, la búsqueda por texto pasa ambos lados a minúsculas, de modo que «vidal», «Vidal» y «VIDAL» encuentran lo mismo.- El índice se construye dentro de la rama 3 y se descarta al salir. Es deliberado: la agenda cambia entre consulta y consulta, y un índice guardado en una global se quedaría obsoleto. Con veinte tareas, reconstruirlo cuesta un suspiro.
No hay búsqueda binaria en TareaFácil, y es una decisión consciente: la agenda no está ordenada por título, y ordenarla solo para poder buscar sería más caro que recorrerla. Con veinte tareas, la lineal es la respuesta correcta.
Errores Comunes y Consejos
Poner el return de «no encontrado» dentro del bucle. Es el error número uno de la lección: la función devuelve -1 o None en la primera vuelta que no coincide, sin mirar el resto. Ese return va fuera del for, a su misma altura.
Olvidar comprobar el resultado. Si buscar_tarea devuelve None y escribes tarea["prioridad"], obtienes TypeError: 'NoneType' object is not subscriptable. Comprueba siempre con if tarea is not None: antes de usar lo devuelto. Y usar búsqueda binaria sobre datos sin ordenar no falla con un error, falla con un resultado equivocado: dice que un elemento no está cuando sí está. Si tu función exige orden, dilo en el docstring. Escribir izquierda = medio en vez de medio + 1 hace que el trozo deje de encoger y el programa se cuelgue en un bucle infinito; si tu binaria se queda parada, mira ahí primero.
Confundir == con in al buscar textos. t["titulo"] == "feria" solo encuentra una tarea que se llame exactamente así; "feria" in t["titulo"] encuentra todas las que la contengan.
Consejo: normaliza antes de comparar. Aplica .strip().lower() a los dos lados cuando busques texto escrito por una persona: un espacio final es invisible y hace fallar la comparación.
Consejo: en producción, la herramienta del lenguaje. in, .index(), next(...), una comprensión, un diccionario o bisect están escritos en C, probados por millones de programas y son más rápidos que cualquier bucle que escribas. Estas implementaciones existen para que entiendas qué hacen por dentro y sepas cuál elegir, no para copiarlas a tu proyecto.
Ejercicios
Ejercicio 1: Trazar una búsqueda binaria
Sobre la lista ordenada [2, 5, 9, 14, 21, 30, 44, 51, 68] (posiciones 0 a 8), traza en una tabla la búsqueda binaria de los valores 44 y 7, indicando en cada vuelta izquierda, derecha, medio, el valor examinado y la acción. Di cuántas comparaciones necesita cada búsqueda y cuántas habría necesitado la búsqueda lineal.
Ejercicio 2: Buscar con criterio y contar
Escribe tres funciones sobre la agenda (lista de diccionarios con titulo, responsable, prioridad, dias, completada):
primera_urgente(agenda): devuelve la primera tarea pendiente de prioridad alta, oNone.pendientes_de(agenda, nombre): devuelve la lista de tareas pendientes de esa persona, sin distinguir mayúsculas.hay_bloqueo(agenda): devuelveTruesi alguien tiene más de tres tareas pendientes. Debe apoyarse en un índice, no en un bucle anidado.
Ejercicio 3: Índice por prioridad
Escribe indice_por_prioridad(agenda) que devuelva un diccionario con las claves "alta", "media" y "baja" —las tres siempre presentes, aunque alguna esté vacía— y como valor la lista de títulos de las tareas pendientes de esa prioridad. Después, escribe informe(indice) que imprima cada prioridad con su número de tareas y sus títulos.
Soluciones
Solución 1.
| Buscado | Vuelta | izquierda |
derecha |
medio |
Valor | Comparación | Acción |
|---|---|---|---|---|---|---|---|
| 44 | 1 | 0 | 8 | 4 | 21 | 21 < 44 | izquierda = 5 |
| 44 | 2 | 5 | 8 | 6 | 44 | igual | return 6 |
| 7 | 1 | 0 | 8 | 4 | 21 | 21 > 7 | derecha = 3 |
| 7 | 2 | 0 | 3 | 1 | 5 | 5 < 7 | izquierda = 2 |
| 7 | 3 | 2 | 3 | 2 | 9 | 9 > 7 | derecha = 1 |
| 7 | — | 2 | 1 | — | — | 2 <= 1 es falso |
return -1 |
Dos comparaciones para el 44 (la lineal habría necesitado siete) y tres para el 7 (la lineal, las nueve del recorrido completo, porque no está). Fíjate en la asimetría: la binaria tarda casi lo mismo encuentre o no, mientras que a la lineal el fracaso le cuesta siempre el máximo.
Solución 2.
def primera_urgente(agenda):
"""Devuelve la primera tarea pendiente de prioridad alta, o None."""
return buscar_si(agenda, lambda t: t["prioridad"] == "alta" and not t["completada"])
def pendientes_de(agenda, nombre):
"""Devuelve las tareas pendientes de esa persona, sin distinguir mayusculas."""
nombre = nombre.strip().lower()
return buscar_todas(agenda,
lambda t: t["responsable"].lower() == nombre and not t["completada"])
def hay_bloqueo(agenda, limite=3):
"""Indica si alguien acumula mas tareas pendientes de las permitidas."""
conteo = {}
for tarea in agenda:
if not tarea["completada"]:
conteo[tarea["responsable"]] = conteo.get(tarea["responsable"], 0) + 1
return any(n > limite for n in conteo.values())hay_bloqueo recorre la agenda una sola vez construyendo un índice de recuentos, en vez de recorrerla una vez por cada miembro del equipo. Con tres personas la diferencia es irrelevante; con trescientas, es la diferencia entre un programa usable y uno que no lo es. any devuelve True en cuanto encuentra un valor que cumple la condición y deja de mirar: es la salida temprana de la sección 2, ya incorporada al lenguaje.
Solución 3.
def indice_por_prioridad(agenda):
"""Devuelve {prioridad: [titulos pendientes]} con las tres claves siempre presentes."""
indice = {p: [] for p in PRIORIDADES} # las tres claves, aunque queden vacias
for tarea in agenda:
if not tarea["completada"]:
indice[tarea["prioridad"]].append(tarea["titulo"])
return indice
def informe(indice):
"""Imprime el reparto de tareas pendientes por prioridad."""
for prioridad in PRIORIDADES:
titulos = indice[prioridad]
print(f"{prioridad.upper():<8}{len(titulos):>3} tareas")
for titulo in titulos:
print(f" - {titulo}")La clave está en la primera línea: {p: [] for p in PRIORIDADES} es una comprensión de diccionario que crea las tres claves de antemano usando la constante PRIORIDADES. Gracias a eso el bucle puede hacer indice[...].append(...) sin comprobar nada, e informe recorre las prioridades en su orden lógico —alta, media, baja— en vez del orden en que aparecieran en la agenda. Elegir bien la estructura simplifica el código que viene después.
Conclusión
Buscar no es una operación, son tres preguntas: si existe, dónde está y cuál es; y una cuarta decisión, qué devolver cuando no hay nada (-1, None o un error). La búsqueda lineal recorre uno por uno con salida temprana mediante return: no exige nada de los datos, encuentra en una comparación en el mejor caso y necesita recorrerlo todo en el peor, que es precisamente cuando el elemento no está. Convertida en función de orden superior con un callback, la misma función busca por título, por responsable o por texto contenido; y sin salida temprana se transforma en la búsqueda de todas las coincidencias, que en Python se escribe con una comprensión de lista.
La búsqueda binaria cambia recorrer por descartar: exige datos ordenados y en cada vuelta parte el problema en dos, de modo que un millón de elementos se resuelven en veinte comparaciones. Sus tres trampas son el while izquierda <= derecha, el (izquierda + derecha) // 2 y el medio + 1 / medio - 1 que garantizan que el trozo encoge. Y la búsqueda por clave en diccionarios y conjuntos gana a las dos anteriores gracias a la tabla hash, que calcula la casilla en vez de comparar; de ahí que las claves deban ser inmutables. Cuando una consulta se repite mucho, merece la pena pagar un recorrido y algo de memoria para construir un índice invertido. Y en producción, siempre, la herramienta del lenguaje: in, .index(), next, una comprensión, un diccionario o bisect. TareaFácil llega a la v0.12 y Marta ya puede preguntarle a su agenda. Pero la búsqueda binaria se ha quedado sin usar por una razón concreta: la agenda no está ordenada, y ordenarla es justo lo que hemos estado delegando en sorted sin preguntar. En Algoritmos de ordenamiento abriremos esa segunda caja negra: qué hace sorted por dentro, qué significa que una ordenación sea estable y por qué ordenar es la operación más rentable de todo el curso.
Fundamentos de la Programación
Módulo 1: Introducción a la Programación
- ¿Qué es la programación?
- Historia de la programación
- Lenguajes de programación
- Entornos de desarrollo
- Del problema al algoritmo
Módulo 2: Conceptos Básicos
- Variables y tipos de datos
- Operadores y expresiones
- Entrada y salida de datos
- Conversión de tipos y validación de datos
Módulo 3: Estructuras de Control
Módulo 4: Funciones y Procedimientos
- Definición y uso de funciones
- Parámetros y retorno de valores
- Ámbito de variables
- Descomponer un programa en funciones
- Funciones como valores: lambda y orden superior
Módulo 5: Estructuras de Datos
- Listas y arreglos
- Cadenas de caracteres
- Diccionarios y conjuntos
- Tuplas y estructuras anidadas
- Guardar datos en archivos: texto, CSV y JSON
Módulo 6: Algoritmos Básicos
Módulo 7: Objetos y Organización del Código
- De los datos a los objetos: clases e instancias
- Atributos, métodos y constructor
- Colecciones de objetos
- Módulos, paquetes e importaciones
Módulo 8: Buenas Prácticas y Herramientas
- Documentación y comentarios
- Depuración y manejo de errores
- Control de versiones
- Pruebas automatizadas
- Estilo, legibilidad y refactorización
