Ottieni 100 numeri più alti da un elenco infinito


53

A uno dei miei amici è stata posta questa domanda dell'intervista -

"Esiste un flusso costante di numeri provenienti da un infinito elenco di numeri da cui è necessario mantenere una struttura di dati in modo da restituire i primi 100 numeri più alti in un dato momento. Supponiamo che tutti i numeri siano solo numeri interi."

Questo è semplice, è necessario mantenere un elenco ordinato in ordine decrescente e tenere traccia del numero più basso in tale elenco. Se il nuovo numero ottenuto è maggiore di quel numero più basso, è necessario rimuovere quel numero più basso e inserire il nuovo numero nell'elenco ordinato come richiesto.

Quindi la domanda è stata estesa -

"Puoi assicurarti che l'ordine per l'inserimento sia O (1)? È possibile?"

Per quanto ne sapevo, anche se aggiungessi un nuovo numero all'elenco e lo riordinassi usando qualsiasi algoritmo di ordinamento, sarebbe meglio O (logn) per quicksort (penso). Quindi il mio amico ha detto che non era possibile. Ma non era convinto, ha chiesto di mantenere qualsiasi altra struttura di dati piuttosto che un elenco.

Ho pensato all'albero binario bilanciato, ma anche lì non otterrai l'inserimento con l'ordine di 1. Quindi la stessa domanda che ho anche adesso. Volevo sapere se esiste una tale struttura di dati che può fare l'inserimento nell'ordine di 1 per il problema di cui sopra o non è affatto possibile.


19
Forse sono solo io a fraintendere la domanda, ma perché hai bisogno di tenere un elenco ordinato ? Perché non tenere semplicemente traccia del numero più basso e se si incontra un numero più alto di quello, rimuovere il numero più basso e inserire il nuovo numero, senza mantenere ordinato l'elenco. Questo ti darebbe O (1).
EdoDodo,

36
@EdoDodo - e dopo quell'operazione, come fai a sapere qual è il nuovo numero più basso?
Damien_The_Unbeliever,

19
Ordina l'elenco [O (100 * log (100)) = O (1)] o esegui una ricerca lineare attraverso di esso per il minimo [O (100) = O (1)] per ottenere il nuovo numero più basso. Il tuo elenco ha una dimensione costante, quindi tutte queste operazioni sono anche a tempo costante.
Casuale 832,

6
Non è necessario mantenere l'intero elenco ordinato. Non ti interessa quale sia il numero più alto o il 2 ° più alto. Devi solo sapere qual è il più basso. Quindi dopo aver inserito un nuovo numero, basta attraversare i 100 numeri e vedere quale è il più basso. Questo è un tempo costante.
Tom Zych,

27
L' ordine asintotico di un'operazione è interessante solo quando la dimensione del problema può crescere senza limiti. Non è molto chiaro dalla tua domanda quale quantità stia crescendo senza limiti; sembra che tu stia chiedendo quale sia l'ordine asintotico per un problema la cui dimensione è limitata a 100; non è nemmeno una domanda ragionevole da porre; qualcosa deve crescere senza limiti. Se la domanda è "puoi farlo per mantenere la top n, non la top 100, in O (1) volta?" allora la domanda è sensata.
Eric Lippert,

Risposte:


35

Supponiamo che k sia il numero di numeri più alti che vuoi conoscere (100 nel tuo esempio). Quindi, è possibile aggiungere un nuovo numero in O(k)cui è anche O(1). A causa O(k*g) = O(g) if k is not zero and constant.


6
O (50) è O (n), non O (1). L'inserimento in un elenco di lunghezza N nel tempo O (1) significa che il tempo non dipende dal valore di N. Ciò significa che se 100 diventa 10000, 50 NON deve diventare 5000.

18
@hamstergene - ma nel caso di questa domanda, è Nla dimensione dell'elenco ordinato o il numero di elementi che sono stati elaborati finora? Se si elaborano 10000 articoli e si mantengono i primi 100 articoli in un elenco, oppure si elaborano 1000000000 articoli e si mantengono i primi 100 articoli in un elenco ordinato, i costi di inserimento in tale elenco rimangono gli stessi.
Damien_The_Unbeliever,

6
@hamstergene: in quel caso hai sbagliato le basi. Nel tuo link wikipedia c'è una proprietà ( "Moltiplicazione per una costante"): O(k*g) = O(g) if k not zero and constant. => O(50*1) = O(1).
duedl0r,

