Obtén 100 números más altos de una lista infinita


53

A uno de mis amigos se le hizo esta pregunta de entrevista:

"Hay un flujo constante de números que provienen de una lista infinita de números de los cuales necesita mantener una estructura de datos para devolver los 100 números más altos en cualquier momento dado. Suponga que todos los números son números enteros".

Esto es simple, debe mantener una lista ordenada en orden descendente y realizar un seguimiento del número más bajo en esa lista. Si el nuevo número obtenido es mayor que el número más bajo, entonces debe eliminar ese número más bajo e insertar el nuevo número en la lista ordenada según sea necesario.

Entonces la pregunta se extendió:

"¿Puede asegurarse de que la Orden de inserción sea O (1)? ¿Es posible?"

Hasta donde yo sabía, incluso si agrega un nuevo número a la lista y lo ordena de nuevo usando cualquier algoritmo de clasificación, lo mejor sería O (logn) para quicksort (creo). Entonces mi amigo me dijo que no era posible. Pero no estaba convencido, pidió mantener cualquier otra estructura de datos en lugar de una lista.

Pensé en un árbol binario equilibrado, pero incluso allí no obtendrás la inserción con el orden de 1. Así que la misma pregunta que tengo ahora también. Quería saber si existe alguna estructura de datos que pueda realizar la inserción en el orden de 1 para el problema anterior o si no es posible en absoluto.


19
Tal vez este sea solo yo malinterpretando la pregunta, pero ¿por qué necesita mantener una lista ordenada ? ¿Por qué no simplemente hacer un seguimiento del número más bajo y, si se encuentra un número más alto que ese, elimine el número más bajo y coloque el nuevo número, sin mantener la lista ordenada? Eso te daría O (1).
EdoDodo

36
@EdoDodo - y después de esa operación, ¿cómo sabes cuál es el nuevo número más bajo?
Damien_The_Unbeliever

19
Ordene la lista [O (100 * log (100)) = O (1)] o realice una búsqueda lineal a través de ella para obtener el mínimo [O (100) = O (1)] para obtener el nuevo número más bajo. Su lista tiene un tamaño constante, por lo que todas estas operaciones también son de tiempo constante.
Random832

66
No tiene que mantener toda la lista ordenada. No te importa cuál es el número más alto o el segundo más alto. Solo necesita saber cuál es el más bajo. Entonces, después de insertar un nuevo número, simplemente recorre los 100 números y ve cuál es el más bajo. Eso es tiempo constante.
Tom Zych

27
El orden asintótico de una operación solo es interesante cuando el tamaño del problema puede crecer sin límites. No está muy claro en su pregunta qué cantidad está creciendo sin límite; parece que estás preguntando cuál es el orden asintótico para un problema cuyo tamaño está limitado a 100; esa ni siquiera es una pregunta sensata para hacer; algo tiene que estar creciendo sin límites. Si la pregunta es "¿puede hacerlo para mantener el n superior, no el top 100, en el tiempo O (1)?" entonces la pregunta es sensata.
Eric Lippert

Respuestas:


35

Digamos que k es el número de números más altos que desea saber (100 en su ejemplo). Luego, puede agregar un nuevo número en el O(k)que también está O(1). Debido O(k*g) = O(g) if k is not zero and constant.


66
O (50) es O (n), no O (1). Insertar en una lista de longitud N en O (1) tiempo significa que el tiempo no depende del valor de N. Eso significa que si 100 se convierte en 10000, 50 NO debe convertirse en 5000.

18
@hamstergene, pero en el caso de esta pregunta, ¿ Nel tamaño de la lista ordenada o la cantidad de elementos que se han procesado hasta ahora? Si procesa 10000 artículos y mantiene los 100 mejores artículos en una lista, o procesa 1000000000 artículos y mantiene los 100 mejores artículos en una lista ordenada, los costos de inserción en esa lista siguen siendo los mismos.
Damien_The_Unbeliever

66
@hamstergene: En ese caso, entendiste mal lo básico. En su enlace de wikipedia no es una propiedad ( "La multiplicación por una constante"): O(k*g) = O(g) if k not zero and constant. => O(50*1) = O(1).
duedl0r

99
Creo que duedl0r tiene razón. Reduzcamos el problema y digamos que solo necesita los valores mínimo y máximo. ¿Es esto O (n) porque el mínimo y el máximo son 2? (n = 2) El número 2 es parte de la definición del problema. Es una constante, por lo que es ak en el O (k * algo) que es equivalente a O (algo)
xanatos

