Algoritmos de ordenamiento

El algoritmo de ordenamiento ideal

  • Estable: No intercambia el orden de elementos con el mismo valor.
  • No tiene overhead.
  • En el peor de los casos tiene O(n log n) comparaciones.
  • En el peor de los casos tiene O(n) intercambios.
  • Flexible: La complejidad temporal tiende a O(n) cuando los datos están semi-ordenados o cuando hay pocas llaves únicas.

Ordenamiento por inserción

  • Definición: Es una manera natural de ordenar datos. De la lista de n datos, se toma el elemento k y se compara con los k-1 anteriores para encontrar su posición.
  • Complejidad espacial adicional: O(1)
  • Complejidad temporal
    • Mejor de los casos: O(n)
    • Caso promedio: O(n^2)
    • Peor de los casos: O(n^2)
  • Intercambios: O(n^2)
template<typename T_>
void insertionSort(T_ arr[], int length)
{
    for(int i = 1; i < length; i++)
    {
        // Invariante: a[1..i] está ordenado
        for(int j = i; j > 0 && arr[j] < arr[j - 1]; j--) 
        {
            T_ temp = arr[j];
            arr[j] = arr[j-1];
            arr[j-1] = temp;
        }
    }
}

Ejemplo

Ventajas

  • Simple de implementar.
  • Eficiente para conjuntos de datos reducidos o cuando los datos están semi-ordenados.
  • El más efectivo que otros algoritmos de O(n^2).
  • Estable.
  • Requiere poco overhead.

Desventajas

  • Complejidad temporal O(n^2) en promedio y en el peor de los casos.

Ordenamiento por selección

  • Definición: En cada iteración se toma el menor de los números y se coloca en la posición k de la lista, donde k es el número de iteración.
  • Complejidad espacial adicional: O(1)
  • Complejidad temporal
    • Mejor de los casos: O(n^2)
    • Caso promedio: O(n^2)
    • Peor de los casos: O(n^2)
  • Intercambios: O(n)
template<typename T_>
void selectionSort(T_ arr[], int length)
{
    for(int i = 0; i < length; i++)
    {
        int k = i;
        for(int j = i; j < length; j++)
        {
            if(arr[j] < arr[k])
            {
                // k es el índice
                // del elemento más pequeño
                // de arr[i...n]
                k = j;
            }
        }
        T_ temp = arr[i];
        arr[i] = arr[k];
        arr[k] = temp;
    }
}

Ventajas

  • Simple de implementar.
  • Reduce el número de comparaciones.

Desventajas

  • No es estable.
  • Complejidad temporal O(n^2) para todos los casos.

Ordenamiento Shell

  • Definición: Ordena pares de elementos separados entre sí por una distancia considerable reduciendo con esto el espacio entre los elementos a comparar.
  • Complejidad espacial adicional: O(1)
  • Complejidad temporal
    • Mejor de los casos: O(n log^2 n)
    • Caso promedio: Siguiente diapositiva
    • Peor de los casos: O(n^2)

Secuencia de espacios

#include <QVector>

template<typename T_>
void shellSort(T_ arr[], int length)
{
    int gap = 1;
    QVector<int> gaps;
    while(gap < length)
    {
        gaps.append(gap);
        gap = gap * 3 + 1;
    }
    while(gaps.size() > 0)
    {
        int curGap = gaps.at(gaps.size() - 1);
        gaps.pop_back();
        // Invariante: a[0...curGap-1] ya están ordenados
        for(int i = curGap; i < length; ++i)
        {
            T_ temp = arr[i];
            int j;
            // Ordenamiento por inserción
            for(j = i - curGap; j >= 0 && arr[j] > temp; j -= curGap)
            {
                arr[j + curGap] = arr[j];
            }
            arr[j + curGap] = temp;
        }
    }
}

Ventajas

  • La complejidad temporal depende de la secuencia de incremento utilizada.
  • Requiere poco overhead.
  • Implementación relativamente fácil.
  • Complejidad temporal sub-cuadrática.

Desventajas

  • Es un problema abierto, su mejor rendimiento en términos de complejidad temporal no ha sido encontrada.
  • Su rendimiento no es siginificativamente mayor a otros algoritmos de ordenamiento.

Ordenamiento por mezcla

(Mergesort)

  • Definición: Es un algoritmo de divide y vencerás. Divide la lista desordenada en subconjuntos, hasta llegar a subconjuntos de un elemento, comienza a ordenar los subconjuntos mientras los va mezclando, la última lista mezclada es la lista ordenada.
  • Complejidad espacial adicional: O(n)
  • Complejidad temporal
    • Mejor de los casos: O(n log n)
    • Caso promedio: O(n log n)
    • Peor de los casos: O(n log n)
template<typename T_>
void mergeSort(T_ arr[], int low, int high)
{
    if(low < high)
    {
        int mid = (low + high)/2;
        // Ordenar recursivamente 
        // las mitades del arreglo
        mergeSort(arr, low, mid);
        mergeSort(arr, mid + 1, high);
        // Mezclar las mitades del 
        // arreglo ordenadas
        merge(arr, low, mid, high);
    }
}

template<typename S_>
void merge(S_ arr[], int low, int mid, int high)
{
    S_ *b = new int[high - low + 1];
    int h = low;
    int i = 0;
    int j = mid +1;
    // Mezcla las dos mitades ordenadas en interaciones anteriores
    while(h <= mid && j <= high)
    {
        if(arr[h] <= arr[j])
        {
            b[i] = arr[h];
            ++h;
        }
        else
        {
            b[i] = arr[j];
            ++j;
        }
        ++i;
    }
    // Completa el arreglo con los valores restantes
    if(h > mid)
    {
        for(int k = j; k <= high; ++k)
        {
            b[i] = arr[k];
            ++i;
        }
    }
    else
    {
        for(int k = h; k <= mid; ++k)
        {
            b[i] = arr[k];
            ++i;
        }
    }
    // Mover la parte ordenada al arreglo original
    memcpy(arr + low, b, (high - low + 1) * sizeof(S_));
    delete[] b;
}