9
Penso che duedl0r abbia ragione. Riduciamo il problema e diciamo che sono necessari solo i valori minimo e massimo. È O (n) perché il minimo e il massimo sono 2? (n = 2). Il n. 2 fa parte della definizione del problema. È una costante, quindi è ak in O (k * qualcosa) che equivale a O (qualcosa)
xanatos,

9
@hamstergene: di quale funzione stai parlando? il valore 100 mi sembra abbastanza costante ..
duedl0r

19

Mantieni l'elenco non ordinato. Capire se inserire o meno un nuovo numero richiederà più tempo, ma l' inserimento sarà O (1).


7
Penso che questo ti darebbe il premio smart-aleck se non altro. * 8 ')
Mark Booth,

4
@Emilio, sei tecnicamente corretto - e ovviamente questo è il miglior tipo di corretto ...
Gareth,

1
Ma puoi anche mantenere il più basso dei tuoi 100 numeri, quindi puoi anche decidere se devi inserire in O (1). Quindi solo quando inserisci un numero, devi cercare il nuovo numero più basso. Ma ciò accade più raramente che decidere di inserire o meno, cosa che accade per ogni nuovo numero.
Andrei Vajna II,

12

Questo è facile. La dimensione dell'elenco di costante, quindi il tempo di ordinamento dell'elenco è costante. Un'operazione che viene eseguita a tempo costante si dice O (1). Pertanto l'ordinamento dell'elenco è O (1) per un elenco di dimensioni fisse.


9

Una volta passati 100 numeri, il costo massimo che dovrai sostenere per il numero successivo è il costo per verificare se il numero è tra i 100 numeri più alti (etichettiamo CheckTime ) più il costo per inserirlo in quel set ed espellere il uno più basso (chiamiamolo EnterTime ), che è tempo costante (almeno per i numeri limitati), o O (1) .

Worst = CheckTime + EnterTime

Successivamente, se la distribuzione dei numeri è casuale, il costo medio diminuisce con l'aumentare dei numeri che hai. Ad esempio, la possibilità di inserire il 101 ° numero nel set massimo è 100/101, le possibilità per il 1000 ° numero sarebbero 1/10 e le probabilità per l'ennesimo numero sarebbero 100 / n. Pertanto, la nostra equazione per il costo medio sarà:

Average = CheckTime + EnterTime / n

Pertanto, poiché n si avvicina all'infinito, è importante solo CheckTime :

Average = CheckTime

Se i numeri sono associati, CheckTime è costante e quindi è O (1) tempo.

Se i numeri non sono associati, il tempo di controllo aumenterà con più numeri. Teoricamente, questo perché se il numero più piccolo nel set massimo diventa abbastanza grande, il tuo tempo di controllo sarà maggiore perché dovrai considerare più bit. Ciò fa sembrare che sarà leggermente superiore al tempo costante. Tuttavia, potresti anche sostenere che la possibilità che il numero successivo sia nell'insieme più alto si avvicina a zero quando n si avvicina all'infinito e quindi la possibilità che dovrai considerare più bit si avvicina anche a 0, che sarebbe un argomento per O (1) tempo.

Non sono positivo, ma il mio istinto dice che è O (log (log (n))) . Questo perché la possibilità che il numero più basso aumenti è logaritmica e anche la possibilità che il numero di bit che è necessario considerare per ogni controllo sia logaritmico. Sono interessato alle altre persone che affrontano questo, perché non sono davvero sicuro ...


Tranne che l'elenco è arbitrario, e se fosse un elenco di numeri sempre crescenti?
dan_waterworth,

@dan_waterworth: se la lista infinita è arbritraria e sembra che aumenti (le probabilità sono 1 / ∞!), ciò si adatterebbe allo scenario peggiore CheckTime + EnterTimeper ogni numero. Ciò ha senso solo se i numeri sono illimitati, e quindi CheckTimee EnterTimeentrambi aumenteranno almeno logaritmicamente a causa dell'aumento delle dimensioni dei numeri.
Briguy37,

1
I numeri non sono casuali, ci sono arbitrari. Non ha senso parlare di probabilità.
dan_waterworth,