99
@hamstergene: ¿de qué función estás hablando? el valor 100 me parece bastante constante ..
duedl0r

19

Mantenga la lista sin clasificar. Averiguar si se inserta o no un nuevo número llevará más tiempo, pero la inserción será O (1).


77
Creo que esto te daría el premio smart-aleck si nada más. * 8 ')
Mark Booth

44
@ Emilio, eres técnicamente correcto, y, por supuesto, ese es el mejor tipo de correcto ...
Gareth

1
Pero también puede mantener el más bajo de sus 100 números, luego también puede decidir si necesita insertar en O (1). Entonces, solo cuando inserta un número, tiene que buscar el nuevo número más bajo. Pero eso sucede más raro que decidir insertar o no, lo que sucede para cada nuevo número.
Andrei Vajna II

12

Esto es facil. El tamaño de la lista de constantes, por lo tanto, el tiempo de clasificación de la lista es constante. Se dice que una operación que se ejecuta en tiempo constante es O (1). Por lo tanto, ordenar la lista es O (1) para una lista de tamaño fijo.


9

Una vez que pase 100 números, el costo máximo en el que incurrirá para el próximo número es el costo para verificar si el número está en los 100 números más altos (etiquetemos ese CheckTime ) más el costo para ingresarlo en ese conjunto y expulsar el el más bajo (llamemos a eso EnterTime ), que es tiempo constante (al menos para números acotados) u O (1) .

Worst = CheckTime + EnterTime

Luego, si la distribución de números es aleatoria, el costo promedio disminuye a medida que tenga más números. Por ejemplo, la posibilidad de que tenga que ingresar el número 101 en el conjunto máximo es 100/101, las posibilidades para el número 1000 serían 1/10, y las posibilidades para el enésimo número serían 100 / n. Por lo tanto, nuestra ecuación para el costo promedio será:

Average = CheckTime + EnterTime / n

Por lo tanto, a medida que n se acerca al infinito, solo CheckTime es importante:

Average = CheckTime

Si los números están vinculados, CheckTime es constante y, por lo tanto, es el tiempo O (1) .

Si los números no están vinculados, el tiempo de verificación aumentará con más números. Teóricamente, esto se debe a que si el número más pequeño en el conjunto máximo es lo suficientemente grande, su tiempo de verificación será mayor porque tendrá que considerar más bits. Eso hace que parezca que será un poco más alto que el tiempo constante. Sin embargo, también podría argumentar que la posibilidad de que el próximo número esté en el conjunto más alto se aproxima a cero cuando n se acerca al infinito y, por lo tanto, la posibilidad de que necesite considerar más bits también se acerca a 0, lo que sería un argumento para O (1) hora.

No soy positivo, pero mi instinto dice que es el momento O (log (log (n))) . Esto se debe a que la probabilidad de que aumente el número más bajo es logarítmica, y la posibilidad de que el número de bits que debe considerar para cada verificación sea también logarítmico. Estoy interesado en que otras personas asuman esto, porque no estoy realmente seguro ...


Excepto que la lista es arbitraria, ¿qué pasa si es una lista de números cada vez mayores?
dan_waterworth

@dan_waterworth: Si la lista infinita es arbitraria y solo aumenta (¡las probabilidades de que sean 1 / ∞!), eso encajaría en el peor de los casos CheckTime + EnterTimepara cada número. Esto sólo tiene sentido si los números son ilimitados, y así CheckTimey EnterTimelo hará tanto en aumento, al menos de forma logarítmica debido al aumento en el tamaño de los números.
Briguy37

1
Los números no son aleatorios, son arbitrarios. No tiene sentido hablar de probabilidades.
dan_waterworth

@dan_waterworth: Has dicho dos veces que los números son arbitrarios. ¿De dónde estás sacando esto? Además, creo que aún puede aplicar estadísticas a números arbitrarios que comienzan con el caso aleatorio y mejorar su precisión a medida que sepa más sobre el árbitro. Por ejemplo, si usted fuera el árbitro, parece que habría una mayor posibilidad de seleccionar números cada vez mayores que si, por ejemplo, yo fuera el árbitro;)
Briguy37

7

