¿Cuál es la forma más eficiente de ordenar un millón de enteros de 32 bits?

Como sabemos que estamos ordenando enteros de tamaño fijo, la ordenación por radix es mucho más eficiente que cualquier ordenación basada en comparación (ordenación rápida, introducción, ordenación por fusión, etc.).

Aquí hay algunos puntos de referencia en mi computadora:

  • clasificación rápida (codificada a mano): 79 milisegundos
  • introsort ( std::sort ): 71 milisegundos
  • ordenar combinación ( std::stable_sort ): 62 milisegundos
  • tipo de fusión ( qsort de glibc) 148 milisegundos, más del doble debido a la sobrecarga del puntero de función
  • base- [matemática] 2 ^ 8 [/ matemática] clasificación de radios LSD (codificada a mano, código a continuación): 14 milisegundos
  • base- [matemática] 2 ^ 8 [/ matemática] Clasificación de radios LSD con un pase inicial de MSD (vea el comentario de Gregory Popovitch): 11 milisegundos

El presidente tiene razón en que el tipo de burbuja sería el camino equivocado, ¡eso tomaría unos 33 minutos !


Código de clasificación Radix

 void radix_sort(unsigned *begin, unsigned *end) { unsigned *begin1 = new unsigned[end - begin]; unsigned *end1 = begin1 + (end - begin); for (unsigned shift = 0; shift > shift) & 0xFF]++; unsigned *bucket[0x100], *q = begin1; for (int i = 0; i > shift) & 0xFF]++ = *p; std::swap(begin, begin1); std::swap(end, end1); } delete[] begin1; } 

Suponiendo que tuviera una tarjeta de video NVidia / AMD relativamente reciente que admitiera CUDA u OpenCL, integraría algo como esto:

Paralelo Radix Sort en la GPU usando C ++ AMP

“Notará que para muy pocas claves (menos de ~ 30,000 claves), la CPU funciona mejor. Sin embargo, para un mayor número de claves (más de ~ 1M), la GPU funciona alrededor de 50x – 80x más rápido” (que la cpu radix ordenar)”

Debido al espacio de dominio finito de los datos (es decir, “enteros”), se puede resolver en una variante de [matemática] O (n) [/ matemática] que, en ciertos escenarios, es mejor que la ordenación regular. Como el presidente Obama dice correctamente, “Bubble Sort no es el camino a seguir”.

Primero, examinemos cómo se pueden ordenar los datos con un espacio de dominio finito (como Enteros de 32 bits) en tiempo lineal:

Un ejemplo es el tipo Radix:

  para el índice j de cero a 31
   cubo de clasificación de enteros por su jésimo índice desde el lado derecho // O (n)

La complejidad de esta clasificación es [matemática] O (lg (domainSize) * n) [/ matemática] en este caso [matemática] O (32 * n) [/ matemática], equivalente a [matemática] O (n) [/ matemáticas] o lineal. En inglés simple, pones n números binarios en dos cubos cero o uno, 32 veces. Ignora esos multiplicadores constantes y tienes un proceso lineal.

Ejemplo en decimales de 3 dígitos:

431, 235, 521, 236

después del primer pase (ordenar por el extremo derecho): 431, 521, 235, 236
después del segundo pase (ordenar por medio dígito): 521, 431, 235, 236
después del tercer pase (ordenar por el extremo izquierdo): 235, 236, 431, 521

En este ejemplo, necesita 10 cubos (es decimal) y 3 pasa a través de los datos (son 3 dígitos)

En el caso entero general, necesita 2 cubos (es binario) y 32 pasa a través de los datos. Entonces, la complejidad es [matemática] O (32 * n) [/ matemática] que es [matemática] O (n) [/ matemática], o lineal.

A veces, según el múltiplo lineal y el tamaño de los datos, [matemática] O (n * lg (n)) [/ matemática] es mejor que [matemática] O (n) [/ matemática]

Pensándolo bien, tenga en cuenta que [matemática] O (n) [/ matemática] es solo mejor que [matemática] O (n * lg (n)) [/ matemática] cuando el tamaño de los datos se aproxima al infinito. En este caso particular, dado que tenemos un número conocido de puntos de datos (1 millón), la ventaja de utilizar la ordenación lineal como hemos demostrado no necesariamente entra en acción. La ordenación lineal, como hemos observado, toma [matemática] O (32 * n) [/ math], pero el orden regular toma:

[matemática] O (n * lg (n)) = O (n * lg (1M)) ~ = O (20 * n) [/ matemática]

Entonces, la ordenación regular es claramente mejor que la solución lineal anterior, y esto es cierto hasta 32 cubos y un millón de enteros.

¡Pero espera! Hay espacio para la optimización.

Por lo tanto, demostramos que la ordenación regular en datos finitos es tan buena como cualquier ordenación lineal (por ejemplo, Radix), siempre que el múltiplo de [math] n [/ math] en la complejidad del algoritmo de ordenación lineal sea peor que (mayor o igual que ) 20, requerido por la ordenación regular (Recuerde que 20 es el número mágico para la comparación binaria entre un máximo de 1,000,000 de enteros, y la comparación binaria se usa en la ordenación regular).

El desafío es el siguiente: ¿Podemos diseñar un tipo Radix que produzca un multiplicador “k” en complejidad lineal [matemática] O (k * n) [/ matemática] que sea menor que 20?

Recuerde que el multiplicador en la clasificación Radix es el número de cubos. En nuestro algoritmo anterior, utilizamos 32 cubos. ¿Podemos usar menos?

Porque?, si. Veamos qué sucede si definimos nuestro cubo como cada dos bits:

  para el índice j de cero a 31 paso a paso por 2
   tipo de cubo enteros por su j + 1 y j'th bit del lado derecho // O (n)

Esto para el bucle se ejecuta 16 veces. Entonces, la complejidad de este ordenamiento lineal es [matemática] O (16 * n) [/ matemática] o mejor que el ordenamiento regular, incluso para estos datos finitos.

Entonces, ¿dónde está el límite inferior?

Es tentador tratar de llevar esto al límite. ¿Por qué no simplemente hacer un cubo de cada entero único y hacer una clasificación de Radix en eso? ¿Seguramente eso será solo una pasada o [matemáticas] O (1 * n) [/ matemáticas]?

No tan rapido. En la aproximación “aproximada” inicial de la complejidad del ordenamiento Radix, pasamos por alto el costo de fusionar los cubos, porque solo teníamos dos cubos para cada una de las 32 iteraciones.

La verdadera complejidad de una clasificación Radix es [matemática] O (k * (n + 2 ^ b)) [/ matemática] para enteros de 32 bits, donde [matemática] k * b == 32 [/ matemática]. k es el número de veces que el Entero se “divide” en el algoritmo (el número de iteraciones) y, por lo tanto, b es el número de bits que debemos observar para descubrir la clave del depósito. Por lo tanto, hay cubos [matemática] 2 ^ b [/ matemática] y en cada iteración necesitamos visitar cada uno de ellos para recopilar los resultados.

Probemos esto para algunas configuraciones de k y b (donde eventualmente n es sustituido por [math] 2 ^ {20} [/ math], que es una aproximación cercana para 1M):

k = 32 (es decir, 2 cubos):
[matemáticas] O (32 * (n + 2)) = O (32n + 2 ^ 6) [/ matemáticas]
[matemáticas] \ aprox. O (2 ^ {25} + 2 ^ 6) \ aprox. O (2 ^ {25}) [/ matemáticas]

k = 16 (es decir, 2 ^ 2 cubos):
[matemáticas] O (16 * (n + 2 ^ 2)) = O (16n + 2 ^ 6) [/ matemáticas]
[matemáticas] \ aprox. O (2 ^ {24} + 2 ^ 6) \ aprox. O (2 ^ {24}) [/ matemáticas]

k = 8 (es decir, 2 ^ 4 cubos):
[matemáticas] O (8 * (n + 2 ^ 4)) = O (8n + 2 ^ 7) [/ matemáticas]
[matemáticas] \ aprox. O (2 ^ {23} + 2 ^ 7) \ aprox. O (2 ^ {23}) [/ matemáticas]

k = 4 (es decir, 2 ^ 8 cubos):
[matemáticas] O (4 * (n + 2 ^ 8)) = O (4n + 2 ^ {10}) [/ matemáticas]
[matemáticas] \ aprox. O (2 ^ {22} + 2 ^ {10}) \ aprox. O (2 ^ {22}) [/ matemáticas]

k = 2 (es decir, 2 ^ 16 cubos):
[matemáticas] O (2 * (n + 2 ^ {16})) = O (2n + 2 ^ {17}) [/ matemáticas]
[matemática] \ aprox. O (2 ^ {21} + 2 ^ {17}) \ aprox. O (2 ^ {21}) [/ matemática]

k = 1 (es decir, 2 ^ 32 cubos):
[matemáticas] O (1 * (n + 2 ^ {32})) = O (n + 2 ^ {32}) [/ matemáticas]
[matemáticas] \ aprox. O (2 ^ {20} + 2 ^ {32}) \ aprox. O (2 ^ {32}) [/ matemáticas]

Tenga en cuenta que al disminuir el número de iteraciones a uno (y aumentar el número de cubos que se representarán por todos los 32 bits, en la última línea), el costo de cómputo diverge debido a la sobrecarga de los cubos visitantes.

La respuesta óptima es en algún lugar entre k = 2 ([matemática] 2 ^ {16} [/ matemática] cubos) y k = 4 ([matemática] 2 ^ 8 [/ matemática] cubetas), y depende de la cantidad de gastos generales. pasé fusionando cubos de nuevo juntos. De acuerdo con experimentos prácticos de Anders y otras personas en este hilo, parece que Radix sortear con [math] 2 ^ 8 [/ math] es el ganador .

Dejaré el escenario de procesamiento paralelo al lector.

Por cierto, el presidente tiene razón, porque Bubble Sort tomaría [math] O (n ^ 2) [/ math] o, en este caso, aproximadamente [math] O (2 ^ {40}) [/ math].

(Editado con especial agradecimiento a los comentarios de Anders, Ryan, Erik y Sergei!)

More Interesting

En F (n) -F (n-1) = n ^ 8, ¿qué es F (n)?

¿Puede la búsqueda de profundización iterativa encontrar una solución más rápida que A * en algunos casos?

¿Funciona la siguiente implementación para encontrar la subcadena común más larga dentro de dos cadenas?

¿Está bien inicializar una matriz que contiene fracciones en c ++?

¿Cómo podemos demostrar que el reconocimiento de objetos basado en la visión es un problema np completo?

¿Podría alguien ayudarme con el problema del algoritmo 'Intervalo casi ordenado'?

¿Qué tan difícil es CS50x?

¿Qué es la estructura de datos, algoritmo en informática? ¿Cuál es la mejor forma en línea para aprenderlo?

¿Qué algoritmos utilizan Bing, Ask y DuckDuckGo para mostrar los resultados de búsqueda?

Cómo encontrar subrangos no decrecientes y no crecientes en una matriz

Siempre sueño con trabajar en grandes empresas tecnológicas como Google o Facebook, pero mi habilidad con los algoritmos es muy débil. Intento resolver problemas en Google Code Jam y CodeChef, pero solo puedo resolver los fáciles. ¿Qué tengo que hacer?

¿Qué otros algoritmos de clasificación utilizan la estrategia 'Divide y vencerás' además de la clasificación rápida y la combinación?

¿Alguien podría explicar la respuesta a este problema de mecánica?

¿Qué es un árbol de búsqueda binario en una estructura de datos?

¿Cuál es la forma más rápida de encontrar el número original antes del descuento a mano? (números grandes)