Obtenha 100 números mais altos de uma lista infinita


53

Um dos meus amigos recebeu essa pergunta da entrevista -

"Existe um fluxo constante de números vindo de uma lista infinita de números, dos quais você precisa manter uma estrutura de dados para retornar os 100 números mais altos em qualquer ponto do tempo. Suponha que todos os números sejam apenas números inteiros."

Isso é simples, você precisa manter uma lista classificada em ordem decrescente e acompanhar o número mais baixo da lista. Se o novo número obtido for maior que o número mais baixo, você deverá remover o número mais baixo e inserir o novo número na lista classificada, conforme necessário.

Então a pergunta foi estendida -

"Você pode garantir que o Pedido de inserção seja O (1)? É possível?"

Tanto quanto eu sabia, mesmo se você adicionar um novo número à lista e classificá-lo novamente usando qualquer algoritmo de classificação, seria melhor O (logn) para quicksort (eu acho). Então, meu amigo disse que não era possível. Mas ele não estava convencido, ele pediu para manter qualquer outra estrutura de dados em vez de uma lista.

Pensei em árvore binária balanceada, mas mesmo lá você não receberá a inserção na ordem de 1. Portanto, a mesma pergunta que eu tenho agora. Queria saber se existe alguma estrutura de dados que possa inserir na ordem de 1 para o problema acima ou não é possível.


19
Talvez isso seja apenas eu que entendi mal a pergunta, mas por que você precisa manter uma lista classificada ? Por que não apenas acompanhar o número mais baixo e, se um número maior que esse for encontrado, remova o número mais baixo e insira o novo número, sem manter a lista classificada. Isso daria a você O (1).
EdoDodo

36
@EdoDodo - e após essa operação, como você sabe qual é o novo número mais baixo?
Damien_The_Unbeliever 26/10

19
Classifique a lista [O (100 * log (100)) = O (1)] ou faça uma pesquisa linear pelo mínimo [O (100) = O (1)] para obter o novo número mais baixo. Sua lista é de tamanho constante; portanto, todas essas operações também são constantes.
Random832

6
Você não precisa manter a lista inteira classificada. Você não se importa qual é o número mais alto ou o segundo mais alto. Você só precisa saber qual é o menor. Então, depois de inserir um novo número, basta percorrer os 100 números e ver qual é agora o menor. Isso é tempo constante.
Tom Zych

27
A ordem assintótica de uma operação é interessante apenas quando o tamanho do problema pode aumentar sem limites. Não está muito claro em sua pergunta qual quantidade está crescendo sem limites; parece que você está perguntando qual é a ordem assintótica para um problema cujo tamanho é limitado a 100; essa nem é uma pergunta sensata a ser feita; algo tem que estar crescendo sem limites. Se a pergunta for "você pode fazer isso para manter os primeiros n, e não os 100 primeiros, em O (1)?" então a questão é sensata.
Eric Lippert

Respostas:


35

Digamos que k é o número de números mais altos que você deseja conhecer (100 no seu exemplo). Em seguida, você pode adicionar um novo número no O(k)qual também está O(1). Porque O(k*g) = O(g) if k is not zero and constant.


6
O (50) é O (n), não O (1). Inserir em uma lista de comprimento N em O (1) meios de tempo que o tempo não dependem de valor de N. Isso significa que se 100 torna-se 10000, 50 não deve tornar-se 5000.

18
@hamstergene - mas, no caso desta pergunta, No tamanho da lista classificada ou o número de itens que foram processados ​​até agora? Se você processar 10000 itens e manter os 100 itens principais em uma lista ou processar 1000000000 itens e manter os 100 itens principais em uma lista classificada, os custos de inserção nessa lista permanecerão os mesmos.
Damien_The_Unbeliever 26/10

6
@ hamstergene: Nesse caso, você entendeu errado o básico. Em seu link wikipedia existe uma propriedade ( "Multiplicação por uma constante"): O(k*g) = O(g) if k not zero and constant. => O(50*1) = O(1).
duedl0r

