Scrivi un programma per trovare 100 numeri più grandi in un array di 1 miliardo di numeri


300

Di recente ho partecipato a un'intervista in cui mi è stato chiesto di "scrivere un programma per trovare 100 numeri più grandi in un array di 1 miliardo di numeri".

Sono stato solo in grado di fornire una soluzione di forza bruta che era quella di ordinare l'array nella complessità temporale O (nlogn) e prendere gli ultimi 100 numeri.

Arrays.sort(array);

L'intervistatore era alla ricerca di una migliore complessità temporale, ho provato un paio di altre soluzioni ma non gli ho risposto. Esiste una soluzione di complessità temporale migliore?


70
Forse il problema è che non era una domanda di smistamento , ma una ricerca .
geomagas,

11
Come nota tecnica, l'ordinamento potrebbe non essere il modo migliore per risolvere il problema, ma non penso che sia una forza bruta - posso pensare a modi molto peggiori di farlo.
Bernhard Barker,

88
Ho appena pensato a un metodo di forza bruta ancora più stupido ... Trova tutte le possibili combinazioni di 100 elementi dall'array da 1 miliardo di elementi e vedi quale di queste combinazioni ha la somma più grande.
Shashank,

10
Si noti che tutti gli algoritmi deterministici (e corretti) sono O(1)in questo caso, poiché non vi è alcun aumento della dimensione. L'intervistatore avrebbe dovuto chiedere "Come trovare m elementi più grandi da una matrice di n con n >> m?".
Bakuriu,

Risposte:


328

È possibile mantenere una coda prioritaria dei 100 numeri più grandi, scorrere i miliardi di numeri, ogni volta che si incontra un numero maggiore del numero più piccolo nella coda (il capo della coda), rimuovere il capo della coda e aggiungere il nuovo numero alla coda.

EDIT: come ha notato Dev, con una coda prioritaria implementata con un heap, la complessità dell'inserimento nella coda èO(logN)

Nel peggiore dei casi ottieni quale è meglio dibillionlog2(100)billionlog2(billion)

In generale, se hai bisogno dei più grandi numeri K da un insieme di N numeri, la complessità è O(NlogK)piuttosto che O(NlogN), questo può essere molto significativo quando K è molto piccolo rispetto a N.

EDIT2:

Il tempo previsto di questo algoritmo è piuttosto interessante, poiché in ogni iterazione può verificarsi o meno un inserimento. La probabilità che l'i-esimo numero venga inserito nella coda è la probabilità che una variabile casuale sia maggiore di almeno i-Kle variabili casuali della stessa distribuzione (i primi k numeri vengono automaticamente aggiunti alla coda). Possiamo usare le statistiche degli ordini (vedi link ) per calcolare questa probabilità. Ad esempio, supponiamo che i numeri siano stati scelti casualmente in modo uniforme da {0, 1}, il valore atteso del (iK) th numero (fuori dai numeri i) è (i-k)/i, e la probabilità che una variabile casuale sia maggiore di questo valore è 1-[(i-k)/i] = k/i.

Pertanto, il numero previsto di inserzioni è:

inserisci qui la descrizione dell'immagine

E il tempo di esecuzione previsto può essere espresso come:

inserisci qui la descrizione dell'immagine

(k tempo per generare la coda con i primi kelementi, quindi i n-kconfronti e il numero previsto di inserimenti come descritto sopra, ciascuno richiede un log(k)/2tempo medio )

Si noti che quando Nè molto grande rispetto a K, questa espressione è molto più vicina a npiuttosto che NlogK. Questo è in qualche modo intuitivo, come nel caso della domanda, anche dopo 10000 iterazioni (che è molto piccola rispetto a un miliardo), la possibilità che un numero venga inserito nella coda è molto piccola.


6
In realtà è solo O (100) per ciascun inserto.
MrSmith42,

8
@RonTeller Non è possibile cercare in modo binario un elenco collegato in modo efficiente, per questo motivo una coda di priorità viene di solito implementata con un heap. Il tempo di inserimento come descritto è O (n) non O (logn). Hai fatto bene la prima volta (coda ordinata o coda prioritaria) fino a quando Skizz non ti ha fatto indovinare da solo.
Dev

17
@ThomasJungblut miliardi è anche una costante, quindi se è così è O (1): P
Ron Teller

9
@RonTeller: normalmente questo tipo di domande riguarda come trovare 10 prime pagine da miliardi di risultati di ricerca di Google, o 50 parole più frequenti per una nuvola di parole, o 10 canzoni più popolari su MTV, ecc. Quindi, credo, in circostanze normali è sicuro considerare k costante e piccolo rispetto a n. Tuttavia, si dovrebbe sempre tenere presente queste "circostanze normali".
amico

5
Dato che hai oggetti 1G, campiona 1000 elementi in modo casuale e scegli i 100 più grandi. Ciò dovrebbe evitare i casi degeneri (ordinati, in ordine inverso, per lo più ordinati), riducendo notevolmente il numero di inserti.
ChuckCottrill

136

Se questo viene richiesto in un'intervista, penso che l'intervistatore probabilmente voglia vedere il tuo processo di risoluzione dei problemi, non solo la tua conoscenza degli algoritmi.

La descrizione è abbastanza generale, quindi forse puoi chiedergli l'intervallo o il significato di questi numeri per chiarire il problema. Ciò potrebbe impressionare un intervistatore. Se, ad esempio, questi numeri rappresentano l'età delle persone all'interno di un paese (ad esempio la Cina), allora è un problema molto più semplice. Partendo dal presupposto ragionevole che nessuno dei vivi abbia più di 200 anni, è possibile utilizzare un array int di dimensioni 200 (forse 201) per contare il numero di persone della stessa età in una sola iterazione. Qui l'indice indica l'età. Dopo questo è un gioco da ragazzi trovare il numero 100 più grande. A proposito questo algo è chiamato ordinamento di conteggio .

