Ordenamiento Shell
El ordenamiento Shell (Shell Sort) es una versión eficiente y mejorada del ordenamiento por inserción, propuesta por Donald Shell en 1959.
La idea central del ordenamiento Shell esdividir la lista original en múltiples subsecuencias, aplicar primero el ordenamiento por inserción a cada subsecuencia, luego reducir gradualmente el intervalo entre subsecuencias y, finalmente, realizar un ordenamiento por inserción estándar sobre toda la lista.Este método permite que los elementos se muevan en grandes saltos, eliminando así una gran cantidad de inversiones en las primeras etapas y mejorando significativamente la eficiencia del ordenamiento.
Imagina ordenar una baraja de cartas desordenada. El ordenamiento por inserción es como tomar las cartas una por una e insertarlas en la posición correcta de la pila ya ordenada en la mano. El ordenamiento Shell, en cambio, es como primero extraer una carta cada varias para formar una pequeña pila y ordenarla, luego reducir el intervalo de extracción y repetir el proceso. Cuando el intervalo se reduce a 1, se convierte en un ordenamiento por inserción estándar, pero para entonces toda la secuencia ya está casi ordenada, por lo que el ordenamiento por inserción final será muy rápido.
Principio de funcionamiento
La clave del ordenamiento Shell radica en un concepto llamadosecuencia de incrementosLa secuencia de incrementos determina el intervalo para dividir las subsecuencias en cada pasada. El algoritmo realiza varias rondas de ordenamiento sobre la lista siguiendo una secuencia de incrementos que disminuye gradualmente.
Pasos del algoritmo
- Seleccionar la secuencia de incrementos: Determinar una secuencia de enteros de mayor a menor; el último incremento debe ser 1. El incremento inicial más común es la mitad de la longitud del arreglo, y luego se reduce a la mitad sucesivamente.
- Agrupar por incremento: Para el incremento actual
gap, se divide toda la lista engapsubsecuencias. Cada subsecuencia está formada por todos los elementos separados porgapese intervalo. - Aplicar el ordenamiento por inserción a las subsecuencias: Aplicar el ordenamiento por inserción a cada subsecuencia por separado.
- Reducir el incremento: Obtener un nuevo incremento más pequeño y repetir los pasos 2 y 3.
- Ordenamiento final: Cuando el incremento se reduce a 1, se realiza un ordenamiento por inserción estándar sobre toda la lista; en ese momento la lista ya está casi ordenada y el ordenamiento se completa rápidamente.
Notas de aplicación
La complejidad temporal del ordenamiento Shell esO(n^(1.3-2)), y la complejidad espacial es de orden constanteO(1). El ordenamiento Shell no es más rápido que el algoritmo de ordenamiento rápido (quicksort)O(n(logn)); por lo tanto, se comporta bien en tamaños medianos, pero no es la opción óptima para ordenar conjuntos de datos muy grandes; en resumen, es mucho más rápido que los algoritmos deO(n^2 )complejidad común.
Ilustración del proceso
El propósito del ordenamiento Shell es mejorar el ordenamiento por inserción para acelerar la velocidad: intercambia elementos no adyacentes para ordenar localmente el arreglo y, finalmente, usa el ordenamiento por inserción para ordenar el arreglo parcialmente ordenado.
Aquí elegimos el incrementogap=length/2, reduciendo el incrementogap = gap/2mediante el método, y usamos la secuencia{n/2,(n/2)/2...1}para representarlo.
Como se muestra en el ejemplo de la figura:
(1) Primera pasada con el incremento inicialgap = length/2 = 4

(2) Segunda pasada, el incremento se reduce a 2

(3) Tercera pasada, el incremento se reduce a 1, obteniendo el resultado final ordenado.