9
Eu acho que duedl0r está certo. Vamos reduzir o problema e dizer que você só precisa dos valores mínimo e máximo. É este O (n) porque o mínimo e o máximo são 2? (n = 2) O número 2 faz parte da definição do problema. É uma constante, por isso é ak no O (k * alguma coisa) que é equivalente a S (algo)
Xanatos

9
@hamstergene: de que função você está falando? o valor 100 parece bastante constante para mim ..
duedl0r

19

Mantenha a lista não classificada. Descobrir se um novo número deve ou não ser inserido levará mais tempo, mas a inserção será O (1).


7
Acho que isso lhe daria o prêmio smart-aleck , se nada mais. * 8 ')
Mark Booth

4
@Emilio, você está tecnicamente correto - e, claro, que é o melhor tipo de correto ...
Gareth

11
Mas você também pode manter o menor dos seus 100 números e também decidir se precisa inserir O (1). Somente quando você insere um número, é necessário procurar o novo número mais baixo. Mas isso acontece mais raro do que decidir inserir ou não, o que acontece para cada novo número.
Andrei Vajna II

12

Isso é facil. O tamanho da lista de constantes, portanto, o tempo de classificação da lista é constante. Uma operação que é executada em tempo constante é considerada O (1). Portanto, a classificação da lista é O (1) para uma lista de tamanho fixo.


9

Depois de passar 100 números, o custo máximo que você incorrerá para o próximo número é o custo para verificar se o número está nos 100 números mais altos (vamos rotular esse CheckTime ) mais o custo para inseri-lo nesse conjunto e ejetar o número o menor (vamos chamar de EnterTime ), que é tempo constante (pelo menos para números limitados) ou O (1) .

Worst = CheckTime + EnterTime

Em seguida, se a distribuição dos números for aleatória, o custo médio diminuirá quanto mais números você tiver. Por exemplo, a chance de você inserir o 101º número no conjunto máximo é 100/101, as chances para o 1000º número seriam 1/10 e as chances para o enésimo número seriam 100 / n. Assim, nossa equação para o custo médio será:

Average = CheckTime + EnterTime / n

Assim, quando n se aproxima do infinito, apenas o CheckTime é importante:

Average = CheckTime

Se os números estiverem vinculados, CheckTime é constante e, portanto, é hora O (1) .

Se os números não estiverem vinculados, o tempo de verificação aumentará com mais números. Teoricamente, isso ocorre porque se o menor número no conjunto máximo ficar grande o suficiente, o tempo de verificação será maior, pois você terá que considerar mais bits. Isso faz parecer que será um pouco maior que o tempo constante. No entanto, você também pode argumentar que a chance de o próximo número estar no conjunto mais alto se aproxima de zero quando n se aproxima do infinito e, portanto, a chance de você precisar considerar mais bits também se aproxima de 0, o que seria um argumento para O (1) Tempo.

Não sou positivo, mas meu intestino diz que é hora O (log (log (n))) . Isso ocorre porque a chance de o número mais baixo aumentar é logarítmica e a chance de que o número de bits que você precisa considerar para cada verificação também seja logarítmico. Estou interessado em outros povos, porque não tenho muita certeza ...


Exceto que a lista é arbitrária, e se for uma lista de números sempre crescentes?
2141111

@dan_waterworth: Se a lista infinita é arbitrária e só aumenta (as chances são de 1 / ∞!), isso seria o pior cenário possível CheckTime + EnterTimepara cada número. Isto só faz sentido se os números são sem limites, e assim CheckTimee EnterTimeserá tanto aumento, pelo menos de forma logarítmica, devido ao aumento no tamanho dos números.
Briguy37

11
Os números não são aleatórios, existem arbitrários. Não faz sentido falar sobre probabilidades.
dan_waterworth

