Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Decálogo de Algoritmos de Busqueda y Ordenamiento

Compendio completo de métodos de ordenamiento y búsqueda

Universidad Nacional de Rio Negro - Sede Andina

Un algoritmo de ordenamiento es un procedimiento computacional que toma un conjunto de datos de entrada (como un arreglo) y produce una permutación de esos datos que los organiza según una relación de orden específica (generalmente, ascendente o descendente). El ordenamiento es una de las operaciones más fundamentales en la informática, crucial para optimizar la eficiencia de otros algoritmos, como los de búsqueda.

Este documento presenta un compendio exhaustivo de los principales algoritmos de ordenamiento, clasificados por su naturaleza y características. Para evaluar la eficiencia de estos algoritmos, se utilizan dos métricas clave: la complejidad temporal y la complejidad espacial.

Conceptos Fundamentales de Complejidad

Al analizar un algoritmo, no solo nos interesa que funcione, sino cuán eficientemente lo hace. La complejidad algorítmica es el estudio formal de los recursos que un algoritmo consume. Lo interesante de esta técnica de análisis es la separación concreta con el hardware que lleva a cabo la tarea y el algoritmo en sí.

Complejidad Temporal

La complejidad temporal es una medida de cuánto tiempo tarda un algoritmo en ejecutarse en función del tamaño de su entrada, denotado comúnmente como nn. No se mide en segundos o milisegundos, ya que eso depende del hardware. En su lugar, se cuantifica el número de operaciones elementales (comparaciones, asignaciones, operaciones aritméticas) que el algoritmo realiza.

El objetivo es entender cómo crece el tiempo de ejecución a medida que crece el tamaño de la entrada. Para expresar esta relación, se utiliza la notación Big O (Gran O):

Complejidad Espacial (Memoria)

La complejidad espacial mide la cantidad total de memoria o espacio de almacenamiento que un algoritmo necesita para ejecutarse, también en función del tamaño de la entrada nn. Este análisis incluye tanto el espacio ocupado por los datos de entrada como cualquier memoria adicional (espacio auxiliar) que el algoritmo utilice.

La complejidad espacial también se expresa con la notación Big O. Un algoritmo que modifica la entrada sin requerir memoria adicional significativa se denomina “in-place” y tiene una complejidad espacial auxiliar de O(1)O(1).

Algoritmos de Búsqueda

Mientras que los algoritmos de ordenamiento reorganizan una colección de datos, los algoritmos de búsqueda están diseñados para encontrar uno o más elementos específicos dentro de una estructura de datos. La eficiencia de un algoritmo de búsqueda a menudo depende de si la estructura de datos subyacente está ordenada.

Es el método de búsqueda más simple. Consiste en recorrer una colección de datos (por ejemplo, un arreglo) elemento por elemento desde el principio hasta el final, comparando cada uno con el valor buscado. La búsqueda termina cuando se encuentra el elemento o cuando se ha recorrido toda la colección sin éxito.

Análisis de Complejidad:

Pseudocódigo:

procedimiento linearSearch(arreglo A, tamaño n, valorBuscado x)
  para i desde 0 hasta n-1
    si A[i] es igual a x
      retornar i
    fin si
  fin para
  retornar -1
fin procedimiento

Es un algoritmo de búsqueda mucho más eficiente, pero con un requisito fundamental: la colección de datos debe estar previamente ordenada. Funciona bajo el principio de “Divide y Vencerás”.

El algoritmo compara el elemento buscado con el elemento central del arreglo. Si coinciden, la búsqueda termina. Si el elemento buscado es menor que el central, la búsqueda continúa en la mitad inferior del arreglo. Si es mayor, continúa en la mitad superior. Este proceso se repite, descartando la mitad del espacio de búsqueda en cada iteración.

Análisis de Complejidad:

Pseudocódigo (Iterativo):

procedimiento binarySearch(arreglo A, tamaño n, valorBuscado x)
  bajo = 0
  alto = n - 1
  mientras bajo <= alto
    medio = bajo + (alto - bajo) / 2
    si A[medio] == x
      retornar medio
    si A[medio] < x
      bajo = medio + 1
    sino
      alto = medio - 1
  fin mientras
  retornar -1
fin procedimiento

Decálogo de Algoritmos de Ordenamiento

Clasificación Principal

