Análisis e Implementación de Algoritmos de Ordenamiento

Conceptos Fundamentales

El ordenamiento organiza elementos según criterios específicos para facilitar su búsqueda y comparación. La estabilidad de un algoritmo garantiza que elementos iguales mantengan su orden relativo original. Los algoritmos estables incluyen enserción directa y ordenamiento por fusión, mientras que rápido y por montón son inestables. El ordenamiento se clasifica en interno (datos en memoria) y externo (datos en disco).

Inserción Directa

Este algoritmo construye una secuencia ordenada incrementando elementos uno a uno. Para cada posición, se compara el elemento actual con los anteriores y se desplazan los mayores para insertarlo en su lugar correcto.

void ordenarPorInsercion(int* vector, int longitud) {
    for (int i = 1; i < longitud; ++i) {
        int valorActual = vector[i];
        int j = i - 1;
        
        while (j >= 0 && vector[j] > valorActual) {
            vector[j + 1] = vector[j];
            j--;
        }
        vector[j + 1] = valorActual;
    }
}

Su complejidad temporal es O(n²) en el peor caso (arreglo inversamente ordenado) y O(n) en el mejor caso (arreglo casi ordenado). Es estable y usa O(1) de espacio adicional.

Ordenamiento de Shell

Mejora la inserción directa mediante comparaciones entre elementos distantes, reduciendo progresivamente la brecha. Esto acerca los elementos a su posición final antes del refinamiento final.

void ordenamientoShell(int* vector, int tam) {
    for (int brecha = tam/2; brecha > 0; brecha /= 2) {
        for (int i = brecha; i < tam; i += 1) {
            int temp = vector[i];
            int j;
            
            for (j = i; j >= brecha && vector[j - brecha] > temp; j -= brecha) {
                vector[j] = vector[j - brecha];
            }
            vector[j] = temp;
        }
    }
}

Su complejidad es O(n^(1.3-2)) en promedio, inestable y usa O(1) de espacio adicional.

Selección Directa

Busca iterativamente el elemento mínimo/máximo y lo intercambia con la posición actual. Una versión optimizada busca ambos extremos en cada pasada.

void ordenarPorSeleccion(int* datos, int n) {
    int izquierda = 0, derecha = n - 1;
    
    while (izquierda < derecha) {
        int minIdx = izquierda, maxIdx = derecha;
        
        for (int k = izquierda; k <= derecha; ++k) {
            if (datos[k] < datos[minIdx]) minIdx = k;
            if (datos[k] > datos[maxIdx]) maxIdx = k;
        }
        
        intercambiar(&datos[izquierda], &datos[minIdx]);
        if (izquierda == maxIdx) maxIdx = minIdx;
        intercambiar(&datos[derecha], &datos[maxIdx]);
        
        izquierda++;
        derecha--;
    }
}

Su complejidad es O(n²) en todos los casos, inestable y usa O(1) de espacio adicional.

Ordenamiento por Burbuja

Compara elemetnos adyacentes y los intercambia si están en orden incorrecto, propagando los elementos más grandes al final.

void ordenamientoBurbuja(int* arreglo, int tam) {
    bool intercambio;
    
    for (int i = 0; i < tam - 1; ++i) {
        intercambio = false;
        
        for (int j = 0; j < tam - 1 - i; ++j) {
            if (arreglo[j] > arreglo[j + 1]) {
                intercambiar(&arreglo[j], &arreglo[j + 1]);
                intercambio = true;
            }
        }
        
        if (!intercambio) break;
    }
}

Su complejidad es O(n²) en el peor caso, O(n) en el mejor caso. Es estable y usa O(1) de espacio adicional.

Ordenamiento Rápido (Quicksort)

Selecciona un pivote y particiona el arreglo en elementos menores y mayores, aplicando recursión a cada subarreglo.

Implementación con método de los punteros

int particionar(int* datos, int inicio, int fin) {
    int pivote = datos[inicio];
    int i = inicio + 1;
    
    for (int j = inicio + 1; j <= fin; ++j) {
        if (datos[j] < pivote) {
            intercambiar(&datos[i], &datos[j]);
            i++;
        }
    }
    intercambiar(&datos[inicio], &datos[i - 1]);
    return i - 1;
}