este es fácil si conoces árboles de montón binarios . Los montones binarios admiten la inserción en tiempo constante promedio, O (1). Y le brinda fácil acceso a los primeros x elementos.


¿Por qué almacenar los elementos que no necesita? (los valores que son demasiado bajos) Parece que un algoritmo personalizado es más apropiado. No digo que no pueda 'no agregar' los valores cuando no sean más altos que los más bajos.
Steven Jeuris

No sé, mi intuición me dice que un montón (de algún sabor) podría lograr esto bastante bien. No significa que tendría que quedarse con todos los elementos para hacerlo. No lo investigué pero "se siente bien" (TM).
Aparejo

3
Se podría modificar un montón para descartar cualquier cosa por debajo de un nivel de mth (para montones binarios yk = 100, m sería 7, ya que el número de nodos = 2 ^ m-1). Esto lo ralentizaría, pero aún se amortizaría a tiempo constante.
Plutor

3
Si usó un min-montón binario (porque entonces la parte superior es el mínimo, que está comprobando todo el tiempo) y encuentra un nuevo número> min, entonces debe eliminar el elemento superior antes de poder insertar uno nuevo . Eliminar el elemento superior (min) será O (logN) porque debes atravesar cada nivel del árbol una vez. Por lo tanto, solo es técnicamente cierto que las inserciones son O promedio (1) porque en la práctica sigue siendo O (logN) cada vez que encuentra un número> min.
Scott Whitlock

1
@Plutor, estás asumiendo algunas garantías que los montones binarios no te dan. Visualizándolo como un árbol binario, podría ser el caso de que cada elemento en la rama izquierda sea más pequeño que cualquier elemento en la rama derecha, pero está asumiendo que los elementos más pequeños están más cerca de la raíz.
Peter Taylor

6

Si por la pregunta que el entrevistador realmente quería preguntar "podemos asegurarnos de que cada número entrante se procese en tiempo constante", entonces, como muchos ya señalaron (por ejemplo, ver la respuesta de @ duedl0r), la solución de su amigo ya es O (1), y Sería así incluso si hubiera usado una lista sin clasificar, o hubiera usado un tipo de burbuja, o cualquier otra cosa. En este caso, la pregunta no tiene mucho sentido, a menos que sea una pregunta difícil o la recuerdes mal.

Supongo que la pregunta del entrevistador fue significativa, que no estaba preguntando cómo hacer que algo sea O (1), lo cual ya es muy obvio.

Porque cuestionar la complejidad del algoritmo solo tiene sentido cuando el tamaño de la entrada crece indefinidamente, y la única entrada que puede crecer aquí es 100: el tamaño de la lista; Supongo que la pregunta real era "¿podemos asegurarnos de que Top N pase O (1) tiempo por número (no O (N) como en la solución de su amigo), ¿es posible?".

Lo primero que viene a la mente es contar el tipo, que comprará la complejidad de O (1) tiempo por número para el problema Top-N por el precio de usar el espacio O (m), donde m es la longitud del rango de números entrantes . Entonces sí, es posible.


4

Use una cola de prioridad mínima implementada con un montón de Fibonacci , que tiene un tiempo de inserción constante:

1. Insert first 100 elements into PQ
2. loop forever
       n = getNextNumber();
       if n > PQ.findMin() then
           PQ.deleteMin()
           PQ.insert(n)

44
"Las operaciones eliminan y eliminan el trabajo mínimo en O(log n)tiempo amortizado" , por lo que esto aún generaría O(log k)dónde kestá la cantidad de artículos para almacenar.
Steven Jeuris

1
Esto no es diferente a la respuesta de Emilio, que recibió el nombre de "premio smart-aleck", ya que el min de eliminación opera en O (log n) (según Wikipedia).
Nicole

La respuesta de @Renesis Emilio sería O (k) para encontrar el mínimo, la mía es O (log k)
Gabe Moothart

1
@ Gabe Bastante justo, solo quiero decir en principio. En otras palabras, si no considera que 100 es una constante, entonces esta respuesta tampoco es tiempo de contento.
Nicole

@Renesis He eliminado la declaración (incorrecta) de la respuesta.
Gabe Moothart

2

La tarea es claramente encontrar un algoritmo que sea O (1) en la longitud N de la lista de números requerida. Por lo tanto, no importa si necesita el número 100 superior o 10000 números, el tiempo de inserción debe ser O (1).