Los algoritmos de ordenamiento se pueden clasificar en dos grandes categorías:

  1. Algoritmos basados en comparación: Determinan el orden de los elementos comparándolos entre sí.

  2. Algoritmos no basados en comparación: Utilizan propiedades específicas de los datos (como ser enteros en un rango limitado) para ordenarlos sin comparaciones directas.


I. Ordenamiento de Burbuja (Bubble Sort)

Concepto y Funcionamiento

El ordenamiento de burbuja es uno de los algoritmos de ordenamiento más simples conceptualmente. Su mecanismo consiste en revisar repetidamente el arreglo, comparando cada par de elementos adyacentes y permutándolos si no están en el orden correcto. El nombre proviene de la forma en que los elementos más grandes “burbujean” hacia el final del arreglo en cada pasada.

En cada iteración completa del arreglo, el elemento más grande de la porción no ordenada se mueve a su posición final. Esto significa que después de kk pasadas, los últimos kk elementos ya están en su posición correcta.

Análisis de Complejidad

Características

Pseudocódigo

procedimiento bubbleSort(arreglo A, tamaño n)
  para i desde 0 hasta n-2
    para j desde 0 hasta n-i-2
      si A[j] > A[j+1]
        intercambiar(A[j], A[j+1])
      fin si
    fin para
  fin para
fin procedimiento

II. Ordenamiento por Selección (Selection Sort)

Concepto y Funcionamiento

El ordenamiento por selección divide conceptualmente el arreglo en dos partes: una sublista ordenada (inicialmente vacía) y una sublista desordenada (que contiene todos los elementos). En cada iteración, el algoritmo encuentra el elemento de menor valor en la sublista desordenada y lo intercambia con el primer elemento de esa misma sublista, expandiendo así la porción ordenada.

El proceso se repite hasta que toda la sublista desordenada se ha agotado y todos los elementos están en la sublista ordenada. A diferencia del ordenamiento de burbuja, este algoritmo realiza un número mínimo de intercambios (exactamente n1n-1 intercambios).

Análisis de Complejidad

Características

Pseudocódigo

procedimiento selectionSort(arreglo A, tamaño n)
  para i desde 0 hasta n-2
    indiceMin = i
    para j desde i+1 hasta n-1
      si A[j] < A[indiceMin]
        indiceMin = j
      fin si
    fin para
    si indiceMin != i
      intercambiar(A[i], A[indiceMin])
    fin si
  fin para
fin procedimiento

III. Ordenamiento por Inserción (Insertion Sort)

Concepto y Funcionamiento

El ordenamiento por inserción funciona de manera similar a como una persona ordena una mano de cartas. Los elementos de la parte no ordenada se toman uno por uno y se colocan en su posición correcta dentro de la parte ordenada. El algoritmo mantiene una sublista ordenada en la parte inferior del arreglo y va insertando los elementos restantes uno a uno en su posición correcta.

Para cada elemento, se compara con los elementos de la sublista ordenada (de derecha a izquierda) y se desplazan los elementos mayores una posición hacia la derecha hasta encontrar la posición correcta para el nuevo elemento.

Análisis de Complejidad

Características

Pseudocódigo

procedimiento insertionSort(arreglo A, tamaño n)
  para i desde 1 hasta n-1
    clave = A[i]
    j = i - 1
    mientras j >= 0 y A[j] > clave
      A[j + 1] = A[j]
      j = j - 1
    fin mientras
    A[j + 1] = clave
  fin para
fin procedimiento

IV. Ordenamiento por Fusión (Merge Sort)

Concepto y Funcionamiento

El ordenamiento por fusión es un algoritmo de tipo “Divide y Vencerás”. Divide el arreglo en dos mitades, se llama a sí mismo para ordenar las dos mitades de forma recursiva y, finalmente, fusiona (merge) las dos mitades ordenadas en un único arreglo ordenado.

El proceso de fusión es clave: toma dos subarreglos ordenados y los combina en un solo arreglo ordenado, comparando los elementos menores de cada subarreglo y colocándolos en orden en un arreglo auxiliar.

La recursión continúa hasta que los subarreglos tienen un solo elemento (que por definición ya está ordenado). Luego, la fase de combinación reconstruye el arreglo ordenado desde estos elementos individuales.

Análisis de Complejidad

Características

Pseudocódigo

procedimiento mergeSort(arreglo A, izquierda, derecha)
  si izquierda < derecha
    medio = izquierda + (derecha - izquierda) / 2
    mergeSort(A, izquierda, medio)
    mergeSort(A, medio + 1, derecha)
    merge(A, izquierda, medio, derecha)
  fin si
