Escreva um programa para encontrar 100 maiores números de uma matriz de 1 bilhão de números


300

Recentemente, participei de uma entrevista na qual me pediram "escreva um programa para encontrar os 100 maiores números de uma matriz de 1 bilhão de números".

Eu só consegui fornecer uma solução de força bruta que classificasse a matriz na complexidade de tempo O (nlogn) e levasse os últimos 100 números.

Arrays.sort(array);

O entrevistador estava procurando uma complexidade de tempo melhor. Tentei outras soluções, mas não consegui responder. Existe uma solução melhor para a complexidade do tempo?


70
Talvez o problema seja que não era uma questão de classificação , mas uma questão de busca .
geomagas

11
Como observação técnica, a classificação pode não ser a melhor maneira de resolver o problema, mas não acho que seja força bruta - posso pensar em maneiras muito piores de fazer isso.
Bernhard Barker

88
Pensei em um método de força bruta ainda mais estúpido ... Encontre todas as combinações possíveis de 100 elementos da matriz de 1 bilhão de elementos e veja qual dessas combinações tem a maior soma.
Shashank

10
Observe que todos os algoritmos determinísticos (e corretos) estão O(1)neste caso, porque não há aumento de dimensão. O entrevistador deveria ter perguntado "Como encontrar m maiores elementos de uma matriz de n com n >> m?".
Bakuriu

Respostas:


328

Você pode manter uma fila prioritária dos 100 maiores números, percorrer os bilhões de números, sempre que encontrar um número maior que o menor número da fila (a cabeça da fila), remover a cabeça da fila e adicionar o novo número para a fila.

EDIT: como Dev observou, com uma fila de prioridade implementada com um heap, a complexidade da inserção na fila éO(logN)

No pior dos casos, você obtém o que é melhor do quebillionlog2(100)billionlog2(billion)

Em geral, se você precisar dos maiores números K de um conjunto de N números, a complexidade é O(NlogK)mais do que O(NlogN)isso pode ser muito significativo quando K for muito pequeno comparado a N.

EDIT2:

O tempo esperado desse algoritmo é bastante interessante, pois em cada iteração uma inserção pode ou não ocorrer. A probabilidade do i-ésimo número a ser inserido na fila é a probabilidade de uma variável aleatória ser maior que pelo menos i-Kvariáveis ​​aleatórias da mesma distribuição (os primeiros k números são adicionados automaticamente à fila). Podemos usar estatísticas de pedidos (veja o link ) para calcular essa probabilidade. Por exemplo, vamos assumir que os números foram selecionados aleatoriamente de maneira uniforme {0, 1}, o valor esperado de (iK) o número (de i números) é (i-k)/ie a chance de uma variável aleatória ser maior que esse valor 1-[(i-k)/i] = k/i.

Assim, o número esperado de inserções é:

insira a descrição da imagem aqui

E o tempo de execução esperado pode ser expresso como:

insira a descrição da imagem aqui

( ktempo para gerar a fila com os primeiros kelementos, n-kcomparações e o número esperado de inserções, conforme descrito acima, cada um leva um log(k)/2tempo médio )

Observe que quando Né muito grande em comparação com K, essa expressão está muito mais próxima ne não NlogK. Isso é um pouco intuitivo, como no caso da pergunta, mesmo após 10000 iterações (que é muito pequena em comparação a um bilhão), a chance de um número ser inserido na fila é muito pequena.


6
Na verdade, é apenas O (100) para cada inserção.
precisa saber é o seguinte

8
@RonTeller Você não pode pesquisar binariamente uma lista vinculada com eficiência, é por isso que uma fila de prioridade geralmente é implementada com uma pilha. Seu tempo de inserção, conforme descrito, é O (n), não O (logn). Você acertou na primeira vez (fila ordenada ou fila prioritária) até o Skizz fazer você se acostumar.
Dev

17
@ThomasJungblut bilhões também é uma constante, então, se for esse o caso, é O (1): P
Ron Teller

9
@ RonTeller: normalmente esse tipo de pergunta diz respeito a encontrar 10 páginas principais de bilhões de resultados de pesquisa do Google, ou 50 palavras mais frequentes para uma nuvem de palavras ou 10 músicas mais populares na MTV, etc. Então, acredito que em circunstâncias normais é seguro considerar k constante e pequeno em comparação com n. Porém, é preciso sempre ter em mente essas "circunstâncias normais".
F7

5
Como você possui itens 1G, colete 1000 elementos aleatoriamente e escolha os 100 maiores. Isso deve evitar os casos degenerados (classificados, classificados inversamente, principalmente classificados), reduzindo consideravelmente o número de inserções.
ChuckCottrill

136

Se isso for perguntado em uma entrevista, acho que o entrevistador provavelmente deseja ver seu processo de solução de problemas, não apenas seu conhecimento de algoritmos.

A descrição é bastante geral, então talvez você possa perguntar a ele o alcance ou o significado desses números para esclarecer o problema. Fazer isso pode impressionar um entrevistador. Se, por exemplo, esses números representam a idade das pessoas dentro de um país (por exemplo, China), é um problema muito mais fácil. Com uma suposição razoável de que ninguém vivo tem mais de 200 anos, você pode usar uma matriz int de tamanho 200 (talvez 201) para contar o número de pessoas com a mesma idade em apenas uma iteração. Aqui, o índice significa a idade. Depois disso, é fácil encontrar o número 100 maior. A propósito, esse algo é chamado de contagem de contagem .