Código de ejemplo en Java
Descargar el paquete de código fuente:Download
El bucle más interno es en realidad el ordenamiento por inserción:
Código del archivo ShellSort.java:
//Inicio del código central---
public static void sort(Comparable[] arr) {
int j;
for (int gap = arr.length / 2; gap > 0; gap /= 2) {
for (int i = gap; i < arr.length; i++) {
Comparable tmp = arr[i];
for (j = i; j >= gap && tmp.compareTo(arr[j - gap]) < 0; j -= gap) {
arr[j] = arr[j - gap];
}
arr[j] = tmp;
}
}
}
//Fin del código central---
public static void main(String[] args) {
int N = 2000;
Integer[] arr = SortTestHelper.generateRandomArray(N, 0, 10);
ShellSort.sort(arr);
for( int i = 0 ; i < arr.length ; i ++ ){
System.out.print(arr[i]);
System.out.print(' ');
}
}
}
Implementación del algoritmo en Python
Usaremos Python para implementar el ordenamiento Shell y adoptaremos la configuración de incrementos más común (la secuencia original de Shell):gap = n // 2, y continuamos dividiendogapentre 2, hasta quegapsea 1.
Ejemplo
"""
Ordena la lista en orden ascendente in situ utilizando el algoritmo de ordenamiento Shell.
Parámetros:
arr (list): la lista a ordenar.
Retorna:
list: la lista ordenada (modificada in situ, también se retorna).
"""
n = len(arr)
# El incremento inicial gap se establece como la mitad de la longitud del arreglo
gap = n // 2
# Bucle hasta que gap se reduzca a 0
while gap > 0:
# Comenzando desde gap, se aplica "ordenamiento por inserción" a cada elemento
# Nota: aquí i va de gap a n-1, recorriendo todos los elementos
for i in range(gap, n):
# Guardar arr[i] en la variable temporal temp
temp = arr[i]
j = i
# Aplicar ordenamiento por inserción a la subsecuencia correspondiente al gap actual
# Si el elemento anterior en la misma subsecuencia es mayor que temp, desplazarlo hacia la derecha
while j >= gap and arr[j - gap] > temp:
arr[j] = arr[j - gap]
j -= gap
# Insertar temp en la posición correcta
arr[j] = temp
# Reducir el incremento, por ejemplo, reducir gap a la mitad
gap //= 2
return arr
# Código de prueba
if __name__ == "__main__":
# Datos de prueba 1: aleatorio desordenado
test_array = [8, 3, 5, 1, 4, 2, 7, 6]
print("Antes de ordenar:", test_array)
sorted_array = shell_sort(test_array.copy()) # Usar copy para evitar modificar el arreglo original
print("Después de ordenar:", sorted_array)
# Datos de prueba 2: con elementos repetidos
test_array2 = [5, 2, 9, 5, 2, 3, 5]
print("\n"Antes de ordenar:", test_array2)
print("Después de ordenar:", shell_sort(test_array2.copy()))
# Datos de prueba 3: arreglo en orden inverso
test_array3 = [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
print("\n"Antes de ordenar:", test_array3)
print("Después de ordenar:", shell_sort(test_array3.copy()))
Análisis del código:
def shell_sort(arr):Definir la función de ordenamiento Shell.n = len(arr)Obtener la longitud del arreglo.gap = n // 2Inicializar el incremento. Este es el punto de partida más clásico de la secuencia de incrementos del ordenamiento Shell.while gap > 0:Bucle externo: controla que el incremento disminuya gradualmente hasta 0 (después de ejecutarse con el valor 1,gap //= 2se convierte en 0 y sale del bucle).for i in range(gap, n):Bucle interno. Desde el índicegapse comienza a recorrer.iSeñala el elemento actual que se va a insertar. Nota: este bucle maneja hábilmente a la vez todas las subsecuencias que tienengapcomo intervalo.temp = arr[i]yj = i: guarda el valor del elemento actual y usajpara registrar la posición donde debe insertarse en la subsecuencia actual.while j >= gap and arr[j - gap] > temp:Este es el núcleo del ordenamiento por inserción. Dentro de la misma subsecuencia (mediante el índicej - gapse accede al elemento anterior), si el elemento anterior es mayor quetemp, se desplaza el elemento anteriorgapposiciones hacia la derecha.arr[j] = temp: el valor guardadotempse inserta en la posición correcta.gap //= 2: después de una ronda de ordenamiento, se reduce el incremento a la mitad para realizar la siguiente ronda de ordenamiento más fino.return arr: retorna el arreglo ordenado.
Selección de la secuencia de incrementos
La elección de la secuencia de incrementos afecta directamente el rendimiento del ordenamiento Shell. Arriba hemos usado la secuencia original de Shell (n/2, n/4, ..., 1), pero no es la óptima. A continuación se presentan varias secuencias de incrementos comunes:
| Nombre de la secuencia | Fórmula de generación | Características y complejidad |
|---|---|---|
| Secuencia original de Shell | $gap = \lfloor \frac{n}{2} \rfloor, gap = \lfloor \frac{gap}{2} \rfloor...$ | Implementación sencilla; la complejidad temporal en el peor caso es $O(n^2)$. |
| Secuencia de Hibbard | $1, 3, 7, 15, ..., 2^k - 1$ | La complejidad temporal en el peor caso puede mejorarse a $O(n^{3/2})$. |
| Secuencia de Knuth | $1, 4, 13, 40, ..., (3^k - 1) / 2$ (menor que n) | La complejidad temporal promedio es aproximadamente $O(n^{1.25})$, de uso común en la práctica. |
| Secuencia de Sedgewick | $1, 5, 19, 41, 109, ...$ (combina $9 \times 4^k - 9 \times 2^k + 1$ y $4^k - 3 \times 2^k + 1$) | Es una de las mejores secuencias conocidas; la complejidad en el peor caso puede alcanzar $O(n^{4/3})$. |
Ejemplo de implementación con la secuencia de Knuth
Ejemplo
n = len(arr)
# Generar la secuencia de incrementos de Knuth: 1, 4, 13, 40, 121, ...
gap = 1
while gap < n // 3:
gap = 3 * gap + 1 # Generar el incremento inicial máximo
while gap > 0:
for i in range(gap, n):
temp = arr[i]
j = i
while j >= gap and arr[j - gap] > temp:
arr[j] = arr[j - gap]
j -= gap
arr[j] = temp
gap //= 3 # Knuth sugiere dividir por 3 cada vez
return arr
# Probar la versión de la secuencia de Knuth
test_array = [33, 12, 56, 78, 2, 45, 67, 89, 1, 23]
print("Antes de ordenar con la secuencia de Knuth:", test_array)
print("Después de ordenar con la secuencia de Knuth:", shell_sort_knuth(test_array.copy()))
Características del algoritmo y análisis de complejidad
Complejidad temporal
El análisis de la complejidad temporal del ordenamiento Shell es muy complejo, ya que depende de la secuencia de incrementos elegida.
- Peor caso: Al usar la secuencia original de Shell, la complejidad temporal en el peor caso es $O(n^2)$.
- Caso promedio: Para buenas secuencias de incremento (como Knuth, Sedgewick), la complejidad temporal promedio está entre $O(n^{1.25})$ y $O(n^{1.5})$.
- Mejor caso: Cuando el array ya está ordenado, la complejidad puede acercarse a $O(n \log n)$.
Complejidad espacial
- El ordenamiento Shell esun algoritmo de ordenamiento in situque solo requiere espacio adicional de nivel constante (como
temp,gap,i,jvariables), por lo que la complejidad espacial es $O(1)$.
Estabilidad
- El ordenamiento Shell esun algoritmo de ordenamiento inestable. Porque al ordenar por grupos según el incremento, los elementos iguales pueden ser asignados a diferentes subsecuencias y cambiar su orden relativo debido a los movimientos.
Comparación breve con otros algoritmos de ordenamiento
| Característica | Ordenamiento Shell | Ordenamiento por inserción | Ordenamiento por mezcla (Merge sort) | Ordenamiento rápido (Quicksort) |
|---|---|---|---|---|
| Complejidad temporal promedio | $O(n^{1.25})$ ~ $O(n^{1.5})$ | $O(n^2)$ | $O(n \log n)$ | $O(n \log n)$ |
| Complejidad espacial | $O(1)$ | $O(1)$ | $O(n)$ | $O(\log n)$ |
| Estabilidad | Inestable | Estable | Estable | Inestable |
| Ventajas | Buen rendimiento con volúmenes de datos medianos, ordenamiento in situ | Simple, eficiente con datos pequeños o casi ordenados | Estable, excelente complejidad temporal | Más rápido en el caso promedio |
| Desventajas | Análisis de complejidad difícil, la elección de la secuencia de incrementos tiene gran influencia | Baja eficiencia con grandes volúmenes de datos | Requiere espacio adicional | El peor caso degenera a $O(n^2)$ |