fin procedimiento

procedimiento merge(arreglo A, izq, med, der)
  n1 = med - izq + 1
  n2 = der - med
  crear arreglos temporales L[n1] y R[n2]
  
  copiar A[izq..med] en L[]
  copiar A[med+1..der] en R[]
  
  i = 0, j = 0, k = izq
  mientras i < n1 y j < n2
    si L[i] <= R[j]
      A[k] = L[i]
      i++
    sino
      A[k] = R[j]
      j++
    fin si
    k++
  fin mientras
  
  copiar elementos restantes de L[] si los hay
  copiar elementos restantes de R[] si los hay
fin procedimiento

V. Ordenamiento Rápido (Quick Sort)

Concepto y Funcionamiento

El ordenamiento rápido también aplica el paradigma “Divide y Vencerás”, pero de una manera diferente a Merge Sort. Selecciona un elemento llamado pivote y particiona los demás elementos en dos subarreglos: aquellos menores o iguales que el pivote y aquellos mayores. Luego, ordena los subarreglos de forma recursiva.

La clave del algoritmo está en la función de partición, que reorganiza el arreglo de tal manera que el pivote quede en su posición final correcta, con todos los elementos menores a su izquierda y todos los mayores a su derecha.

La elección del pivote puede hacerse de varias formas: primer elemento, último elemento, elemento del medio, elemento aleatorio o mediante la “mediana de tres” (que toma la mediana del primer, medio y último elemento).

Análisis de Complejidad

Características

Pseudocódigo

procedimiento quickSort(arreglo A, bajo, alto)
  si bajo < alto
    pi = partition(A, bajo, alto)
    quickSort(A, bajo, pi - 1)
    quickSort(A, pi + 1, alto)
  fin si
fin procedimiento

procedimiento partition(arreglo A, bajo, alto)
  pivote = A[alto]
  i = bajo - 1
  para j desde bajo hasta alto - 1
    si A[j] <= pivote
      i++
      intercambiar(A[i], A[j])
    fin si
  fin para
  intercambiar(A[i + 1], A[alto])
  retornar i + 1
fin procedimiento

VI. Ordenamiento por Montículos (Heap Sort)

Concepto y Funcionamiento

El ordenamiento por montículos utiliza una estructura de datos de montículo binario (binary heap). Un montículo es un árbol binario completo donde cada nodo padre satisface una propiedad de orden con respecto a sus hijos: en un montículo máximo (max-heap), cada padre es mayor o igual que sus hijos; en un montículo mínimo (min-heap), cada padre es menor o igual que sus hijos.

El algoritmo funciona en dos fases:

  1. Construcción del montículo: Convierte el arreglo en un montículo máximo.

  2. Extracción ordenada: Extrae repetidamente el elemento máximo (la raíz del montículo), lo coloca al final del arreglo, reduce el tamaño del montículo y reordena el montículo restante (operación llamada heapify).

Análisis de Complejidad

Características

Pseudocódigo

procedimiento heapSort(arreglo A, tamaño n)
  // Construir un montículo máximo
  para i desde n / 2 - 1 hasta 0
    heapify(A, n, i)
  fin para
  
  // Extraer elementos uno por uno del montículo
  para i desde n - 1 hasta 0
    intercambiar(A[0], A[i])
    heapify(A, i, 0)
  fin para
fin procedimiento

procedimiento heapify(arreglo A, tamaño n, índice i)
  masGrande = i
  izquierda = 2 * i + 1
  derecha = 2 * i + 2
  
  si izquierda < n y A[izquierda] > A[masGrande]
    masGrande = izquierda
  fin si
  
  si derecha < n y A[derecha] > A[masGrande]
    masGrande = derecha
  fin si
  
  si masGrande != i
    intercambiar(A[i], A[masGrande])
    heapify(A, n, masGrande)
  fin si
fin procedimiento

VII. Ordenamiento Shell (Shell Sort)

Concepto y Funcionamiento

El ordenamiento Shell es una mejora del ordenamiento por inserción diseñada por Donald Shell en 1959. La idea fundamental es permitir el intercambio de elementos que están lejos uno del otro, superando una de las principales limitaciones del ordenamiento por inserción.

