Fundamentos
Tutorial Fundamentos: Algoritmos de ordenamiento
Aprende a implementar y optimizar algoritmos de ordenamiento por burbuja, inserción, y selección en PseInt y Python. Aumenta tu eficiencia en programación.
Aprende Fundamentos GRATIS y certifícateOrdenamiento por burbuja, inserción y selección: fundamentos y ejemplos
Los algoritmos de ordenamiento son fundamentales en programación, ya que permiten organizar datos de manera eficiente. Entre los más básicos y conocidos se encuentran el ordenamiento por burbuja, el ordenamiento por inserción y el ordenamiento por selección. A continuación, exploraremos los principios de cada uno y proporcionaremos ejemplos prácticos en PseInt y su equivalente en Python.
El ordenamiento por burbuja es un método sencillo que compara pares de elementos adyacentes y los intercambia si están en el orden incorrecto. Este proceso se repite hasta que no se requieran más intercambios, lo que indica que la lista está ordenada.
Ejemplo en PseInt:
Algoritmo OrdenamientoBurbuja
Definir n, i, j, temp Como Entero
Dimension lista[100]
Escribir "Ingrese la cantidad de elementos:"
Leer n
Para i <- 1 Hasta n Hacer
Escribir "Elemento ", i, ":"
Leer lista[i]
Fin Para
Para i <- 1 Hasta n - 1 Hacer
Para j <- 1 Hasta (n - i) Hacer
Si lista[j] > lista[j + 1] Entonces
temp <- lista[j]
lista[j] <- lista[j + 1]
lista[j + 1] <- temp
Fin Si
Fin Para
Fin Para
Escribir "Lista ordenada por burbuja:"
Para i <- 1 Hasta n Hacer
Escribir lista[i]
Fin Para
FinAlgoritmo
Ejemplo equivalente en Python:
n = int(input("Ingrese la cantidad de elementos: "))
lista = []
for i in range(n):
elemento = int(input(f"Elemento {i+1}: "))
lista.append(elemento)
for i in range(n - 1):
for j in range(n - i - 1):
if lista[j] > lista[j + 1]:
lista[j], lista[j + 1] = lista[j + 1], lista[j]
print("Lista ordenada por burbuja:")
for elemento in lista:
print(elemento)
El ordenamiento por inserción construye la lista ordenada de forma incremental. Cada elemento se inserta en la posición correcta respecto a los elementos ya ordenados, simulando cómo ordenaríamos cartas en la mano.
Ejemplo en PseInt:
Algoritmo OrdenamientoInsercion
Definir n, i, j, clave Como Entero
Dimension lista[100]
Escribir "Ingrese la cantidad de elementos:"
Leer n
Para i <- 1 Hasta n Hacer
Escribir "Elemento ", i, ":"
Leer lista[i]
Fin Para
Para i <- 2 Hasta n Hacer
clave <- lista[i]
j <- i - 1
Mientras j >= 1 y lista[j] > clave Hacer
lista[j + 1] <- lista[j]
j <- j - 1
Fin Mientras
lista[j + 1] <- clave
Fin Para
Escribir "Lista ordenada por inserción:"
Para i <- 1 Hasta n Hacer
Escribir lista[i]
Fin Para
FinAlgoritmo
Ejemplo equivalente en Python:
n = int(input("Ingrese la cantidad de elementos: "))
lista = []
for i in range(n):
elemento = int(input(f"Elemento {i+1}: "))
lista.append(elemento)
for i in range(1, n):
clave = lista[i]
j = i - 1
while j >= 0 and lista[j] > clave:
lista[j + 1] = lista[j]
j -= 1
lista[j + 1] = clave
print("Lista ordenada por inserción:")
for elemento in lista:
print(elemento)
El ordenamiento por selección busca el elemento mínimo en la lista y lo intercambia con el que está en la posición inicial. Luego, se repite el proceso con el resto de la lista hasta completar el ordenamiento.
Ejemplo en PseInt:
Algoritmo OrdenamientoSeleccion
Definir n, i, j, minPos, temp Como Entero
Dimension lista[100]
Escribir "Ingrese la cantidad de elementos:"
Leer n
Para i <- 1 Hasta n Hacer
Escribir "Elemento ", i, ":"
Leer lista[i]
Fin Para
Para i <- 1 Hasta n - 1 Hacer
minPos <- i
Para j <- i + 1 Hasta n Hacer
Si lista[j] < lista[minPos] Entonces
minPos <- j
Fin Si
Fin Para
Si minPos <> i Entonces
temp <- lista[i]
lista[i] <- lista[minPos]
lista[minPos] <- temp
Fin Si
Fin Para
Escribir "Lista ordenada por selección:"
Para i <- 1 Hasta n Hacer
Escribir lista[i]
Fin Para
FinAlgoritmo
Ejemplo equivalente en Python:
n = int(input("Ingrese la cantidad de elementos: "))
lista = []
for i in range(n):
elemento = int(input(f"Elemento {i+1}: "))
lista.append(elemento)
for i in range(n - 1):
min_idx = i
for j in range(i + 1, n):
if lista[j] < lista[min_idx]:
min_idx = j
if min_idx != i:
lista[i], lista[min_idx] = lista[min_idx], lista[i]
print("Lista ordenada por selección:")
for elemento in lista:
print(elemento)
Estos algoritmos son esenciales para entender cómo funcionan los métodos de ordenamiento. Aunque no son los más eficientes para grandes conjuntos de datos, su simplicidad los hace ideales para aprender los conceptos básicos de algoritmia. Comprender su funcionamiento es clave para avanzar hacia algoritmos más complejos y eficientes.
Ordenamiento rápido (Quick Sort) y por fusión (Merge Sort): conceptos básicos
El ordenamiento rápido, conocido como Quick Sort, es un algoritmo eficiente que utiliza el enfoque de divide y vencerás. Su idea principal es seleccionar un elemento como pivote y reorganizar el array de manera que los elementos menores que el pivote queden a su izquierda y los mayores a su derecha. Luego, se aplica recursivamente el mismo proceso a las sublistas izquierda y derecha.
Implementación de Quick Sort en PseInt:
Algoritmo QuickSortIterativo
// Declarar arreglos y variables
Dimension lista[100]
Dimension LStack[100], RStack[100]
Definir n, i, top Como Entero
Definir izquierda, derecha, indicePivote, valorPivote, temp, pivote Como Entero
// 1. Lectura de datos
Escribir "Ingrese la cantidad de elementos:"
Leer n
// 2. Leer los valores en 'lista'
Para i <- 1 Hasta n Hacer
Escribir "Elemento ", i, ":"
Leer lista[i]
Fin Para
// 3. Inicializar la "pila" con un solo rango [1..n]
top <- 1
LStack[top] <- 1
RStack[top] <- n
// 4. Bucle principal: mientras haya rangos en la pila
Mientras top > 0 Hacer
// Extraer los límites
izquierda <- LStack[top]
derecha <- RStack[top]
top <- top - 1
// Solo trabajar si izquierda < derecha
Si izquierda < derecha Entonces
// PARTICION en línea
// Escogemos lista[izquierda] como pivote
indicePivote <- izquierda
valorPivote <- lista[izquierda]
// Recorremos de (izquierda+1) a derecha
Para i <- izquierda + 1 Hasta derecha Hacer
Si lista[i] < valorPivote Entonces
indicePivote <- indicePivote + 1
// Intercambio (swap)
temp <- lista[i]
lista[i] <- lista[indicePivote]
lista[indicePivote] <- temp
Fin Si
Fin Para
// Colocar el pivote en su posición final
temp <- lista[izquierda]
lista[izquierda] <- lista[indicePivote]
lista[indicePivote] <- temp
pivote <- indicePivote
// Empujar subrango izquierdo [izquierda..(pivote-1)], si válido
Si (pivote - 1) > izquierda Entonces
top <- top + 1
LStack[top] <- izquierda
RStack[top] <- pivote - 1
Fin Si
// Empujar subrango derecho [(pivote+1)..derecha], si válido
Si (pivote + 1) < derecha Entonces
top <- top + 1
LStack[top] <- pivote + 1
RStack[top] <- derecha
Fin Si
Fin Si
Fin Mientras
// 5. Imprimir el resultado final
Escribir "Lista ordenada con Quick Sort (iterativo):"
Para i <- 1 Hasta n Hacer
Escribir lista[i]
Fin Para
FinAlgoritmo
Implementación equivalente en Python:
def quicksort(lista, izquierda, derecha):
if izquierda < derecha:
pivote = particionar(lista, izquierda, derecha)
quicksort(lista, izquierda, pivote - 1)
quicksort(lista, pivote + 1, derecha)
def particionar(lista, izquierda, derecha):
indice_pivote = izquierda
pivote = lista[izquierda]
for i in range(izquierda + 1, derecha + 1):
if lista[i] < pivote:
indice_pivote += 1
lista[i], lista[indice_pivote] = lista[indice_pivote], lista[i]
lista[izquierda], lista[indice_pivote] = lista[indice_pivote], lista[izquierda]
return indice_pivote
n = int(input("Ingrese la cantidad de elementos: "))
lista = []
for i in range(n):
elemento = int(input(f"Elemento {i+1}: "))
lista.append(elemento)
quicksort(lista, 0, n - 1)
print("Lista ordenada con Quick Sort:")
for elemento in lista:
print(elemento)
El ordenamiento por fusión, o Merge Sort, también se basa en el principio de divide y vencerás. Este algoritmo divide repetidamente la lista en mitades hasta que cada sublista tiene un solo elemento, y luego fusiona estas sublistas de manera ordenada hasta reconstruir la lista completa.
Implementación de Merge Sort en PseInt:
Algoritmo MergeSortIterativo
// Declaración de los arreglos
Dimension lista[100], izquierdo[100], derecho[100]
Definir n, s, start, mid, end, i, j, k Como Entero
Definir n1, n2 Como Entero
// 1. Leer la cantidad de elementos
Escribir "Ingrese la cantidad de elementos:"
Leer n
// 2. Leer los valores en 'lista'
Para i <- 1 Hasta n Hacer
Escribir "Elemento ", i, ":"
Leer lista[i]
Fin Para
// 3. Fase de ordenamiento bottom-up
s <- 1 // Tamaño del sub-arreglo
Mientras s < n Hacer
start <- 1 // Inicio de cada par de sub-arreglos
Mientras start <= n - s Hacer
mid <- start + s - 1
end <- mid + s
Si end > n Entonces
end <- n
Fin Si
// 3.1 Calcular tamaños n1 y n2
n1 <- mid - start + 1
n2 <- end - mid
// 3.2 Copiar sub-arreglo izquierdo en 'izquierdo[]'
Para i <- 1 Hasta n1 Hacer
izquierdo[i] <- lista[start + i - 1]
Fin Para
// 3.3 Copiar sub-arreglo derecho en 'derecho[]'
Para j <- 1 Hasta n2 Hacer
derecho[j] <- lista[mid + j]
Fin Para
// 3.4 Fusionar los dos sub-arreglos en 'lista'
i <- 1
j <- 1
k <- start
// Combinar mientras queden elementos en ambos
Mientras i <= n1 y j <= n2 Hacer
Si izquierdo[i] <= derecho[j] Entonces
lista[k] <- izquierdo[i]
i <- i + 1
Sino
lista[k] <- derecho[j]
j <- j + 1
Fin Si
k <- k + 1
Fin Mientras
// Si quedaron elementos en 'izquierdo[]'
Mientras i <= n1 Hacer
lista[k] <- izquierdo[i]
i <- i + 1
k <- k + 1
Fin Mientras
// Si quedaron elementos en 'derecho[]'
Mientras j <= n2 Hacer
lista[k] <- derecho[j]
j <- j + 1
k <- k + 1
Fin Mientras
// Aumentar 'start' para el siguiente par
start <- start + 2 * s
Fin Mientras
// Doblar el tamaño del sub-arreglo que se va fusionando
s <- s * 2
Fin Mientras
// 4. Mostrar el resultado final
Escribir "Lista ordenada con Merge Sort (iterativo):"
Para i <- 1 Hasta n Hacer
Escribir lista[i]
Fin Para
FinAlgoritmo
Implementación equivalente en Python:
def mergesort(lista, inicio, fin):
if inicio < fin:
mitad = (inicio + fin) // 2
mergesort(lista, inicio, mitad)
mergesort(lista, mitad + 1, fin)
fusionar(lista, inicio, mitad, fin)
def fusionar(lista, inicio, mitad, fin):
izquierda = lista[inicio:mitad+1]
derecha = lista[mitad+1:fin+1]
i = 0
j = 0
k = inicio
while i < len(izquierda) and j < len(derecha):
if izquierda[i] <= derecha[j]:
lista[k] = izquierda[i]
i += 1
else:
lista[k] = derecha[j]
j += 1
k += 1
while i < len(izquierda):
lista[k] = izquierda[i]
i += 1
k += 1
while j < len(derecha):
lista[k] = derecha[j]
j += 1
k += 1
n = int(input("Ingrese la cantidad de elementos: "))
lista = []
for i in range(n):
elemento = int(input(f"Elemento {i+1}: "))
lista.append(elemento)
mergesort(lista, 0, n - 1)
print("Lista ordenada con Merge Sort:")
for elemento in lista:
print(elemento)
Es fundamental destacar que tanto Quick Sort como Merge Sort tienen una complejidad temporal promedio de O(n log n), lo que los hace altamente eficientes para ordenar grandes volúmenes de datos. Mientras que Quick Sort suele ser más rápido en la práctica, Merge Sort es estable y garantiza un rendimiento consistente, ya que su complejidad en el peor caso también es O(n log n).
Comprender estos algoritmos es esencial para resolver problemas que requieren ordenamiento eficiente. Además, muchos lenguajes de programación incorporan versiones optimizadas de estos algoritmos en sus bibliotecas estándar, por lo que conocer su funcionamiento interno permite hacer un uso más consciente de estas herramientas.
Comparación de algoritmos: eficiencia y casos de uso
La eficiencia de un algoritmo de ordenamiento es un factor crucial a considerar al elegir el más adecuado para una tarea específica. Los algoritmos varían en su complejidad temporal y espacial, lo que afecta directamente al rendimiento del programa, especialmente con grandes volúmenes de datos.
La complejidad temporal mide el tiempo requerido por un algoritmo en función del tamaño de la entrada n. A continuación, se detallan las complejidades temporales promedio y en el peor caso de los algoritmos estudiados:
- Ordenamiento por burbuja: O(n²) en promedio y en el peor caso.
- Ordenamiento por inserción: O(n²) en promedio y en el peor caso.
- Ordenamiento por selección: O(n²) en promedio y en el peor caso.
- Quick Sort: O(n log n) en promedio, pero O(n²) en el peor caso.
- Merge Sort: O(n log n) tanto en promedio como en el peor caso.
Los algoritmos con complejidad de O(n log n) son significativamente más eficientes que aquellos de O(n²) para grandes valores de n, lo cual es fundamental en aplicaciones donde el rendimiento es crítico.
En cuanto a los casos de uso recomendados, los algoritmos de ordenamiento por burbuja, inserción y selección son ideales para conjuntos de datos pequeños o cuando la simplicidad es prioritaria. Su implementación es sencilla, lo que los hace útiles para propósitos educativos y situaciones donde el rendimiento no es un factor determinante.
El ordenamiento por inserción es particularmente eficiente para listas casi ordenadas, ya que su complejidad en el mejor caso es O(n). Esto lo hace apropiado cuando se anticipa que los datos están ordenados en gran medida.
Por otro lado, Quick Sort es generalmente el algoritmo más rápido en la práctica para datos aleatorios y se utiliza ampliamente. Sin embargo, su rendimiento puede degradarse a O(n²) si los pivotes elegidos no son adecuados, como en el caso de una lista ya ordenada. Para mitigar esto, se pueden implementar técnicas como la elección aleatoria del pivote.
El Merge Sort es estable y garantiza una complejidad de O(n log n) en el peor caso. Es el algoritmo preferido cuando se requiere estabilidad en el ordenamiento o cuando se trabaja con estructuras de datos de acceso secuencial, como listas enlazadas.
A pesar de que los algoritmos tienen la misma lógica fundamental, su implementación puede variar ligeramente entre PseInt y Python. En Python, algunos algoritmos como el ordenamiento por inserción pueden ser más eficientes gracias a optimizaciones internas del lenguaje y uso de funciones integradas.
Por ejemplo, en Python es posible utilizar la función sorted(), que está altamente optimizada y utiliza el algoritmo TimSort, una combinación de Merge Sort e Insertion Sort, logrando un rendimiento superior en la mayoría de los casos.
Ejemplo en Python utilizando sorted():
lista = [5, 2, 9, 1, 5, 6]
lista_ordenada = sorted(lista)
print("Lista ordenada:", lista_ordenada)
Un algoritmo de ordenamiento es estable si mantiene el orden relativo de los elementos con claves iguales. Esto es importante cuando los elementos tienen atributos secundarios que no deben alterarse.
- Ordenamiento por burbuja, inserción y Merge Sort son algoritmos estables por naturaleza.
- Ordenamiento por selección y Quick Sort no son estables en sus implementaciones estándar, aunque pueden modificarse para serlo.
La estabilidad es vital en aplicaciones donde se requiere ordenar por múltiples criterios sin perder el orden establecido previamente.
La complejidad espacial indica la cantidad de memoria adicional que un algoritmo necesita más allá de los datos de entrada.
- Ordenamiento por burbuja, inserción y selección son algoritmos in situ (in-place), ya que no requieren espacio adicional significativo.
- Quick Sort es in situ, pero puede requerir espacio en la pila debido a la recursividad, especialmente si la implementación no es correcta.
- Merge Sort no es in situ en su versión estándar, ya que necesita espacio adicional proporcional a n para las listas temporales utilizadas durante la fusión.
En sistemas con limitaciones de memoria, elegir un algoritmo in-place puede ser determinante para el correcto funcionamiento del programa.
A continuación, se presentan un resumen de las ventajas y desventajas de cada algoritmo:
Ordenamiento por burbuja:
- Ventajas: Implementación sencilla y fácil de entender.
- Desventajas: Muy ineficiente para listas grandes; no es práctico en producción.
Ordenamiento por inserción:
- Ventajas: Eficiente para listas pequeñas o casi ordenadas; sencillo de implementar.
- Desventajas: Ineficiente en listas grandes no ordenadas.
Ordenamiento por selección:
- Ventajas: Bueno para listas pequeñas; número de operaciones de escritura mínimo.
- Desventajas: Ineficiente en términos de comparación; alto tiempo de ejecución en listas grandes.
Quick Sort:
- Ventajas: Generalmente rápido; buen rendimiento promedio; in-place.
- Desventajas: Peor caso lento; no es estable sin modificaciones; puede consumir mucha pila en implementaciones recursivas.
Merge Sort:
- Ventajas: Rendimiento consistente; estable; excelente para listas enlazadas.
- Desventajas: Necesita espacio adicional; más complejo de implementar.
La elección del algoritmo de ordenamiento depende de múltiples factores:
- Tamaño de los datos: Para conjuntos pequeños, algoritmos simples pueden ser suficientes.
- Naturaleza de los datos: Si se espera que la lista esté casi ordenada o contenga muchos elementos iguales.
- Requerimientos de estabilidad: Necesidad de mantener el orden relativo de elementos iguales.
- Limitaciones de memoria: Disponibilidad de espacio adicional para operar.
- Rendimiento esperado: Requerimientos de eficiencia en tiempo de ejecución.
Conocer las características y limitaciones de cada algoritmo permite al programador seleccionar la opción más adecuada para la situación específica, optimizando así el rendimiento y la eficiencia del programa.
Implementación práctica y optimización de algoritmos de ordenamiento
Al implementar algoritmos de ordenamiento en la práctica, es esencial considerar aspectos que mejoren su rendimiento y adaptabilidad. En esta sección, exploraremos cómo llevar a cabo implementaciones eficientes en PseInt y Python, y veremos técnicas para optimizar estos algoritmos.
Para comenzar, es importante elegir el algoritmo adecuado según el contexto. En aplicaciones reales, factores como el tamaño de los datos, la necesidad de estabilidad y las limitaciones de memoria influyen en la selección del método de ordenamiento.
Optimización de Quick Sort
El Quick Sort es altamente eficiente, pero su rendimiento puede variar dependiendo de la elección del pivote. Una implementación optimizada incluye técnicas para mejorar esta selección y reducir el riesgo del peor caso.
Selección aleatoria del pivote:
Para minimizar la probabilidad de que el algoritmo caiga en su peor caso O(n²), podemos seleccionar el pivote de forma aleatoria.
Implementación en PseInt:
Algoritmo QuickSortAleatorioIter
// Declarar arreglos y variables globales
Dimension lista[100]
Dimension LStack[100], RStack[100]
Definir n, top, i Como Entero
Definir izquierda, derecha, indicePivote, valorPivote, temp, pivote Como Entero
// Para aleatoriedad
// Asegurarse de habilitar 'GenerarSemillaAleatoria' o uso de 'Azar(...)'
pivote <- izquierda + Azar(derecha - izquierda + 1) - 1
// 1. Lectura de datos
Escribir "Ingrese la cantidad de elementos:"
Leer n
Para i <- 1 Hasta n Hacer
Escribir "Elemento ", i, ":"
Leer lista[i]
Fin Para
// 2. Inicializar la "pila" con un único rango [1..n]
top <- 1
LStack[top] <- 1
RStack[top] <- n
// 3. Bucle principal (iterativo) mientras la pila no esté vacía
Mientras top > 0 Hacer
izquierda <- LStack[top]
derecha <- RStack[top]
top <- top - 1
Si izquierda < derecha Entonces
// 3.1 Elección aleatoria del pivote
pivote <- Aleatorio(izquierda, derecha)
// Intercambiar lista[izquierda] con lista[pivote]
temp <- lista[izquierda]
lista[izquierda] <- lista[pivote]
lista[pivote] <- temp
// 3.2 Particionar en línea
indicePivote <- izquierda
valorPivote <- lista[izquierda]
Para i <- izquierda + 1 Hasta derecha Hacer
Si lista[i] < valorPivote Entonces
indicePivote <- indicePivote + 1
// swap
temp <- lista[i]
lista[i] <- lista[indicePivote]
lista[indicePivote] <- temp
Fin Si
Fin Para
// Colocar el pivote en su posición final
temp <- lista[izquierda]
lista[izquierda] <- lista[indicePivote]
lista[indicePivote] <- temp
// Subrangos izquierdo y derecho
Si (indicePivote - 1) > izquierda Entonces
top <- top + 1
LStack[top] <- izquierda
RStack[top] <- indicePivote - 1
Fin Si
Si (indicePivote + 1) < derecha Entonces
top <- top + 1
LStack[top] <- indicePivote + 1
RStack[top] <- derecha
Fin Si
Fin Si
Fin Mientras
// 4. Mostrar el resultado
Escribir "Lista ordenada con Quick Sort (pivote aleatorio, iterativo):"
Para i <- 1 Hasta n Hacer
Escribir lista[i]
Fin Para
FinAlgoritmo
Implementación equivalente en Python:
import random
def quicksort_optimizado(lista, izquierda, derecha):
if izquierda < derecha:
indice_pivote = random.randint(izquierda, derecha)
lista[izquierda], lista[indice_pivote] = lista[indice_pivote], lista[izquierda]
pivote = particionar(lista, izquierda, derecha)
quicksort_optimizado(lista, izquierda, pivote - 1)
quicksort_optimizado(lista, pivote + 1, derecha)
def particionar(lista, izquierda, derecha):
pivote = lista[izquierda]
indice = izquierda + 1
for i in range(indice, derecha + 1):
if lista[i] < pivote:
lista[i], lista[indice] = lista[indice], lista[i]
indice += 1
lista[izquierda], lista[indice - 1] = lista[indice - 1], lista[izquierda]
return indice - 1
n = int(input("Ingrese la cantidad de elementos: "))
lista = []
for i in range(n):
elemento = int(input(f"Elemento {i+1}: "))
lista.append(elemento)
quicksort_optimizado(lista, 0, n - 1)
print("Lista ordenada con Quick Sort optimizado:")
for elemento in lista:
print(elemento)
Esta mejora reduce la probabilidad de seleccionar un pivote desfavorable, incrementando la eficiencia general del algoritmo.
Implementación híbrida con inserción
Para listas pequeñas, Quick Sort puede ser menos eficiente debido a la sobrecarga de llamadas recursivas. Una práctica común es combinarlo con inserción cuando la sublista es pequeña.
Ejemplo en PseInt:
Algoritmo QuickSortHibrido
// Declarar arreglos y variables
Dimension lista[100]
Dimension LStack[100], RStack[100] // Pila para simular recursión
Definir n, top, i Como Entero
Definir izquierda, derecha, size, indicePivote, pivoteValor, temp Como Entero
Definir j, start, key Como Entero // Para uso en insertion sort
// Leer datos
Escribir "Ingrese la cantidad de elementos:"
Leer n
Para i <- 1 Hasta n Hacer
Escribir "Elemento ", i, ":"
Leer lista[i]
Fin Para
// Inicializar la "pila" con un único subrango [1..n]
top <- 1
LStack[top] <- 1
RStack[top] <- n
// Bucle principal: mientras la pila no esté vacía
Mientras top > 0 Hacer
izquierda <- LStack[top]
derecha <- RStack[top]
top <- top - 1
size <- derecha - izquierda + 1
// Si el subrango es pequeño, usar Insertion Sort
Si size <= 10 Entonces
// Insertion Sort en [izquierda..derecha]
Para i <- izquierda + 1 Hasta derecha Hacer
key <- lista[i]
j <- i - 1
Mientras j >= izquierda y lista[j] > key Hacer
lista[j + 1] <- lista[j]
j <- j - 1
Fin Mientras
lista[j + 1] <- key
Fin Para
Sino
// Quick Sort (partición + push de subrangos)
// PARTITION inline
indicePivote <- izquierda
pivoteValor <- lista[izquierda]
Para i <- izquierda + 1 Hasta derecha Hacer
Si lista[i] < pivoteValor Entonces
indicePivote <- indicePivote + 1
// swap
temp <- lista[i]
lista[i] <- lista[indicePivote]
lista[indicePivote] <- temp
Fin Si
Fin Para
// Colocar el pivote en su posición final
temp <- lista[izquierda]
lista[izquierda] <- lista[indicePivote]
lista[indicePivote] <- temp
// Subrango izquierdo
Si (indicePivote - 1) > izquierda Entonces
top <- top + 1
LStack[top] <- izquierda
RStack[top] <- indicePivote - 1
Fin Si
// Subrango derecho
Si (indicePivote + 1) < derecha Entonces
top <- top + 1
LStack[top] <- indicePivote + 1
RStack[top] <- derecha
Fin Si
Fin Si
Fin Mientras
// Mostrar resultado final
Escribir "Lista ordenada con Quick Sort Híbrido:"
Para i <- 1 Hasta n Hacer
Escribir lista[i]
Fin Para
FinAlgoritmo
Ejemplo equivalente en Python:
def quicksort_hibrido(lista, izquierda, derecha):
if derecha - izquierda <= 10:
ordenar_insercion(lista, izquierda, derecha)
else:
pivote = particionar(lista, izquierda, derecha)
quicksort_hibrido(lista, izquierda, pivote - 1)
quicksort_hibrido(lista, pivote + 1, derecha)
def ordenar_insercion(lista, inicio, fin):
for i in range(inicio + 1, fin + 1):
clave = lista[i]
j = i - 1
while j >= inicio and lista[j] > clave:
lista[j + 1] = lista[j]
j -= 1
lista[j + 1] = clave
def particionar(lista, izquierda, derecha):
pivote = lista[izquierda]
indice = izquierda + 1
for i in range(indice, derecha + 1):
if lista[i] < pivote:
lista[i], lista[indice] = lista[indice], lista[i]
indice += 1
lista[izquierda], lista[indice - 1] = lista[indice - 1], lista[izquierda]
return indice - 1
n = int(input("Ingrese la cantidad de elementos: "))
lista = []
for i in range(n):
elemento = int(input(f"Elemento {i+1}: "))
lista.append(elemento)
quicksort_hibrido(lista, 0, n - 1)
print("Lista ordenada con Quick Sort híbrido:")
for elemento in lista:
print(elemento)
Esta implementación combina la rapidez del Quick Sort con la eficiencia del ordenamiento por inserción en listas pequeñas, mejorando el rendimiento total.
Optimización de Merge Sort
El Merge Sort estándar requiere espacio adicional para fusionar las sublistas. Una forma de optimizar su uso de memoria es implementar una versión in situ, aunque esto aumenta la complejidad. Además, se puede aplicar la misma técnica híbrida, utilizando inserción para sublistas pequeñas.
Implementación optimizada en PseInt:
Algoritmo MergeSortHibridoIter
// Declaración de arreglos y variables
Dimension lista[100], izquierdo[100], derecho[100]
Definir n, s, start, mid, end, size Como Entero
Definir i, j, k, n1, n2 Como Entero
Definir temp Como Entero // para uso en insertion
// 1. Leer la cantidad de elementos
Escribir "Ingrese la cantidad de elementos:"
Leer n
// 2. Leer los valores en 'lista'
Para i <- 1 Hasta n Hacer
Escribir "Elemento ", i, ":"
Leer lista[i]
Fin Para
// 3. Fase de ordenamiento bottom-up
s <- 1 // Tamaño de sub-arreglo a combinar
Mientras s < n Hacer
start <- 1
Mientras start <= (n - s + 1) Hacer
mid <- start + s - 1
end <- mid + s
Si end > n Entonces
end <- n
Fin Si
size <- end - start + 1
// 3.1 Si el subrango es pequeño, usar insertion sort
Si size <= 10 Entonces
// Insertion sort en [start..end]
Para i <- start + 1 Hasta end Hacer
temp <- lista[i]
j <- i - 1
Mientras j >= start y lista[j] > temp Hacer
lista[j + 1] <- lista[j]
j <- j - 1
Fin Mientras
lista[j + 1] <- temp
Fin Para
Sino
// 3.2 Copiar sub-arreglos izquierdo y derecho y fusionar
n1 <- mid - start + 1
n2 <- end - mid
// Copiar en 'izquierdo'
Para i <- 1 Hasta n1 Hacer
izquierdo[i] <- lista[start + i - 1]
Fin Para
// Copiar en 'derecho'
Para j <- 1 Hasta n2 Hacer
derecho[j] <- lista[mid + j]
Fin Para
i <- 1
j <- 1
k <- start
// Combinar mientras queden elementos en ambos sub-arreglos
Mientras i <= n1 y j <= n2 Hacer
Si izquierdo[i] <= derecho[j] Entonces
lista[k] <- izquierdo[i]
i <- i + 1
Sino
lista[k] <- derecho[j]
j <- j + 1
Fin Si
k <- k + 1
Fin Mientras
// Si aún quedan elementos en 'izquierdo'
Mientras i <= n1 Hacer
lista[k] <- izquierdo[i]
i <- i + 1
k <- k + 1
Fin Mientras
// Si aún quedan elementos en 'derecho'
Mientras j <= n2 Hacer
lista[k] <- derecho[j]
j <- j + 1
k <- k + 1
Fin Mientras
Fin Si
// siguiente par de sub-arreglos
start <- start + 2 * s
Fin Mientras
// duplicar el tamaño del sub-arreglo
s <- s * 2
Fin Mientras
// 4. Mostrar el resultado final
Escribir "Lista ordenada con Merge Sort Híbrido (Iterativo):"
Para i <- 1 Hasta n Hacer
Escribir lista[i]
Fin Para
FinAlgoritmo
Implementación equivalente en Python:
def mergesort_optimizado(lista, inicio, fin):
if fin - inicio <= 10:
ordenar_insercion(lista, inicio, fin)
else:
mitad = (inicio + fin) // 2
mergesort_optimizado(lista, inicio, mitad)
mergesort_optimizado(lista, mitad + 1, fin)
fusionar(lista, inicio, mitad, fin)
def ordenar_insercion(lista, inicio, fin):
for i in range(inicio + 1, fin + 1):
clave = lista[i]
j = i - 1
while j >= inicio and lista[j] > clave:
lista[j + 1] = lista[j]
j -= 1
lista[j + 1] = clave
def fusionar(lista, inicio, mitad, fin):
izquierda = lista[inicio:mitad+1]
derecha = lista[mitad+1:fin+1]
i = 0
j = 0
k = inicio
while i < len(izquierda) and j < len(derecha):
if izquierda[i] <= derecha[j]:
lista[k] = izquierda[i]
i += 1
else:
lista[k] = derecha[j]
j += 1
k += 1
while i < len(izquierda):
lista[k] = izquierda[i]
i += 1
k += 1
while j < len(derecha):
lista[k] = derecha[j]
j += 1
k += 1
n = int(input("Ingrese la cantidad de elementos: "))
lista = []
for i in range(n):
elemento = int(input(f"Elemento {i+1}: "))
lista.append(elemento)
mergesort_optimizado(lista, 0, n - 1)
print("Lista ordenada con Merge Sort optimizado:")
for elemento en lista:
print(elemento)
Estas optimizaciones logran un equilibrio entre la eficiencia y el uso adecuado de recursos, lo que es especialmente útil en aplicaciones de gran escala.
Uso de funciones integradas y bibliotecas
En entornos de desarrollo reales, es común utilizar funciones y bibliotecas ya optimizadas. En Python, por ejemplo, podemos aprovechar la función sorted() o el método sort() de las listas, que implementan algoritmos altamente optimizados como TimSort.
Ejemplo de uso en Python:
n = int(input("Ingrese la cantidad de elementos: "))
lista = []
for i in range(n):
elemento = int(input(f"Elemento {i+1}: "))
lista.append(elemento)
lista.sort()
print("Lista ordenada con sort():")
for elemento in lista:
print(elemento)
El uso de estas funciones es recomendable cuando se busca eficiencia y confiabilidad, ya que están diseñadas para performar bien en una amplia gama de escenarios.
Buenas prácticas en la implementación
Al programar algoritmos de ordenamiento, es importante seguir ciertas prácticas para asegurar un código de calidad:
- Evitar duplicación de código: Utilizar funciones y procedimientos para operaciones repetitivas.
- Comentarios claros: Añadir explicaciones cuando la lógica no sea evidente.
- Manejo adecuado de recursos: Liberar memoria si es necesario y evitar variables innecesarias.
- Pruebas exhaustivas: Verificar el funcionamiento con diferentes tipos de datos, incluyendo casos borde.
Consideraciones sobre la complejidad espacial
La complejidad espacial puede ser un factor limitante. Los algoritmos que requieren espacio adicional, como Merge Sort, pueden no ser ideales en sistemas con recursos limitados. Optar por algoritmos in-place, como Quick Sort, puede ser más adecuado.
Sin embargo, es importante balancear la eficiencia con la estabilidad y otras características requeridas. Por ejemplo, si se necesita un algoritmo estable y el espacio adicional no es un problema, Merge Sort sigue siendo una excelente opción.
Paralelización y algoritmos modernos
Con el auge de los sistemas multicore y paralelos, existen técnicas para paralelizar algoritmos de ordenamiento y mejorar aún más su rendimiento. Aunque en PseInt no es posible implementar paralelismo, en lenguajes como Python podemos utilizar bibliotecas como multiprocessing o concurrent.futures para paralelizar operaciones.
Ejemplo sencillo en Python utilizando threading:
from threading import Thread
def quicksort_paralelo(lista):
if len(lista) <= 1:
return lista
else:
pivote = lista[0]
menores = []
mayores = []
for elemento in lista[1:]:
if elemento < pivote:
menores.append(elemento)
else:
mayores.append(elemento)
thread_menores = Thread(target=lambda: quicksort_paralelo(menores))
thread_mayores = Thread(target=lambda: quicksort_paralelo(mayores))
thread_menores.start()
thread_mayores.start()
thread_menores.join()
thread_mayores.join()
return quicksort_paralelo(menores) + [pivote] + quicksort_paralelo(mayores)
n = int(input("Ingrese la cantidad de elementos: "))
lista = []
for i in range(n):
elemento = int(input(f"Elemento {i+1}: "))
lista.append(elemento)
lista = quicksort_paralelo(lista)
print("Lista ordenada con Quick Sort paralelo:")
for elemento in lista:
print(elemento)
Es importante notar que la paralelización añade complejidad y puede no siempre resultar en mejoras significativas, dependiendo del entorno y el tamaño de los datos.
Todas las lecciones de Fundamentos
Accede a todas las lecciones de Fundamentos y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.
¿Qué Es La Programación?
Introducción Y Entorno
Lenguajes De Programación
Introducción Y Entorno
Ciclo De Vida Del Desarrollo De Software
Introducción Y Entorno
Herramientas Y Entornos De Desarrollo
Introducción Y Entorno
Instalar Y Configurar Pseint Y Python
Introducción Y Entorno
Estructura De Un Programa Pseint
Introducción Y Entorno
Pensamiento Algorítmico
Lógica
Tipos De Datos Y Variables
Lógica
Operadores
Lógica
Estructuras De Control Condicional
Lógica
Estructuras De Control De Repetición
Lógica
Diagramas De Flujo
Lógica
Depuración De Programas
Lógica
Arrays
Estructuras De Datos
Matrices
Estructuras De Datos
Cadenas De Caracteres
Estructuras De Datos
Algoritmos De Ordenamiento
Ordenamiento Y Búsqueda
Algoritmos De Búsqueda
Ordenamiento Y Búsqueda
Complejidad Temporal Y Espacial
Ordenamiento Y Búsqueda
Definición Y Utilidad De Las Funciones
Funciones
Paso De Parámetros
Funciones
Recursividad
Funciones
Funciones Anónimas
Funciones
Concepto De Clases Y Objetos
Programación Orientada A Objetos
Método Constructor
Programación Orientada A Objetos
Encapsulación
Programación Orientada A Objetos
Herencia
Programación Orientada A Objetos
Polimorfismo
Programación Orientada A Objetos
Composición
Programación Orientada A Objetos
En esta lección
Objetivos de aprendizaje de esta lección
- Comprender el funcionamiento básico de los algoritmos de ordenamiento por burbuja, inserción y selección.
- Aprender a implementar estos algoritmos en PseInt y Python.
- Analizar la eficiencia y aplicabilidad de cada algoritmo en diferentes contextos.
- Aplicar conceptos de complejidad temporal y espacial en la programación.
- Adaptar y optimizar algoritmos básicos para mejorar su rendimiento.