@dan_waterworth: Você disse duas vezes agora que os números são arbitrários. De onde você está pegando isso? Além disso, acredito que você ainda pode aplicar estatísticas a números arbitrários começando com o caso aleatório e melhorar sua precisão à medida que souber mais sobre o árbitro. Por exemplo, se você fosse o árbitro, parece que haveria uma maior chance de selecionar números cada vez maiores do que se, por exemplo, eu era o árbitro;)
Briguy37

7

este é fácil se você conhece árvores binárias de heap . Montes binários suportam a inserção em tempo constante médio, O (1). E você terá acesso fácil aos primeiros x elementos.


Por que armazenar os elementos que você não precisa? (os valores que são muito baixos) Parece que um algoritmo personalizado é mais apropriado. Não estou dizendo que você não pode 'não adicionar' os valores quando eles não são mais altos que os mais baixos.
Steven Jeuris

Não sei, minha intuição me diz que um monte (de algum sabor) poderia fazer isso muito bem. Não significa que ele teria que manter todos os elementos para fazê-lo. Eu não pesquisei, mas "parece certo" (TM).
Rig

3
Uma pilha pode ser modificada para descartar qualquer coisa abaixo de um mésimo nível (para pilhas binárias ek = 100, m seria 7, já que o número de nós = 2 ^ m-1). Isso reduziria a velocidade, mas ainda assim seria amortizado em tempo constante.
Plutor

3
Se você usou um min-heap binário (porque o topo é o mínimo, que você está verificando o tempo todo) e encontra um novo número> min, é necessário remover o elemento top antes de inserir um novo . A remoção do elemento superior (min) será O (logN) porque você precisará percorrer todos os níveis da árvore uma vez. Portanto, é tecnicamente verdade que as inserções têm média O (1), porque na prática ainda é O (logN) toda vez que você encontra um número> min.
Scott Whitlock

11
@Plutor, você está assumindo algumas garantias de que as pilhas binárias não lhe dão. Visualizando-o como uma árvore binária, pode ser que cada elemento no ramo esquerdo seja menor que qualquer elemento no ramo direito, mas você está assumindo que os elementos menores estão mais próximos da raiz.
Peter Taylor

6

Se pela pergunta que o entrevistador realmente quis fazer "podemos garantir que cada número recebido seja processado em tempo constante", como muitos já apontaram (por exemplo, veja a resposta de @ duedl0r), a solução do seu amigo já é O (1) e seria assim mesmo se ele tivesse usado lista não classificada, ou tipo bolha, ou qualquer outra coisa. Nesse caso, a pergunta não faz muito sentido, a menos que seja uma pergunta complicada ou você se lembre errado.

Suponho que a pergunta do entrevistador foi significativa, que ele não estava perguntando como fazer algo para ser O (1), o que já é muito óbvio.

Como a complexidade do algoritmo de questionamento só faz sentido quando o tamanho da entrada aumenta indefinidamente, e a única entrada que pode crescer aqui é 100 - o tamanho da lista; Suponho que a verdadeira questão era "podemos garantir que obtemos o Top N gastando O (1) tempo por número (não O (N) como na solução de seu amigo), é possível?".

A primeira coisa que vem à mente é contar a classificação, que comprará a complexidade do tempo O (1) por número para o problema Top-N pelo preço da utilização do espaço O (m), em que m é o comprimento do intervalo dos números recebidos . Então sim, é possível.


4

Use uma fila de prioridade mínima implementada com um heap Fibonacci , que tenha tempo de inserção constante:

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

4
"As operações excluem e excluem o trabalho mínimo no O(log n)tempo amortizado" , portanto isso ainda resultaria em O(log k)onde kestá a quantidade de itens a serem armazenados.
Steven Jeuris

11
Isso não é diferente da resposta de Emilio, que foi apelidada de "prêmio smart-aleck", já que o min de exclusão opera em O (log n) (de acordo com a Wikipedia).
26611 Nicole

A resposta de @Renesis Emilio seria O (k) para encontrar o mínimo, o meu é O (log k)
Gabe Moothart

