Il modo più efficiente per memorizzare migliaia di numeri di telefono


94

Questa è una domanda di un'intervista a Google:

Ci sono circa mille numeri di telefono da memorizzare, ciascuno composto da 10 cifre. Puoi presumere che le prime 5 cifre di ciascuna siano le stesse per migliaia di numeri. È necessario eseguire le seguenti operazioni: a. Cerca se esiste un determinato numero. b. Stampa tutto il numero

Qual è il modo più efficiente per risparmiare spazio per farlo?

Ho risposto alla tabella hash e successivamente alla codifica di Huffman, ma il mio intervistatore ha detto che non stavo andando nella giusta direzione. Per favore aiutami qui.

L'utilizzo di un suffisso trie potrebbe aiutare?

Idealmente, la memorizzazione di 1000 numeri richiede 4 byte per numero, quindi in tutto ci vorrebbero 4000 byte per memorizzare 1000 numeri. Quantitativamente, desidero ridurre la memoria a <4000 byte, questo è ciò che mi ha spiegato il mio intervistatore.


28
Risponderei che utilizzando un normale database è possibile archiviarli come testo, anche migliaia / milioni, e le operazioni di ricerca saranno comunque molto veloci. Sconsiglio di fare cose "intelligenti" poiché l'intero sistema dovrà essere rifatto se in futuro vorranno supportare i numeri internazionali, o se i numeri di telefono che iniziano con uno "0" iniziano a comparire, o se il governo decide di farlo modificare il formato del numero di telefono e così via.
Thomas Bonini

1
@AndreasBonini: probabilmente darei quella risposta, a meno che non stavo intervistando in un'azienda come Google o Facebook, se le soluzioni fuori dagli schemi non lo tagliassero. Sebbene postgres, ad esempio, abbia anche dei tentativi, non sarei sicuro che questi riducano il throughput dei dati di cui Google ha bisogno.
LiKao

1
@LiKao: tieni presente che l'OP ha espressamente dichiarato "circa un migliaio di numeri"
Thomas Bonini

@AndreasBonini: Vero, potrebbe anche essere stato un test, che l'intervistato sappia interpretare correttamente tali vincoli e scegliere la soluzione migliore in base a questo.
LiKao

4
"efficiente" in questa domanda ha davvero bisogno di essere definito - efficiente in che modo? spazio, tempo, entrambi?
matt b

Risposte:


36

Ecco un miglioramento alla risposta di Aix . Considerare l'utilizzo di tre "livelli" per la struttura dei dati: il primo è una costante per le prime cinque cifre (17 bit); quindi da qui in poi ogni numero di telefono ha solo le restanti cinque cifre. Consideriamo queste cinque cifre rimanenti come interi binari a 17 bit e memorizziamo k di quei bit utilizzando un metodo e 17 - k = m con un metodo diverso, determinando k alla fine per ridurre al minimo lo spazio richiesto.

Per prima cosa ordiniamo i numeri di telefono (tutti ridotti a 5 cifre decimali). Quindi contiamo quanti numeri di telefono ci sono per i quali il numero binario costituito dai primi m bit è tutto 0, per quanti numeri di telefono i primi m bit sono al massimo 0 ... 01, per quanti numeri di telefono i primi m i bit sono al massimo 0 ... 10, eccetera, fino al conteggio dei numeri di telefono per i quali i primi m bit sono 1 ... 11 - quest'ultimo conteggio è 1000 (decimale). Ci sono 2 ^ m di tali conteggi e ogni conteggio è al massimo 1000. Se omettiamo l'ultimo (perché sappiamo che è comunque 1000), possiamo memorizzare tutti questi numeri in un blocco contiguo di (2 ^ m - 1) * 10 bit. (10 bit sono sufficienti per memorizzare un numero inferiore a 1024.)

Gli ultimi k bit di tutti i numeri di telefono (ridotti) vengono memorizzati in modo contiguo in memoria; quindi se k è, diciamo, 7, allora i primi 7 bit di questo blocco di memoria (bit da 0 a 6) corrispondono agli ultimi 7 bit del primo numero di telefono (ridotto), i bit da 7 a 13 corrispondono agli ultimi 7 bit del secondo numero di telefono (ridotto), ecc. Ciò richiede 1000 * k bit per un totale di 17 + (2 ^ (17 - k ) - 1) * 10 + 1000 * k , che raggiunge il suo minimo 11287 per k = 10. Quindi possiamo memorizzare tutti i numeri di telefono in ceil ( 11287/8) = 1411 byte.

È possibile risparmiare ulteriore spazio osservando che nessuno dei nostri numeri può iniziare con, ad esempio, 1111111 (binario), perché il numero più basso che inizia con quello è 130048 e abbiamo solo cinque cifre decimali. Questo ci permette di eliminare alcune voci dal primo blocco di memoria: invece di 2 ^ m - 1 conta, abbiamo bisogno solo di ceil (99999/2 ^ k ). Ciò significa che la formula diventa

17 + ceil (99999/2 ^ k ) * 10 + 1000 * k

che abbastanza sorprendentemente raggiunge il suo minimo 10997 sia per k = 9 che per k = 10, o ceil (10997/8) = 1375 byte.