El truco aquí es que, aunque ese requisito O (1) se menciona para la inserción de la lista, la pregunta no dice nada sobre el orden del tiempo de búsqueda en el espacio de números enteros, pero resulta que esto puede hacerse O (1) también. La solución entonces es la siguiente:

  1. Organice una tabla hash con números para claves y pares de punteros de lista vinculados para valores. Cada par de punteros es el comienzo y el final de una secuencia de lista vinculada. Esto normalmente será solo un elemento y luego el siguiente. Cada elemento en la lista vinculada va al lado del elemento con el siguiente número más alto. Por lo tanto, la lista vinculada contiene la secuencia ordenada de números requeridos. Mantenga un registro del número más bajo.

  2. Tome un nuevo número x de la secuencia aleatoria.

  3. ¿Es más alto que el último número más bajo registrado? Sí => Paso 4, No => Paso 2

  4. Golpee la tabla hash con el número que acaba de tomar. ¿Hay una entrada? Sí => Paso 5. No => Tome un nuevo número x-1 y repita este paso (esta es una simple búsqueda lineal descendente, solo tenga paciencia conmigo aquí, esto se puede mejorar y le explicaré cómo)

  5. Con el elemento de lista recién obtenido de la tabla hash, inserte el nuevo número justo después del elemento en la lista vinculada (y actualice el hash)

  6. Tome el número más bajo l registrado (y elimínelo del hash / list).

  7. Golpee la tabla hash con el número que acaba de tomar. ¿Hay una entrada? Sí => Paso 8. No => Tome un nuevo número l + 1 y repita este paso (esta es una simple búsqueda lineal ascendente)

  8. Con un golpe positivo, el número se convierte en el nuevo número más bajo. Ir al paso 2

Para permitir valores duplicados, el hash realmente necesita mantener el inicio y el final de la secuencia de la lista vinculada de elementos que son duplicados. Agregar o eliminar un elemento en una tecla dada aumenta o disminuye el rango al que apunta.

El inserto aquí es O (1). Las búsquedas mencionadas son, supongo, algo así como O (diferencia promedio entre números). La diferencia promedio aumenta con el tamaño del espacio numérico, pero disminuye con la longitud requerida de la lista de números.

Entonces, la estrategia de búsqueda lineal es bastante pobre, si el espacio numérico es grande (por ejemplo, para un tipo int de 4 bytes, 0 a 2 ^ 32-1) y N = 100. Para evitar este problema de rendimiento, puede mantener conjuntos paralelos de tablas hash, donde los números se redondean a magnitudes más altas (por ejemplo, 1s, 10s, 100s, 1000s) para hacer las teclas adecuadas. De esta manera, puede subir y bajar marchas para realizar las búsquedas necesarias más rápidamente. El rendimiento se convierte en un O (rango de números de registro), creo, que es constante, es decir, O (1) también.

Para aclarar esto, imagine que tiene a mano el número 197. Llegaste a la tabla hash de los 10, con '190', se redondea a los diez más cercanos. ¿Cualquier cosa? No. Entonces bajas en 10 segundos hasta que alcanzas decir 120. Luego puedes comenzar en 129 en la tabla hash de 1, luego prueba 128, 127 hasta que alcances algo. Ahora ha encontrado en qué parte de la lista vinculada insertar el número 197. Al ponerlo, también debe actualizar la tabla hash 1 con la entrada 197, la tabla hash 10 con el número 190, 100 con 100, etc. La mayoría de los pasos alguna vez tienes que hacer aquí son 10 veces el registro del rango de números.

Podría haber equivocado algunos de los detalles, pero dado que este es el intercambio de programadores, y el contexto fue entrevistas, espero que lo anterior sea una respuesta lo suficientemente convincente para esa situación.

EDITAR Agregué algunos detalles adicionales aquí para explicar el esquema de tabla hash paralela y cómo significa que las búsquedas lineales pobres que mencioné pueden reemplazarse con una búsqueda O (1). También me di cuenta de que, por supuesto, no hay necesidad de buscar el siguiente número más bajo, porque puede avanzar directamente hacia él al buscar en la tabla hash con el número más bajo y avanzar al siguiente elemento.


1
La búsqueda tiene que ser parte de la función de inserción; no son funciones independientes. Como su búsqueda es O (n), su función de inserción también es O (n).
Kirk Broadhurst