void quicksort(int* arr, int ini, int fin) {
    if (ini < fin) {
        int pivoteIdx = particionar(arr, ini, fin);
        quicksort(arr, ini, pivoteIdx - 1);
        quicksort(arr, pivoteIdx + 1, fin);
    }
}

Optimizaciones

Selección de pivote con mediana de tres: evita el peor caso en datos ordenados.

int obtenerMediana(int* arr, int a, int b, int c) {
    if ((arr[a] <= arr[b] && arr[b] <= arr[c]) || (arr[c] <= arr[b] && arr[b] <= arr[a]))
        return b;
    else if ((arr[b] <= arr[a] && arr[a] <= arr[c]) || (arr[c] <= arr[a] && arr[a] <= arr[b]))
        return a;
    else
        return c;
}

Optimización para subarreglos pequeños: usa inserción directa para menos de 10 elementos.

Complejidad promedio: O(n log n). Espacio: O(log n) a O(n). Inestable.

Ordenamiento por Fusión (Merge Sort)

Divide recursivamente el arreglo y fusiona subarreglos ordenados.

void fusionar(int* arr, int izq, int med, int der) {
    int n1 = med - izq + 1;
    int n2 = der - med;
    
    int L[n1], R[n2];
    for (int i = 0; i < n1; ++i) L[i] = arr[izq + i];
    for (int j = 0; j < n2; ++j) R[j] = arr[med + 1 + j];
    
    int i = 0, j = 0, k = izq;
    while (i < n1 && j < n2) {
        if (L[i] <= R[j]) {
            arr[k++] = L[i++];
        } else {
            arr[k++] = R[j++];
        }
    }
    
    while (i < n1) arr[k++] = L[i++];
    while (j < n2) arr[k++] = R[j++];
}

void mergeSort(int* arr, int ini, int fin) {
    if (ini < fin) {
        int med = ini + (fin - ini) / 2;
        mergeSort(arr, ini, med);
        mergeSort(arr, med + 1, fin);
        fusionar(arr, ini, med, fin);
    }
}

Complejidad: O(n log n). Espacio: O(n). Estable.

Ordenamiento por Conteo

Algoritmo no comparativo que cuenta ocurrencias de cada valor, adecuado para anteros no negativos en rango acotado.

void ordenamientoConteo(int* arr, int tam) {
    int max = *max_element(arr, arr + tam);
    int min = *min_element(arr, arr + tam);
    int rango = max - min + 1;
    
    int* conteo = new int[rango]();
    int* salida = new int[tam];
    
    for (int i = 0; i < tam; ++i) conteo[arr[i] - min]++;
    for (int i = 1; i < rango; ++i) conteo[i] += conteo[i - 1];
    
    for (int i = tam - 1; i >= 0; --i) {
        salida[conteo[arr[i] - min] - 1] = arr[i];
        conteo[arr[i] - min]--;
    }
    
    for (int i = 0; i < tam; ++i) arr[i] = salida[i];
    
    delete[] conteo;
    delete[] salida;
}

Complejidad: O(n + k) donde k es el rango. Espacio: O(n + k). Estable.

Análisis Comparativo

Algoritmo Caso Promedio Mejor Caso Peor Caso Espacio Estabilidad
Burbuja O(n²) O(n) O(n²) O(1) Estable
Selección O(n²) O(n²) O(n²) O(1) Inestable
Inserción O(n²) O(n) O(n²) O(1) Estable
Shell O(n^(1.3)) O(n log n) O(n²) O(1) Inestable
Fusión O(n log n) O(n log n) O(n log n) O(n) Estable
Rápido O(n log n) O(n log n) O(n²) O(log n) Inestable
Conteo O(n + k) O(n + k) O(n + k) O(n + k) Estable

Etiquetas: algoritmos de ordenamiento inserción directa Shell sort selección directa burbuja

Publicado el 6-3 20:28