Se vogliamo sapere se un certo numero di telefono è nel nostro set, controlliamo prima se le prime cinque cifre binarie corrispondono alle cinque cifre che abbiamo memorizzato. Quindi dividiamo le restanti cinque cifre nella sua parte superiore m = 7 bit (che è, diciamo, il numero m- bit M ) e la sua inferiore k = 10 bit (il numero K ). Ora troviamo il numero a [M-1] di numeri di telefono ridotti per i quali le prime m cifre sono al massimo M - 1, e il numero a [M] di numeri di telefono ridotti per i quali le prime m cifre sono al massimo M , entrambi dal primo blocco di bit. Ora controlliamo tra a[M-1] esima e una [M] esima sequenza di k bit nel secondo blocco di memoria per vedere se troviamo K ; nel peggiore dei casi ci sono 1000 di queste sequenze, quindi se usiamo la ricerca binaria possiamo finire in O (log 1000) operazioni.

Segue lo pseudocodice per la stampa di tutti i 1000 numeri, dove accedo alla voce K -esimo k- bit del primo blocco di memoria come a [K] e alla M -esima voce m- bit del secondo blocco di memoria come b [M] (entrambi richiederebbero alcune operazioni di bit che sono noiose da scrivere). Le prime cinque cifre sono nel numero c .

i := 0;
for K from 0 to ceil(99999 / 2^k) do
  while i < a[K] do
    print(c * 10^5 + K * 2^k + b[i]);
    i := i + 1;
  end do;
end do;

Forse qualcosa va storto con il caso al contorno per K = ceil (99999/2 ^ k ), ma è abbastanza facile da risolvere.

Infine, da un punto di vista entropico, non è possibile memorizzare un sottoinsieme di 10 ^ 3 interi positivi tutti inferiori a 10 ^ 5 in meno di ceil (log [2] (binomiale (10 ^ 5, 10 ^ 3)) ) = 8073. Includendo il 17 di cui abbiamo bisogno per le prime 5 cifre, c'è ancora un intervallo di 10997 - 8090 = 2907 bit. È una sfida interessante vedere se esistono soluzioni migliori in cui è ancora possibile accedere ai numeri in modo relativamente efficiente!


4
La struttura dei dati che stai descrivendo qui in realtà è solo una versione molto efficiente di trie, che utilizza solo il minimo necessario per l'indicizzazione e solo due livelli. In pratica sarebbe bello vedere se questo può battere un trie con più livelli, ma credo che questo dipenda molto dalla distribuzione dei numeri (in real live i numeri di telefono non sono del tutto casuali, ma solo quasi).
LiKao

Ciao Erik, poiché hai detto che saresti interessato a vedere altre alternative, controlla la mia soluzione. Lo risolve in 8.580 bit, che è a soli 490 bit dal minimo teorico. È un po 'inefficiente cercare numeri individuali, ma lo spazio di archiviazione è molto compatto.
Briguy37

1
Suppongo che un intervistatore sano di mente preferirebbe la risposta "un trie" invece di "un database complesso personalizzato". Se vuoi dimostrare le tue capacità di hacking, puoi aggiungere: "sarebbe possibile creare un algoritmo ad albero specifico per questo caso speciale, se necessario".
KarlP

Ciao, puoi spiegare come 5 cifre richiedono 17 bit per essere memorizzate?
Tushar Banne,

@tushar Cinque cifre codificano un numero compreso tra 00000 e 99999 inclusi. Rappresenta quel numero in binario. 2 ^ 17 = 131072, quindi 17 bit sono sufficienti per questo ma 16 no.
Erik P.

43

In quanto segue, tratto i numeri come variabili intere (al contrario delle stringhe):

  1. Ordina i numeri.
  2. Dividi ogni numero nelle prime cinque cifre e nelle ultime cinque cifre.
  3. Le prime cinque cifre sono le stesse per tutti i numeri, quindi memorizzale solo una volta. Ciò richiederà 17 bit di archiviazione.
  4. Memorizza le ultime cinque cifre di ogni numero individualmente. Ciò richiederà 17 bit per numero.

Ricapitolando: i primi 17 bit sono il prefisso comune, i successivi 1000 gruppi di 17 bit sono le ultime cinque cifre di ogni numero memorizzato in ordine crescente.

In totale stiamo esaminando 2128 byte per i 1000 numeri o 17.017 bit per numero di telefono a 10 cifre.

La ricerca è O(log n)(ricerca binaria) e l'enumerazione completa è O(n).


Uhm, dov'è la complessità spaziale?
aioobe