@dan_waterworth: hai detto due volte che i numeri sono arbitrari. Da dove prendi questo? Inoltre, credo che tu possa ancora applicare le statistiche ai numeri arbitrari a partire dal caso casuale e migliorare la loro accuratezza mentre sai di più sull'arbitro. Ad esempio, se tu fossi l'arbitro, sembra che ci sarebbe una maggiore possibilità di selezionare numeri sempre crescenti rispetto a se, diciamo, io fossi l'arbitro;)
Briguy37,

7

questo è facile se conosci alberi heap binari . I cumuli binari supportano l'inserimento nel tempo medio costante, O (1). E ti dà un facile accesso ai primi x elementi.


Perché conservare gli elementi che non ti servono? (valori troppo bassi) Sembra un algoritmo personalizzato più appropriato. Non dire che non puoi "non aggiungere" i valori quando non sono più alti di quelli più bassi.
Steven Jeuris,

Non lo so, la mia intuizione mi dice che un mucchio (di qualche sapore) potrebbe farlo abbastanza bene. Non significa che dovrebbe mantenere tutti gli elementi per farlo. Non l'ho ricercato ma "sembra giusto" (TM).
Rig

3
Un heap potrebbe essere modificato per scartare qualsiasi cosa al di sotto di un certo livello (per heap binari e k = 100, m sarebbe 7, poiché il numero di nodi = 2 ^ m-1). Ciò rallenterebbe, ma sarebbe comunque ammortizzato a tempo costante.
Plutor,

3
Se hai usato un min-heap binario (perché la parte superiore è il minimo, che stai controllando tutto il tempo) e trovi un nuovo numero> min, devi rimuovere l'elemento superiore prima di poterne inserire uno nuovo . La rimozione dell'elemento superiore (min) sarà O (logN) perché devi attraversare ogni livello dell'albero una volta. Quindi è tecnicamente vero che gli inserti sono in media O (1) perché in pratica è ancora O (logN) ogni volta che trovi un numero> min.
Scott Whitlock,

1
@Plutor, stai assumendo alcune garanzie che gli heap binari non ti danno. Visualizzandolo come un albero binario, potrebbe essere il caso che ogni elemento nel ramo sinistro sia più piccolo di qualsiasi elemento nel ramo destro, ma stai assumendo che gli elementi più piccoli siano più vicini alla radice.
Peter Taylor,

6

Se per la domanda l'intervistatore intendeva davvero porsi “possiamo assicurarci che ogni numero in arrivo sia processato in tempo costante”, allora come molti hanno già sottolineato (ad esempio vedi la risposta di @ duedl0r), la soluzione del tuo amico è già O (1), e sarebbe così anche se avesse usato una lista non ordinata, o usato l'ordinamento a bolle, o qualsiasi altra cosa. In questo caso la domanda non ha molto senso, a meno che non fosse una domanda delicata o se non la ricordi male.

Suppongo che la domanda dell'intervistatore fosse significativa, che non stava chiedendo come fare qualcosa per essere O (1), il che è ovviamente già così.

Perché la complessità dell'algoritmo in discussione ha senso solo quando la dimensione dell'input cresce indefinitamente e l'unico input che può crescere qui è 100: la dimensione dell'elenco; Suppongo che la vera domanda fosse: "possiamo assicurarci di ottenere la Top N spendendo O (1) tempo per numero (non O (N) come nella soluzione del tuo amico), è possibile?".

La prima cosa che viene in mente è contare l'ordinamento, che comprerà la complessità del tempo O (1) per numero per il problema Top-N per il prezzo dell'uso dello spazio O (m), dove m è la lunghezza dell'intervallo di numeri in entrata . Quindi sì, è possibile.


4

Utilizzare una coda con priorità minima implementata con un heap di Fibonacci , con tempo di inserimento costante:

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

4
"Operazioni di eliminazione e di lavoro minima di eliminazione in O(log n)tempo ammortizzato" , quindi questo sarebbe ancora causare O(log k)dove kè la quantità di oggetti da memorizzare.
Steven Jeuris,

1
Questo non è diverso dalla risposta di Emilio che è stata soprannominata "premio smart-aleck" poiché il min di cancellazione opera in O (log n) (secondo Wikipedia).
Nicole,

@Renesis La risposta di Emilio sarebbe O (k) per trovare il minimo, la mia è O (log k)
Gabe Moothart,