De qualquer forma, tornar a pergunta mais específica e clara é boa para você em uma entrevista.


26
Muito bons pontos. Ninguém mais perguntou ou indicou nada sobre a distribuição desses números - isso poderia fazer toda a diferença em como abordar o problema.
NealB 08/10

13
Gostaria desta resposta o suficiente para estendê-la. Leia os números uma vez para obter os valores mínimo / máximo para que você possa assumir a distribuição. Em seguida, escolha uma das duas opções. Se o intervalo for pequeno o suficiente, crie uma matriz em que você possa simplesmente marcar os números à medida que eles ocorrerem. Se o intervalo for muito grande, use o algoritmo de heap classificado discutido acima .... Apenas um pensamento.
`` #

2
Eu concordo, fazer a pergunta de volta ao entrevistador realmente faz muita diferença. De fato, uma pergunta como você está limitado pelo poder de computação ou não também pode ajudá-lo a paralelizar a solução usando vários nós de computação.
Sumit Nigam

1
@R_G Não há necessidade de percorrer toda a lista. O suficiente para provar uma pequena fração (por exemplo, um milhão) de membros aleatórios da lista para obter estatísticas úteis.
Itamar

Para aqueles que não pensariam nessa solução, recomendo ler sobre o tipo de contagem en.wikipedia.org/wiki/Counting_sort . Essa é realmente uma pergunta de entrevista bastante comum: você pode classificar uma matriz melhor que O (nlogn). Esta questão é apenas uma extensão.
Maxime Chéramy

69

Você pode iterar sobre os números que levam O (n)

Sempre que você encontrar um valor maior que o mínimo atual, adicione o novo valor a uma fila circular com tamanho 100.

O minuto dessa fila circular é o seu novo valor de comparação. Continue adicionando a essa fila. Se cheio, extraia o mínimo da fila.


3
Isso não funciona. por exemplo, encontre o top 2 de {1, 100, 2, 99} resultará em {100,1} como o top 2. #
Skizz

7
Você não pode dar a volta para manter a fila classificada. (se você não quiser procurar o buraco fila de cada vez para a próxima menor elemento)
MrSmith42

3
@ MrSmith42 A classificação parcial, como em uma pilha, é suficiente. Veja a resposta de Ron Teller.
Christopher Creutzig

1
Sim, assumi silenciosamente que uma extração-min-fila é implementada como um heap.
Regenschein

Em vez da fila circular, use um heap mínimo de tamanho 100, isso terá um número mínimo de cem no topo. Isto irá levar apenas O (N log N) para inserção em comparação com o (n) no caso de fila
techexplorer

33

Percebi que isso está marcado com 'algoritmo', mas descartará outras opções, pois provavelmente também deve estar marcado com 'entrevista'.

Qual é a fonte dos 1 bilhão de números? Se for um banco de dados, 'selecionar valor da ordem da tabela pelo valor desc limite 100' faria o trabalho muito bem - pode haver diferenças de dialeto.

Isso é único, ou algo que será repetido? Se repetido, com que frequência? Se for único e os dados estiverem em um arquivo, 'cat srcfile | ordenar (opções conforme necessário) | O cabeçalho -100 'fará com que você faça rapidamente um trabalho produtivo que será pago enquanto o computador lida com essa tarefa trivial.

Se repetir, você recomendaria escolher uma abordagem decente para obter a resposta inicial e armazenar / armazenar em cache os resultados, para que você pudesse continuamente reportar os 100 melhores.

Finalmente, há essa consideração. Você está procurando um emprego básico e entrevistando um gerente nerd ou futuro colega de trabalho? Nesse caso, você pode descartar todo tipo de abordagem que descreva os prós e contras técnicos relativos. Se você estiver procurando por um trabalho mais gerencial, faça-o como um gerente, preocupado com os custos de desenvolvimento e manutenção da solução, diga "muito obrigado" e saia, se esse for o entrevistador que deseja se concentrar nas curiosidades da CS . É improvável que ele e você tenham muito potencial de progresso lá.

Melhor sorte na próxima entrevista.


2
Resposta excepcional. Todo mundo se concentrou no lado técnico da questão, enquanto essa resposta aborda a parte social dos negócios.
Vbocan # 9/13

2
Eu nunca imaginei que você poderia dizer obrigado e deixar uma entrevista e não esperar que ela terminasse. Obrigado por abrir minha mente.
UrsulRosu

1
Por que não podemos criar um monte de bilhões de elementos e extrair 100 maiores elementos? Dessa forma, custo = O (bilhão) + 100 * O (log (bilhão)) ??
Mohit Shah

17

Minha reação imediata a isso seria usar uma pilha, mas há uma maneira de usar o QuickSelect sem manter todos os valores de entrada em mãos a qualquer momento.

Crie uma matriz de tamanho 200 e preencha-a com os primeiros 200 valores de entrada. Execute o QuickSelect e descarte os 100 baixos, deixando-o com 100 lugares gratuitos. Leia os próximos 100 valores de entrada e execute o QuickSelect novamente. Continue até ter executado a entrada inteira em lotes de 100.