11
@ Gabe Justo, quero dizer apenas em princípio. Em outras palavras, se você não considera 100 uma constante, essa resposta também não é um tempo contante.
26611 Nicole

@ Renesis Eu removi a declaração (incorreta) da resposta.
Gabe Moothart 26/10/11

2

A tarefa é claramente encontrar um algoritmo que seja O (1) no comprimento N da lista de números necessária. Portanto, não importa se você precisa do número 100 ou 10000, o tempo de inserção deve ser O (1).

O truque aqui é que, embora esse requisito O (1) seja mencionado na inserção da lista, a pergunta não disse nada sobre a ordem do tempo de pesquisa no espaço numérico inteiro, mas acontece que isso pode ser feito O (1) também. A solução é a seguinte:

  1. Organize uma hashtable com números para chaves e pares de ponteiros de lista vinculada para valores. Cada par de ponteiros é o início e o fim de uma sequência de lista vinculada. Normalmente, este será apenas um elemento e depois o próximo. Cada elemento da lista vinculada fica próximo ao elemento com o próximo número mais alto. Portanto, a lista vinculada contém a sequência classificada dos números obrigatórios. Mantenha um registro do número mais baixo.

  2. Pegue um novo número x do fluxo aleatório.

  3. É superior ao último número mais baixo registrado? Sim => Etapa 4, Não => Etapa 2

  4. Bata na tabela de hash com o número acabado de obter. Existe uma entrada? Sim => Etapa 5. Não => Pegue um novo número x-1 e repita esta etapa (esta é uma pesquisa linear descendente simples, aceite aqui, isso pode ser melhorado e eu explicarei como)

  5. Com o elemento list obtido apenas na tabela de hash, insira o novo número logo após o elemento na lista vinculada (e atualize o hash)

  6. Pegue o número mais baixo l registrado (e remova-o da lista / hash).

  7. Bata na tabela de hash com o número acabado de obter. Existe uma entrada? Sim => Etapa 8. Não => Pegue um novo número l + 1 e repita esta etapa (esta é uma pesquisa linear ascendente simples)

  8. Com um acerto positivo, o número se torna o novo número mais baixo. Avance para o passo 2

Para permitir valores duplicados, o hash realmente precisa manter o início e o fim da sequência de lista vinculada de elementos duplicados. Adicionar ou remover um elemento em uma determinada tecla aumenta ou diminui o intervalo apontado.

A inserção aqui é O (1). As pesquisas mencionadas são, acho que algo como, O (diferença média entre números). A diferença média aumenta com o tamanho do espaço numérico, mas diminui com o comprimento necessário da lista de números.

Portanto, a estratégia de pesquisa linear é muito ruim, se o espaço numérico for grande (por exemplo, para um tipo int de 4 bytes, 0 a 2 ^ 32-1) e N = 100. Para contornar esse problema de desempenho, você pode manter conjuntos paralelos de tabelas de hash, onde os números são arredondados para magnitudes mais altas (por exemplo, 1s, 10s, 100s, 1000s) para criar as teclas adequadas. Dessa forma, você pode acelerar e diminuir as marchas para realizar as pesquisas necessárias mais rapidamente. O desempenho então se torna um O (log numberrange), eu acho, que é constante, ou seja, O (1) também.

Para deixar isso mais claro, imagine que você tenha o número 197 em mãos. Você atinge a tabela de hash 10s, com '190', é arredondado para o próximo dez. Qualquer coisa? Não. Então você diminui em 10s até pressionar, digamos, 120. Então você pode começar em 129 na hashtable 1s e tentar 128, 127 até atingir alguma coisa. Agora você encontrou o local na lista vinculada para inserir o número 197. Ao inseri-lo, você também deve atualizar a hashtable 1s com a entrada 197, a hashtable 10s com o número 190, 100s com 100, etc. o que você precisa fazer aqui é 10 vezes o log do intervalo de números.