Ejemplo

Ventajas

  • Simple de implementar.
  • Estable (el único del tipo O(n log n)).
  • Mejor rendimiento ordenando listas enlazadas (complejidad espacial adicional      O(log n)).

Desventajas

  • Tiene overhead de O(n).
  • No es flexible.

Ordenamiento por montículos

(Heapsort)

  • Definición:  Se puede considerar como una versión mejorada de ordenamiento por selección. La diferencia radica en utilizar un montículo (heap) en lugar de una búsqueda lineal para encontrar el mínimo.
  • Complejidad espacial adicional: O(1)
  • Complejidad temporal:
    • Mejor de los casos: O(n log n)
    • Caso promedio: O(n log n)
    • Peor de los casos: O(n log n)

Concepto de montículo

La raíz está en 0. Por cada índice i, se cumple:

  • Sus hijos están en los índices 2i+1 y 2i+2.
  • Su padre está en floor((i-1)/2). (La raíz no tiene padre)
template<typename V_>
void siftDown(V_ arr[], int start, int end)
{
    int root = start;

    while(root * 2 + 1 <= end)
    {
        int child = root * 2 +1;
        int swap = root;

        if(arr[swap] < arr[child])
        {
            swap = child;
        }
        if(child + 1 <= end && arr[swap] < arr[child + 1])
        {
            swap = child + 1;
        }
        // Si la raíz es el elemento mayor de ese montículo,
        // quiere decir que el montículo está ordenado.
        if(swap == root)
        {
            return;
        }
        else
        {
            V_ temp = arr[swap];
            arr[swap] = arr[root];
            arr[root] = temp;
            root = swap;
        }
    }
}

template<typename S_>
void heapify(S_ arr[], int length)
{
    // Se obtiene el índice del padre del último nodo
    int start = floor((length - 2)/2);

    while(start >= 0)
    {
        // Ordenar el montículo cuya raíz es start
        siftDown(arr, start, length - 1);
        start -= 1; // Ir a un nodo superior.
    }
}

template<typename T_>
void heapSort(T_ arr[], int length)
{
    // Poner el elemento mayor del arreglo
    // como raíz del montículo
    heapify(arr, length);

    int end = length - 1;
    // Invariante: arr[0...end] es un montículo
    // y todo elemento después de end es mayor.
    // Por lo que arr[end...length - 1] está ordenado
    while(end > 0)
    {
        // arr[0] es el elemento más grande del arreglo
        // se moverá al frente de la parte ordenada del
        // arreglo.
        T_ temp = arr[0];
        arr[0] = arr[end];
        arr[end] = temp;

        end -= 1;

        // El intercambio arruinó el montículo,
        // por lo que hay que volver a ordenar.
        siftDown(arr, 0, end);
    }
}

Ejemplo

Ventajas

  • Sencillo de implementar.
  • Tiene overhead constante.
  • Complejidad temporal es O(n log n).

Desventajas

  • No es estable.
  • No es flexible.

Ordenamiento rápido

(Quicksort)

  • Definición:  Es un algoritmo de divide y vencerás. Divide la lista desordenada en dos subconjuntos que se ordenan recursivamente en base a un pivote.
  • Complejidad espacial adicional: O(n) o O(log n)
  • Complejidad temporal:
    • Mejor de los casos: O(n log n)
    • Caso promedio: O(n log n)
    • Peor de los casos: O(n log n) amortizado
template<typename T_>
void quickSort(T_ arr[], int left, int right)
{
      int i = left;
      int j = right;
      T_ pivot = arr[(left + right) / 2];

      // Partición
      while (i <= j) {
          while (arr[i] < pivot)
          {
              ++i;
          }
          while (arr[j] > pivot)
          {
              --j;
          }
          if (i <= j)
          {
              T_ tmp = arr[i];
              arr[i] = arr[j];
              arr[j] = tmp;
              ++i;
              --j;
          }
      };

      // Recursión
      if (left < j)
      {
          quickSort(arr, left, j);
      }
      if (i < right)
      {
          quickSort(arr, i, right);
      }
}

Ventajas

  • Poco overhead.

Desventajas

  • No es estable.
  • No es flexible.
  • O(n^2) cuando hay pocas llaves únicas.
  • Existe una mejor versión.

Ordenamiento rápido mejorado

(Quicksort 3-way partition)

  • Definición:  Es un algoritmo de divide y vencerás. Divide la lista desordenada en tres subconjuntos que se ordenan recursivamente en base a un pivote.
  • Complejidad espacial adicional: O(log n)
  • Complejidad temporal:
    • Mejor de los casos: O(n log n)
    • Caso promedio: O(n log n)
    • Peor de los casos: O(n log n) amortizado
# choose pivot
swap a[n,rand(1,n)]

# 3-way partition
i = 1, k = 1, p = n
while i < p,
  if a[i] < a[n], swap a[i++,k++]
  else if a[i] == a[n], swap a[i,--p]
  else i++
end
→ invariant: a[p..n] all equal
→ invariant: a[1..k-1] < a[p..n] < a[k..p-1]

# move pivots to center
m = min(p-k,n-p+1)
swap a[k..k+m-1,n-m+1..n]

# recursive sorts
sort a[1..k-1]
sort a[n-p+k+1,n]

Ventajas

  • Poco overhead.
  • Es flexible.

Desventajas

  • No es estable.

Algoritmos de ordenamiento

By Victor Romero

Algoritmos de ordenamiento

  • 844