Algoritmos de Ordenamiento
Publicado el 1 de diciembre de 2025
Algoritmos de Ordenamiento: Fundamentos y Aplicaciones Esenciales
El ordenamiento (sorting) es uno de los bloques de construcción más fundamentales en la informática. Comprender sus algoritmos y aplicaciones es esencial para cualquier desarrollador que busque escribir código eficiente.
1. Definición: ¿Qué es el Ordenamiento?
Sorting - Ordenamiento
El ordenamiento (sorting) es el procedimiento mediante el cual se organiza una colección de elementos (generalmente claves o registros) en una secuencia específica, ya sea ascendente o descendente.
Problema Formal
Entrada: Una secuencia de elementos
Salida: Una permutación tal que:
- Orden ascendente:
- Orden descendente:
# Entrada (desordenada)
numeros = [64, 34, 25, 12, 22, 11, 90]
# Salida (ordenada ascendentemente)
numeros_ordenados = [11, 12, 22, 25, 34, 64, 90]
# Verificación
for i in range(len(numeros_ordenados) - 1):
assert numeros_ordenados[i] <= numeros_ordenados[i + 1]
print("✓ Correctamente ordenado")
Pro Tip
El problema del ordenamiento ha sido el más exhaustivamente estudiado en la ciencia de la computación, resultando en docenas de diferentes algoritmos, cada uno con ventajas particulares en ciertas situaciones.
¿Qué define formalmente que una secuencia esté ordenada ascendentemente?
2. Propósito: La Relevancia del Ordenamiento Eficiente
El propósito de dedicar atención al ordenamiento es triple:
2.1 Fundamento Algorítmico
Bloque de Construcción
El ordenamiento es el bloque básico sobre el que se construyen muchos otros algoritmos, otorgando una gran capacidad para resolver otros problemas.
2.2 Paradigmas de Diseño
Información
Gran parte de las ideas clave utilizadas en el diseño de algoritmos aparecen y se explican en el contexto del ordenamiento:
- Divide y Vencerás (Merge Sort, Quick Sort)
- Estructuras de Datos (Heap Sort)
- Algoritmos Aleatorios (Randomized Quick Sort)
2.3 Eficiencia de Procesos
Impacto en el Rendimiento
La existencia de algoritmos de ordenamiento inteligentes que se ejecutan en representa una mejora sustancial sobre los algoritmos ingenuos que ejecutan en , especialmente cuando la entrada es grande.
Comparación de Rendimiento
Comparación de tiempos (asumiendo 1 millón de operaciones por segundo):
| Tamaño | Mejora | ||
|---|---|---|---|
| 100 | 0.01 ms | 0.0007 ms | 14x más rápido |
| 1,000 | 1 ms | 0.01 ms | 100x más rápido |
| 10,000 | 100 ms | 0.13 ms | 770x más rápido |
| 100,000 | 10 segundos | 1.7 segundos | 6x más rápido |
| 1,000,000 | 16.7 minutos | 20 segundos | 50x más rápido |
Conclusión: Para 1 millón de elementos, la diferencia es de 16.7 minutos vs 20 segundos. ¡Una mejora dramática!
import time
def bubble_sort(arr): # O(n²)
n = len(arr)
for i in range(n):
for j in range(0, n-i-1):
if arr[j] > arr[j+1]:
arr[j], arr[j+1] = arr[j+1], arr[j]
def merge_sort(arr): # O(n log n)
if len(arr) <= 1:
return arr
mid = len(arr) // 2
left = merge_sort(arr[:mid])
right = merge_sort(arr[mid:])
return merge(left, right)
# Prueba con 10,000 elementos
import random
datos = [random.randint(1, 10000) for _ in range(10000)]
# Bubble Sort
inicio = time.time()
bubble_sort(datos.copy())
tiempo_bubble = time.time() - inicio
# Merge Sort
inicio = time.time()
merge_sort(datos.copy())
tiempo_merge = time.time() - inicio
print(f"Bubble Sort: {tiempo_bubble:.2f}s")
print(f"Merge Sort: {tiempo_merge:.2f}s")
print(f"Mejora: {tiempo_bubble/tiempo_merge:.1f}x más rápido") 3. Aplicaciones: El Poder de los Datos Ordenados
Pro Tip
El ordenamiento se comporta más como una estructura de datos que como un problema en sí mismo. Una vez que un conjunto de elementos está ordenado, muchos otros procesos se vuelven sencillos.
Aplicaciones Fundamentales
3.1 Búsqueda (Searching)
Búsqueda Binaria
La búsqueda binaria puede determinar si un elemento se encuentra en un conjunto en tiempo , siempre que las claves estén ordenadas.
El preprocesamiento de búsqueda es la aplicación más importante del ordenamiento.
def busqueda_lineal(arr, objetivo):
"""O(n) - Sin requisito de orden"""
for i, elemento in enumerate(arr):
if elemento == objetivo:
return i
return -1
def busqueda_binaria(arr, objetivo):
"""O(log n) - Requiere array ordenado"""
izq, der = 0, len(arr) - 1
while izq <= der:
mid = (izq + der) // 2
if arr[mid] == objetivo:
return mid
elif arr[mid] < objetivo:
izq = mid + 1
else:
der = mid - 1
return -1
# Ejemplo
numeros = [11, 12, 22, 25, 34, 64, 90] # Ya ordenado
# Buscar 25
print(busqueda_lineal(numeros, 25)) # Resultado: 3 (después de 4 comparaciones)
print(busqueda_binaria(numeros, 25)) # Resultado: 3 (después de 2 comparaciones)
# Para 1,000,000 elementos:
# Lineal: hasta 1,000,000 comparaciones
# Binaria: máximo 20 comparaciones (log₂ 1,000,000 ≈ 20)
3.2 Singularidad de Elementos (Element Uniqueness)
Información
Para determinar si existen duplicados, un procedimiento eficiente ordena los números y luego realiza un barrido lineal, comprobando si algún par adyacente tiene una diferencia de cero.
def tiene_duplicados_ingenuo(arr):
"""O(n²) - Compara todos los pares"""
n = len(arr)
for i in range(n):
for j in range(i+1, n):
if arr[i] == arr[j]:
return True
return False
def tiene_duplicados_eficiente(arr):
"""O(n log n) - Ordena primero"""
arr_ordenado = sorted(arr) # O(n log n)
# Barrido lineal O(n)
for i in range(len(arr_ordenado) - 1):
if arr_ordenado[i] == arr_ordenado[i + 1]:
return True
return False
# Ejemplo
numeros = [3, 7, 2, 9, 2, 11]
print(tiene_duplicados_eficiente(numeros)) # True
# Explicación:
# Ordenado: [2, 2, 3, 7, 9, 11]
# ↑ ↑
# Duplicados adyacentes - fácil de detectar 3.3 Selección (Selection)
Información
Para encontrar el -ésimo elemento más grande, si las claves están en orden, se encuentra en tiempo constante , ya que se ubica en la -ésima posición del arreglo.
def kavo_elemento_mas_grande(arr, k):
"""
Encuentra el k-ésimo elemento más grande
Tiempo: O(n log n) por el ordenamiento
"""
arr_ordenado = sorted(arr, reverse=True)
return arr_ordenado[k - 1] # O(1) después de ordenar
# Ejemplos
numeros = [64, 34, 25, 12, 22, 11, 90]
print(kavo_elemento_mas_grande(numeros, 1)) # 90 (el más grande)
print(kavo_elemento_mas_grande(numeros, 3)) # 34 (tercero más grande)
print(kavo_elemento_mas_grande(numeros, 7)) # 11 (el más pequeño)
# Ordenado descendente: [90, 64, 34, 25, 22, 12, 11]
# 1° 2° 3° 4° 5° 6° 7°
3.4 Cascos Convexos (Convex Hulls)
Pro Tip
Al ordenar los puntos por la coordenada , se pueden insertar de izquierda a derecha en el casco, lo que permite completar la construcción en tiempo lineal después del ordenamiento.
Resumen de Aplicaciones
| Aplicación | Sin Ordenar | Con Ordenamiento | Mejora |
|---|---|---|---|
| Búsqueda | Logarítmica | ||
| Duplicados | después de ordenar | Cuadrática → Lineal | |
| k-ésimo elemento | después de ordenar | Constante | |
| Cascos convexos | después de ordenar | Lineal |
¿Cuál es la principal ventaja de ordenar los datos antes de buscar elementos repetidamente?
4. Consideraciones Prácticas de Implementación
Al implementar un algoritmo de ordenamiento en la práctica, se deben considerar varios parámetros clave:
4.1 Criterio de Orden
Información
Se debe especificar si el orden es:
- Ascendente:
- Descendente:
numeros = [64, 34, 25, 12, 22, 11, 90]
# Ascendente (por defecto)
ascendente = sorted(numeros)
print(f"Ascendente: {ascendente}")
# [11, 12, 22, 25, 34, 64, 90]
# Descendente
descendente = sorted(numeros, reverse=True)
print(f"Descendente: {descendente}")
# [90, 64, 34, 25, 22, 12, 11] 4.2 Registros de Datos
Advertencia
Si se ordena un registro complejo (como una lista de correo), se debe: 1. Especificar cuál es el campo clave 2. Mantener la integridad de todo el registro
# Registro complejo: estudiantes con múltiples campos
estudiantes = [
{"nombre": "Ana", "edad": 22, "promedio": 8.5},
{"nombre": "Carlos", "edad": 20, "promedio": 9.2},
{"nombre": "María", "edad": 21, "promedio": 7.8},
{"nombre": "Pedro", "edad": 20, "promedio": 8.9}
]
# Ordenar por edad (campo clave)
por_edad = sorted(estudiantes, key=lambda x: x["edad"])
print("Por edad:")
for est in por_edad:
print(f" {est['nombre']}: {est['edad']} años")
# Ordenar por promedio (descendente)
por_promedio = sorted(estudiantes, key=lambda x: x["promedio"], reverse=True)
print("\nPor promedio:")
for est in por_promedio:
print(f" {est['nombre']}: {est['promedio']}")
# Ordenar por múltiples criterios: primero edad, luego promedio
por_multiple = sorted(estudiantes, key=lambda x: (x["edad"], -x["promedio"]))
4.3 Estabilidad del Ordenamiento
Ordenamiento Estable
Un algoritmo es estable si mantiene el orden relativo original de los elementos con claves iguales.
Ejemplo: Ordenar estudiantes por edad
estudiantes = [
{"nombre": "Ana", "edad": 20},
{"nombre": "Carlos", "edad": 22},
{"nombre": "María", "edad": 20},
{"nombre": "Pedro", "edad": 21},
{"nombre": "Luis", "edad": 20}
]
# Orden original: Ana, Carlos, María, Pedro, Luis
# Ordenamiento ESTABLE por edad
estable = sorted(estudiantes, key=lambda x: x["edad"])
print("Ordenamiento estable:")
for est in estable:
print(f" {est['nombre']}: {est['edad']}")
# Ana: 20 ← Mantiene orden original
# María: 20 ← de los elementos con edad=20
# Luis: 20 ←
# Pedro: 21
# Carlos: 22
# Ordenamiento INESTABLE (hipotético)
# Podría resultar:
# Luis: 20 ← Orden cambiado
# Ana: 20 ← entre elementos
# María: 20 ← con edad=20
# Pedro: 21
# Carlos: 22 ¿Por qué importa?
La estabilidad es crucial cuando:
- Se ordena por múltiples criterios secuencialmente
- Se necesita preservar un orden previo
- Se trabaja con datos que ya tienen algún orden parcial
Algoritmos estables vs inestables:
| Estables | Inestables |
|---|---|
| Merge Sort | Quick Sort (típica implementación) |
| Insertion Sort | Heap Sort |
| Bubble Sort | Selection Sort |
| Counting Sort | - |
La estabilidad se puede lograr agregando la posición inicial como una clave secundaria.
4.4 Función de Comparación Personalizada
Pro Tip
Para datos no numéricos o reglas complejas, se utiliza una función de comparación que se pasa como argumento al procedimiento de ordenamiento.
from functools import cmp_to_key
# Ejemplo 1: Ordenar strings por longitud
palabras = ["python", "es", "genial", "y", "poderoso"]
por_longitud = sorted(palabras, key=len)
print(f"Por longitud: {por_longitud}")
# ['y', 'es', 'genial', 'python', 'poderoso']
# Ejemplo 2: Orden alfabético inverso
alfabetico_inverso = sorted(palabras, key=str.lower, reverse=True)
print(f"Alfabético inverso: {alfabetico_inverso}")
# Ejemplo 3: Comparación compleja con cmp_to_key
def comparar_numeros_especial(a, b):
"""
Pares antes que impares
Dentro de cada grupo, orden ascendente
""" # Ambos pares o ambos impares
if (a % 2) == (b % 2):
return a - b # a es par, b es impar
if a % 2 == 0:
return -1 # a es impar, b es par
return 1
numeros = [7, 2, 9, 4, 1, 8, 3, 6, 5]
ordenado = sorted(numeros, key=cmp_to_key(comparar_numeros_especial))
print(f"Pares primero: {ordenado}")
# [2, 4, 6, 8, 1, 3, 5, 7, 9]
# ← pares → ← impares →
¿Qué significa que un algoritmo de ordenamiento sea 'estable'?
5. Heapsort: Mejora por Estructura de Datos
El Problema del Selection Sort Simple
Selection Sort - O(n²)
El Selection Sort clásico opera en iteraciones. En cada iteración, requiere un barrido lineal a través de la porción no ordenada del arreglo para encontrar el elemento más pequeño.
Complejidad total:
def selection_sort(arr):
"""
Selection Sort ingenuo - O(n²)
"""
n = len(arr)
for i in range(n):
# Encontrar el mínimo en el resto del array
min_idx = i
for j in range(i + 1, n): # ← Barrido lineal O(n)
if arr[j] < arr[min_idx]:
min_idx = j
# Intercambiar
arr[i], arr[min_idx] = arr[min_idx], arr[i]
return arr
# Análisis de complejidad:
# Iteración 1: n-1 comparaciones
# Iteración 2: n-2 comparaciones
# ...
# Iteración n: 1 comparación
# Total: (n-1) + (n-2) + ... + 1 = n(n-1)/2 = O(n²) La Solución: Heapsort
Heapsort - O(n log n)
Heapsort es una implementación del Selection Sort que utiliza una estructura de datos especializada llamada heap (montículo) para mejorar la complejidad de a .
¿Qué es un Heap?
Información
Un heap es un árbol binario completo que satisface la propiedad de heap: - Min-heap: Cada nodo padre es menor o igual que sus hijos - Max-heap: Cada nodo padre es mayor o igual que sus hijos
Representación de un Min-Heap:
1
/ \
3 2
/ \ / \
7 5 4 6Representación en array:
[1, 3, 2, 7, 5, 4, 6]
0 1 2 3 4 5 6Relaciones padre-hijo:
- Para nodo en índice
i:- Padre:
(i - 1) // 2 - Hijo izquierdo:
2 * i + 1 - Hijo derecho:
2 * i + 2
- Padre:
Operaciones principales:
| Operación | Complejidad | Descripción |
|---|---|---|
insert(x) | Insertar elemento y mantener propiedad | |
extract_min() | Remover y retornar el mínimo | |
get_min() | Ver el mínimo sin remover | |
heapify() | Construir heap desde array |
El Método Heapsort
def heapify(arr, n, i):
"""
Mantener la propiedad de max-heap
para el subárbol con raíz en índice i
"""
largest = i
left = 2 * i + 1
right = 2 * i + 2
# Si hijo izquierdo es mayor que la raíz
if left < n and arr[left] > arr[largest]:
largest = left
# Si hijo derecho es mayor que el mayor hasta ahora
if right < n and arr[right] > arr[largest]:
largest = right
# Si el mayor no es la raíz
if largest != i:
arr[i], arr[largest] = arr[largest], arr[i]
# Recursivamente heapify el subárbol afectado
heapify(arr, n, largest)
def heap_sort(arr):
"""
Heapsort - O(n log n)
"""
n = len(arr)
# Paso 1: Construir max-heap - O(n)
for i in range(n // 2 - 1, -1, -1):
heapify(arr, n, i)
# Paso 2: Extraer elementos uno por uno - O(n log n)
for i in range(n - 1, 0, -1):
# Mover raíz actual al final
arr[0], arr[i] = arr[i], arr[0]
# Heapify la raíz reducida
heapify(arr, i, 0)
return arr
# Ejemplo
numeros = [64, 34, 25, 12, 22, 11, 90]
print(f"Original: {numeros}")
heap_sort(numeros)
print(f"Ordenado: {numeros}")
Análisis de Heapsort
Ventajas de Heapsort
Complejidad: garantizado en el peor caso
Proceso:
- Construir heap: tiempo
- Extraer n elementos: Cada extracción
- Total:
Características:
- ✅ Simple de programar
- ✅ Rendimiento garantizado
- ✅ Ordenamiento “in-place” (no requiere memoria extra)
- ⚠️ No es estable
- ⚠️ En la práctica, puede ser más lento que Quick Sort
Comparación: Selection Sort vs Heapsort
| Aspecto | Selection Sort | Heapsort |
|---|---|---|
| Complejidad | ||
| Encontrar mínimo | Barrido lineal | Extracción del heap |
| Estructura | Array simple | Heap (array con propiedad especial) |
| Iteraciones | ||
| Total |
Ejemplo: Ordenar [64, 34, 25, 12, 22, 11, 90]
Paso 1: Construir Max-Heap
Array inicial: [64, 34, 25, 12, 22, 11, 90]
Heapify desde el último nodo interno:
90
/ \
34 64
/ \ / \
12 22 11 25
Array: [90, 34, 64, 12, 22, 11, 25]
Paso 2: Extracciones sucesivas
Extracción 1: Remover 90
[25, 34, 64, 12, 22, 11 | 90]
Heapify → [64, 34, 25, 12, 22, 11 | 90]
Extracción 2: Remover 64
[11, 34, 25, 12, 22 | 64, 90]
Heapify → [34, 22, 25, 12, 11 | 64, 90]
Extracción 3: Remover 34
[11, 22, 25, 12 | 34, 64, 90]
Heapify → [25, 22, 11, 12 | 34, 64, 90]
Extracción 4: Remover 25
[12, 22, 11 | 25, 34, 64, 90]
Heapify → [22, 12, 11 | 25, 34, 64, 90]
Extracción 5: Remover 22
[11, 12 | 22, 25, 34, 64, 90]
Heapify → [12, 11 | 22, 25, 34, 64, 90]
Extracción 6: Remover 12
[11 | 12, 22, 25, 34, 64, 90]
Resultado final: [11, 12, 22, 25, 34, 64, 90]
¿Cuál es la mejora clave que Heapsort proporciona sobre Selection Sort simple?
6. Panorama de Algoritmos de Ordenamiento
Comparación de Algoritmos Principales
| Algoritmo | Mejor Caso | Caso Promedio | Peor Caso | Espacio | Estable | Método |
|---|---|---|---|---|---|---|
| Bubble Sort | ✅ Sí | Intercambio | ||||
| Insertion Sort | ✅ Sí | Inserción | ||||
| Selection Sort | ❌ No | Selección | ||||
| Merge Sort | ✅ Sí | Divide y vencerás | ||||
| Quick Sort | ❌ No | Divide y vencerás | ||||
| Heap Sort | ❌ No | Estructura de datos | ||||
| Counting Sort | ✅ Sí | No comparativo | ||||
| Radix Sort | ✅ Sí | No comparativo |
Guía de Selección de Algoritmo
Para arrays pequeños (n < 50):
- ✅ Insertion Sort: Simple, eficiente para pocos elementos
Para arrays casi ordenados:
- ✅ Insertion Sort: en el mejor caso
- ✅ Bubble Sort: También si está optimizado
Para garantía de O(n log n) en peor caso:
- ✅ Merge Sort: Siempre , pero usa espacio extra
- ✅ Heap Sort: garantizado e in-place
Para el mejor rendimiento promedio:
- ✅ Quick Sort: Más rápido en la práctica, aunque peor caso es
- ✅ Tim Sort (híbrido): Usado en Python y Java, combina Merge e Insertion
Para datos con rango limitado:
- ✅ Counting Sort: si no es muy grande
- ✅ Radix Sort: Para enteros o strings
Para mantener estabilidad:
- ✅ Merge Sort
- ✅ Insertion Sort
- ✅ Bubble Sort
- ❌ Evitar Quick Sort, Heap Sort, Selection Sort
Para uso limitado de memoria:
- ✅ Heap Sort: In-place y garantizado
- ✅ Quick Sort: espacio en el stack
- ❌ Evitar Merge Sort ( espacio extra)
7. Cierre: La Lección Fundamental
Lección Clave
El ordenamiento reside en el corazón de numerosos algoritmos. La lección fundamental para cualquier diseñador de algoritmos es:
Ordenar los datos es una de las primeras cosas que debe intentar en la búsqueda de la eficiencia.
Principios a Recordar
- No temas al ordenamiento
- No temas dedicar tiempo al ordenamiento, siempre y cuando se utilice una rutina eficiente
- El costo se amortiza rápidamente con operaciones posteriores más eficientes
- Usa librerías estándar
- La mayoría de lenguajes proporcionan rutinas optimizadas (ej:
qsorten C,sorted()en Python) - Estas implementaciones suelen ser la mejor opción para uso general
- Comprende los paradigmas
- Divide y Vencerás: Merge Sort, Quick Sort
- Estructuras de Datos: Heap Sort
- Aleatorización: Randomized Quick Sort
Estos conceptos se extienden más allá del ordenamiento
- Considera el contexto
- Tamaño de los datos
- Si están parcialmente ordenados
- Restricciones de memoria
- Necesidad de estabilidad
En la Práctica
Aunque algoritmos como Quick Sort pueden ser más rápidos en la práctica (con tiempo esperado ), el conocimiento de las diferentes técnicas ilustradas por Heap Sort, Merge Sort y Quick Sort es fundamental para abordar cualquier problema de procesamiento de datos.
Casos de Uso en Sistemas Reales
1. Bases de Datos
- Ordenamiento para índices B-tree
- ORDER BY en consultas SQL
- Merge joins entre tablas
2. Sistemas Operativos
- Planificación de procesos
- Gestión de memoria (ordenar bloques libres)
- Sistemas de archivos
3. Compiladores
- Ordenamiento de símbolos
- Optimización de código
- Análisis de dependencias
4. Aplicaciones Web
- Ordenar resultados de búsqueda
- Rankings y leaderboards
- Feeds de redes sociales (timeline)
5. Algoritmos de Grafos
- Algoritmo de Kruskal (MST)
- Algoritmo de Dijkstra
- Procesamiento de eventos
6. Procesamiento de Big Data
- MapReduce: fase de shuffle y sort
- Merge de datasets distribuidos
- Procesamiento de logs
Resumen de Conceptos Clave
Definición
- Ordenamiento: Organizar elementos en secuencia ascendente o descendente
Importancia
- Bloque de construcción fundamental
- Ilustra paradigmas de diseño clave
- Mejora dramática: →
Aplicaciones
- Búsqueda binaria:
- Detección de duplicados: después de ordenar
- Selección k-ésimo: después de ordenar
Consideraciones Prácticas
- Criterio de orden (ascendente/descendente)
- Registros complejos (campo clave)
- Estabilidad
- Función de comparación personalizada
Algoritmos Clave
- Ingenuous: Bubble, Insertion, Selection →
- Eficientes: Merge, Quick, Heap →
- Especializados: Counting, Radix → o
Heapsort
- Mejora Selection Sort: →
- Usa heap para extraer mínimo en
- Garantía de rendimiento in-place
Regla de Oro
“Ordena primero, optimiza después”
Ordenar los datos suele ser el primer paso hacia la eficiencia.
¿Cuál es la principal razón por la que el ordenamiento es considerado un 'bloque de construcción' en algoritmos?
¡Excelente trabajo!
Has completado los fundamentos de algoritmos de ordenamiento. Ahora comprendes:
- Qué es el ordenamiento y por qué es fundamental
- Las aplicaciones prácticas de datos ordenados
- Consideraciones de implementación (estabilidad, funciones de comparación)
- Cómo Heapsort mejora Selection Sort usando estructuras de datos
- El panorama de algoritmos disponibles y cuándo usar cada uno
Próximo paso: Profundizar en algoritmos específicos (Quick Sort, Merge Sort) y explorar algoritmos avanzados de búsqueda y optimización.