No final, você tem os 100 principais valores. Para valores N, você executou o QuickSelect aproximadamente N / 100 vezes. Cada Quickselect custa cerca de 200 vezes uma constante, portanto, o custo total é 2N vezes uma constante. Isso parece linear no tamanho da entrada para mim, independentemente do tamanho do parâmetro que eu estou conectando para ser 100 nesta explicação.


10
Você pode adicionar uma otimização pequena, mas possivelmente importante: Depois de executar o QuickSelect para particionar a matriz de tamanho 200, o mínimo dos 100 principais elementos é conhecido. Em seguida, ao iterar todo o conjunto de dados, preencha apenas os 100 valores mais baixos se o valor atual for maior que o mínimo atual. Uma implementação simples desse algoritmo no C ++ é comparável à partial_sortexecução do libstdc ++ diretamente em um conjunto de dados de 200 milhões de 32 bits int(criado por meio de um MT19937, distribuído uniformemente).
dyp

1
Boa ideia - não afeta a pior análise, mas parece valer a pena.
Mcdowella # 8/13

@mcdowella Vale a pena tentar e vou fazê-lo, obrigado!
userx

8
É exatamente isso que a goiaba Ordering.greatestOf(Iterable, int) faz. É absolutamente linear-time e single-pass, e é um algoritmo super fofo. FWIW, também temos algumas referências reais: seus fatores constantes são um pouco mais lentos do que a fila de prioridade tradicional no caso médio, mas essa implementação é muito mais resistente à entrada do "pior caso" (por exemplo, entrada estritamente ascendente).
Louis Wasserman

15

Você pode usar o algoritmo de seleção rápida para encontrar o número no índice (por ordem) [bilhões-101] e, em seguida, iterar sobre os números e encontrar os números que são maiores a partir desse 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]);

Este algoritmo Tempo é: 2 XO (N) = O (N) (desempenho médio do caso)

A segunda opção, como sugerida por Thomas Jungblut, é:

Use o Heap ao criar o heap MAX, que receberá O (N) e, em seguida, os 100 números máximos máximos estarão no topo do Heap. Tudo o que você precisa é tirá-los do heap (100 XO (Log (N)).

Este algoritmo Tempo é: O (N) + 100 XO (Log (N)) = O (N)


8
Você está trabalhando na lista inteira três vezes. 1 bio. números inteiros têm aproximadamente 4 GB, o que você faria se não os encaixasse na memória? A seleção rápida é a pior escolha possível nesse caso. Iterar uma vez e manter um monte dos 100 principais itens é IMHO a solução com melhor desempenho em O (n) (observe que você pode cortar o O (log n) das inserções de heap, pois n no heap é 100 = constante = muito pequeno )
Thomas Jungblut

3
Mesmo assim O(N), realizar duas QuickSelects e outra verificação linear é muito mais caro do que o necessário.
7273 Kevin

Esta é pseudo-código todas as soluções aqui vai demorar mais tempo (O (NLog (N) ou 100 * O (N))
Um Homem Grupo

1
100*O(N)(se isso é uma sintaxe válida) = O(100*N)= O(N)(é certo que 100 pode ser variável; nesse caso, isso não é estritamente verdadeiro). Ah, e o Quickselect tem o pior desempenho de O (N ^ 2) (ai). E se não couber na memória, você recarregará os dados do disco duas vezes, o que é muito pior que uma vez (esse é o gargalo).
Bernhard Barker

Existe o problema de que esse tempo de execução é esperado, e não o pior caso, mas usando uma estratégia de seleção de pivô decente (por exemplo, escolha 21 elementos aleatoriamente e escolha a mediana desses 21 como pivô), o número de comparações poderá ser garantido com alta probabilidade de ser no máximo (2 + c) n para uma constante arbitrariamente pequena c.
One Man Crew

10

Embora a outra solução de seleção rápida tenha sido rebaixada, o fato é que a seleção rápida encontrará a solução mais rapidamente do que usando uma fila de tamanho 100. O Quickselect tem um tempo de execução esperado de 2n + o (n), em termos de comparações. Uma implementação muito simples seria

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

Isso levará 3n + o (n) comparações em média. Além disso, pode ser mais eficiente usando o fato de que a seleção rápida deixará os 100 maiores itens da matriz nos 100 locais mais à direita. Portanto, o tempo de execução pode ser aprimorado para 2n + o (n).

Existe o problema de que esse tempo de execução é esperado, e não o pior caso, mas usando uma estratégia de seleção de pivô decente (por exemplo, escolha 21 elementos aleatoriamente e escolha a mediana desses 21 como pivô), o número de comparações poderá ser garantido com alta probabilidade de ser no máximo (2 + c) n para uma constante arbitrariamente pequena c.

De fato, usando uma estratégia de amostragem otimizada (por exemplo, elementos sqrt (n) de amostra aleatoriamente e escolha o 99º percentil), o tempo de execução pode ser reduzido para (1 + c) n + o (n) para c arbitrariamente pequeno (assumindo que K, o número de elementos a serem selecionados é o (n)).

Por outro lado, o uso de uma fila de tamanho 100 exigirá comparações O (log (100) n) e a base de log 2 de 100 é aproximadamente igual a 6,6.

Se pensarmos nesse problema no sentido mais abstrato de escolher os maiores elementos K de uma matriz de tamanho N, onde K = o (N), mas K e N vão para o infinito, o tempo de execução da versão de seleção rápida será O (N) e a versão da fila serão O (N log K), portanto, nesse sentido, a seleção rápida também é assintoticamente superior.

Nos comentários, foi mencionado que a solução da fila será executada no tempo esperado N + K log N em uma entrada aleatória. Evidentemente, a suposição de entrada aleatória nunca é válida, a menos que a pergunta indique explicitamente. A solução de fila pode ser feita para atravessar a matriz em uma ordem aleatória, mas isso implicará o custo adicional de chamadas N para um gerador de números aleatórios, além de permitir toda a matriz de entrada ou alocar uma nova matriz de comprimento N contendo o índices aleatórios.

Se o problema não permitir que você se mova pelos elementos da matriz original, e o custo da alocação de memória for alto, a duplicação da matriz não é uma opção, isso é outra questão. Mas estritamente em termos de tempo de execução, esta é a melhor solução.


4
Seu último parágrafo é o ponto principal: com um bilhão de números, não é possível armazenar todos os dados na memória ou trocar elementos. (Pelo menos é assim que eu iria interpretar o problema, dado que era uma pergunta da entrevista.)
Ted Hopp

14
Em qualquer pergunta algorítmica, se a leitura dos dados for um problema, eles deverão ser mencionados na pergunta. A pergunta declara "dado um array" e não "dado um array no disco que não cabe na memória e não pode ser manipulado de acordo com o modelo de von neuman, que é o padrão na análise de algoritmos". Hoje em dia você pode adquirir um laptop com 8gigs de ram. Não sei de onde é viável a idéia de guardar um bilhão de números na memória. Eu tenho vários bilhões de números na memória na minha estação de trabalho agora.
Mrip # 7/13

Para sua informação, o pior caso de execução rápida é O (n ^ 2) (consulte en.wikipedia.org/wiki/Quickselect ) e também modifica a ordem dos elementos na matriz de entrada. É possível ter uma solução O (n) do pior caso, com uma constante muito grande ( en.wikipedia.org/wiki/Median_of_medians ).
pts

O pior caso de seleção rápida é exponencialmente improvável de acontecer, o que significa que, para fins práticos, isso é irrelevante. É fácil modificar a seleção rápida para que, com alta probabilidade, o número de comparações seja (2 + c) n + o (n) para arbitrariamente pequeno c.
Mrip # 7/13

"permanece o fato de que a seleção rápida encontrará a solução mais rapidamente do que usar uma fila de tamanho 100" - Não. A solução heap leva comparações de N + Klog (N) versus média de 2N para seleção rápida e 2,95 para Mediana de medianas. É evidente que é mais rápido para o dado K.
Neil G

5

pegue os 100 primeiros números do bilhão e ordene-os. agora basta percorrer o bilhão, se o número de origem for maior que o menor de 100, insira na ordem de classificação. Você termina com algo muito mais próximo de O (n) sobre o tamanho do conjunto.


3
oops não viu a resposta mais detalhada que a minha.
Samuel Thurston

Pegue os primeiros 500 números e pare para classificar (e jogue fora os 400 baixos) quando a lista for preenchida. (E escusado será dizer que você só então adicionar à lista se o novo número é> a mais baixa do selecionado 100.)
Hot Licks

4

Duas opções:

(1) Heap (priorityQueue)

Mantenha um min-heap com tamanho de 100. Atravesse a matriz. Quando o elemento for menor que o primeiro elemento no heap, substitua-o.

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 redução de mapa.

Isso é muito semelhante ao exemplo de contagem de palavras no hadoop. Trabalho de mapa: conte a frequência ou os tempos de todos os elementos exibidos. Reduzir: obtenha o elemento K superior.

Normalmente, eu daria ao recrutador duas respostas. Dê a eles o que quiserem. Obviamente, a codificação de redução de mapa seria trabalhosa, porque você precisa conhecer todos os parâmetros exatos. Nenhum dano para praticá-lo. Boa sorte.


+1 no MapReduce, não acredito que você foi o único a mencionar o Hadoop por um bilhão de números. E se o entrevistador pedir 1 bilhão de números? Você merece mais votos na minha opinião.
Silviu Burcea

@Silviu Burcea Muito obrigado. Também valorizo ​​o MapReduce. :)
Chris Su

Embora o tamanho de 100 seja constante neste exemplo, você realmente deve generalizar isso para uma variável separada, ie. k) Como 100 é tão constante quanto 1 bilhão, então por que você está atribuindo ao tamanho do grande conjunto de números uma variável de tamanho de n, e não para o menor conjunto de números? Realmente sua complexidade deve ser O (nlogk), que não é O (n).
quer

1
Mas o que quero dizer é que, se você está apenas respondendo à pergunta, 1 bilhão também é corrigido na pergunta. Por que generalizar 1 bilhão para ne não 100 para k? Seguindo sua lógica, a complexidade deve realmente ser O (1) porque 1 bilhão e 100 são fixos nesta questão.
Tom Heard

1
@ TomHeard Tudo bem. O (nlogk) Existe apenas um fator que afetará os resultados. Isso significa que, se n estiver aumentando cada vez mais, o "nível de resultado" aumentará linearmente. Ou podemos dizer que, mesmo com trilhões de números, ainda posso obter 100 números maiores. No entanto, você não pode dizer: Com o aumento de n, k está aumentando, de modo que k afetará o resultado. É por isso que eu uso O (nlogk), mas não O (nlogn)
Chris Su

4

Uma solução muito fácil seria percorrer a matriz 100 vezes. Qual é O(n).

Cada vez que você obtém o maior número (e altera seu valor para o valor mínimo, para que você não o veja na próxima iteração, ou acompanha os índices das respostas anteriores (mantendo o controle dos índices que a matriz original pode ter múltiplo do mesmo número)). Após 100 iterações, você tem os 100 maiores números.


1
Duas desvantagens - (1) Você está destruindo a entrada no processo - isso é preferencialmente evitado. (2) Você está analisando a matriz várias vezes - se a matriz estiver armazenada em disco e não puder caber na memória, isso poderá ser quase 100 vezes mais lento que a resposta aceita. (Sim, eles são ambos O (n), mas ainda assim)
Bernhard Barker

Boa chamada @Dukeling, adicionei uma redação adicional sobre como evitar a alteração da entrada original, mantendo o controle dos índices de resposta anteriores. O que ainda seria muito fácil de codificar.
James Oravec

Um exemplo brilhante de uma solução O (n) muito mais lenta que O (n log n). log2 (1 bilhão) é de apenas 30 ...
gnasher729

@ gnasher729 Qual é o tamanho da constante oculta em O (n log n)?
miracle173

1

Inspirado na resposta do @ron teller, aqui está um programa C de barebones para fazer o que você deseja.

#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);
}

Na minha máquina (core i3 com um SSD rápido), são necessários 25 segundos e 1724 classificações. Eu gerei um arquivo binário com dd if=/dev/urandom/ count=1000000000 bs=1para esta execução.

Obviamente, há problemas de desempenho com a leitura de apenas 4 bytes por vez - do disco, mas isso é por exemplo. No lado positivo, é necessária muito pouca memória.


1

A solução mais simples é varrer a matriz grande de bilhões de números e manter os 100 maiores valores encontrados até agora em um buffer de matriz pequena sem nenhuma classificação e lembrar o menor valor desse buffer. Primeiro, achei que esse método foi proposto pelo fordprefect, mas em um comentário, ele disse que assumiu a estrutura de dados com 100 números sendo implementada como um heap. Sempre que for encontrado um novo número maior, o mínimo no buffer será substituído pelo novo valor encontrado e o buffer será procurado pelo mínimo atual novamente. Se os números em bilhões de matrizes numéricas forem distribuídos aleatoriamente na maioria das vezes, o valor da matriz grande será comparado ao mínimo da matriz pequena e descartado. Somente para uma fração muito pequena de número, o valor deve ser inserido na matriz pequena. Portanto, a diferença de manipular a estrutura de dados que contém os pequenos números pode ser negligenciada. Para um pequeno número de elementos, é difícil determinar se o uso de uma fila prioritária é realmente mais rápido do que usar minha abordagem ingênua.

Quero estimar o número de inserções no pequeno buffer de matriz de 100 elementos quando a matriz de 10 ^ 9 elementos é digitalizada. O programa varre os primeiros 1000 elementos dessa grande matriz e precisa inserir no máximo 1000 elementos no buffer. O buffer contém 100 elementos dos 1000 elementos verificados, ou seja, 0,1 do elemento verificado. Portanto, assumimos que a probabilidade de um valor da matriz grande ser maior que o mínimo atual do buffer é de cerca de 0,1. Esse elemento deve ser inserido no buffer. Agora o programa varre os próximos 10 ^ 4 elementos da matriz grande. Porque o mínimo do buffer aumentará toda vez que um novo elemento for inserido. Estimamos que a proporção de elementos maior que o mínimo atual seja de cerca de 0,1 e, portanto, haja 0,1 * 10 ^ 4 = 1000 elementos a serem inseridos. Na verdade, o número esperado de elementos que são inseridos no buffer será menor. Após a varredura desses 10 ^ 4 elementos, a fração dos números no buffer será de cerca de 0,01 dos elementos varridos até o momento. Portanto, ao digitalizar os próximos 10 ^ 5 números, assumimos que não mais de 0,01 * 10 ^ 5 = 1000 serão inseridos no buffer. Continuando essa argumentação, inserimos cerca de 7000 valores após a varredura de 1000 + 10 ^ 4 + 10 ^ 5 + ... + 10 ^ 9 ~ 10 ^ 9 elementos da matriz grande. Portanto, ao digitalizar uma matriz com 10 ^ 9 elementos de tamanho aleatório, esperamos não mais do que 10 ^ 4 (= 7000 arredondadas) inserções no buffer. Após cada inserção no buffer, o novo mínimo deve ser encontrado. Se o buffer é uma matriz simples, precisamos de 100 comparações para encontrar o novo mínimo. Se o buffer for outra estrutura de dados (como um heap), precisamos de pelo menos 1 comparação para encontrar o mínimo. Para comparar os elementos da grande matriz, precisamos de 10 ^ 9 comparações. Portanto, em geral, precisamos de comparações de 10 ^ 9 + 100 * 10 ^ 4 = 1,001 * 10 ^ 9 ao usar uma matriz como buffer e pelo menos 1.000 * 10 ^ 9 comparações ao usar outro tipo de estrutura de dados (como um heap) . Portanto, o uso de um heap gera apenas um ganho de 0,1% se o desempenho for determinado pelo número de comparação. Mas qual é a diferença no tempo de execução entre inserir um elemento em um heap de 100 elementos e substituir um elemento em uma matriz de 100 elementos e encontrar seu novo mínimo? 000 * 10 ^ 9 comparações ao usar outro tipo de estrutura de dados (como um heap). Portanto, o uso de um heap gera apenas um ganho de 0,1% se o desempenho for determinado pelo número de comparação. Mas qual é a diferença no tempo de execução entre inserir um elemento em um heap de 100 elementos e substituir um elemento em uma matriz de 100 elementos e encontrar seu novo mínimo? 000 * 10 ^ 9 comparações ao usar outro tipo de estrutura de dados (como um heap). Portanto, o uso de um heap gera apenas um ganho de 0,1% se o desempenho for determinado pelo número de comparação. Mas qual é a diferença no tempo de execução entre inserir um elemento em um heap de 100 elementos e substituir um elemento em uma matriz de 100 elementos e encontrar seu novo mínimo?

  • No nível teórico: quantas comparações são necessárias para inserir em uma pilha. Eu sei que é O (log (n)), mas quão grande é o fator constante? Eu

  • No nível da máquina: qual é o impacto do cache e da previsão de ramificação no tempo de execução de uma inserção de heap e uma pesquisa linear em uma matriz.

  • No nível da implementação: Quais custos adicionais estão ocultos em uma estrutura de dados de heap fornecida por uma biblioteca ou compilador?

Penso que estas são algumas das perguntas que precisam ser respondidas antes que se possa tentar estimar a diferença real entre o desempenho de um heap de 100 elementos ou de um array de 100 elementos. Portanto, faria sentido fazer um experimento e medir o desempenho real.


1
Isso é o que um monte faz.
Neil G

@ Neil G: O que "isso"?
miracle173

1
A parte superior da pilha é o elemento mínimo na pilha e novos elementos são rejeitados com uma comparação.
Neil G

1
Entendo o que você está dizendo, mas mesmo que você faça um número absoluto de comparações em vez de um número assintótico de comparações, a matriz ainda é muito mais lenta porque o tempo para "inserir novo elemento, descartar o mínimo antigo e encontrar o novo mínimo" é 100 em vez de cerca de 7.
Neil G

1
Ok, mas sua estimativa é muito indireta. Você pode calcular diretamente o número esperado de inserções como k (digamma (n) - digamma (k)), que é menor que klog (n). De qualquer forma, a solução heap e a matriz gastam apenas uma comparação para descartar um elemento. A única diferença é o número de comparações para um elemento inserido é 100 para a sua solução contra até 14 para a pilha (embora o caso médio é, provavelmente, muito menos.)
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 Maiores x elementos de n:

Vou chamar o valor de retorno LIST . É um conjunto de elementos x (na minha opinião, deve ser uma lista vinculada)

  • Os primeiros x elementos são retirados do pool "quando chegam" e classificados em LIST (isso é feito em tempo constante, pois x é tratado como constante - tempo O (x log (x)))
  • Para cada elemento que vem a seguir, verificamos se ele é maior que o menor elemento em LIST e, se houver, exibimos o menor e inserimos o elemento atual em LIST. Como essa lista é ordenada, todos os elementos devem encontrar seu lugar no tempo logarítmico (pesquisa binária) e, como é ordenada, a inserção de listas não é um problema. Cada passo também é realizado em tempo constante (tempo O (log (x))).

Então, qual é o pior cenário?

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

Então esse é o tempo O (n) para o pior caso. O +1 é a verificação se o número é maior que o menor na LIST. O tempo esperado para o caso médio dependerá da distribuição matemática desses n elementos.

Possíveis melhorias

Esse algoritmo pode ser ligeiramente aprimorado para o pior cenário, mas IMHO (não posso provar essa afirmação) que degradará o comportamento médio. O comportamento assintótico será o mesmo.

A melhoria neste algoritmo será que não verificaremos se o elemento é maior que o menor. Para cada elemento, tentaremos inseri-lo e, se for menor que o menor, desconsideramos. Embora isso pareça absurdo, se considerarmos apenas o pior cenário, teremos

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

operações.

Para este caso de uso, não vejo mais melhorias. No entanto, você deve se perguntar - e se eu tiver que fazer isso mais do que log (n) vezes e para diferentes x-es? Obviamente, classificamos essa matriz em O (n log (n)) e pegamos nosso elemento x sempre que precisamos deles.


1

Esta pergunta seria respondida com complexidade de N log (100) (em vez de N log N) com apenas uma linha 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());

A resposta final seria um vetor em que os 100 primeiros elementos são garantidos como os 100 maiores números de sua matriz, enquanto os demais elementos são desordenados

O C ++ STL (biblioteca padrão) é bastante útil para esse tipo de problema.

Nota: Não estou dizendo que esta é a solução ideal, mas isso salvaria sua entrevista.


1

A solução simples seria usar uma fila prioritária, adicionar os primeiros 100 números à fila e acompanhar o menor número da fila, percorrer os outros bilhões de números e, sempre que encontrarmos um número maior que o maior número na fila de prioridade, removemos o menor número, adicionamos o novo número e, novamente, controlamos o menor número na fila.

Se os números estivessem em ordem aleatória, isso funcionaria muito bem porque, como iteramos através de um bilhão de números aleatórios, seria muito raro que o próximo número estivesse entre os 100 maiores até agora. Mas os números podem não ser aleatórios. Se a matriz já estivesse classificada em ordem crescente, sempre inseriríamos um elemento na fila de prioridade.

Então escolhemos, digamos, 100.000 números aleatórios da matriz primeiro. Para evitar o acesso aleatório que pode ser lento, adicionamos, digamos, 400 grupos aleatórios de 250 números consecutivos. Com essa seleção aleatória, podemos ter certeza de que pouquíssimos números restantes estão entre os cem primeiros; portanto, o tempo de execução será muito próximo ao de um loop simples que compara um bilhão de números a algum valor máximo.


1

É melhor encontrar os 100 melhores de um bilhão de números usando min-heap de 100 elementos.

Primeiro, prepare o min-heap com os 100 primeiros números encontrados. min-heap armazenará o menor dos 100 primeiros números na raiz (em cima).

Agora, à medida que avança o resto dos números, compare-os apenas com a raiz (o menor dos 100).

Se o novo número encontrado for maior que a raiz do min-heap, substitua a raiz por esse número, caso contrário, ignore-a.

Como parte da inserção do novo número no min-heap, o menor número no heap chegará ao topo (raiz).

Depois de passar por todos os números, teremos os 100 maiores números no min-heap.


0

Eu escrevi uma solução simples em Python, caso alguém esteja interessado. Ele usa o bisectmódulo e uma lista de retorno temporária, que é mantida classificada. Isso é semelhante a uma implementação de fila prioritária.

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 com 100.000.000 de elementos e entrada na pior das hipóteses, que é uma lista classificada:

>>> 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]

Demorou cerca de 40 segundos para calcular isso para 100.000.000 de elementos, então estou com medo de fazer isso por 1 bilhão. Para ser justo, porém, eu estava alimentando a entrada do pior caso (ironicamente, uma matriz que já está classificada).


0

Eu vejo muitas discussões de O (N), então proponho algo diferente apenas para o exercício de pensamento.

Existe alguma informação conhecida sobre a natureza desses números? Se for de natureza aleatória, não vá mais longe e veja as outras respostas. Você não obterá melhores resultados do que eles.

Contudo! Veja se qualquer mecanismo de preenchimento de lista preencheu essa lista em uma ordem específica. Eles estão em um padrão bem definido, onde você pode saber com certeza que a maior magnitude de números será encontrada em uma determinada região da lista ou em um determinado intervalo? Pode haver um padrão para isso. Se é assim, por exemplo, se eles garantem que estão em algum tipo de distribuição normal com a característica hump no meio, sempre apresentam tendências ascendentes repetidas entre subconjuntos definidos, têm um pico prolongado em algum momento T no meio dos dados definido como talvez uma incidência de abuso de informações privilegiadas ou falha de equipamento, ou talvez apenas tenha um "pico" a cada enésimo número, como na análise de forças após uma catástrofe, você pode reduzir o número de registros que precisa verificar significativamente.

De qualquer maneira, há algum pensamento para pensar. Talvez isso ajude você a dar uma resposta ponderada aos futuros entrevistadores. Sei que ficaria impressionado se alguém me fizesse essa pergunta em resposta a um problema como esse - isso me diria que eles estão pensando em otimização. Apenas reconheça que nem sempre existe a possibilidade de otimizar.


0
Time ~ O(100 * N)
Space ~ O(100 + N)
  1. Crie uma lista vazia de 100 slots vazios

  2. Para cada número na lista de entrada:

    • Se o número for menor que o primeiro, pule

    • Caso contrário, substitua-o por este número

    • Em seguida, empurre o número pelo swap adjacente; até que seja menor que o próximo

  3. Retornar a lista


Nota: se o log(input-list.size) + c < 100, a melhor maneira é classificar a lista de entrada e, em seguida, divida os 100 primeiros itens.


0

A complexidade é O (N)

Primeiro, crie uma matriz de 100 ints, inicialize o primeiro elemento dessa matriz como o primeiro elemento dos valores N, acompanhe o índice do elemento atual com outra variável, chame-o de CurrentBig

Repita os valores 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)

} 

quando terminar, imprima a matriz M do CurrentBig 100 vezes o módulo 100 :-) Para o aluno: verifique se a última linha do código não supera os dados válidos logo antes do código sair


0

Outro algoritmo O (n) -

O algoritmo encontra os 100 maiores por eliminação

considere todos os milhões de números em sua representação binária. Comece do bit mais significativo. Encontrar se o MSB é 1 pode ser feito por uma multiplicação de operações booleanas com um número apropriado. Se houver mais de 100 1 nesse milhão, elimine os outros números com zeros. Agora, os números restantes prosseguem com o próximo bit mais significativo. mantenha uma contagem do número de números restantes após a eliminação e continue enquanto esse número for maior que 100.