Ad ogni modo, rendere la domanda più specifica e più chiara è utile per te in un'intervista.


26
Punti molto buoni. Nessun altro ha chiesto o indicato nulla sulla distribuzione di quei numeri - potrebbe fare la differenza nel modo di affrontare il problema.
NealB

13
Vorrei questa risposta abbastanza per estenderla. Leggi i numeri una volta per ottenere i valori min / max in modo da poter assumere la distribuzione. Quindi, prendi una delle due opzioni. Se l'intervallo è abbastanza piccolo, crea un array in cui puoi semplicemente spuntare i numeri quando si verificano. Se l'intervallo è troppo ampio, utilizzare l'algoritmo heap ordinato discusso sopra .... Solo un pensiero.
Richard_G,

2
Sono d'accordo, fare una domanda all'intervistatore fa davvero molta differenza. In effetti, una domanda come quella che è limitata dalla potenza di calcolo o meno può anche aiutarti a parallelizzare la soluzione utilizzando più nodi di calcolo.
Sumit Nigam,

1
@R_G Non è necessario scorrere l'intero elenco. Abbastanza per campionare una piccola frazione (ad esempio, un milione) di membri casuali dell'elenco per ottenere statistiche utili.
Itamar,

Per coloro che non avrebbero pensato a quella soluzione, consiglierei di leggere l'ordinamento del conteggio en.wikipedia.org/wiki/Counting_sort . Questa è in realtà una domanda piuttosto comune per l'intervista: puoi ordinare un array in meglio di O (nlogn). Questa domanda è solo un'estensione.
Maxime Chéramy,

69

Puoi scorrere i numeri che prendono O (n)

Ogni volta che trovi un valore maggiore del minimo corrente, aggiungi il nuovo valore a una coda circolare con dimensione 100.

Il minimo di quella coda circolare è il tuo nuovo valore di confronto. Continua ad aggiungere a quella coda. Se pieno, estrarre il minimo dalla coda.


3
Questo non funziona es. trovare i primi 2 di {1, 100, 2, 99} daranno {100,1} come i primi 2.
Skizz

7
Non puoi andare in giro per tenere ordinata la coda. (se non vuoi cercare nella coda delle buche ogni volta il prossimo elemento più piccolo)
MrSmith42

3
@ MrSmith42 L'ordinamento parziale, come in un heap, è sufficiente. Vedi la risposta di Ron Teller.
Christopher Creutzig,

1
Sì, ho supposto in silenzio che una coda extract-min-sia implementata come un heap.
Regenschein,

Invece di una coda circolare usa un mucchio minimo di dimensioni 100, questo avrà almeno un numero di cento in alto. Questo richiederà solo O (log n) per l'inserimento rispetto a o (n) in caso di coda
techExplorer

33

Mi sono reso conto che questo è etichettato con "algoritmo", ma eliminerà alcune altre opzioni, dal momento che probabilmente dovrebbe anche essere etichettato "intervista".

Qual è la fonte di 1 miliardo di numeri? Se si tratta di un database, 'selezionare il valore dall'ordine della tabella in base al valore decrescente limite 100' farebbe il lavoro abbastanza bene - potrebbero esserci differenze dialettali.

È una tantum o qualcosa che verrà ripetuto? Se ripetuto, con quale frequenza? Se è una tantum e i dati sono in un file, allora 'cat srcfile | ordina (opzioni secondo necessità) | head -100 "ti consentirà di svolgere rapidamente un lavoro produttivo che ti verrà pagato mentre il computer gestisce questa banale faccenda.

Se viene ripetuto, consiglieresti di scegliere un approccio decente per ottenere la risposta iniziale e archiviare / memorizzare nella cache i risultati in modo da poter continuamente segnalare i primi 100.

Infine, c'è questa considerazione. Sei alla ricerca di un lavoro entry level e stai intervistando un manager geek o un futuro collaboratore? Se è così, allora puoi lanciare tutti i tipi di approcci che descrivono i relativi pro e contro tecnici. Se stai cercando un lavoro più manageriale, affrontalo come farebbe un manager, preoccupato per i costi di sviluppo e manutenzione della soluzione, e dì "grazie mille" e parti se l'intervistatore vuole concentrarsi sulle curiosità di CS . È improbabile che lui e te abbiano un grande potenziale di avanzamento lì.

Buona fortuna per la prossima intervista.


2
Risposta eccezionale. Tutti gli altri si sono concentrati sul lato tecnico della domanda, mentre questa risposta affronta la parte sociale aziendale di essa.
Vbocan,

2
Non avrei mai immaginato che potessi dire grazie e lasciare un'intervista e non aspettare che finisca. Grazie per avermi aperto la mente.
UrsulRosu,

1
Perché non possiamo creare un mucchio di miliardi di elementi ed estrarre 100 elementi più grandi. In questo modo costo = O (miliardi) + 100 * O (log (miliardi)) ??
Mohit Shah,

17

La mia reazione immediata a questo sarebbe quella di utilizzare un heap, ma c'è modo di usare QuickSelect senza tenere tutti i valori di input disponibili in qualsiasi momento.

Crea un array di dimensioni 200 e riempilo con i primi 200 valori di input. Esegui QuickSelect e scarta i 100 bassi, lasciandoti con 100 posti liberi. Leggi i prossimi 100 valori di input ed esegui nuovamente QuickSelect. Continua fino a quando non hai eseguito l'intero input in batch di 100.