No. Usando la estrategia que he descrito, donde se usan más tablas hash para atravesar el espacio numérico más rápidamente, es O (1). Por favor lea mi respuesta nuevamente.
Benedicto

1
@Benedict, su respuesta dice claramente que tiene búsquedas lineales en los pasos 4 y 7. Las búsquedas lineales no son O (1).
Peter Taylor

Sí, lo hace, pero me ocuparé de eso más tarde. ¿Te importaría leer el resto por favor? Si es necesario, editaré mi respuesta para que quede bastante clara.
Benedicto

@Benedict Tienes razón: excluyendo la búsqueda, tu respuesta es O (1). Lamentablemente, esta solución no funcionará sin la búsqueda.
Kirk Broadhurst

1

¿Podemos suponer que los números son de un tipo de datos fijo, como Integer? Si es así, mantenga un conteo de cada número agregado. Esta es una operación O (1).

  1. Declare una matriz con tantos elementos como números posibles:
  2. Lea cada número a medida que se transmite.
  3. Cuenta el número. Ignórelo si ese número ya se ha contado 100 veces, ya que nunca lo necesitará. Esto evita que los desbordamientos lo cuenten un número infinito de veces.
  4. Repita desde el paso 2.

Código VB.Net:

Const Capacity As Integer = 100

Dim Tally(Integer.MaxValue) As Integer ' Assume all elements = 0
Do
    Value = ReadValue()
    If Tally(Value) < Capacity Then Tally(Value) += 1
Loop

Cuando devuelva la lista, puede tomar el tiempo que desee. Simplemente itere desde el final de la lista y cree una nueva lista de los 100 valores más altos registrados. Esta es una operación O (n), pero eso es irrelevante.

Dim List(Capacity) As Integer
Dim ListCount As Integer = 0
Dim Value As Integer = Tally.Length - 1
Dim ValueCount As Integer = 0
Do Until ListCount = List.Length OrElse Value < 0
    If Tally(Value) > ValueCount Then
        List(ListCount) = Value
        ValueCount += 1
        ListCount += 1
    Else
        Value -= 1
        ValueCount = 0
    End If
Loop
Return List

Editar: de hecho, realmente no importa si se trata de un tipo de datos fijo. Dado que no hay límites impuestos al consumo de memoria (o disco duro), puede hacer que esto funcione para cualquier rango de enteros positivos.


1

Cien números se almacenan fácilmente en una matriz, tamaño 100. Cualquier árbol, lista o conjunto es excesivo, dada la tarea en cuestión.

Si el número entrante es más alto que el más bajo (= último) en la matriz, ejecute todas las entradas. Una vez que encuentre el primero que sea más pequeño que su nuevo número (puede usar búsquedas sofisticadas para hacerlo), recorra el resto de la matriz, presionando cada entrada "hacia abajo" en una.

Dado que mantiene la lista ordenada desde el principio, no necesita ejecutar ningún algoritmo de clasificación. Esto es O (1).


0

Puedes usar un binario Max-Heap. Tendría que realizar un seguimiento de un puntero al nodo mínimo (que podría ser desconocido / nulo).

Empiezas insertando los primeros 100 números en el montón. El máximo estará en la parte superior. Una vez hecho esto, siempre mantendrá 100 números allí.

Luego, cuando obtenga un nuevo número:

if(minimumNode == null)
{
    minimumNode = findMinimumNode();
}
if(newNumber > minimumNode.Value)
{
    heap.Remove(minimumNode);
    minimumNode = null;
    heap.Insert(newNumber);
}

Lamentablemente findMinimumNodees O (n), y usted incurre en ese costo una vez por inserción (pero no durante la inserción :). Eliminar el nodo mínimo e insertar el nuevo nodo son, en promedio, O (1) porque tenderán hacia la parte inferior del montón.

Yendo hacia el otro lado con un Binary Min-Heap, el min está en la parte superior, lo cual es ideal para encontrar el min para comparar, pero apesta cuando tienes que reemplazar el mínimo con un nuevo número que es> min. Esto se debe a que debe eliminar el nodo min (siempre O (logN)) y luego insertar el nuevo nodo (O promedio (1)). Entonces, todavía tienes O (logN) que es mejor que Max-Heap, pero no O (1).

Por supuesto, si N es constante, siempre tiene O (1). :)

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.