A principal operação booleana pode ser realizada paralelamente nas GPUs


0

Eu descobriria quem tinha tempo para colocar um bilhão de números em uma matriz e o demitiria. Deve trabalhar para o governo. Pelo menos, se você tivesse uma lista vinculada, poderia inserir um número no meio sem mover meio bilhão para abrir espaço. Ainda melhor, um Btree permite uma pesquisa binária. Cada comparação elimina metade do seu total. Um algoritmo de hash permitiria preencher a estrutura de dados como um tabuleiro de damas, mas não tão bom para dados esparsos. Como sua melhor aposta é ter uma matriz de soluções com 100 números inteiros e acompanhar o número mais baixo na matriz de soluções, para que você possa substituí-lo quando encontrar um número maior na matriz original. Você precisaria examinar todos os elementos da matriz original, desde que não estejam ordenados para começar.


0

Você pode fazer isso a O(n)tempo. Basta percorrer a lista e acompanhar os 100 maiores números que você já viu em um determinado momento e o valor mínimo nesse grupo. Quando você encontrar um novo número maior, o menor dos seus dez, substitua-o e atualize seu novo valor mínimo de 100 (pode levar um tempo constante de 100 para determinar isso cada vez que você o faz, mas isso não afeta a análise geral )


1
Essa abordagem é quase idêntica às respostas mais e segunda mais votadas para esta pergunta.
Bernhard Barker

0

Gerenciar uma lista separada é um trabalho extra e você precisa mover as coisas pela lista toda vez que encontrar outra substituição. Basta classificá-lo e pegar o top 100.


-1 quicksort é O (n log n), que é exatamente o que o OP fez e está pedindo para melhorar. Você não precisa gerenciar uma lista separada, apenas uma lista de 100 números. Sua sugestão também tem o efeito colateral indesejado de alterar a lista original ou copiá-la. Isso é mais ou menos 4GiB de memória.

0
  1. Use o enésimo elemento para obter o 100º enésimo elemento O (n)
  2. Iterar pela segunda vez, mas apenas uma vez, e gerar todos os elementos maiores que esse elemento específico.

Por favor, note esp. o segundo passo pode ser fácil de calcular em paralelo! E também será eficiente quando você precisar de um milhão de elementos maiores.


0

É uma pergunta do Google ou de outros gigantes da indústria. Talvez o código a seguir seja a resposta certa esperada pelo seu entrevistador. O custo de tempo e de espaço depende do número máximo na matriz de entrada.

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;
                        }
                    }
                }
            }
        }
    }
}

0

eu fiz meu próprio código, não tenho certeza se é o que o "entrevistador" está procurando

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]);

            }

        }

0

Possíveis melhorias.

Se o arquivo contiver 1 bilhão de números, a leitura poderá ser muito longa ...

Para melhorar esse trabalho, você pode:

  • Divida o arquivo em n partes, Crie n threads, faça com que n procure cada um dos 100 maiores números em sua parte do arquivo (usando a fila de prioridade) e, finalmente, obtenha os 100 maiores números de todos os threads.
  • Use um cluster para executar essa tarefa, com uma solução como o hadoop. Aqui você pode dividir o arquivo ainda mais e obter a saída mais rapidamente para um arquivo de números de 1 bilhão (ou 10 ^ 12).

0

Primeiro, pegue 1000 elementos e adicione-os em um heap máximo. Agora retire os primeiros 100 elementos no máximo e guarde-os em algum lugar. Agora escolha os próximos 900 elementos do arquivo e adicione-os ao heap junto com os últimos 100 elementos mais altos.

Continue repetindo esse processo de pegar 100 elementos da pilha e adicionar 900 elementos do arquivo.

A escolha final de 100 elementos nos dará o máximo de 100 elementos de um bilhão de números.


-1

Problema: Encontre m maiores elementos de n itens em que n >>> m

A solução mais simples, que deve ser óbvia para todos, é simplesmente fazer m passes do algoritmo de classificação por bolhas.

depois imprima os últimos n elementos da matriz.

Isso não requer estruturas de dados externas e usa um algoritmo que todos conhecem.

A estimativa do tempo de execução é O (m * n). As melhores respostas até agora são O (n log (m)), portanto, esta solução não é significativamente mais cara para m pequenos.

Não estou dizendo que isso não poderia ser melhorado, mas essa é de longe a solução mais simples.


1
Nenhuma estrutura de dados externa? E quanto à matriz de bilhões de números para classificar? Uma matriz desse tamanho representa uma sobrecarga enorme, tanto no tempo para preencher quanto no espaço para armazenar. E se todos os números "grandes" estivessem no extremo errado da matriz? Você precisaria da ordem de 100 bilhões de swaps para "colocá-los em bolha" - outra grande sobrecarga ... Finalmente, M N = 100 bilhões vs M Log2 (N) = 6,64 bilhões, o que representa quase duas ordens de diferença de magnitude. Talvez repensar este. Uma varredura de uma passagem, mantendo uma estrutura de dados dos maiores números, executará significativamente essa abordagem.
NealB
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.