Alla fine hai i primi 100 valori. Per N valori hai eseguito QuickSelect circa N / 100 volte. Ogni Quickselect costa circa 200 volte una costante, quindi il costo totale è 2N volte una costante. Questo mi sembra lineare nella dimensione dell'input, indipendentemente dalla dimensione del parametro che sto cablando per essere 100 in questa spiegazione.


10
È possibile aggiungere un'ottimizzazione piccola ma forse importante: dopo aver eseguito QuickSelect per partizionare l'array di dimensioni 200, è noto il minimo dei primi 100 elementi. Quindi, durante l'iterazione dell'intero set di dati, riempire i 100 valori inferiori solo se il valore corrente è maggiore del minimo corrente. Una semplice implementazione di questo algoritmo in C ++ è alla pari con l'esecuzione di libstdc ++ partial_sortdirettamente su un set di dati di 200 milioni a 32 bit int(creato tramite un MT19937, distribuito uniformemente).
dyp,

1
Buona idea - non influisce sull'analisi del caso peggiore ma sembra che valga la pena farlo.
mcdowella,

@mcdowella Vale la pena provare e lo farò, grazie!
userx,

8
Questo è esattamente ciò che fa Guava Ordering.greatestOf(Iterable, int) . È un tempo assolutamente lineare e single-pass, ed è un algoritmo super carino. FWIW, abbiamo anche alcuni parametri di riferimento reali: i suoi fattori costanti sono un pelo più lenti rispetto alla tradizionale coda di priorità nel caso medio, ma questa implementazione è molto più resistente all'input del "caso peggiore" (ad esempio input strettamente ascendente).
Louis Wasserman,

15

È possibile utilizzare l' algoritmo di selezione rapida per trovare il numero nell'indice (per ordine) [miliardi-101] e quindi scorrere i numeri e trovare i numeri più grandi di quel numero.

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

Questo algoritmo Il tempo è: 2 XO (N) = O (N) (Prestazione media del caso)

La seconda opzione come suggerisce Thomas Jungblut è:

Utilizzare Heap per costruire l'heap MAX richiederà O (N), quindi i primi 100 numeri massimi si troveranno nella parte superiore dell'heap, tutto ciò che serve è estrarli dall'heap (100 XO (Log (N)).

Questo algoritmo Il tempo è: O (N) + 100 XO (Log (N)) = O (N)


8
Stai lavorando per l'intero elenco tre volte. 1 bio. i numeri interi sono all'incirca 4 GB, cosa faresti se non li inserissi nella memoria? quickselect è la scelta peggiore possibile in questo caso. Iterare una volta e mantenere un mucchio dei primi 100 elementi è IMHO la soluzione più performante in O (n) (nota che puoi tagliare O (log n) degli inserti di heap poiché n nell'heap è 100 = costante = molto piccolo ).
Thomas Jungblut,

3
Anche se è ancora O(N), fare due QuickSelect e un'altra scansione lineare è molto più sovraccarico del necessario.
Kevin,

Questo è il codice PSEUDO che tutte le soluzioni qui impiegheranno più tempo (O (NLOG (N) o 100 * O (N))
One Man Crew

1
100*O(N)(se questa è sintassi valida) = O(100*N)= O(N)(è vero che 100 può essere variabile, in tal caso, ciò non è strettamente vero). Oh, e Quickselect ha prestazioni nel peggiore dei casi di O (N ^ 2) (ahi). E se non si adatta alla memoria, ricaricherai i dati dal disco due volte, il che è molto peggio di una volta (questo è il collo di bottiglia).
Bernhard Barker,

C'è il problema che questo è previsto tempo di esecuzione, e non il caso peggiore, ma utilizzando una strategia di selezione pivot decente (ad esempio, selezionare 21 elementi a caso e scegliere la mediana di quei 21 come pivot), quindi il numero di confronti può essere garantito con alta probabilità di essere al massimo (2 + c) n per una costante arbitrariamente piccola c.
One Man Crew

10

Sebbene l'altra soluzione di selezione rapida sia stata sottoposta a downgrade, resta il fatto che quickselect troverà la soluzione più velocemente rispetto all'utilizzo di una coda di dimensioni 100. Quickselect ha un tempo di esecuzione previsto di 2n + o (n), in termini di confronti. Un'implementazione molto semplice sarebbe

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

Ciò richiederà in media 3n + o (n) confronti. Inoltre, può essere reso più efficiente grazie al fatto che la selezione rapida lascerà i 100 elementi più grandi dell'array nelle 100 posizioni più a destra. Quindi, in effetti, il tempo di esecuzione può essere migliorato a 2n + o (n).

C'è il problema che questo è previsto tempo di esecuzione, e non il caso peggiore, ma utilizzando una strategia di selezione pivot decente (ad esempio, selezionare 21 elementi a caso e scegliere la mediana di quei 21 come pivot), quindi il numero di confronti può essere garantito con alta probabilità di essere al massimo (2 + c) n per una costante arbitrariamente piccola c.

Infatti, utilizzando una strategia di campionamento ottimizzata (ad esempio campionando sqrt (n) elementi a caso e scegliendo il 99 ° percentile), il tempo di esecuzione può essere ridotto a (1 + c) n + o (n) per arbitrariamente piccoli c (supponendo che K, il numero di elementi da selezionare sia o (n)).

D'altra parte, l'utilizzo di una coda di dimensioni 100 richiederà confronti O (log (100) n) e la base di log 2 di 100 è approssimativamente uguale a 6,6.

Se pensiamo a questo problema nel senso più astratto della scelta dei più grandi elementi K da una matrice di dimensioni N, dove K = o (N) ma sia K che N vanno all'infinito, il tempo di esecuzione della versione a selezione rapida sarà O (N) e la versione della coda sarà O (N log K), quindi in questo senso la selezione rapida è anche asintoticamente superiore.

Nei commenti, è stato menzionato che la soluzione della coda verrà eseguita nel tempo previsto N + K log N su un input casuale. Naturalmente, il presupposto dell'input casuale non è mai valido a meno che la domanda non lo affermi esplicitamente. La soluzione in coda potrebbe essere fatta per attraversare l'array in un ordine casuale, ma ciò comporterà il costo aggiuntivo di N chiamate a un generatore di numeri casuali, nonché permutare l'intero array di input oppure allocare un nuovo array di lunghezza N contenente il indici casuali.

Se il problema non ti consente di spostarti tra gli elementi dell'array originale e il costo dell'allocazione della memoria è elevato, quindi duplicare l'array non è un'opzione, è una questione diversa. Ma rigorosamente in termini di tempo di esecuzione, questa è la soluzione migliore.


4
Il tuo ultimo paragrafo è il punto chiave: con un miliardo di numeri, non è possibile conservare tutti i dati in memoria o scambiare elementi. (Almeno è così che interpreterei il problema, dato che si trattava di un'intervista.)
Ted Hopp,

14
In qualsiasi domanda algoritmica, se la lettura dei dati è un problema, devono essere menzionati nella domanda. La domanda afferma "dato un array" non "dato un array su disco che non si adatta alla memoria e non può essere manipolato secondo il modello von neuman che è lo standard nell'analisi degli algoritmi". In questi giorni è possibile ottenere un laptop con 8 grammi di ram. Non sono sicuro da dove l'idea di contenere un miliardo di numeri in memoria non sia fattibile. Ho diversi miliardi di numeri in memoria sulla mia workstation in questo momento.
MRIP

FYI Il tempo di esecuzione peggiore di quickselect è O (n ^ 2) (consultare en.wikipedia.org/wiki/Quickselect ) e modifica anche l'ordine degli elementi nell'array di input. È possibile avere una soluzione O (n) nel peggiore dei casi, con una costante molto grande ( en.wikipedia.org/wiki/Median_of_medians ).
Punti

È improbabile che si verifichi esponenzialmente il caso peggiore di selezione rapida, il che significa che ai fini pratici ciò è irrilevante. È facile modificare la selezione rapida in modo che con alta probabilità il numero di confronti sia (2 + c) n + o (n) per arbitrariamente piccolo c.
MRIP

"Resta il fatto che Quickselect troverà la soluzione più velocemente rispetto all'utilizzo di una coda di dimensioni 100" - No. La soluzione heap richiede circa N + Klog (N) confronti rispetto alla media 2N per la selezione rapida e 2,95 per la mediana delle mediane. È chiaramente più veloce per il dato K.
Neil G

5

prendi i primi 100 numeri del miliardo e ordinali. ora basta scorrere il miliardo, se il numero sorgente è superiore al più piccolo di 100, inserire nell'ordinamento. Ciò che si ottiene è qualcosa di molto più vicino a O (n) rispetto alle dimensioni dell'insieme.


3
oops non ha visto la risposta più dettagliata della mia.
Samuel Thurston,

Prendi i primi circa 500 numeri e fermati a ordinare (e buttare via i 400 bassi) quando l'elenco si riempie. (Va da sé che si aggiunge all'elenco solo se il nuovo numero è> il più basso tra i 100 selezionati.)
Hot Licks

4

Due opzioni:

(1) Heap (priorityue)

Mantieni un heap min con dimensioni di 100. Attraversa l'array. Una volta che l'elemento è più piccolo del primo elemento nell'heap, sostituirlo.

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) Modello di riduzione della mappa.

Questo è molto simile all'esempio del conteggio delle parole in hadoop. Lavoro mappa: conta la frequenza o i tempi di ogni elemento visualizzati. Riduci: ottieni l'elemento K superiore.

Di solito, darei al recruiter due risposte. Dai loro quello che vogliono. Naturalmente, la riduzione della codifica della mappa sarebbe una manodopera, poiché è necessario conoscere tutti i parametri esatti. Nessun danno per praticarlo. In bocca al lupo.


+1 per MapReduce, non posso credere che tu sia stato l'unico a menzionare Hadoop per un miliardo di numeri. E se l'intervistatore chiedesse 1k miliardi di numeri? Mi meriti più voti positivi secondo me.
Silviu Burcea,

@Silviu Burcea Grazie mille. Apprezzo anche MapReduce. :)
Chris Su,

Sebbene la dimensione di 100 sia costante in questo esempio, dovresti davvero generalizzarla in una variabile separata, ad es. K. Dato che 100 è costante come 1 miliardo, allora perché stai dando alla dimensione del grande set di numeri una dimensione variabile di n, e non per il più piccolo set di numeri? In realtà la tua complessità dovrebbe essere O (nlogk) che non è O (n).
Tom Heard,

1
Ma il mio punto è che se stai solo rispondendo alla domanda, 1 miliardo è anche fisso nella domanda, quindi perché generalizzare 1 miliardo a ne 100 non k. Seguendo la tua logica, la complessità dovrebbe effettivamente essere O (1) perché sia ​​1 miliardo che 100 sono fissi in questa domanda.
Tom Heard,

1
@TomHeard Va bene. O (nlogk) C'è solo un fattore che influenzerà i risultati. Ciò significa che se n sta diventando sempre più grande, il "livello di risultato" aumenterà in modo lineare. Oppure possiamo dire, anche se dati trilioni di numeri, posso ancora ottenere 100 numeri più grandi. Tuttavia, non puoi dire: con l'aumentare di n, k aumenta e quindi k influirà sul risultato. Ecco perché uso O (nlogk) ma non O (nlogn)
Chris Su

4

Una soluzione molto semplice sarebbe quella di scorrere 100 volte l'array. Quale è O(n).

Ogni volta che estrai il numero più grande (e ne cambi il valore al valore minimo, in modo da non vederlo nella prossima iterazione o tieni traccia degli indici delle risposte precedenti (tenendo traccia degli indici l'array originale può avere multiplo dello stesso numero)). Dopo 100 iterazioni, hai i 100 numeri più grandi.


1
Due svantaggi - (1) Stai distruggendo l'input nel processo - questo è preferibilmente evitato. (2) Stai attraversando l'array più volte - se l'array è archiviato su disco e non può adattarsi alla memoria, questo potrebbe facilmente essere quasi 100 volte più lento della risposta accettata. (Sì, sono entrambi O (n), ma comunque)
Bernhard Barker,

Buona chiamata @Dukeling, ho aggiunto una formulazione aggiuntiva su come evitare di modificare l'input originale tenendo traccia degli indici di risposta precedenti. Che sarebbe ancora abbastanza facile da codificare.
James Oravec,

Un brillante esempio di una soluzione O (n) che è molto più lenta di O (n log n). log2 (1 miliardo) è solo 30 ...
gnasher729

@ gnasher729 Quanto è grande la costante nascosta in O (n log n)?
miracolo173,

1

Ispirato dalla risposta di @ron teller, ecco un programma barebone C per fare ciò che vuoi.

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

Sulla mia macchina (core i3 con un SSD veloce) ci vogliono 25 secondi e 1724 tipi. Ho generato un file binario con dd if=/dev/urandom/ count=1000000000 bs=1per questa corsa.

Ovviamente, ci sono problemi di prestazioni con la lettura di soli 4 byte alla volta - dal disco, ma questo è per esempio. Tra i lati positivi, è necessaria pochissima memoria.


1

La soluzione più semplice è scansionare l'array di miliardi di numeri e contenere i 100 valori più grandi trovati finora in un buffer di array piccolo senza alcun ordinamento e ricordare il valore più piccolo di questo buffer. Per prima cosa ho pensato che questo metodo fosse stato proposto da fordprefect ma in un commento ha affermato di supporre che la struttura di dati con 100 numeri fosse implementata come un heap. Ogni volta che viene trovato un nuovo numero più grande, il minimo nel buffer viene sovrascritto dal nuovo valore trovato e il buffer viene nuovamente ricercato per il minimo corrente. Se i numeri in miliardi di array di numeri vengono distribuiti casualmente per la maggior parte del tempo, il valore dell'array grande viene confrontato con il minimo dell'array piccolo e scartato. Solo per una frazione molto piccola di numero il valore deve essere inserito nella matrice piccola. Quindi la differenza di manipolare la struttura dei dati che contiene i numeri piccoli può essere trascurata. Per un numero limitato di elementi è difficile determinare se l'utilizzo di una coda prioritaria è effettivamente più veloce rispetto al mio approccio ingenuo.

Voglio stimare il numero di inserimenti nel piccolo buffer dell'array di 100 elementi quando viene acquisito l'array di elementi 10 ^ 9. Il programma analizza i primi 1000 elementi di questo array di grandi dimensioni e deve inserire al massimo 1000 elementi nel buffer. Il buffer contiene 100 elementi dei 1000 elementi scansionati, ovvero 0,1 degli elementi scansionati. Quindi supponiamo che la probabilità che un valore dall'array di grandi dimensioni sia maggiore del minimo corrente del buffer sia di circa 0,1 Tale elemento deve essere inserito nel buffer. Ora il programma analizza i successivi 10 ^ 4 elementi dall'array di grandi dimensioni. Perché il minimo del buffer aumenta ogni volta che viene inserito un nuovo elemento. Abbiamo stimato che il rapporto tra gli elementi più grandi del nostro minimo attuale è di circa 0,1 e quindi ci sono 0,1 * 10 ^ 4 = 1000 elementi da inserire. In realtà il numero previsto di elementi che vengono inseriti nel buffer sarà inferiore. Dopo la scansione di questo 10 ^ 4 elementi la frazione dei numeri nel buffer sarà circa 0,01 degli elementi scansionati finora. Pertanto, durante la scansione dei prossimi 10 ^ 5 numeri assumiamo che nel buffer non vengano inseriti più di 0,01 * 10 ^ 5 = 1000. Continuando questa argomentazione abbiamo inserito circa 7000 valori dopo la scansione di 1000 + 10 ^ 4 + 10 ^ 5 + ... + 10 ^ 9 ~ 10 ^ 9 dell'array di grandi dimensioni. Pertanto, quando si esegue la scansione di un array con 10 ^ 9 elementi di dimensioni casuali, ci aspettiamo non più di 10 ^ 4 (= 7000 arrotondati per eccesso) inserimenti nel buffer. Dopo ogni inserimento nel buffer, è necessario trovare il nuovo minimo. Se il buffer è un array semplice, abbiamo bisogno di 100 confronti per trovare il nuovo minimo. Se il buffer è un'altra struttura di dati (come un heap) abbiamo bisogno di almeno 1 confronto per trovare il minimo. Per confrontare gli elementi dell'array di grandi dimensioni abbiamo bisogno di confronti 10 ^ 9. Quindi tutto sommato abbiamo bisogno di circa 10 ^ 9 + 100 * 10 ^ 4 = 1.001 * 10 ^ 9 confronti quando si utilizza un array come buffer e almeno 1.000 * 10 ^ 9 confronti quando si utilizza un altro tipo di struttura dati (come un heap) . Pertanto, l'utilizzo di un heap comporta solo un guadagno dello 0,1% se le prestazioni sono determinate dal numero di confronto. Ma qual è la differenza nel tempo di esecuzione tra l'inserimento di un elemento in un heap di 100 elementi e la sostituzione di un elemento in un array di 100 elementi e la ricerca del suo nuovo minimo? 000 * 10 ^ 9 confronti quando si utilizza un altro tipo di struttura dati (come un heap). Pertanto, l'utilizzo di un heap comporta solo un guadagno dello 0,1% se le prestazioni sono determinate dal numero di confronto. Ma qual è la differenza nel tempo di esecuzione tra l'inserimento di un elemento in un heap di 100 elementi e la sostituzione di un elemento in un array di 100 elementi e la ricerca del suo nuovo minimo? 000 * 10 ^ 9 confronti quando si utilizza un altro tipo di struttura dati (come un heap). Pertanto, l'utilizzo di un heap comporta solo un guadagno dello 0,1% se le prestazioni sono determinate dal numero di confronto. Ma qual è la differenza nel tempo di esecuzione tra l'inserimento di un elemento in un heap di 100 elementi e la sostituzione di un elemento in un array di 100 elementi e il raggiungimento del suo nuovo minimo?

  • A livello teorico: quanti confronti sono necessari per l'inserimento in un heap. So che è O (log (n)) ma quanto è grande il fattore costante? io

  • A livello di macchina: qual è l'impatto della memorizzazione nella cache e della previsione del ramo sul tempo di esecuzione di un inserto heap e una ricerca lineare in un array.

  • A livello di implementazione: quali costi aggiuntivi sono nascosti in una struttura di dati heap fornita da una libreria o un compilatore?

Penso che queste siano alcune delle domande a cui bisogna rispondere prima di poter provare a stimare la vera differenza tra le prestazioni di un heap di 100 elementi o un array di 100 elementi. Quindi avrebbe senso fare un esperimento e misurare le prestazioni reali.


1
Questo è ciò che fa un mucchio.
Neil G

@Neil G: Cosa "quello"?
miracle173

1
La parte superiore dell'heap è l'elemento minimo nell'heap e i nuovi elementi vengono rifiutati con un confronto.
Neil G

1
Capisco quello che stai dicendo, ma anche se segui un numero assoluto di confronti anziché un numero asintotico di confronti, l'array è ancora molto più lento perché il tempo di "inserire un nuovo elemento, scartare il minimo minimo e trovare un nuovo minimo" è 100 anziché circa 7.
Neil G

1
Va bene, ma la tua stima è molto rotonda. È possibile calcolare direttamente il numero previsto di inserti in k (digamma (n) - digamma (k)), che è inferiore a klog (n). In ogni caso, sia la soluzione heap che quella array impiegano un solo confronto per scartare un elemento. L'unica differenza è che il numero di confronti per un elemento inserito è 100 per la soluzione contro fino a 14 per l'heap (anche se il caso medio è probabilmente molto inferiore.)
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 Più grande x elementi da n:

Chiamerò il valore di ritorno ELENCO . È un insieme di x elementi (secondo me dovrebbe essere un elenco di link)

  • I primi elementi x vengono presi dal pool "come vengono" e ordinati in ELENCO (ciò avviene in tempo costante poiché x viene trattato come costante - O (x log (x)) tempo)
  • Per ogni elemento che viene dopo controlliamo se è più grande dell'elemento più piccolo in ELENCO e se lo facciamo estraiamo il più piccolo e inseriamo l'elemento corrente in ELENCO. Poiché quello è un elenco ordinato, ogni elemento dovrebbe trovare il suo posto nel tempo logaritmico (ricerca binaria) e poiché è ordinato, l'inserimento dell'elenco non è un problema. Ogni passaggio viene eseguito anche a tempo costante (O (log (x)) tempo).

Quindi, qual è lo scenario peggiore?

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

Quindi questo è O (n) tempo per il caso peggiore. Il +1 indica se il numero è maggiore di quello più piccolo in ELENCO. Il tempo previsto per il caso medio dipenderà dalla distribuzione matematica di quegli n elementi.

Possibili miglioramenti

Questo algoritmo può essere leggermente migliorato per lo scenario peggiore, ma IMHO (non posso provare questa affermazione) che degraderà il comportamento medio. Il comportamento asintotico sarà lo stesso.

Il miglioramento di questo algoritmo sarà che non verificheremo se l'elemento è maggiore del più piccolo. Per ogni elemento proveremo a inserirlo e se è più piccolo del più piccolo lo ignoreremo. Anche se sembra assurdo se consideriamo solo lo scenario peggiore che avremo

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

operazioni.

Per questo caso d'uso non vedo ulteriori miglioramenti. Eppure devi chiederti: e se dovessi farlo più dei log (n) volte e per diverse x-es? Ovviamente dovremmo ordinare quell'array in O (n log (n)) e prendere il nostro elemento x ogni volta che ne abbiamo bisogno.


1

A questa domanda verrà data risposta con complessità N log (100) (anziché N log N) con una sola riga di codice 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 risposta finale sarebbe un vettore in cui i primi 100 elementi sono garantiti come i 100 numeri più grandi dell'array mentre gli elementi rimanenti non sono ordinati

C ++ STL (libreria standard) è abbastanza utile per questo tipo di problemi.

Nota: non sto dicendo che questa sia la soluzione ottimale, ma avrebbe salvato la tua intervista.


1

La soluzione semplice sarebbe quella di utilizzare una coda prioritaria, aggiungere i primi 100 numeri alla coda e tenere traccia del numero più piccolo nella coda, quindi scorrere tra gli altri miliardi di numeri e ogni volta che ne troviamo uno più grande del numero più grande nella coda prioritaria, rimuoviamo il numero più piccolo, aggiungiamo il nuovo numero e teniamo di nuovo traccia del numero più piccolo nella coda.

Se i numeri fossero in ordine casuale, questo funzionerebbe benissimo perché mentre passiamo attraverso un miliardo di numeri casuali, sarebbe molto raro che il numero successivo sia tra i 100 più grandi finora. Ma i numeri potrebbero non essere casuali. Se l'array fosse già ordinato in ordine crescente, inseriremmo sempre un elemento nella coda di priorità.

Quindi scegliamo prima 100.000 numeri casuali dall'array. Per evitare l'accesso casuale che potrebbe essere lento, aggiungiamo diciamo 400 gruppi casuali di 250 numeri consecutivi. Con quella selezione casuale, possiamo essere abbastanza sicuri che pochissimi dei numeri rimanenti sono tra i primi cento, quindi il tempo di esecuzione sarà molto vicino a quello di un semplice ciclo che confronta un miliardo di numeri con un valore massimo.


1

Trovare i primi 100 su un miliardo di numeri è meglio farlo usando min-heap di 100 elementi.

Per prima cosa innescare il min-heap con i primi 100 numeri incontrati. min-heap memorizzerà il più piccolo dei primi 100 numeri nella radice (in alto).

Mentre procedi, il resto dei numeri li confronta solo con il root (il più piccolo dei 100).

Se il nuovo numero rilevato è maggiore della radice di min-heap, sostituire la radice con quel numero, altrimenti ignorarlo.

Come parte dell'inserimento del nuovo numero in min-heap, il numero più piccolo nell'heap verrà visualizzato in cima (radice).

Una volta esaminati tutti i numeri, avremo i 100 numeri più grandi nell'heap min.


0

Ho scritto una semplice soluzione in Python nel caso qualcuno fosse interessato. Utilizza il bisectmodulo e un elenco di restituzione temporaneo che mantiene ordinato. Questo è simile all'implementazione di una coda 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

Utilizzo con 100.000.000 di elementi e input nel caso peggiore che è un elenco ordinato:

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

Ci sono voluti circa 40 secondi per calcolare questo per 100.000.000 di elementi, quindi ho paura di farlo per 1 miliardo. Ad essere sinceri, però, gli ho fornito l'input nel caso peggiore (ironicamente, un array che è già ordinato).


0

Vedo molte discussioni O (N), quindi propongo qualcosa di diverso solo per l'esercizio del pensiero.

Esistono informazioni note sulla natura di questi numeri? Se è di natura casuale, non andare oltre e guardare le altre risposte. Non otterrai risultati migliori di loro.

Però! Verifica se il meccanismo di popolamento di elenchi ha popolato tale elenco in un ordine particolare. Sono in uno schema ben definito in cui puoi sapere con certezza che la più grande magnitudine di numeri sarà trovata in una certa regione dell'elenco o in un certo intervallo? Potrebbe esserci un modello. In tal caso, ad esempio se sono garantiti in una sorta di distribuzione normale con la caratteristica gobba nel mezzo, hanno sempre tendenze al rialzo ripetute tra sottoinsiemi definiti, hanno un picco prolungato in qualche momento T nel mezzo dei dati impostato come forse un'incidenza di insider trading o guasti alle apparecchiature, o forse avere un "picco" ogni n numero come nell'analisi delle forze dopo una catastrofe, è possibile ridurre il numero di record che è necessario controllare in modo significativo.

C'è comunque del cibo per pensare. Forse questo ti aiuterà a dare ai futuri intervistatori una risposta ponderata. So che sarei impressionato se qualcuno mi ponesse una domanda del genere in risposta a un problema come questo: mi direbbe che stanno pensando all'ottimizzazione. Basta riconoscere che potrebbe non esserci sempre la possibilità di ottimizzare.


0
Time ~ O(100 * N)
Space ~ O(100 + N)
  1. Crea un elenco vuoto di 100 slot vuoti

  2. Per ogni numero nell'elenco di input:

    • Se il numero è inferiore al primo, salta

    • Altrimenti sostituirlo con questo numero

    • Quindi, spingere il numero attraverso lo scambio adiacente; fino a quando non è più piccolo di quello successivo

  3. Restituisce l'elenco


Nota: se il log(input-list.size) + c < 100, quindi il modo ottimale è ordinare l'elenco di input, quindi dividere i primi 100 elementi.


0

La complessità è O (N)

Per prima cosa crea un array di 100 in Initiazaze il primo elemento di questo array come primo elemento dei valori N, tieni traccia dell'indice dell'elemento corrente con un'altra variabile, chiamalo CurrentBig

Esplorare i valori 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)

} 

al termine, stampare l'array M da CurrentBig 100 volte modulo 100 :-) Per lo studente: assicurarsi che l'ultima riga del codice non superi i dati validi prima che il codice esca


0

Un altro algoritmo O (n) -

L'algoritmo trova i 100 più grandi per eliminazione

considera tutti i milioni di numeri nella loro rappresentazione binaria. Inizia dal bit più significativo. Trovare se MSB è 1 può essere fatto da una moltiplicazione dell'operazione booleana con un numero appropriato. Se ci sono più di 100 1 in questi milioni, elimina gli altri numeri con zeri. Ora dei restanti numeri procedere con il prossimo bit più significativo. tenere conto del numero di numeri rimanenti dopo l'eliminazione e procedere fino a quando questo numero è maggiore di 100.

La principale operazione booleana può essere eseguita in parallelo su GPU


0

Vorrei scoprire chi ha avuto il tempo di mettere un miliardo di numeri in un array e licenziarlo. Deve funzionare per il governo. Almeno se avessi un elenco collegato potresti inserire un numero nel mezzo senza spostare mezzo miliardo per fare spazio. Ancora meglio un Btree consente una ricerca binaria. Ogni confronto elimina la metà del totale. Un algoritmo di hash ti consentirebbe di popolare la struttura dei dati come una scacchiera ma non così buona per i dati sparsi. Poiché la soluzione migliore è avere un array di soluzioni di 100 numeri interi e tenere traccia del numero più basso nell'array di soluzioni in modo da poterlo sostituire quando si incontra un numero più elevato nell'array originale. Dovresti guardare ogni elemento dell'array originale supponendo che non sia ordinato per cominciare.


0

Puoi farlo in O(n)tempo. Basta scorrere l'elenco e tenere traccia dei 100 numeri più grandi che hai visto in un dato punto e il valore minimo in quel gruppo. Quando trovi un nuovo numero più grande del più piccolo dei tuoi dieci, quindi sostituiscilo e aggiorna il tuo nuovo valore minimo di 100 (può richiedere un tempo costante di 100 per determinarlo ogni volta che lo fai, ma ciò non influisce sull'analisi complessiva ).


1
Questo approccio è quasi identico alle risposte più e secondo più votate a questa domanda.
Bernhard Barker,

0

Gestire un elenco separato è un lavoro extra e devi spostare le cose in tutto l'elenco ogni volta che trovi un altro sostituto. Qsortalo e prendi i primi 100.


-1 quicksort è O (n log n) che è esattamente ciò che l'OP ha fatto e chiede di migliorare. Non è necessario gestire un elenco separato, solo un elenco di 100 numeri. Il tuo suggerimento ha anche l'effetto indesiderato di modificare l'elenco originale o copiarlo. Sono circa 4GiB di memoria, spariti.

0
  1. Usa n-element per ottenere il 100 ° elemento O (n)
  2. Scorrere la seconda volta ma solo una volta e generare ogni elemento maggiore di questo specifico elemento.