El algoritmo comienza usando un “salto” (gap) grande para ordenar elementos distantes y progresivamente reduce el salto. En cada pasada con un salto dado, se realiza efectivamente un ordenamiento por inserción en sublistas donde los elementos están separados por ese salto. La última pasada, con un salto de 1, es un ordenamiento por inserción simple, pero se aplica sobre un arreglo que ya está casi ordenado, por lo que es muy eficiente.

Análisis de Complejidad

Características

Pseudocódigo

procedimiento shellSort(arreglo A, tamaño n)
  // Secuencia de saltos (ejemplo con Shell original)
  para salto desde n/2 hasta 1 con paso /2
    // Ordenamiento por inserción con saltos
    para i desde salto hasta n-1
      temp = A[i]
      j = i
      mientras j >= salto y A[j - salto] > temp
        A[j] = A[j - salto]
        j = j - salto
      fin mientras
      A[j] = temp
    fin para
  fin para
fin procedimiento

VIII. Ordenamiento por Cuentas (Counting Sort)

Concepto y Funcionamiento

El ordenamiento por cuentas es el primero de los algoritmos no basados en comparación que presentamos. Funciona contando el número de ocurrencias de cada elemento distinto en el arreglo de entrada. Esta información se usa para calcular las posiciones de cada elemento en el arreglo de salida y colocarlos directamente.

El algoritmo requiere conocer de antemano el rango de valores posibles de los elementos (desde un mínimo hasta un máximo). Crea un arreglo auxiliar de “cuentas” donde cada índice representa un posible valor y el contenido es cuántas veces aparece ese valor.

Análisis de Complejidad

Características

Pseudocódigo

procedimiento countingSort(arreglo A, tamaño n, valorMin, valorMax)
  k = valorMax - valorMin + 1
  crear arreglo de cuentas C de tamaño k, inicializado a 0
  crear arreglo de salida B de tamaño n
  
  // Contar ocurrencias de cada valor
  para j desde 0 hasta n-1
    indice = A[j] - valorMin
    C[indice] = C[indice] + 1
  fin para
  
  // Calcular posiciones acumuladas
  para i desde 1 hasta k-1
    C[i] = C[i] + C[i-1]
  fin para
  
  // Construir el arreglo de salida (de derecha a izquierda para estabilidad)
  para j desde n-1 hasta 0
    indice = A[j] - valorMin
    B[C[indice] - 1] = A[j]
    C[indice] = C[indice] - 1
  fin para
  
  // Copiar el resultado de vuelta a A
  copiar B en A
fin procedimiento

IX. Ordenamiento Radix (Radix Sort)

Concepto y Funcionamiento

El ordenamiento Radix es otro algoritmo no basado en comparación que ordena enteros procesando sus dígitos individuales. Puede procesar los dígitos desde el menos significativo (LSD - Least Significant Digit) hasta el más significativo (MSD - Most Significant Digit), o viceversa.

La versión LSD comienza desde el dígito menos significativo (el de las unidades) y procede hacia los dígitos más significativos. En cada pasada, utiliza un algoritmo de ordenamiento estable (típicamente Counting Sort) para ordenar la lista de números según el dígito actual.

La clave es que el algoritmo de ordenamiento utilizado en cada pasada debe ser estable, de modo que el orden establecido por los dígitos menos significativos se preserve cuando se ordenan los dígitos más significativos.

Análisis de Complejidad

Características

Pseudocódigo

procedimiento radixSort(arreglo A, tamaño n)
  // Encontrar el número máximo para saber el número de dígitos
  maximo = encontrarMaximo(A, n)
  
  // Hacer counting sort para cada dígito
  // exp es 10^i donde i es el número del dígito actual
  exp = 1
  mientras maximo / exp > 0
    countingSortPorDigito(A, n, exp)
    exp = exp * 10
  fin mientras
fin procedimiento

procedimiento countingSortPorDigito(arreglo A, tamaño n, exp)
  crear arreglo de salida B de tamaño n
  crear arreglo de cuentas C de tamaño 10, inicializado a 0
  
  // Contar ocurrencias de cada dígito
  para i desde 0 hasta n-1
    digito = (A[i] / exp) % 10
    C[digito] = C[digito] + 1
  fin para
  
  // Calcular posiciones acumuladas
  para i desde 1 hasta 9
    C[i] = C[i] + C[i-1]
  fin para
  
  // Construir el arreglo de salida
  para i desde n-1 hasta 0
    digito = (A[i] / exp) % 10
    B[C[digito] - 1] = A[i]
    C[digito] = C[digito] - 1
  fin para
  
  // Copiar el resultado de vuelta a A
  copiar B en A
