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ícate

Ordenamiento 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.

Para seguir leyendo hazte Plus

¿Ya eres Plus? Accede a la app

Plan mensual

19.00 € /mes

Precio normal mensual: 19 €
47 % DE DESCUENTO

Plan anual

10.00 € /mes

Ahorras 108 € al año
Precio normal anual: 120 €
Aprende Fundamentos GRATIS online

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.

Accede GRATIS a Fundamentos y certifícate

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.