Un algoritmo es una secuencia finita y ordenada de pasos que transforma una entrada en una salida. No depende necesariamente de un lenguaje de programación: una receta, una ruta de navegación y un procedimiento de clasificación pueden describirse como algoritmos. En desarrollo de software, el objetivo es expresar esos pasos con suficiente claridad para que una persona pueda revisarlos y una computadora pueda ejecutarlos.
Para relacionar este concepto con Python, revisa la guía de lógica de programación, los artículos sobre bucles for, funciones, listas y manejo de errores.
Como referencias externas puedes consultar la definición de algoritmo del NIST Dictionary of Algorithms and Data Structures y el tutorial oficial de control de flujo de Python.
Características de un buen algoritmo
Un algoritmo debe tener entradas claramente definidas, pasos sin ambigüedad, un resultado verificable y una condición de finalización. También debe ser correcto para los casos esperados y razonable en tiempo y memoria. Que un código funcione con un ejemplo no demuestra que el algoritmo sea correcto para todos los datos válidos.
Ejemplo cotidiano
Un algoritmo para preparar té podría expresarse así:
- Calentar agua hasta alcanzar la temperatura adecuada.
- Colocar el té en una taza.
- Verter el agua.
- Esperar el tiempo de infusión.
- Retirar el té y servir.
Los detalles importan. “Calentar un poco” es ambiguo; una temperatura o condición observable es más precisa. En programación ocurre lo mismo: cada paso necesita una regla que pueda comprobarse.
Entradas, proceso y salida
La mayoría de algoritmos puede analizarse con tres partes:
Entrada: lista de precios
Proceso: sumar todos los precios
Salida: total de la compra
En Python:
def calcular_total(precios):
total = 0
for precio in precios:
total += precio
return total
La función recibe la entrada, ejecuta el proceso y devuelve la salida. Separar estas responsabilidades ayuda a probar el algoritmo sin depender del teclado o de la pantalla.
Describir algoritmos con pseudocódigo
El pseudocódigo permite pensar en la lógica antes de preocuparse por la sintaxis:
INICIO
LEER edad
SI edad >= 18
MOSTRAR "Mayor de edad"
SINO
MOSTRAR "Menor de edad"
FIN SI
FIN
Después puedes convertirlo a Python:
edad = int(input("Edad: "))
if edad >= 18:
print("Mayor de edad")
else:
print("Menor de edad")
Diagramas de flujo
Los diagramas de flujo representan inicio, decisiones, procesos y resultados con símbolos. Son útiles para explicar procesos a personas no técnicas y para descubrir rutas olvidadas. Sin embargo, un diagrama demasiado grande puede ser más difícil de mantener que un pseudocódigo breve y pruebas automatizadas.
Algoritmo para encontrar el mayor valor
def mayor(valores):
if not valores:
raise ValueError("La colección está vacía")
resultado = valores[0]
for valor in valores[1:]:
if valor > resultado:
resultado = valor
return resultado
El algoritmo compara cada elemento una vez. También existe la función integrada max, que deberías usar en código normal. Implementarlo manualmente es útil para comprender el proceso.
Corrección y casos límite
Un algoritmo de promedio puede parecer sencillo:
def promedio(valores):
return sum(valores) / len(valores)
Pero falla con una lista vacía. Una versión explícita valida ese caso:
def promedio(valores):
if not valores:
raise ValueError("No hay valores")
return sum(valores) / len(valores)
Los casos límite incluyen colecciones vacías, cero, números negativos, duplicados, valores muy grandes, entradas inválidas y límites exactos.
Búsqueda lineal
def buscar(valores, objetivo):
for indice, valor in enumerate(valores):
if valor == objetivo:
return indice
return -1
En el peor caso revisa todos los elementos. Para una lista de tamaño n, su complejidad temporal es O(n). Es una solución correcta y sencilla cuando los datos no están ordenados o la colección es pequeña.
Búsqueda binaria
Si los datos están ordenados, la búsqueda binaria reduce el intervalo a la mitad en cada paso:
def busqueda_binaria(valores, objetivo):
izquierda = 0
derecha = len(valores) - 1
while izquierda <= derecha:
medio = (izquierda + derecha) // 2
actual = valores[medio]
if actual == objetivo:
return medio
if actual < objetivo:
izquierda = medio + 1
else:
derecha = medio - 1
return -1
Su complejidad es O(log n), pero exige una colección ordenada. Si necesitas ordenar solo para una búsqueda, el coste total puede no compensar.
Algoritmos de ordenación
Una ordenación por selección puede implementarse con fines educativos:
def seleccion(valores):
resultado = valores.copy()
for i in range(len(resultado)):
menor = i
for j in range(i + 1, len(resultado)):
if resultado[j] < resultado[menor]:
menor = j
resultado[i], resultado[menor] = resultado[menor], resultado[i]
return resultado
Este algoritmo es O(n²). En aplicaciones reales, utiliza sorted o list.sort, que implementan un algoritmo altamente optimizado y estable.
Complejidad temporal
La notación Big O describe cómo crece el trabajo al aumentar la entrada. Algunos órdenes habituales son O(1), O(log n), O(n), O(n log n) y O(n²). No representa segundos exactos, sino tendencia de crecimiento. Dos algoritmos O(n) pueden tener constantes diferentes, pero ambos escalan de forma similar.
Complejidad espacial
También importa la memoria. Crear una copia completa de una lista requiere memoria adicional O(n), mientras que modificarla en el lugar puede usar O(1) adicional. La elección depende de si debes conservar la entrada original y de los límites del sistema.
Recursión e iteración
El factorial puede expresarse de forma recursiva:
def factorial(n):
if n < 0:
raise ValueError("n debe ser positivo")
if n <= 1:
return 1
return n * factorial(n - 1)
También puede hacerse con un bucle:
def factorial_iterativo(n):
if n < 0:
raise ValueError("n debe ser positivo")
resultado = 1
for numero in range(2, n + 1):
resultado *= numero
return resultado
Python tiene un límite de recursión, por lo que la versión iterativa suele ser más segura para valores grandes.
Elegir estructuras de datos
El algoritmo y la estructura están relacionados. Buscar pertenencia repetidamente en una lista suele ser O(n), mientras que un set ofrece búsquedas promedio O(1). Un diccionario permite asociar claves y valores. Elegir la estructura correcta puede mejorar el rendimiento más que una microoptimización.
Probar un algoritmo
def test_buscar():
assert buscar([10, 20, 30], 20) == 1
assert buscar([10, 20, 30], 99) == -1
assert buscar([], 10) == -1
Incluye casos normales, límites y entradas inválidas. Si el algoritmo modifica datos, verifica también que no cambie accidentalmente la entrada cuando el contrato promete conservarla.
Medir antes de optimizar
La complejidad orienta, pero el perfilado muestra dónde se consume tiempo en un programa real. Primero garantiza la corrección y la claridad; luego mide con datos representativos. Optimizar una sección que apenas se ejecuta aumenta complejidad sin beneficio.
Errores frecuentes
Los errores habituales son no definir cuándo termina el proceso, ignorar una entrada vacía, usar bucles anidados innecesarios, confundir corrección con rendimiento y elegir una estructura inadecuada. También es común optimizar demasiado pronto o copiar un algoritmo sin comprender sus requisitos.
Conclusión
Un algoritmo es una solución expresada como pasos claros, finitos y verificables. Aprender a separar entrada, proceso y salida, escribir pseudocódigo, contemplar casos límite y analizar tiempo y memoria mejora cualquier programa. Python facilita convertir la idea en código, pero la calidad final depende de la lógica, las pruebas y la elección consciente de estructuras de datos.