Talvez eu tenha entendido errado alguns detalhes, mas como essa é a troca de programadores e o contexto foi de entrevistas, espero que o texto acima seja uma resposta suficientemente convincente para essa situação.

EDIÇÃO Adicionei alguns detalhes extras aqui para explicar o esquema de hashtable paralelo e como isso significa que as pesquisas lineares ruins que eu mencionei podem ser substituídas por uma pesquisa O (1). Também percebi que, obviamente, não há necessidade de procurar o próximo número mais baixo, porque você pode ir direto para ele procurando na hashtable com o número mais baixo e progredindo para o próximo elemento.


11
A pesquisa deve fazer parte da função de inserção - elas não são funções independentes. Como sua pesquisa é O (n), sua função de inserção também é O (n).
precisa

Não. Usando a estratégia que descrevi, onde mais tabelas de hash são usadas para percorrer o espaço numérico mais rapidamente, é O (1). Por favor, leia minha resposta novamente.
Benedict

11
@Benedict, sua resposta diz claramente que há pesquisas lineares nas etapas 4 e 7. As pesquisas lineares não são O (1).
Peter Taylor

Sim, mas lido com isso mais tarde. Você se importaria de ler o resto, por favor? Se necessário, editarei minha resposta para deixar bem clara.
Benedict

@Benedict Você está correto - excluindo a pesquisa, sua resposta é O (1). Infelizmente, esta solução não funcionará sem a pesquisa.
precisa

1

Podemos assumir que os números são de um tipo de dados fixo, como Inteiro? Nesse caso, mantenha um registro de cada número adicionado. Esta é uma operação O (1).

  1. Declare uma matriz com o maior número possível de elementos:
  2. Leia cada número conforme é transmitido.
  3. Registre o número. Ignore-o se esse número já tiver sido contabilizado 100 vezes, pois você nunca precisará dele. Isso evita que os transbordamentos o calculem um número infinito de vezes.
  4. Repita da etapa 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

Quando você retorna a lista, pode demorar o quanto quiser. Simplesmente itere no final da lista e crie uma nova lista dos 100 valores mais altos registrados. Esta é uma operação O (n), mas é irrelivante.

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

Edit: Na verdade, não importa se é um tipo de dados fixo. Como não há limites impostos ao consumo de memória (ou disco rígido), você pode fazer isso funcionar para qualquer intervalo de números inteiros positivos.


1

Cem números são facilmente armazenados em uma matriz, tamanho 100. Qualquer árvore, lista ou conjunto é um exagero, dada a tarefa em questão.

Se o número recebido for maior que o menor (= último) na matriz, execute todas as entradas. Depois de encontrar o primeiro menor que o seu novo número (você pode usar pesquisas sofisticadas para fazer isso), percorra o restante da matriz, pressionando cada entrada "para baixo" por uma.

Como você mantém a lista classificada desde o início, não é necessário executar nenhum algoritmo de classificação. Este é O (1).


0

Você pode usar um binário Max-Heap. Você precisaria acompanhar um ponteiro para o nó mínimo (que pode ser desconhecido / nulo).

Você começa inserindo os 100 primeiros números na pilha. O máximo estará no topo. Depois disso, você sempre manterá 100 números lá.

Então, quando você receber um novo número:

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

Infelizmente, findMinimumNodeé O (n) e você incorre nesse custo uma vez por inserção (mas não durante a inserção :). Remover o nó mínimo e inserir o novo nó é, em média, O (1), porque eles tendem para a parte inferior do heap.

Indo para o outro lado com um heap mínimo binário, o min está no topo, o que é ótimo para encontrar o min para comparação, mas é péssimo quando você precisa substituir o mínimo por um novo número que seja> min. Isso ocorre porque você deve remover o nó min (sempre O (logN)) e depois inserir o novo nó (O (média 1)). Portanto, você ainda possui O (logN), que é melhor que o Max-Heap, mas não O (1).

Obviamente, se N for constante, você sempre terá O (1). :)

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.