1
@Gabe Abbastanza giusto, intendo solo in linea di principio. In altre parole, se non si considera 100 come costante, anche questa risposta non è tempo contante.
Nicole,

@Renesis Ho rimosso l'istruzione (errata) dalla risposta.
Gabe Moothart,

2

Il compito è chiaramente quello di trovare un algoritmo O (1) nella lunghezza N dell'elenco di numeri richiesto. Quindi non importa se hai bisogno dei primi 100 numeri o 10000 numeri, il tempo di inserimento dovrebbe essere O (1).

Il trucco qui è che sebbene quel requisito O (1) sia menzionato per l'inserimento della lista, la domanda non ha detto nulla sull'ordine del tempo di ricerca nell'intero spazio numerico, ma si scopre che questo può essere fatto O (1) anche. La soluzione è quindi la seguente:

  1. Organizzare una tabella hash con numeri per chiavi e coppie di puntatori di elenchi collegati per valori. Ogni coppia di puntatori è l'inizio e la fine di una sequenza di elenchi collegati. Normalmente questo sarà solo un elemento e poi il successivo. Ogni elemento nell'elenco collegato va accanto all'elemento con il successivo numero più alto. L'elenco collegato contiene quindi la sequenza ordinata dei numeri richiesti. Tenere un registro del numero più basso.

  2. Prendi un nuovo numero x dal flusso casuale.

  3. È superiore all'ultimo numero più basso registrato? Sì => Passaggio 4, No => Passaggio 2

  4. Premi la tabella hash con il numero appena preso. C'è una voce? Sì => Passaggio 5. No => Prendi un nuovo numero x-1 e ripeti questo passaggio (questa è una semplice ricerca lineare verso il basso, porta con me qui, questo può essere migliorato e spiegherò come)

  5. Con l'elemento elenco appena ottenuto dalla tabella hash, inserire il nuovo numero subito dopo l'elemento nell'elenco collegato (e aggiornare l'hash)

  6. Prendi il numero più basso l registrato (e rimuovilo dall'hash / lista).

  7. Premi la tabella hash con il numero appena preso. C'è una voce? Sì => Passaggio 8. No => Prendi un nuovo numero l + 1 e ripeti questo passaggio (questa è una semplice ricerca lineare verso l'alto)

  8. Con un colpo positivo il numero diventa il nuovo numero più basso. Vai al passaggio 2

Per consentire valori duplicati, l'hash deve effettivamente mantenere l'inizio e la fine della sequenza di elenchi collegati di elementi duplicati. L'aggiunta o la rimozione di un elemento in una determinata chiave aumenta o diminuisce l'intervallo indicato.

L'inserto qui è O (1). Le ricerche citate sono, immagino qualcosa del genere, O (differenza media tra i numeri). La differenza media aumenta con la dimensione dello spazio numerico, ma diminuisce con la lunghezza richiesta dell'elenco di numeri.

Quindi la strategia di ricerca lineare è piuttosto scarsa, se lo spazio numerico è grande (ad esempio per un tipo int a 4 byte, da 0 a 2 ^ 32-1) e N = 100. Per ovviare a questo problema di prestazioni è possibile mantenere serie parallele di hashtable, in cui i numeri sono arrotondati a magnitudini più elevate (ad es. 1s, 10s, 100s, 1000s) per creare chiavi adatte. In questo modo è possibile aumentare o ridurre le marce per eseguire più rapidamente le ricerche richieste. La performance diventa quindi una O (log numberrange), penso, che è costante, cioè anche O (1).

Per chiarire questo, immagina di avere a portata di mano il numero 197. Colpisci la tabella hash 10s, con '190', viene arrotondata alle dieci più vicine. Nulla? No. Quindi scendi tra 10 secondi fino a quando non premi 120, quindi puoi iniziare a 129 nell'hashtable 1s, quindi prova 128, 127 fino a quando non colpisci qualcosa. Ora hai trovato dove nell'elenco collegato inserire il numero 197. Mentre lo inserisci, devi anche aggiornare la tabella 1s con la voce 197, la tabella 10s con il numero 190, 100s con 100, ecc. Il maggior numero di passaggi devi mai fare qui sono 10 volte il registro dell'intervallo di numeri.

Potrei aver sbagliato alcuni dettagli, ma poiché si tratta dello scambio di programmatori e il contesto era interviste, spero che quanto sopra sia una risposta abbastanza convincente per quella situazione.