fin procedimiento

X. Ordenamiento por Cubetas (Bucket Sort)

Concepto y Funcionamiento

El ordenamiento por cubetas (también llamado Bin Sort) es otro algoritmo no basado en comparación que distribuye los elementos en un número de “cubetas” o “contenedores” y luego ordena cada cubeta individualmente, ya sea usando otro algoritmo de ordenamiento o aplicando recursivamente el ordenamiento por cubetas.

El algoritmo funciona mejor cuando los elementos se distribuyen uniformemente en un rango conocido. Los pasos básicos son:

  1. Distribución: Crear un arreglo de cubetas vacías y distribuir los elementos del arreglo original en las cubetas según su valor.

  2. Ordenamiento individual: Ordenar cada cubeta individualmente (usando Insertion Sort, Quick Sort u otro algoritmo apropiado).

  3. Concatenación: Recorrer las cubetas en orden y concatenar los elementos para formar el arreglo ordenado.

Análisis de Complejidad

Características

Pseudocódigo

procedimiento bucketSort(arreglo A, tamaño n)
  // Determinar el rango de valores
  min = encontrarMinimo(A, n)
  max = encontrarMaximo(A, n)
  rango = max - min + 1
  
  // Crear cubetas vacías
  numeroCubetas = n
  crear arreglo de cubetas B de tamaño numeroCubetas
  
  // Distribuir elementos en cubetas
  para i desde 0 hasta n-1
    indiceCubeta = ((A[i] - min) * numeroCubetas) / rango
    si indiceCubeta >= numeroCubetas
      indiceCubeta = numeroCubetas - 1
    fin si
    agregar A[i] a B[indiceCubeta]
  fin para
  
  // Ordenar cada cubeta individualmente
  para i desde 0 hasta numeroCubetas-1
    ordenar B[i] usando insertionSort o quickSort
  fin para
  
  // Concatenar todas las cubetas en A
  indice = 0
  para i desde 0 hasta numeroCubetas-1
    para cada elemento e en B[i]
      A[indice] = e
      indice++
    fin para
  fin para
fin procedimiento

Comparación de Algoritmos de Ordenamiento

La siguiente tabla resume las características principales de los diez algoritmos presentados:

Algoritmo

Mejor Caso

Caso Promedio

Peor Caso

Espacio

Estable

Bubble Sort

O(n)O(n)

O(n2)O(n^2)

O(n2)O(n^2)

O(1)O(1)

Selection Sort

O(n2)O(n^2)

O(n2)O(n^2)

O(n2)O(n^2)

O(1)O(1)

No

Insertion Sort

O(n)O(n)

O(n2)O(n^2)

O(n2)O(n^2)

O(1)O(1)

Merge Sort

O(nlogn)O(n \log n)

O(nlogn)O(n \log n)

O(nlogn)O(n \log n)

O(n)O(n)

Quick Sort

O(nlogn)O(n \log n)

O(nlogn)O(n \log n)

O(n2)O(n^2)

O(logn)O(\log n)

No

Heap Sort

O(nlogn)O(n \log n)

O(nlogn)O(n \log n)

O(nlogn)O(n \log n)

O(1)O(1)

No

Shell Sort

O(nlogn)O(n \log n)

O(n4/3)O(n^{4/3})

O(n2)O(n^2)

O(1)O(1)

No

Counting Sort

O(n+k)O(n + k)

O(n+k)O(n + k)

O(n+k)O(n + k)

O(k)O(k)

Radix Sort

O(d(n+b))O(d(n + b))

O(d(n+b))O(d(n + b))

O(d(n+b))O(d(n + b))

O(n+b)O(n + b)

Bucket Sort

O(n+k)O(n + k)

O(n+k)O(n + k)

O(n2)O(n^2)

O(n+k)O(n + k)

Varía

Guía de Selección de Algoritmo

La elección del algoritmo de ordenamiento apropiado depende de varios factores:

Para conjuntos de datos pequeños (n < 50):

Para conjuntos de datos medianos (50 < n < 10000):

Para conjuntos de datos grandes (n > 10000):

Para casos especiales:

Consideraciones adicionales:

Referencias y Profundización

Este capítulo ha presentado una visión panorámica de los algoritmos de ordenamiento. Para profundizar en los fundamentos teóricos que sustentan estos algoritmos, se recomienda consultar: