Escriba un programa para encontrar 100 números más grandes de una matriz de mil millones de números

300

Hace poco asistí a una entrevista en la que me pidieron "escribir un programa para encontrar los 100 números más grandes de un conjunto de mil millones de números".

Solo pude dar una solución de fuerza bruta que consistía en ordenar la matriz en complejidad de tiempo O (nlogn) y tomar los últimos 100 números.

Arrays.sort(array);

El entrevistador estaba buscando una mejor complejidad temporal, probé un par de otras soluciones pero no pude responderle. ¿Existe una mejor solución de complejidad de tiempo?

userx
fuente
70
Tal vez el problema es que no era una pregunta de clasificación , sino una pregunta de búsqueda .
geomagas
11
Como nota técnica, la clasificación puede no ser la mejor manera de resolver el problema, pero no creo que sea la fuerza bruta; puedo pensar en formas mucho peores de hacerlo.
Bernhard Barker
88
Acabo de pensar en un método de fuerza bruta aún más estúpido ... Encuentre todas las combinaciones posibles de 100 elementos de la matriz de mil millones de elementos y vea cuál de estas combinaciones tiene la suma más grande.
Shashank
10
Tenga en cuenta que todos los algoritmos deterministas (y correctos) son O(1)en este caso, porque no hay aumento de dimensión. El entrevistador debería haber preguntado "¿Cómo encontrar m elementos más grandes de una matriz de n con n >> m?".
Bakuriu

Respuestas:

328

Puede mantener una cola prioritaria de los 100 números más grandes, recorrer los mil millones de números cada vez que encuentre un número mayor que el número más pequeño en la cola (el encabezado de la cola), elimine el encabezado de la cola y agregue el nuevo número a la cola

EDITAR: como señaló Dev, con una cola prioritaria implementada con un montón, la complejidad de la inserción en la cola esO(logN)

En el peor de los casos obtienes cuál es mejor quebillionlog2(100)billionlog2(billion)

En general, si necesita los números K más grandes de un conjunto de números N, la complejidad es O(NlogK)más que O(NlogN)esto, esto puede ser muy significativo cuando K es muy pequeño en comparación con N.

EDIT2:

El tiempo esperado de este algoritmo es bastante interesante, ya que en cada iteración puede ocurrir o no una inserción. La probabilidad de que el i-ésimo número se inserte en la cola es la probabilidad de que una variable aleatoria sea mayor que al menos i-Kvariables aleatorias de la misma distribución (los primeros k números se agregan automáticamente a la cola). Podemos usar estadísticas de pedidos (ver enlace ) para calcular esta probabilidad. Por ejemplo, supongamos que los números se seleccionaron aleatoriamente de manera uniforme {0, 1}, el valor esperado de (iK) th número (de i números) es (i-k)/i, y la posibilidad de que una variable aleatoria sea mayor que este valor 1-[(i-k)/i] = k/i.

Por lo tanto, el número esperado de inserciones es:

ingrese la descripción de la imagen aquí

Y el tiempo de ejecución esperado se puede expresar como:

ingrese la descripción de la imagen aquí

( ktiempo para generar la cola con los primeros kelementos, luego las n-kcomparaciones y el número esperado de inserciones como se describió anteriormente, cada una toma un log(k)/2tiempo promedio )

Tenga en cuenta que cuando Nes muy grande en comparación con K, esta expresión está mucho más cerca que en nlugar de NlogK. Esto es algo intuitivo, como en el caso de la pregunta, incluso después de 10000 iteraciones (que es muy pequeño en comparación con mil millones), la posibilidad de que se inserte un número en la cola es muy pequeña.

Ron Teller
fuente
66
En realidad, es solo O (100) para cada inserto.
MrSmith42
8
@RonTeller No puede realizar búsquedas binarias en una lista vinculada de manera eficiente, es por eso que una cola prioritaria generalmente se implementa con un montón. Su tiempo de inserción como se describe es O (n) no O (logn). Lo acertó la primera vez (cola ordenada o cola prioritaria) hasta que Skizz lo hizo adivinar por su cuenta.
Dev
17
@ThomasJungblut mil millones también es una constante, así que si ese es el caso, es O (1): P
Ron Teller
99
@RonTeller: normalmente este tipo de preguntas se refiere a encontrar 10 páginas principales de miles de millones de resultados de búsqueda de Google, o 50 palabras más frecuentes para una nube de palabras, o 10 canciones más populares en MTV, etc. Entonces, creo, en circunstancias normales es seguro considerar k constante y pequeño en comparación con n. Sin embargo, uno siempre debe tener en cuenta estas "circunstancias normales".
amigo
55
Como tiene elementos 1G, tome muestras de 1000 elementos al azar y elija los 100 más grandes. Eso debería evitar los casos degenerados (ordenados, ordenados de forma inversa, ordenados en su mayoría), reduciendo considerablemente el número de inserciones.
ChuckCottrill
136

Si se pregunta esto en una entrevista, creo que el entrevistador probablemente quiera ver su proceso de resolución de problemas, no solo su conocimiento de algoritmos.

La descripción es bastante general, por lo que quizás pueda preguntarle el rango o el significado de estos números para aclarar el problema. Hacer esto puede impresionar a un entrevistador. Si, por ejemplo, estos números representan la edad de las personas dentro de un país (por ejemplo, China), entonces es un problema mucho más fácil. Con una suposición razonable de que nadie vivo tiene más de 200 años, puede usar una matriz int de tamaño 200 (quizás 201) para contar el número de personas con la misma edad en una sola iteración. Aquí el índice significa la edad. Después de esto, es fácil encontrar el número 100 más grande. Por cierto, este algo se llama orden de conteo .

De todos modos, hacer la pregunta más específica y más clara es bueno para usted en una entrevista.

jin
fuente
26
Muy buenos puntos. Nadie más ha preguntado o indicado nada acerca de la distribución de esos números: podría marcar la diferencia en cómo abordar el problema.
NealB
13
Me gustaría esta respuesta lo suficiente como para extenderla. Lea los números una vez para obtener los valores min / max para que pueda asumir la distribución. Luego, toma una de dos opciones. Si el rango es lo suficientemente pequeño, cree una matriz donde simplemente pueda marcar los números a medida que ocurren. Si el rango es demasiado grande, use el algoritmo de montón ordenado descrito anteriormente ... Solo un pensamiento.
Richard_G
2
Estoy de acuerdo, hacer preguntas al entrevistador realmente hace una gran diferencia. De hecho, una pregunta como si está limitado por la potencia de cálculo o no también puede ayudarlo a paralelizar la solución mediante el uso de múltiples nodos de cálculo.
Sumit Nigam
1
@R_G No es necesario revisar toda la lista. Suficiente para muestrear una pequeña fracción (por ejemplo, un millón) de miembros aleatorios de la lista para obtener estadísticas útiles.
Itamar
Para aquellos que no hubieran pensado en esa solución, recomendaría leer sobre el tipo de conteo en.wikipedia.org/wiki/Counting_sort . Esa es en realidad una pregunta de entrevista bastante común: ¿puede ordenar una matriz mejor que O (nlogn)? Esta pregunta es solo una extensión.
Maxime Chéramy
69

Puede iterar sobre los números que toman O (n)

Siempre que encuentre un valor mayor que el mínimo actual, agregue el nuevo valor a una cola circular con tamaño 100.

El mínimo de esa cola circular es su nuevo valor de comparación. Sigue agregando a esa cola. Si está lleno, extraiga el mínimo de la cola.

Regenschein
fuente
3
Esto no funciona por ejemplo, encontrar el top 2 de {1, 100, 2, 99} dará {100,1} como el top 2.
Skizz
77
No puede moverse para mantener ordenada la cola. (si no desea buscar en la cola de agujeros el siguiente elemento más pequeño)
MrSmith42
3
@ MrSmith42 La clasificación parcial, como en un montón, es suficiente. Ver la respuesta de Ron Teller.
Christopher Creutzig
1
Sí, silenciosamente asumí que una extracción-min-cola se implementa como un montón.
Regenschein
En lugar de utilizar la cola circular, use un montón mínimo de tamaño 100, esto tendrá un mínimo de cien números en la parte superior. Esto tomará solo O (log n) para insertar en comparación con o (n) en caso de cola
techExplorer
33

Me di cuenta de que esto está etiquetado con 'algoritmo', pero arrojará algunas otras opciones, ya que probablemente también debería etiquetarse 'entrevista'.

¿Cuál es la fuente de los mil millones de números? Si se trata de una base de datos, entonces 'seleccionar el valor del orden de la tabla por el valor desc límite 100' haría el trabajo bastante bien, puede haber diferencias de dialecto.

¿Es esto único o algo que se repetirá? Si se repite, ¿con qué frecuencia? Si es único y los datos están en un archivo, entonces 'cat srcfile | ordenar (opciones según sea necesario) | head -100 'lo hará hacer rápidamente un trabajo productivo por el que le pagan mientras la computadora maneja esta tarea trivial.

Si se repite, aconsejaría elegir un enfoque decente para obtener la respuesta inicial y almacenar / almacenar en caché los resultados para poder informar continuamente sobre los 100 mejores.

Finalmente, hay esta consideración. ¿Está buscando un trabajo de nivel de entrada y se entrevista con un gerente geek o un futuro compañero de trabajo? Si es así, puede descartar todo tipo de enfoques que describan los pros y los contras técnicos relativos. Si está buscando un trabajo más gerencial, acéptelo como lo haría un gerente, preocupado por los costos de desarrollo y mantenimiento de la solución, y diga "muchas gracias" y abandone si ese es el entrevistador que quiere centrarse en la trivia de CS . Es poco probable que él y usted tengan mucho potencial de avance allí.

Mejor suerte en la próxima entrevista.

Fred Mitchell
fuente
2
Respuesta excepcional Todos los demás se han concentrado en el aspecto técnico de la pregunta, mientras que esta respuesta aborda la parte social de la empresa.
vbocan
2
Nunca imaginé que podrías decir gracias y dejar una entrevista y no esperar a que termine. Gracias por abrir mi mente.
UrsulRosu
1
¿Por qué no podemos crear un montón de mil millones de elementos y extraer 100 elementos más grandes? De esta manera, el costo = O (billones) + 100 * O (log (billones)) ??
Mohit Shah
17

Mi reacción inmediata para esto sería usar un montón, pero hay una manera de usar QuickSelect sin tener a mano todos los valores de entrada en cualquier momento.

Cree una matriz de tamaño 200 y llénela con los primeros 200 valores de entrada. Ejecute QuickSelect y descarte los 100 bajos, dejándolo con 100 lugares libres. Lea los siguientes 100 valores de entrada y ejecute QuickSelect nuevamente. Continúe hasta que haya ejecutado toda la entrada en lotes de 100.

Al final tienes los 100 mejores valores. Para los valores de N, ha ejecutado QuickSelect aproximadamente N / 100 veces. Cada selección rápida cuesta aproximadamente 200 veces alguna constante, por lo que el costo total es 2N veces alguna constante. Esto se ve lineal en el tamaño de la entrada para mí, independientemente del tamaño del parámetro que estoy cableado para ser 100 en esta explicación.

mcdowella
fuente
10
Puede agregar una optimización pequeña pero posiblemente importante: después de ejecutar QuickSelect para particionar la matriz de tamaño 200, se conoce el mínimo de los 100 elementos principales. Luego, al iterar sobre todo el conjunto de datos, solo complete los 100 valores inferiores si el valor actual es mayor que el mínimo actual. Una implementación simple de este algoritmo en C ++ está a la par con la partial_sortejecución de libstdc ++ directamente en un conjunto de datos de 200 millones de 32 bits int(creado a través de un MT19937, distribuido uniformemente).
dyp
1
Buena idea: no afecta el análisis del peor de los casos, pero parece que vale la pena hacerlo.
mcdowella
@mcdowella Vale la pena intentarlo y lo haré, ¡gracias!
userx
8
Esto es exactamente lo que hace Guava's Ordering.greatestOf(Iterable, int) . Es absolutamente de tiempo lineal y de un solo paso, y es un algoritmo súper lindo. FWIW, también tenemos algunos puntos de referencia reales: sus factores constantes son mucho más lentos que la cola de prioridad tradicional en el caso promedio, pero esta implementación es mucho más resistente a la entrada del "peor de los casos" (por ejemplo, entrada estrictamente ascendente).
Louis Wasserman
15

Puede usar el algoritmo de selección rápida para encontrar el número en el índice (por orden) [mil millones-101] y luego iterar sobre los números y encontrar los números que aparecen en ese número.

array={...the billion numbers...} 
result[100];

pivot=QuickSelect(array,billion-101);//O(N)

for(i=0;i<billion;i++)//O(N)
   if(array[i]>=pivot)
      result.add(array[i]);

El tiempo de este algoritmo es: 2 XO (N) = O (N) (Rendimiento promedio del caso)

La segunda opción como sugiere Thomas Jungblut es:

Use Heap para construir el montón MAX tomará O (N), luego los 100 números máximos superiores estarán en la parte superior del montón, todo lo que necesita es sacarlos del montón (100 XO (Log (N)).

El tiempo de este algoritmo es: O (N) + 100 XO (Log (N)) = O (N)

One Man Crew
fuente
8
Estás trabajando en toda la lista tres veces. 1 bio. los enteros son aproximadamente de 4 gb, ¿qué harías si no puedes guardarlos en la memoria? La selección rápida es la peor opción posible en este caso. Iterar una vez y mantener un montón de los 100 elementos principales es, en mi humilde opinión, la mejor solución en O (n) (tenga en cuenta que puede cortar el O (log n) de las inserciones de montón ya que n en el montón es 100 = constante = muy pequeño )
Thomas Jungblut
3
Aunque todavía es así O(N), hacer dos QuickSelects y otro escaneo lineal es mucho más sobrecarga de lo necesario.
Kevin
Este es el código PSEUDO, todas las soluciones aquí tomarán más tiempo (O (NLOG (N) o 100 * O (N))
One Man Crew
1
100*O(N)(si esa es una sintaxis válida) = O(100*N)= O(N)(es cierto que 100 puede ser variable, de ser así, esto no es estrictamente cierto). Ah, y Quickselect tiene el peor rendimiento de O (N ^ 2) (ouch). Y si no cabe en la memoria, volverá a cargar los datos del disco dos veces, lo que es mucho peor que una vez (este es el cuello de botella).
Bernhard Barker
Existe el problema de que este es el tiempo de ejecución esperado, y no el peor de los casos, pero al usar una estrategia de selección pivote decente (p. Ej., Elegir 21 elementos al azar y elegir la mediana de esos 21 como pivote), entonces el número de comparaciones puede ser garantizado con alta probabilidad de ser como máximo (2 + c) n para una constante arbitrariamente pequeña c.
One Man Crew
10

Aunque la otra solución de selección rápida se ha rechazado, el hecho es que la selección rápida encontrará la solución más rápido que usar una cola de tamaño 100. La selección rápida tiene un tiempo de ejecución esperado de 2n + o (n), en términos de comparaciones. Una implementación muy simple sería

array = input array of length n
r = Quickselect(array,n-100)
result = array of length 100
for(i = 1 to n)
  if(array[i]>r)
     add array[i] to result

Esto tomará 3n + o (n) comparaciones en promedio. Además, puede hacerse más eficiente utilizando el hecho de que la selección rápida dejará los 100 elementos más grandes de la matriz en las 100 ubicaciones más a la derecha. De hecho, el tiempo de ejecución se puede mejorar a 2n + o (n).

Existe el problema de que este es el tiempo de ejecución esperado, y no el peor de los casos, pero al usar una estrategia de selección pivote decente (p. Ej., Elegir 21 elementos al azar y elegir la mediana de esos 21 como pivote), entonces el número de comparaciones puede ser garantizado con alta probabilidad de ser como máximo (2 + c) n para una constante arbitrariamente pequeña c.

De hecho, mediante el uso de una estrategia de muestreo optimizada (por ejemplo, elementos sqrt (n) de muestra al azar y elegir el percentil 99), el tiempo de ejecución se puede reducir a (1 + c) n + o (n) para c arbitrariamente pequeño (suponiendo que K, el número de elementos a seleccionar es o (n)).

Por otro lado, usar una cola de tamaño 100 requerirá comparaciones O (log (100) n), y la base de registro 2 de 100 es aproximadamente igual a 6.6.

Si pensamos en este problema en el sentido más abstracto de elegir los elementos K más grandes de una matriz de tamaño N, donde K = o (N) pero K y N van al infinito, entonces el tiempo de ejecución de la versión de selección rápida será O (N) y la versión de la cola será O (N log K), por lo que en este sentido la selección rápida también es asintóticamente superior.

En los comentarios, se mencionó que la solución de cola se ejecutará en el tiempo esperado N + K log N en una entrada aleatoria. Por supuesto, el supuesto de entrada aleatoria nunca es válido a menos que la pregunta lo indique explícitamente. Se podría hacer que la solución de la cola atraviese la matriz en un orden aleatorio, pero esto incurrirá en el costo adicional de N llamadas a un generador de números aleatorios, así como también permutando toda la matriz de entrada o asignando una nueva matriz de longitud N que contiene el índices aleatorios

Si el problema no le permite moverse por los elementos en la matriz original, y el costo de asignar memoria es alto, entonces duplicar la matriz no es una opción, eso es algo diferente. Pero estrictamente en términos de tiempo de ejecución, esta es la mejor solución.

mrip
fuente
44
Su último párrafo es el punto clave: con mil millones de números, no es factible mantener todos los datos en la memoria o intercambiar elementos. (Al menos así es como interpretaría el problema, dado que era una pregunta de entrevista).
Ted Hopp
14
En cualquier pregunta algorítmica, si leer los datos es un problema, debe mencionarse en la pregunta. La pregunta establece "dado un conjunto" no "dado un conjunto en el disco que no cabe en la memoria y no puede ser manipulado de acuerdo con el modelo von Neuman, que es el estándar en el análisis de algoritmos". En estos días puede obtener una computadora portátil con 8 gigas de ram. No estoy seguro de dónde surge la idea de mantener un billón de números en la memoria. Tengo varios billones de números en memoria en mi estación de trabajo en este momento.
mrip
Para su información, el tiempo de ejecución de selección rápida en el peor de los casos es O (n ^ 2) (consulte en.wikipedia.org/wiki/Quickselect ), y también modifica el orden de los elementos en la matriz de entrada. Es posible tener una solución O (n) en el peor de los casos, con una constante muy grande ( en.wikipedia.org/wiki/Median_of_medians ).
pts
El peor de los casos de selección rápida es exponencialmente improbable, lo que significa que, a efectos prácticos, esto es irrelevante. Es fácil modificar la selección rápida para que, con alta probabilidad, el número de comparaciones sea (2 + c) n + o (n) para c arbitrariamente pequeño.
mrip
"El hecho es que quickselect encontrará la solución más rápido que usar una cola de tamaño 100" - No. La solución de almacenamiento dinámico toma aproximadamente N + Klog (N) comparaciones versus el promedio de 2N para la selección rápida y 2.95 para la mediana de las medianas. Es claramente más rápido para el K. dado
Neil G
5

toma los primeros 100 números de los mil millones y clasifícalos. ahora solo repita el billón, si el número de origen es mayor que el menor de 100, inserte en orden de clasificación. Lo que termina es algo mucho más cercano a O (n) sobre el tamaño del conjunto.

Samuel Thurston
fuente
3
Vaya, no vi la respuesta más detallada que la mía.
Samuel Thurston
Tome los primeros 500 números más o menos y solo pare para ordenar (y deseche los 400 bajos) cuando la lista se llene. (Y no hace falta decir que solo agrega a la lista si el nuevo número es> el más bajo en los 100 seleccionados).
Hot Licks
4

Dos opciones:

(1) Montón (PriorityQueue)

Mantenga un montón mínimo con un tamaño de 100. Atraviese la matriz. Una vez que el elemento es más pequeño que el primer elemento en el montón, reemplácelo.

InSERT ELEMENT INTO HEAP: O(log100)
compare the first element: O(1)
There are n elements in the array, so the total would be O(nlog100), which is O(n)

(2) Modelo de reducción de mapa.

Esto es muy similar al ejemplo de conteo de palabras en hadoop. Trabajo de mapa: cuente la frecuencia o los tiempos de cada elemento aparecidos. Reducir: Obtener el elemento K superior.

Por lo general, le daría al reclutador dos respuestas. Dales lo que quieran. Por supuesto, la codificación de reducción de mapa sería laboriosa, ya que debe conocer todos los parámetros exactos. No hace daño practicarlo. Buena suerte.

Chris Su
fuente
+1 para MapReduce, no puedo creer que fueras el único que mencionara a Hadoop por mil millones de números. ¿Qué pasa si el entrevistador pidió mil millones de números? Te mereces más votos en mi opinión.
Silviu Burcea
@ Silviu Burcea Muchas gracias. También valoro MapReduce. :)
Chris Su
Aunque el tamaño de 100 es constante en este ejemplo, realmente debería generalizar esto a una variable separada, es decir. k. Como 100 es tan constante como mil millones, entonces ¿por qué le das al tamaño del conjunto grande de números una variable de tamaño de n, y no al conjunto más pequeño de números? Realmente su complejidad debería ser O (nlogk) que no es O (n).
Tom escuchó el
1
Pero mi punto es que si solo está respondiendo la pregunta, mil millones también están fijados en la pregunta, entonces, ¿por qué generalizar mil millones a n y no 100 a k? Siguiendo su lógica, la complejidad debería ser O (1) porque tanto 1 billón como 100 están arreglados en esta pregunta.
Tom escuchó el
1
@TomHeard Muy bien. O (nlogk) Solo hay un factor que afectará los resultados. Esto significa que si n aumenta cada vez más, el "nivel de resultado" aumentará linealmente. O podemos decir que, incluso con un billón de números, todavía puedo obtener 100 números más grandes. Sin embargo, no puede decir: al aumentar n, la k aumenta, de modo que la k afectará el resultado. Es por eso que uso O (nlogk) pero no O (nlogn)
Chris Su
4

Una solución muy fácil sería recorrer la matriz 100 veces. Lo cual es O(n).

Cada vez que extrae el número más grande (y cambia su valor al valor mínimo, para que no lo vea en la próxima iteración, o realice un seguimiento de los índices de respuestas anteriores (al realizar un seguimiento de los índices que puede tener la matriz original) múltiplo del mismo número)). Después de 100 iteraciones, tienes los 100 números más grandes.

James Oravec
fuente
1
Dos desventajas: (1) Está destruyendo la entrada en el proceso; esto preferiblemente se evita. (2) Está revisando la matriz varias veces: si la matriz se almacena en el disco y no puede caber en la memoria, esto podría ser casi 100 veces más lento que la respuesta aceptada. (Sí, ambos son O (n), pero aún así)
Bernhard Barker
Buena llamada @Dukeling, agregué un texto adicional sobre cómo evitar alterar la entrada original haciendo un seguimiento de los índices de respuestas anteriores. Lo que aún sería bastante fácil de codificar.
James Oravec
Un ejemplo brillante de una solución O (n) que es mucho más lenta que O (n log n). log2 (mil millones) es solo 30 ...
gnasher729
@ gnasher729 ¿Qué tan grande es la constante oculta en O (n log n)?
milagro173
1

Inspirado por la respuesta de @ron teller, aquí hay un programa básico de C para hacer lo que quieras.

#include <stdlib.h>
#include <stdio.h>

#define TOTAL_NUMBERS 1000000000
#define N_TOP_NUMBERS 100

int 
compare_function(const void *first, const void *second)
{
    int a = *((int *) first);
    int b = *((int *) second);
    if (a > b){
        return 1;
    }
    if (a < b){
        return -1;
    }
    return 0;
}

int 
main(int argc, char ** argv)
{
    if(argc != 2){
        printf("please supply a path to a binary file containing 1000000000"
               "integers of this machine's wordlength and endianness\n");
        exit(1);
    }
    FILE * f = fopen(argv[1], "r");
    if(!f){
        exit(1);
    }
    int top100[N_TOP_NUMBERS] = {0};
    int sorts = 0;
    for (int i = 0; i < TOTAL_NUMBERS; i++){
        int number;
        int ok;
        ok = fread(&number, sizeof(int), 1, f);
        if(!ok){
            printf("not enough numbers!\n");
            break;
        }
        if(number > top100[0]){
            sorts++;
            top100[0] = number;
            qsort(top100, N_TOP_NUMBERS, sizeof(int), compare_function);
        }

    }
    printf("%d sorts made\n"
    "the top 100 integers in %s are:\n",
    sorts, argv[1] );
    for (int i = 0; i < N_TOP_NUMBERS; i++){
        printf("%d\n", top100[i]);
    }
    fclose(f);
    exit(0);
}

En mi máquina (core i3 con un SSD rápido) tarda 25 segundos y 1724. Generé un archivo binario con dd if=/dev/urandom/ count=1000000000 bs=1para esta ejecución.

Obviamente, hay problemas de rendimiento con la lectura de solo 4 bytes a la vez, desde el disco, pero esto es por el bien. En el lado positivo, se necesita muy poca memoria.


fuente
1

La solución más simple es escanear la matriz grande de mil millones de números y mantener los 100 valores más grandes encontrados hasta ahora en un búfer de matriz pequeña sin ningún tipo de clasificación y recordar el valor más pequeño de este búfer. Primero pensé que este método fue propuesto por fordprefect, pero en un comentario dijo que asumió que la estructura de datos de 100 números se implementaba como un montón. Cada vez que se encuentra un nuevo número que es mayor, el mínimo en el búfer se sobrescribe con el nuevo valor encontrado y se busca nuevamente en el búfer el mínimo actual. Si los números en una matriz de mil millones de números se distribuyen aleatoriamente la mayor parte del tiempo, el valor de la matriz grande se compara con el mínimo de la matriz pequeña y se descarta. Solo para una fracción muy pequeña del número, el valor debe insertarse en la matriz pequeña. Por lo tanto, se puede ignorar la diferencia de manipular la estructura de datos que contiene los números pequeños. Para una pequeña cantidad de elementos, es difícil determinar si el uso de una cola prioritaria es realmente más rápido que usar mi enfoque ingenuo.

Quiero estimar el número de inserciones en el pequeño búfer de matriz de 100 elementos cuando se escanea la matriz de 10 ^ 9 elementos. El programa escanea los primeros 1000 elementos de esta gran matriz y tiene que insertar como máximo 1000 elementos en el búfer. El búfer contiene 100 elementos de los 1000 elementos escaneados, es decir, 0.1 del elemento escaneado. Por lo tanto, suponemos que la probabilidad de que un valor de la matriz grande sea mayor que el mínimo actual del búfer es de aproximadamente 0.1 Este elemento debe insertarse en el búfer. Ahora el programa escanea los siguientes 10 ^ 4 elementos de la gran matriz. Debido a que el mínimo del búfer aumentará cada vez que se inserte un nuevo elemento. Estimamos que la proporción de elementos mayores que nuestro mínimo actual es de aproximadamente 0.1 y, por lo tanto, hay 0.1 * 10 ^ 4 = 1000 elementos para insertar. En realidad, el número esperado de elementos que se insertan en el búfer será menor. Después del escaneo de esta fracción de 10 ^ 4 elementos de los números en el búfer será aproximadamente 0.01 de los elementos escaneados hasta ahora. Entonces, al escanear los siguientes 10 ^ 5 números, suponemos que no se insertará más de 0.01 * 10 ^ 5 = 1000 en el búfer. Continuando con esta argumentación, hemos insertado unos 7000 valores después de escanear 1000 + 10 ^ 4 + 10 ^ 5 + ... + 10 ^ 9 ~ 10 ^ 9 elementos de la matriz grande. Entonces, al escanear una matriz con 10 ^ 9 elementos de tamaño aleatorio, esperamos no más de 10 ^ 4 (= 7000 inserciones redondeadas) en el búfer. Después de cada inserción en el búfer, se debe encontrar el nuevo mínimo. Si el búfer es una matriz simple, necesitamos una comparación de 100 para encontrar el nuevo mínimo. Si el búfer es otra estructura de datos (como un montón), necesitamos al menos 1 comparación para encontrar el mínimo. Para comparar los elementos de la gran matriz, necesitamos 10 ^ 9 comparaciones. Así que, en general, necesitamos aproximadamente 10 ^ 9 + 100 * 10 ^ 4 = 1.001 * 10 ^ 9 comparaciones cuando se usa una matriz como buffer y al menos 1.000 * 10 ^ 9 comparaciones cuando se usa otro tipo de estructura de datos (como un montón) . Por lo tanto, usar un montón solo genera una ganancia del 0.1% si el rendimiento está determinado por el número de comparación. Pero, ¿cuál es la diferencia en el tiempo de ejecución entre insertar un elemento en un montón de 100 elementos y reemplazar un elemento en una matriz de 100 elementos y encontrar su nuevo mínimo? 000 * 10 ^ 9 comparaciones cuando se utiliza otro tipo de estructura de datos (como un montón). Por lo tanto, usar un montón solo genera una ganancia del 0.1% si el rendimiento está determinado por el número de comparación. Pero, ¿cuál es la diferencia en el tiempo de ejecución entre insertar un elemento en un montón de 100 elementos y reemplazar un elemento en una matriz de 100 elementos y encontrar su nuevo mínimo? 000 * 10 ^ 9 comparaciones cuando se utiliza otro tipo de estructura de datos (como un montón). Por lo tanto, usar un montón solo genera una ganancia del 0.1% si el rendimiento está determinado por el número de comparación. Pero, ¿cuál es la diferencia en el tiempo de ejecución entre insertar un elemento en un montón de 100 elementos y reemplazar un elemento en una matriz de 100 elementos y encontrar su nuevo mínimo?

  • En el nivel teórico: cuántas comparaciones son necesarias para insertar en un montón. Sé que es O (log (n)) pero ¿qué tan grande es el factor constante? yo

  • A nivel de máquina: ¿Cuál es el impacto del almacenamiento en caché y la predicción de ramificación en el tiempo de ejecución de una inserción de montón y una búsqueda lineal en una matriz?

  • En el nivel de implementación: ¿Qué costos adicionales están ocultos en una estructura de datos de montón provista por una biblioteca o un compilador?

Creo que estas son algunas de las preguntas que deben responderse antes de poder intentar estimar la diferencia real entre el rendimiento de un montón de 100 elementos o una matriz de 100 elementos. Por lo tanto, tendría sentido hacer un experimento y medir el rendimiento real.

milagro173
fuente
1
Eso es lo que hace un montón.
Neil G
@Neil G: ¿Qué "eso"?
milagro173
1
La parte superior del montón es el elemento mínimo en el montón, y los elementos nuevos se rechazan con una comparación.
Neil G
1
Entiendo lo que está diciendo, pero incluso si va por un número absoluto de comparaciones en lugar de un número asintótico de comparaciones, la matriz sigue siendo mucho más lenta porque el tiempo para "insertar un nuevo elemento, descartar el mínimo anterior y encontrar el nuevo mínimo" es 100 en lugar de alrededor de 7.
Neil G
1
Está bien, pero su estimación es muy indirecta. Puede calcular directamente el número esperado de inserciones para que sea k (digamma (n) - digamma (k)), que es menor que klog (n). En cualquier caso, tanto el montón como la solución de matriz solo gastan una comparación para descartar un elemento. La única diferencia es que el número de comparaciones para un elemento insertado es 100 para su solución versus hasta 14 para el montón (aunque el caso promedio es probablemente mucho menor)
Neil G
1
 Although in this question we should search for top 100 numbers, I will 
 generalize things and write x. Still, I will treat x as constant value.

Algoritmo Los elementos x más grandes de n:

Llamaré al valor de retorno LIST . Es un conjunto de elementos x (en mi opinión, debería estar lista enlazada)

  • Los primeros x elementos se toman del grupo "tal como vienen" y se ordenan en LIST (esto se hace en tiempo constante ya que x se trata como constante - O (x log (x)) tiempo)
  • Para cada elemento que viene a continuación, verificamos si es más grande que el elemento más pequeño en LIST y, si es así, sacamos el elemento más pequeño e insertamos el elemento actual en LIST. Como esa lista está ordenada, cada elemento debe encontrar su lugar en el tiempo logarítmico (búsqueda binaria) y, dado que está ordenada, la inserción de la lista no es un problema. Cada paso también se realiza en tiempo constante (O (log (x)) tiempo).

Entonces, ¿cuál es el peor de los casos?

x log (x) + (nx) (log (x) +1) = nlog (x) + n - x

Entonces ese es el momento O (n) para el peor de los casos. El +1 es la comprobación de si el número es mayor que el más pequeño en la LISTA. El tiempo esperado para el caso promedio dependerá de la distribución matemática de esos n elementos.

Posibles mejoras.

Este algoritmo puede mejorarse ligeramente para el peor de los casos, pero en mi humilde opinión (no puedo probar esta afirmación), eso degradará el comportamiento promedio. El comportamiento asintótico será el mismo.

La mejora en este algoritmo será que no verificaremos si el elemento es mayor que el más pequeño. Intentaremos insertarlo para cada elemento y, si es más pequeño que el más pequeño, lo ignoraremos. Aunque eso suena absurdo si consideramos solo el peor de los casos, tendremos

x log (x) + (nx) log (x) = nlog (x)

operaciones

Para este caso de uso, no veo más mejoras. Sin embargo, debe preguntarse: ¿qué pasa si tengo que hacer esto más que log (n) veces y para diferentes x-es? Obviamente, ordenaríamos esa matriz en O (n log (n)) y tomaríamos nuestro elemento x siempre que los necesitemos.

Rouz
fuente
1

Esta pregunta se respondería con la complejidad N log (100) (en lugar de N log N) con solo una línea de código C ++.

 std::vector<int> myvector = ...; // Define your 1 billion numbers. 
                                 // Assumed integer just for concreteness 
 std::partial_sort (myvector.begin(), myvector.begin()+100, myvector.end());

La respuesta final sería un vector donde se garantiza que los primeros 100 elementos serán los 100 números más grandes de su matriz, mientras que los elementos restantes están desordenados

C ++ STL (biblioteca estándar) es bastante útil para este tipo de problemas.

Nota: No estoy diciendo que esta sea la solución óptima, pero habría salvado su entrevista.

Vivian Miranda
fuente
1

La solución simple sería usar una cola prioritaria, agregar los primeros 100 números a la cola y realizar un seguimiento del número más pequeño en la cola, luego iterar a través de los otros mil millones de números, y cada vez que encontremos uno que sea más grande que el número más grande en la cola de prioridad, eliminamos el número más pequeño, agregamos el nuevo número y nuevamente hacemos un seguimiento del número más pequeño en la cola.

Si los números estuvieran en orden aleatorio, esto funcionaría de maravilla porque a medida que iteramos a través de mil millones de números aleatorios, sería muy raro que el siguiente número esté entre los 100 más grandes hasta ahora. Pero los números pueden no ser aleatorios. Si la matriz ya estaba ordenada en orden ascendente, siempre insertaríamos un elemento en la cola de prioridad.

Por lo tanto, primero elegimos 100,000 números aleatorios de la matriz. Para evitar el acceso aleatorio que podría ser lento, agregamos, por ejemplo, 400 grupos aleatorios de 250 números consecutivos. Con esa selección aleatoria, podemos estar bastante seguros de que muy pocos de los números restantes están entre los primeros cien, por lo que el tiempo de ejecución será muy cercano al de un bucle simple que compara mil millones de números con algún valor máximo.

gnasher729
fuente
1

Encontrar el top 100 de un billón de números se hace mejor usando un montón mínimo de 100 elementos.

Primero imprima el montón mínimo con los primeros 100 números encontrados. min-heap almacenará el más pequeño de los primeros 100 números en la raíz (arriba).

Ahora, a medida que avanza, el resto de los números solo los compara con la raíz (el más pequeño de los 100).

Si el nuevo número encontrado es mayor que la raíz del montón mínimo, reemplace la raíz con ese número; de lo contrario, ignórelo.

Como parte de la inserción del nuevo número en min-heap, el número más pequeño en el montón llegará a la parte superior (raíz).

Una vez que hayamos pasado por todos los números, tendremos los 100 números más grandes en el montón mínimo.

imsaar
fuente
0

He escrito una solución simple en Python en caso de que alguien esté interesado. Utiliza el bisectmódulo y una lista de retorno temporal que mantiene ordenada. Esto es similar a una implementación de cola prioritaria.

import bisect

def kLargest(A, k):
    '''returns list of k largest integers in A'''
    ret = []
    for i, a in enumerate(A):
        # For first k elements, simply construct sorted temp list
        # It is treated similarly to a priority queue
        if i < k:
            bisect.insort(ret, a) # properly inserts a into sorted list ret
        # Iterate over rest of array
        # Replace and update return array when more optimal element is found
        else:
            if a > ret[0]:
                del ret[0] # pop min element off queue
                bisect.insort(ret, a) # properly inserts a into sorted list ret
    return ret

Uso con 100,000,000 elementos y entrada en el peor de los casos, que es una lista ordenada:

>>> from so import kLargest
>>> kLargest(range(100000000), 100)
[99999900, 99999901, 99999902, 99999903, 99999904, 99999905, 99999906, 99999907,
 99999908, 99999909, 99999910, 99999911, 99999912, 99999913, 99999914, 99999915,
 99999916, 99999917, 99999918, 99999919, 99999920, 99999921, 99999922, 99999923,
 99999924, 99999925, 99999926, 99999927, 99999928, 99999929, 99999930, 99999931,
 99999932, 99999933, 99999934, 99999935, 99999936, 99999937, 99999938, 99999939,
 99999940, 99999941, 99999942, 99999943, 99999944, 99999945, 99999946, 99999947,
 99999948, 99999949, 99999950, 99999951, 99999952, 99999953, 99999954, 99999955,
 99999956, 99999957, 99999958, 99999959, 99999960, 99999961, 99999962, 99999963,
 99999964, 99999965, 99999966, 99999967, 99999968, 99999969, 99999970, 99999971,
 99999972, 99999973, 99999974, 99999975, 99999976, 99999977, 99999978, 99999979,
 99999980, 99999981, 99999982, 99999983, 99999984, 99999985, 99999986, 99999987,
 99999988, 99999989, 99999990, 99999991, 99999992, 99999993, 99999994, 99999995,
 99999996, 99999997, 99999998, 99999999]

Tomó alrededor de 40 segundos calcular esto para 100,000,000 elementos, así que tengo miedo de hacerlo por 1 billón. Sin embargo, para ser justos, estaba alimentando la entrada del peor de los casos (irónicamente, una matriz que ya está ordenada).

Shashank
fuente
0

Veo muchas discusiones de O (N), así que propongo algo diferente solo para el ejercicio de pensamiento.

¿Existe alguna información conocida sobre la naturaleza de estos números? Si es de naturaleza aleatoria, no vaya más allá y mire las otras respuestas. No obtendrá mejores resultados que ellos.

¡Sin embargo! Vea si cualquier mecanismo de llenado de listas llenó esa lista en un orden particular. ¿Están en un patrón bien definido donde se puede saber con certeza que la mayor magnitud de números se encontrará en una determinada región de la lista o en un determinado intervalo? Puede haber un patrón para ello. Si es así, por ejemplo, si se garantiza que están en algún tipo de distribución normal con la joroba característica en el medio, siempre tienen tendencias ascendentes repetidas entre subconjuntos definidos, tienen un pico prolongado en algún momento T en el medio de los datos establecido como tal vez una incidencia de uso de información privilegiada o una falla del equipo, o tal vez solo tenga un "pico" cada número N como en el análisis de fuerzas después de una catástrofe, puede reducir la cantidad de registros que debe verificar de manera significativa.

De todos modos hay algo para pensar. Tal vez esto le ayude a dar a los futuros entrevistadores una respuesta reflexiva. Sé que me impresionaría si alguien me hiciera esa pregunta en respuesta a un problema como este: me diría que está pensando en la optimización. Solo reconozca que no siempre existe la posibilidad de optimizar.

djdanlib
fuente
0
Time ~ O(100 * N)
Space ~ O(100 + N)
  1. Crea una lista vacía de 100 espacios vacíos

  2. Para cada número en la lista de entrada:

    • Si el número es más pequeño que el primero, omita

    • De lo contrario, reemplácelo con este número.

    • Luego, empuje el número a través del intercambio adyacente; hasta que sea más pequeño que el siguiente

  3. Devuelve la lista


Nota: si el log(input-list.size) + c < 100, entonces la forma óptima es ordenar la lista de entrada, luego dividir los primeros 100 elementos.

Khaled.K
fuente
0

La complejidad es O (N)

Primero cree una matriz de 100 ints; inicialice el primer elemento de esta matriz como el primer elemento de los valores N, realice un seguimiento del índice del elemento actual con otra variable, llámelo CurrentBig

Iterar aunque los valores de N

if N[i] > M[CurrentBig] {

M[CurrentBig]=N[i]; ( overwrite the current value with the newly found larger number)

CurrentBig++;      ( go to the next position in the M array)

CurrentBig %= 100; ( modulo arithmetic saves you from using lists/hashes etc.)

M[CurrentBig]=N[i];    ( pick up the current value again to use it for the next Iteration of the N array)

} 

cuando termine, imprima la matriz M desde CurrentBig 100 veces módulo 100 :-) Para el alumno: asegúrese de que la última línea del código no supere los datos válidos justo antes de que salga el código

Angelos Karageorgiou
fuente
0

Otro algoritmo O (n):

El algoritmo encuentra los 100 más grandes por eliminación.

considere todos los millones de números en su representación binaria. Comience desde el bit más significativo. Encontrar si el MSB es 1 puede hacerse mediante una operación booleana de multiplicación con un número apropiado. Si hay más de 100 1 en estos millones, elimine los otros números con ceros. Ahora, de los números restantes, proceda con el siguiente bit más significativo. mantenga un recuento del número de números restantes después de la eliminación y continúe siempre que este número sea mayor que 100.

La operación booleana principal puede realizarse paralelamente en GPU

Panduranga Rao Sadhu
fuente
0

Descubriría quién tuvo tiempo de poner mil millones de números en una matriz y despedirlo. Debe trabajar para el gobierno. Al menos si tuviera una lista vinculada, podría insertar un número en el medio sin mover medio billón para hacer espacio. Aún mejor, un Btree permite una búsqueda binaria. Cada comparación elimina la mitad de su total. Un algoritmo hash le permitiría llenar la estructura de datos como un tablero de ajedrez, pero no es tan bueno para datos dispersos. Como su mejor opción es tener una matriz de solución de 100 enteros y realizar un seguimiento del número más bajo en su matriz de solución para que pueda reemplazarlo cuando encuentre un número más alto en la matriz original. Tendría que mirar cada elemento en la matriz original suponiendo que no esté ordenado para empezar.

David Allan Houser Jr
fuente
0

Puedes hacerlo a O(n)tiempo. Simplemente recorra la lista y realice un seguimiento de los 100 números más grandes que haya visto en un punto dado y el valor mínimo en ese grupo. Cuando encuentre un nuevo número más grande, el más pequeño de sus diez, reemplácelo y actualice su nuevo valor mínimo de 100 (puede tomar un tiempo constante de 100 para determinar esto cada vez que lo haga, pero esto no afecta el análisis general )

James Oravec
fuente
1
Este enfoque es casi idéntico a las respuestas más y segunda más votadas a esta pregunta.
Bernhard Barker
0

Administrar una lista separada es un trabajo adicional y debe mover las cosas por toda la lista cada vez que encuentre otro reemplazo. Solo q clasifícalo y toma el top 100.

Chris Fox
fuente
-1 quicksort es O (n log n), que es exactamente lo que hizo el OP y está pidiendo mejorar. No necesita administrar una lista separada, solo una lista de 100 números. Su sugerencia también tiene el efecto secundario no deseado de cambiar la lista original o copiarla. Eso es 4GiB más o menos de memoria, desaparecido.
0
  1. Use el enésimo elemento para obtener el centésimo elemento O (n)
  2. Itere la segunda vez, pero solo una vez, y genere cada elemento que sea mayor que este elemento específico.

Tenga en cuenta especialmente ¡El segundo paso puede ser fácil de calcular en paralelo! Y también será eficiente cuando necesite un millón de elementos más grandes.

matemáticas
fuente
0

Es una pregunta de Google o de otros gigantes de la industria. Tal vez el siguiente código sea la respuesta correcta esperada por su entrevistador. El costo de tiempo y el costo de espacio dependen del número máximo en la matriz de entrada. Para la entrada de matriz de 32 bits int, el costo de espacio máximo es de 4 * 125M Bytes, el costo de tiempo es de 5 * mil millones.

public class TopNumber {
    public static void main(String[] args) {
        final int input[] = {2389,8922,3382,6982,5231,8934
                            ,4322,7922,6892,5224,4829,3829
                            ,6892,6872,4682,6723,8923,3492};
        //One int(4 bytes) hold 32 = 2^5 value,
        //About 4 * 125M Bytes
        //int sort[] = new int[1 << (32 - 5)];
        //Allocate small array for local test
        int sort[] = new int[1000];
        //Set all bit to 0
        for(int index = 0; index < sort.length; index++){
            sort[index] = 0;
        }
        for(int number : input){
            sort[number >>> 5] |= (1 << (number % 32));
        }
        int topNum = 0;
        outer:
        for(int index = sort.length - 1; index >= 0; index--){
            if(0 != sort[index]){
                for(int bit = 31; bit >= 0; bit--){
                    if(0 != (sort[index] & (1 << bit))){
                        System.out.println((index << 5) + bit);
                        topNum++;
                        if(topNum >= 3){
                            break outer;
                        }
                    }
                }
            }
        }
    }
}
Su Xiang
fuente
0

hice mi propio código, no estoy seguro de si es lo que está buscando el "entrevistador"

private static final int MAX=100;
 PriorityQueue<Integer> queue = new PriorityQueue<>(MAX);
        queue.add(array[0]);
        for (int i=1;i<array.length;i++)
        {

            if(queue.peek()<array[i])
            {
                if(queue.size() >=MAX)
                {
                    queue.poll();
                }
                queue.add(array[i]);

            }

        }
Javier
fuente
0

Posibles mejoras.

Si el archivo contiene 1 billón de números, leerlo podría ser muy largo ...

Para mejorar este funcionamiento puedes:

  • Divida el archivo en n partes, cree n subprocesos, haga que n subprocesos busquen cada uno los 100 números más grandes en su parte del archivo (usando la cola de prioridad), y finalmente obtengan los 100 números más grandes de todos los hilos de salida.
  • Use un clúster para realizar una tarea de este tipo, con una solución como hadoop. Aquí puede dividir el archivo aún más y obtener la salida más rápido para un archivo de mil millones (o 10 ^ 12) de números.
Maxime B.
fuente
0

Primero tome 1000 elementos y agréguelos en un montón máximo. Ahora saque los primeros 100 elementos máximos y guárdelos en algún lugar. Ahora elija los siguientes 900 elementos del archivo y agréguelos en el montón junto con los últimos 100 elementos más altos.

Siga repitiendo este proceso de recoger 100 elementos del montón y agregar 900 elementos del archivo.

La selección final de 100 elementos nos dará el máximo de 100 elementos de un billón de números.

Juvenik
fuente
-1

Problema: Encuentre m elementos más grandes de n elementos donde n >>> m

La solución más simple, que debería ser obvia para todos, es simplemente hacer m pasos del algoritmo de clasificación de burbujas.

luego imprima los últimos n elementos de la matriz.

Esto no requiere estructuras de datos externas y utiliza un algoritmo que todos conocen.

El tiempo estimado de ejecución es O (m * n). Las mejores respuestas hasta ahora son O (n log (m)), por lo que esta solución no es significativamente más costosa para pequeños m.

No digo que esto no pueda mejorarse, pero esta es, con mucho, la solución más simple.

Chris Cudmore
fuente
1
¿Sin estructuras de datos externas? ¿Qué pasa con la matriz de millones de números para ordenar? Una matriz de este tamaño es una gran sobrecarga en tiempo para llenar y espacio para almacenar. ¿Qué pasaría si todos los números "grandes" estuvieran en el extremo incorrecto de la matriz? Necesitaría del orden de 100 mil millones de swaps para "burbujearlos" en su posición, otra gran sobrecarga ... Finalmente, M N = 100 mil millones frente a M Log2 (N) = 6.64 mil millones, que es casi dos órdenes de diferencia de magnitud. Tal vez repensar este. Un análisis de una pasada mientras se mantiene una estructura de datos de los números más grandes va a superar significativamente este enfoque.
NealB