Troppo tempo per costruire (O (log (n) * n k) (k è la lunghezza) per l'ordinamento, rispetto a O (n k) per la costruzione di un trie). Anche lo spazio è tutt'altro che ottimale, perché i prefissi comuni più lunghi vengono memorizzati individualmente. Anche il tempo di ricerca non è ottimale. Per dati stringa come questo è facile dimenticare la lunghezza dei numeri, che domina la ricerca. Cioè la ricerca binaria è O (log (n) * k), mentre un trie necessita solo di O (k). È possibile ridurre queste espressioni, quando k è costante, ma questo per mostrare un problema generale quando si ragiona sulle strutture dati che memorizzano stringhe.
LiKao

@LiKao: Chi ha parlato di archi? Ho a che fare esclusivamente con variabili intere, quindi kè irrilevante.
NPE

1
Ok, allora ho letto male la risposta. Tuttavia, le parti comuni non vengono memorizzate insieme, quindi rimane il punto sull'efficienza dello spazio. Per 1000 numeri di 5 cifre, ci sarà una discreta quantità di prefissi comuni, quindi ridurli sarà di grande aiuto. Anche in caso di numeri abbiamo O (log (n)) contro O (k) per le stringhe, che è ancora più veloce.
LiKao

1
@ Geek: 1001 gruppi di 17 bit corrispondono a 17017 bit o 2128 byte (con qualche modifica).
NPE

22

http://en.wikipedia.org/wiki/Acyclic_deterministic_finite_automaton

Una volta ho avuto un'intervista in cui hanno chiesto informazioni sulle strutture dei dati. Ho dimenticato "Array".


1
+1 è decisamente la strada da percorrere. Ho imparato questo sotto un altro nome, albero della biblioteca o albero di ricerca lessicale o qualcosa del genere quando ero uno studente (se qualcuno ricorda quel vecchio nome per favore dillo).
Valmond

6
Questo non soddisfa il requisito di 4000 byte. Solo per l'archiviazione dei puntatori, lo scenario peggiore è che avresti bisogno di 1 puntatore per l'1-4 ° lascia al livello successivo, 10 puntatori per il 5 °, 100 per il 6 ° e 1000 per il 7 °, 8 ° e 9 ° livello , che porta il totale del nostro puntatore a 3114. Ciò fornisce almeno 3114 posizioni di memoria distinte necessarie affinché i puntatori puntino, il che significa che avresti bisogno di almeno 12 bit per ogni puntatore. 12 * 3114 = 37368 bit = 4671 byte> 4000 byte, e questo non figura nemmeno nel modo in cui rappresenti il ​​valore di ogni foglia!
Briguy37

16

Probabilmente prenderei in considerazione l'utilizzo di una versione compressa di un Trie (forse un DAWG come suggerito da @Misha).

Ciò trarrebbe automaticamente vantaggio dal fatto che hanno tutti un prefisso comune.

La ricerca verrà eseguita in tempo costante e la stampa verrà eseguita in tempo lineare.


La domanda riguarda il modo più efficiente in termini di spazio per memorizzare i dati. Ti dispiacerebbe fornire una stima di quanto spazio richiederebbe questo metodo per i 1000 numeri di telefono? Grazie.
NPE

Lo spazio per il trie è al massimo O (n * k) dove n è il numero di stringhe e k è la lunghezza di ciascuna stringa. Tenendo conto che non sono necessari caratteri a 8 bit per rappresentare i numeri, suggerirei di memorizzare 4 indici esadecimali esadecimali e uno per il bit rimanente. In questo modo sono necessari al massimo 17 bit per numero. Poiché in tutti i casi ci saranno scontri a tutti i livelli con questa codifica, puoi effettivamente arrivare al di sotto di questo. Prevedendo di memorizzare 1000 numeri, possiamo già salvare un totale di 250 bit per gli scontri di primo livello. È meglio testare la codifica corretta sui dati di esempio.
LiKao

@LiKao, giusto, e notando che, ad esempio, 1000 numeri non possono avere più di 100 ultime due cifre diverse, il trie potrebbe essere ridotto in modo significativo agli ultimi livelli.
aioobe

@ aioobe: le foglie potrebbero essere crollate all'ultimo livello perché non ci sono bambini. Tuttavia, le foglie al penultimo livello necessitano di 2 ^ 10 = 1024 stati (ogni ultima cifra potrebbe essere attivata o disattivata), quindi non è riducibile in questo caso poiché ci sono solo 1000 numeri. Ciò significa che il numero di puntatori nel caso peggiore rimane a 3114 (vedi il mio commento sulla risposta di Misha) mentre le foglie necessarie vanno a 5 + 10 + 100 + 1000 + 1000 + 10 = 2125, che non cambia i 12 byte necessari per ciascuno puntatore. Quindi, questo pone ancora una soluzione trie a 4671 byte considerando solo i puntatori.
Briguy37

@ Briguy37, non sono sicuro di aver capito il tuo argomento " ogni ultima cifra potrebbe essere attivata o disattivata ". Tutti i numeri sono lunghi 10 cifre, giusto?
aioobe

15

Ho già sentito parlare di questo problema (ma senza che le prime 5 cifre siano lo stesso presupposto), e il modo più semplice per farlo è stato Rice Coding :

1) Poiché l'ordine non ha importanza, possiamo ordinarli e salvare solo le differenze tra valori consecutivi. Nel nostro caso le differenze medie sarebbero 100.000 / 1000 = 100

2) Codifica le differenze utilizzando codici Rice (base 128 o 64) o anche codici Golomb (base 100).

EDIT: una stima per la codifica Rice con base 128 (non perché darebbe i migliori risultati, ma perché è più facile da calcolare):

Salveremo il primo valore così com'è (32 bit).
Il resto dei 999 valori sono differenze (ci aspettiamo che siano piccoli, in media 100) conterrà:

valore unario value / 128(numero variabile di bit + 1 bit come terminatore)
valore binario per value % 128(7 bit)