EDIT Ho aggiunto alcuni dettagli extra qui per spiegare lo schema di hashtable parallelo e come significhi che le scarse ricerche lineari che ho citato possono essere sostituite con una ricerca O (1). Ho anche capito che ovviamente non è necessario cercare il numero più basso successivo, perché puoi passare direttamente ad esso guardando nella tabella con il numero più basso e passando all'elemento successivo.


1
La ricerca deve far parte della funzione di inserimento - non sono funzioni indipendenti. Poiché la tua ricerca è O (n), anche la tua funzione di inserimento è O (n).
Kirk Broadhurst,

No. Usando la strategia che ho descritto, dove vengono utilizzati più hashtable per attraversare lo spazio numerico più rapidamente, è O (1). Per favore, leggi di nuovo la mia risposta.
Benedetto

1
@Benedict, la tua risposta dice chiaramente che ha ricerche lineari nei passaggi 4 e 7. Le ricerche lineari non sono O (1).
Peter Taylor,

Sì, lo fa, ma ci penserò più tardi. Ti dispiacerebbe davvero leggere il resto per favore. Se necessario, modificherò la mia risposta per chiarirla abbondantemente.
Benedetto

@Benedict Hai ragione - esclusa la ricerca, la tua risposta è O (1). Purtroppo questa soluzione non funzionerà senza la ricerca.
Kirk Broadhurst,

1

Possiamo supporre che i numeri siano di un tipo di dati fisso, come Integer? In tal caso, tenere un conto di ogni singolo numero aggiunto. Questa è un'operazione O (1).

  1. Dichiarare un array con tanti elementi quanti sono i numeri possibili:
  2. Leggi ogni numero mentre viene trasmesso in streaming.
  3. Calcola il numero. Ignoralo se quel numero è già stato conteggiato 100 volte perché non ne avrai mai bisogno. Ciò impedisce agli overflow di calcolarlo un numero infinito di volte.
  4. Ripetere dal passaggio 2.

Codice VB.Net:

Const Capacity As Integer = 100

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

Quando ritorni l'elenco, puoi impiegare tutto il tempo che desideri. Basta scorrere dalla fine dell'elenco e creare un nuovo elenco dei più alti 100 valori registrati. Questa è un'operazione O (n), ma è irrelivante.

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

Modifica: in realtà, non importa se si tratta di un tipo di dati fisso. Dato che non ci sono limiti imposti al consumo di memoria (o disco rigido), è possibile farlo funzionare per qualsiasi intervallo di numeri interi positivi.


1

Cento numeri possono essere facilmente memorizzati in un array, dimensione 100. Qualsiasi albero, elenco o set è eccessivo, dato il compito da svolgere.

Se il numero in entrata è superiore al più basso (= ultimo) nell'array, eseguire il passaggio su tutte le voci. Una volta trovato il primo che è più piccolo del tuo nuovo numero (puoi usare ricerche fantasiose per farlo), passa attraverso il resto dell'array, spingendo ciascuna voce "giù" di una.

Poiché mantieni l'elenco ordinato dall'inizio, non è necessario eseguire alcun algoritmo di ordinamento. Questo è O (1).


0

È possibile utilizzare un Max-Heap binario. Dovresti tenere traccia di un puntatore al nodo minimo (che potrebbe essere sconosciuto / nullo).

Si inizia inserendo i primi 100 numeri nell'heap. Il massimo sarà in alto. Fatto ciò, manterrai sempre 100 numeri.

Quindi quando ottieni un nuovo numero:

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

Sfortunatamente findMinimumNodeè O (n) e si incorre in quel costo una volta per inserimento (ma non durante l'inserimento :). La rimozione del nodo minimo e l'inserimento del nuovo nodo sono, in media, O (1) perché tenderanno verso il fondo dell'heap.

Andando dall'altra parte con un Min-Heap binario, il minimo è in alto, il che è ottimo per trovare il minimo per il confronto, ma fa schifo quando devi sostituire il minimo con un nuovo numero>> min. Questo perché è necessario rimuovere il nodo minimo (sempre O (logN)) e quindi inserire il nuovo nodo (O (1) medio). Quindi, hai ancora O (logN) che è meglio di Max-Heap, ma non O (1).

Naturalmente, se N è costante, allora hai sempre O (1). :)

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.