Si prega di notare esp. il secondo passo potrebbe essere facile da calcolare in parallelo! E sarà anche efficiente quando avrai bisogno di un milione di elementi più grandi.


0

È una domanda di Google o di altri giganti del settore. È possibile che il seguente codice sia la risposta giusta prevista dal tuo intervistatore. Il costo del tempo e lo spazio dipendono dal numero massimo nell'array di input. Per input di array int a 32 bit, il costo dello spazio massimo è 4 * 125 milioni di byte, il costo del tempo è 5 * miliardi.

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

ho fatto il mio codice, non sono sicuro che sia l'aspetto dell'intervistatore

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

Possibili miglioramenti.

Se il file contiene 1 miliardo di numeri, la lettura potrebbe essere davvero lunga ...

Per migliorare questo lavoro puoi:

  • Dividi il file in n parti, Crea n thread, fai in modo che n thread guardino ciascuno per i 100 numeri più grandi nella loro parte del file (usando la coda di priorità) e infine ottieni i 100 numeri più grandi di tutti i thread di output.
  • Usa un cluster per svolgere tale compito, con una soluzione come hadoop. Qui puoi dividere il file ancora di più e avere l'output più veloce per un file di numeri 1 miliardo (o 10 ^ 12).

0

Prima prendi 1000 elementi e aggiungili in un heap massimo. Ora prendi i primi max 100 elementi e conservali da qualche parte. Ora scegli i successivi 900 elementi dal file e aggiungili nell'heap insieme agli ultimi 100 elementi più alti.

Continua a ripetere questo processo raccogliendo 100 elementi dall'heap e aggiungendo 900 elementi dal file.

La scelta finale di 100 elementi ci darà il massimo di 100 elementi da un miliardo di numeri.


-1

Problema: trova m elementi più grandi di n elementi in cui n >>> m

La soluzione più semplice, che dovrebbe essere ovvia per tutti, è semplicemente fare m passi dell'algoritmo di ordinamento delle bolle.

quindi stampa gli ultimi n elementi dell'array.

Ciò non richiede strutture di dati esterne e utilizza un algoritmo che tutti conoscono.

La stima del tempo di esecuzione è O (m * n). Le migliori risposte finora sono O (n log (m)), quindi questa soluzione non è significativamente più costosa per i piccoli m.

Non sto dicendo che questo non possa essere migliorato, ma questa è di gran lunga la soluzione più semplice.


1
Nessuna struttura di dati esterna? Che dire dell'array di miliardi di numeri da ordinare? Un array di queste dimensioni è un enorme sovraccarico sia nel tempo di riempimento che nello spazio per l'archiviazione. E se tutti i "grandi" numeri fossero dalla parte sbagliata dell'array? Avresti bisogno dell'ordine di 100 miliardi di swap per "metterli in bolla" in posizione - un altro grande sovraccarico ... Infine, M N = 100 miliardi contro M Log2 (N) = 6,64 miliardi che è quasi due ordini di differenza di grandezza. Forse ripensaci. Una scansione a un passaggio, pur mantenendo una struttura di dati con i numeri più grandi, riuscirà significativamente a eseguire questo approccio.
NealB
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.