Dobbiamo stimare in qualche modo i limiti (chiamiamoli così VBL) per il numero di bit variabili:
limite inferiore: si consideri fortunato, e nessuna differenza è maggiore della nostra base (128 in questo caso). questo significherebbe dare 0 bit aggiuntivi.
limite alto: poiché tutte le differenze inferiori alla base saranno codificate nella parte binaria del numero, il numero massimo che dovremmo codificare in unario è 100000/128 = 781,25 (anche meno, perché non ci aspettiamo che la maggior parte delle differenze sia zero ).

Quindi, il risultato è 32 + 999 * (1 + 7) + variabile (0..782) bit = 1003 + variabile (0..98) byte.


Puoi fornire maggiori dettagli sul modo in cui stai codificando e sul calcolo delle dimensioni finali. 1101 byte o 8808 bit sembrano molto vicini al limite teorico di 8091 bit, quindi sono molto sorpreso che sia possibile ottenere qualcosa di simile in pratica.
LiKao

Non sarebbero 32 + 999 * (1 + 7 + variable(0..782))bit? Ciascuno dei numeri 999 necessita di una rappresentazione di value / 128.
Kirk Broadhurst

1
@ Kirk: no, se tutti sono nel range di 5 cifre. Questo perché ci aspetteremmo che la somma di tutte queste differenze (ricorda, codifichiamo differenze tra valori consecutivi, non tra il primo e l'ennesimo valore) sarebbe inferiore a 100000 (anche nello scenario peggiore)
ruslik

Sono necessari 34 bit invece di 32 bit per rappresentare il primo valore (9.999.999.999> 2 ^ 32 = 4.294.967.296). Inoltre, la differenza massima sarebbe da 00000 a 99001 poiché i numeri sono univoci, il che aggiungerebbe 774 1 invece di 782 per la base 128. Pertanto, l'intervallo per la memorizzazione di 1.000 numeri per la base 128 è 8026-8800 bit o 1004-1100 byte. La base a 64 bit offre una migliore archiviazione, con intervalli da 879-1072 byte.
Briguy37

1
@raisercostin: questo è ciò che Kirk ha chiesto. Nel tuo esempio, codificando una volta la differenza di 20k tra i primi due valori, in futuro sarà possibile che si verifichino solo 80k di portata massima. Questo utilizzerà fino a 20k / 128 = 156 bit unari su un massimo di 782 (che corrispondono a 100k)
ruslik

7

Questo è un problema ben noto da Bentley's Programming Pearls.

Soluzione: elimina le prime cinque cifre dai numeri poiché sono uguali per ogni numero. Quindi utilizzare operazioni bit per bit per rappresentare il restante 9999 valore possibile. Avrai solo bisogno di 2 ^ 17 bit per rappresentare i numeri. Ogni bit rappresenta un numero. Se il bit è impostato, il numero è nella rubrica del telefono.

Per stampare tutti i numeri, è sufficiente stampare tutti i numeri in cui è impostato il bit concatenati con il prefisso. Per cercare un dato numero eseguire la necessaria aritmetica dei bit per verificare la rappresentazione bit per bit del numero.

È possibile cercare un numero in O (1) e l'efficienza dello spazio è massima a causa della rappresentazione dei bit.

HTH Chris.


3
Questo sarebbe un buon approccio per un insieme denso di numeri. Purtroppo qui l'insieme è molto scarso: ci sono solo 1.000 numeri su 100.000 possibili. Questo approccio richiederebbe quindi in media 100 bit per numero. Vedi la mia risposta per un'alternativa che richiede solo ~ 17 bit.
NPE

1
Il tempo necessario per stampare tutti i numeri non sarebbe proporzionale a 100.000 invece di 1.000?
aioobe

Combinando le due idee fondamentalmente ottieni immediatamente il trie. L'utilizzo di un bitvector con 100.000 voci è una sovraallocazione e richiede molto spazio. Tuttavia, la ricerca di O (log (n)) è spesso troppo lenta (dipende dal numero di query qui). Quindi, utilizzando una gerarchia di set di bit per l'indicizzazione, memorizzerai al massimo 17 bit per numero, pur continuando a ottenere la ricerca O (1). Ecco come funziona il trie. Anche il tempo per la stampa è in O (n) per il trie, che eredita dal caso ordinato.
LiKao

Questo non è "il modo più efficiente per risparmiare spazio".
Jake Berger

5

Memoria fissa di 1073 byte per 1.000 numeri:

Il formato di base di questo metodo di archiviazione è memorizzare le prime 5 cifre, un conteggio per ogni gruppo e l'offset per ogni numero in ogni gruppo.

Prefisso: il
nostro prefisso a 5 cifre occupa i primi 17 bit .

Raggruppamento:
Successivamente, dobbiamo trovare un raggruppamento di buone dimensioni per i numeri. Proviamo ad avere circa 1 numero per gruppo. Poiché sappiamo che ci sono circa 1000 numeri da memorizzare, dividiamo 99.999 in circa 1000 parti. Se scegliessimo la dimensione del gruppo come 100, ci sarebbero bit sprecati, quindi proviamo una dimensione del gruppo di 128, che può essere rappresentata con 7 bit. Questo ci dà 782 gruppi con cui lavorare.

Conteggi:
Successivamente, per ciascuno dei 782 gruppi, dobbiamo memorizzare il conteggio delle voci in ciascun gruppo. Un conteggio a 7 bit per ogni gruppo produrrebbe 7*782=5,474 bits, il che è molto inefficiente perché il numero medio rappresentato è circa 1 a causa di come abbiamo scelto i nostri gruppi.

Quindi, invece, abbiamo conteggi di dimensioni variabili con 1 iniziale per ogni numero in un gruppo seguito da uno 0. Quindi, se avessimo xnumeri in un gruppo, avremmo x 1'sseguito da a 0per rappresentare il conteggio. Ad esempio, se avessimo 5 numeri in un gruppo, il conteggio sarebbe rappresentato da 111110. Con questo metodo, se ci sono 1.000 numeri si finisce con 1000 1 e 782 0 per un totale di 1000 + 782 = 1.782 bit per i conteggi .

Offset:
Infine, il formato di ogni numero sarà solo l'offset a 7 bit per ogni gruppo. Ad esempio, se 00000 e 00001 sono gli unici numeri nel gruppo 0-127, i bit per quel gruppo sarebbero 110 0000000 0000001. Supponendo 1.000 numeri, ci saranno 7.000 bit per gli offset .

Quindi il nostro conteggio finale supponendo 1.000 numeri è il seguente:

17 (prefix) + 1,782 (counts) + 7,000 (offsets) = 8,799 bits = 1100 bytes

Ora, controlliamo se la nostra selezione della dimensione del gruppo arrotondando fino a 128 bit è stata la scelta migliore per la dimensione del gruppo. Scegliendo xcome numero di bit per rappresentare ciascun gruppo, la formula per la dimensione è:

Size in bits = 17 (prefix) + 1,000 + 99,999/2^x + x * 1,000

Minimizzando questa equazione per i valori interi di xx=6, che produce 8.580 bit = 1.073 byte . Pertanto, il nostro spazio di archiviazione ideale è il seguente:

  • Dimensione del gruppo: 2 ^ 6 = 64
  • Numero di gruppi: 1.562
  • Spazio di archiviazione totale:

    1017 (prefix plus 1's) + 1563 (0's in count) + 6*1000 (offsets) = 8,580 bits = 1,073 bytes


1

Prendendo questo come un problema puramente teorico e lasciando l'implementazione assunta, il modo più efficiente è quello di indicizzare tutti i possibili set di 10000 ultime cifre in una gigantesca tabella di indicizzazione. Supponendo che tu abbia esattamente 1000 numeri, avresti bisogno di poco più di 8000 bit per identificare in modo univoco l'insieme corrente. Non è possibile una compressione maggiore, perché in tal caso avresti due set identificati con lo stesso stato.

Il problema con questo è che dovresti rappresentare ciascuno dei 2 ^ 8000 set nel tuo programma come un lut, e nemmeno Google sarebbe in grado di farlo a distanza.

La ricerca sarebbe O (1), stampando tutto il numero O (n). L'inserimento sarebbe O (2 ^ 8000) che in teoria è O (1), ma in pratica è inutilizzabile.

In un'intervista darei solo questa risposta, se fossi sicuro, che l'azienda sta cercando qualcuno che sia in grado di pensare molto fuori dagli schemi. Altrimenti questo potrebbe farti sembrare un teorico senza preoccupazioni del mondo reale.

EDIT : Ok, ecco una "implementazione".

Passaggi per costruire l'implementazione:

  1. Prendi una matrice costante di dimensioni 100.000 * (1000 scegli 100.000) bit. Sì, sono consapevole del fatto che questa matrice avrà bisogno di più spazio degli atomi nell'universo di diverse magnitudini.
  2. Separa questo ampio array in blocchi da 100.000 ciascuno.
  3. In ogni blocco memorizzare un array di bit per una combinazione specifica delle ultime cinque cifre.

Questo non è il programma, ma una sorta di meta programma, che costruirà una gigantesca LUT che ora può essere utilizzata in un programma. Le cose costanti del programma normalmente non vengono conteggiate quando si calcola l'efficienza dello spazio, quindi non ci interessa questo array, quando facciamo i nostri calcoli finali.

Ecco come utilizzare questa LUT:

  1. Quando qualcuno ti dà 1000 numeri, memorizzi le prime cinque cifre separatamente.
  2. Scopri quale dei pezzi del tuo array corrisponde a questo set.
  3. Memorizza il numero del set in un unico numero a 8074 bit (chiamalo c).

Ciò significa che per l'archiviazione abbiamo solo bisogno di 8091 bit, che qui abbiamo dimostrato essere la codifica ottimale. Trovare il blocco corretto tuttavia richiede O (100.000 * (100.000 scegli 1000)), che secondo le regole matematiche è O (1), ma in pratica richiederà sempre più tempo del tempo dell'universo.

La ricerca è semplice però:

  1. striscia delle prime cinque cifre (il numero rimanente sarà chiamato n ').
  2. verifica se corrispondono
  3. Calcola i = c * 100000 + n '
  4. Controlla se il bit in i nella LUT è impostato su uno

Anche stampare tutti i numeri è semplice (e richiede O (100000) = O (1) in realtà, perché devi sempre controllare tutti i bit del blocco corrente, quindi ho calcolato male questo sopra).

Non la definirei una "implementazione", a causa del palese disprezzo dei limiti (la dimensione dell'universo e il tempo in cui questo universo ha vissuto o questa terra esisterà). Tuttavia in teoria questa è la soluzione ottimale. Per problemi più piccoli, questo può essere effettivamente fatto, e talvolta lo sarà. Ad esempio, le reti di ordinamento sono un esempio di questo modo di codificare e possono essere utilizzate come passaggio finale negli algoritmi di ordinamento ricorsivo, per ottenere una grande velocità.


1
Qual è il modo più efficiente per risparmiare spazio per farlo?
Sven

1
Quando si eseguono calcoli dello spazio di esecuzione, questo può essere facilmente dimostrato come il modo più efficiente per risparmiare spazio, poiché si enumera ogni possibile stato del sistema con un solo numero. Non può esserci alcuna codifica più piccola per questo problema. Il trucco per questa risposta è che la dimensione del programma non viene quasi mai considerata, quando si eseguono i calcoli (prova a trovare una qualsiasi risposta che tenga conto di ciò e vedrai cosa intendo). Quindi, per qualsiasi problema che abbia un limite di dimensione, puoi sempre enumerare tutti gli stati, per ottenere il modo più salvaspazio per gestirlo.
LiKao

1

Ciò equivale a memorizzare mille interi non negativi ciascuno inferiore a 100.000. Possiamo usare qualcosa come la codifica aritmetica per farlo.

Alla fine, i numeri verranno memorizzati in un elenco ordinato. Noto che la differenza attesa tra i numeri adiacenti nell'elenco è 100.000 / 1000 = 100, che può essere rappresentata in 7 bit. Ci saranno anche molti casi in cui sono necessari più di 7 bit. Un modo semplice per rappresentare questi casi meno comuni è adottare lo schema utf-8 in cui un byte rappresenta un intero a 7 bit a meno che non sia impostato il primo bit, nel qual caso il byte successivo viene letto per produrre un numero intero a 14 bit, a meno che viene impostato il suo primo bit, nel qual caso viene letto il byte successivo per rappresentare un intero a 21 bit.

Quindi almeno la metà delle differenze tra interi consecutivi può essere rappresentata con un byte e quasi tutto il resto richiede due byte. Alcuni numeri, separati da differenze maggiori di 16.384, richiederanno tre byte, ma non possono essere più di 61 di questi. La memoria media quindi sarà di circa 12 bit per numero, o un po 'meno, o al massimo 1500 byte.

Lo svantaggio di questo approccio è che la verifica dell'esistenza di un numero è ora O (n). Tuttavia, non è stato specificato alcun requisito di complessità temporale.

Dopo aver scritto, ho notato che ruslik ha già suggerito il metodo di differenza sopra, l'unica differenza è lo schema di codifica. Il mio è probabilmente più semplice ma meno efficiente.


1

Solo per chiedere rapidamente qualsiasi motivo per cui non vorremmo cambiare i numeri in una base 36. Potrebbe non risparmiare tanto spazio ma sicuramente risparmierebbe tempo nella ricerca poiché guarderai molto meno di 10 cifre. Oppure li dividerei in file a seconda di ciascun gruppo. quindi nominerei un file (111) -222.txt e quindi memorizzerei solo i numeri che si adattano a quel gruppo lì e poi li avrei ricercabili in ordine numerico in questo modo posso sempre controllare se il file esce. prima di eseguire una ricerca più ampia. o per essere corretto corro a binario cerca uno per il file per vedere se esce. e un'altra ricerca disastrosa sul contenuto del file


0

Perché non mantenerlo semplice? Usa un array di strutture.

Quindi possiamo salvare le prime 5 cifre come costanti, quindi dimentica quelle per ora.

65535 è il massimo che può essere memorizzato in un numero a 16 bit e il numero massimo che possiamo avere è 99999, che si adatta al numero 17 bit con un massimo di 131071.

Usare i tipi di dati a 32 bit è uno spreco perché abbiamo solo bisogno di 1 bit di quei 16 bit extra ... quindi, possiamo definire una struttura che ha un booleano (o carattere) e un numero di 16 bit ..

Supponendo C / C ++

typedef struct _number {

    uint16_t number;
    bool overflow;
}Number;

Questa struttura occupa solo 3 byte e abbiamo bisogno di un array di 1000, quindi 3000 byte in totale. Abbiamo ridotto lo spazio totale del 25%!

Per quanto riguarda la memorizzazione dei numeri, possiamo fare semplici calcoli bit per bit

overflow = (number5digits & 0x10000) >> 4;
number = number5digits & 0x1111;

E l'inverso

//Something like this should work
number5digits = number | (overflow << 4);

Per stamparli tutti, possiamo usare un semplice loop sull'array. Il recupero di un numero specifico avviene ovviamente in tempo costante, poiché si tratta di un array.

for(int i=0;i<1000;i++) cout << const5digits << number5digits << endl;

Per cercare un numero, vorremmo un array ordinato. Quindi, quando i numeri vengono salvati, ordina l'array (sceglierei personalmente un ordinamento di unione, O (nlogn)). Ora per la ricerca, utilizzerei un approccio di tipo merge. Dividi l'array e vedi in quale rientra il nostro numero. Quindi chiama la funzione solo su quell'array. Fallo ricorsivamente fino a quando non hai una corrispondenza e restituisci l'indice, altrimenti non esiste e stampa un codice di errore. Questa ricerca sarebbe abbastanza veloce, e il caso peggiore è ancora migliore di O (nlogn) poiché verrà eseguito assolutamente in meno tempo rispetto all'ordinamento di unione (ricorre solo 1 lato della divisione ogni volta, invece di entrambi i lati :)), che è O (nlogn).


0

La mia soluzione: caso migliore 7.025 bit / numero, caso peggiore 14.193 bit / numero, media approssimativa 8.551 bit / numero. Codifica in streaming, nessun accesso casuale.

Anche prima di leggere la risposta di ruslik, ho subito pensato di codificare la differenza tra ogni numero, poiché sarà piccolo e dovrebbe essere relativamente coerente, ma la soluzione deve anche essere in grado di accogliere lo scenario peggiore. Abbiamo uno spazio di 100000 numeri che contengono solo 1000 numeri. In una rubrica telefonica perfettamente uniforme, ogni numero sarebbe maggiore di 100 rispetto al numero precedente:

55555-12 3 45
55555-12 4 45
55555-12 5 45

Se fosse così, richiederebbe zero memoria per codificare le differenze tra i numeri, poiché è una costante nota. Sfortunatamente, i numeri possono variare dai passi ideali di 100. Codificherei la differenza dall'incremento ideale di 100, in modo che se due numeri adiacenti differiscono di 103, codificherei il numero 3 e se due numeri adiacenti differiscono di 92, io codificherebbe -8. Chiamo il delta da un incremento ideale di 100 la " varianza ".

La varianza può variare da -99 (cioè due numeri consecutivi) a 99000 (l'intera rubrica è composta dai numeri 00000… 00999 e da un numero più lontano aggiuntivo 99999), che è un intervallo di 99100 valori possibili.

Mi piacerebbe lo scopo di assegnare un minimo di stoccaggio per codificare le differenze più comuni ed espandere la memoria, se io incontro differenze più grandi (come protobuf s’ varint). Userò blocchi di sette bit, sei per l'archiviazione e un bit flag aggiuntivo alla fine per indicare che questa varianza è memorizzata con un blocco aggiuntivo dopo quello corrente, fino a un massimo di tre blocchi (che fornirà un massimo di 3 * 6 = 18 bit di memoria, che sono 262144 valore possibile, più del numero di varianze possibili (99100). Ogni blocco aggiuntivo che segue un flag rialzato ha bit di importanza maggiore, quindi il primo blocco ha sempre bit 0- 5, il secondo blocco opzionale ha bit 6-11 e il terzo blocco opzionale ha bit 12-17.

Un singolo blocco fornisce sei bit di archiviazione che possono contenere 64 valori. Vorrei mappare le 64 varianze più piccole per adattarle a quel singolo blocco (cioè varianze da -32 a +31) quindi userò la codifica ProtoBuf ZigZag, fino alle varianze da -99 a +98 (poiché non è necessario per una varianza negativa oltre -99), a quel punto passerò alla codifica normale, offset di 98:  

Varianza | Valore codificato
----------- + ----------------
    0 | 0
   -1 | 1
    1 | 2
   -2 | 3
    2 | 4
   -3 | 5
    3 | 6
   ... | ...
  -31 | 61
   31 | 62
  -32 | 63
----------- | --------------- 6 bit
   32 | 64
  -33 | 65
   33 | 66
   ... | ...
  -98 | 195
   98 | 196
  -99 | 197
----------- | --------------- Fine di ZigZag
   100 | 198
   101 | 199
   ... | ...
  3996 | 4094
  3997 | 4095
----------- | --------------- 12 bit
  3998 | 4096
  3999 | 4097
   ... | ...
 262045 | 262143
----------- | --------------- 18 bit

Alcuni esempi di come le varianze verrebbero codificate come bit, incluso il flag per indicare un blocco aggiuntivo:

Varianza | Bit codificati
----------- + ----------------
     0 | 000000 0
     5 | 001010 0
    -8 | 001111 0
   -32 | 111111 0
    32 | 000000 1 000001 0
   -99 | 000101 1 000011 0
   177 | 010011 1 000100 0
 14444 | 001110 1 100011 1 000011 0

Quindi i primi tre numeri di un esempio di rubrica telefonica sarebbero codificati come un flusso di bit come segue:

BIN 000101001011001000100110010000011001 000110 1 010110 1 00001 0
PH # 55555-12345 55555-12448 55555-12491
POS 1 2 3

Nella migliore delle ipotesi , la rubrica è distribuita in modo un po 'uniforme e non ci sono due numeri di telefono con una varianza maggiore di 32, quindi userebbe 7 bit per numero più 32 bit per il numero iniziale per un totale di 32 + 7 * 999 = 7025 bit .
Uno scenario misto , in cui la varianza di 800 numeri di telefono rientra in un blocco (800 * 7 = 5600), 180 numeri si adattano a due blocchi ciascuno (180 * 2 * 7 = 2520) e 19 numeri si adattano a tre blocchi ciascuno (20 * 3 * 7 = 399), più i 32 bit iniziali, per un totale di 8551 bit .
Nel peggiore dei casi , 25 numeri si adattano a tre blocchi (25 * 3 * 7 = 525 bit) e i rimanenti 974 numeri si adattano a due blocchi (974 * 2 * 7 = 13636 bit), più 32 bit per il primo numero per un grande totale di14193 bit .

   Quantità di numeri codificati |
 1 pezzo | 2 pezzi | 3 pezzi | Bit totali
--------- + ---------- + ---------- + ------------
   999 | 0 | 0 | 7025
   800 | 180 | 19 | 8551
    0 | 974 | 25 | 14193

Vedo quattro ulteriori ottimizzazioni che possono essere eseguite per ridurre ulteriormente lo spazio richiesto:

  1. Il terzo blocco non ha bisogno dei sette bit completi, può essere solo di cinque bit e senza un bit flag.
  2. Può esserci un passaggio iniziale dei numeri per calcolare le dimensioni migliori per ogni blocco. Forse per una certa rubrica, sarebbe ottimale avere il primo blocco con 5 + 1 bit, il secondo 7 + 1 e il terzo 5 + 1. Ciò ridurrebbe ulteriormente la dimensione a un minimo di 6 * 999 + 32 = 6026 bit, più due serie di tre bit per memorizzare le dimensioni dei blocchi 1 e 2 (la dimensione del blocco 3 è il resto dei 16 bit richiesti) per un totale di 6032 bit!
  3. Lo stesso passaggio iniziale può calcolare un incremento previsto migliore rispetto al valore predefinito 100. Forse c'è una rubrica telefonica che inizia da 55555-50000 e quindi ha metà dell'intervallo numerico, quindi l'incremento previsto dovrebbe essere 50. O forse c'è un non lineare la distribuzione (forse la deviazione standard) e qualche altro incremento atteso ottimale può essere utilizzato. Ciò ridurrebbe la varianza tipica e potrebbe consentire l'utilizzo di un primo blocco ancora più piccolo.
  4. Ulteriori analisi possono essere eseguite nel primo passaggio per consentire il partizionamento della rubrica, con ogni partizione con il proprio incremento previsto e ottimizzazioni della dimensione del blocco. Ciò consentirebbe una dimensione del primo blocco più piccola per alcune parti altamente uniformi della rubrica telefonica (riducendo il numero di bit consumati) e dimensioni di blocchi più grandi per parti non uniformi (riducendo il numero di bit sprecati in flag di continuazione).

0

La vera domanda è memorizzare i numeri di telefono a cinque cifre.

Il trucco è che avresti bisogno di 17 bit per memorizzare l'intervallo di numeri da 0 a 99.999. Ma memorizzare 17 bit su confini di parola convenzionali a 8 byte è una seccatura. Ecco perché chiedono se puoi fare in meno di 4k non usando numeri interi a 32 bit.

Domanda: sono possibili tutte le combinazioni di numeri?

A causa della natura del sistema telefonico, potrebbero esserci meno di 65.000 combinazioni possibili. Presumo che perché stiamo parlando di queste ultime cinque posizioni nel numero di telefono, al contrario del prefisso o dei prefissi di scambio.

Domanda: questo elenco sarà statico o dovrà supportare gli aggiornamenti?

Se è statico , quando arriva il momento di popolare il database, conta il numero di cifre <50.000 e il numero di cifre> = 50.000. Allocare due array di uint16lunghezza appropriata: uno per gli interi inferiori a 50.000 e uno per l'insieme superiore. Quando si memorizzano i numeri interi nell'array superiore, sottrarre 50.000 e quando si leggono gli interi da quell'array, aggiungere 50.000. Ora hai memorizzato i tuoi 1.000 numeri interi in 2.000 parole da 8 byte.

La creazione della rubrica richiederà due attraversamenti di input, ma le ricerche dovrebbero essere eseguite nella metà del tempo, in media, rispetto a un singolo array. Se il tempo di ricerca fosse molto importante, potresti usare più array per intervalli più piccoli ma penso che a queste dimensioni il tuo limite di prestazioni estrarrebbe gli array dalla memoria e 2k probabilmente si nasconderà nella cache della CPU se non registrerai lo spazio su qualsiasi cosa che useresti questi giorni.

Se è dinamico , alloca un array di 1000 o giù di lì uint16e aggiungi i numeri in ordine ordinato. Imposta il primo byte su 50.001 e imposta il secondo byte su un valore nullo appropriato, come NULL o 65.000. Quando si memorizzano i numeri, memorizzarli in ordine ordinato. Se un numero è inferiore a 50.001, memorizzalo prima del contrassegno di 50.001. Se un numero è 50.001 o maggiore, memorizzalo dopo il contrassegno 50.001, ma sottrai 50.000 dal valore memorizzato.

Il tuo array sarà simile a:

00001 = 00001
12345 = 12345
50001 = reserved
00001 = 50001
12345 = 62345
65000 = end-of-list

Quindi, quando cerchi un numero nella rubrica, attraverserai l'array e se hai raggiunto il valore 50.001 inizi ad aggiungere 50.000 ai valori dell'array.

Questo rende gli inserti molto costosi, ma le ricerche sono facili e non spenderai molto di più di 2k per l'archiviazione.

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.