Die effizienteste Art, tausend Telefonnummern zu speichern

93

Dies ist eine Frage zum Google-Interview:

Es müssen ungefähr tausend Telefonnummern mit jeweils 10 Ziffern gespeichert werden. Sie können davon ausgehen, dass die ersten 5 Ziffern über tausend Zahlen hinweg gleich sind. Sie müssen die folgenden Vorgänge ausführen: a. Suchen Sie, ob eine bestimmte Nummer vorhanden ist. b. Drucken Sie die ganze Nummer

Was ist der effizienteste platzsparende Weg, um dies zu tun?

Ich antwortete auf die Hash-Tabelle und später auf die Huffman-Codierung, aber mein Interviewer sagte, ich gehe nicht in die richtige Richtung. Bitte helfen Sie mir hier.

Könnte die Verwendung eines Suffixes helfen?

Idealerweise benötigt das Speichern von 1000 Nummern 4 Bytes pro Nummer, sodass insgesamt 4000 Bytes zum Speichern von 1000 Nummern benötigt werden. Quantitativ möchte ich den Speicher auf <4000 Bytes reduzieren, erklärte mir mein Interviewer.

Prinzessin von Persien
quelle
28
Ich würde antworten, dass Sie mit einer normalen Datenbank diese als Text speichern können, sogar Tausende / Millionen, und Suchvorgänge immer noch sehr schnell sind. Ich werde davon abraten, "clevere" Dinge zu tun, da das gesamte System überarbeitet werden muss, wenn in Zukunft internationale Nummern unterstützt werden sollen oder wenn Telefonnummern erscheinen, die mit einer "0" beginnen, oder wenn die Regierung dies beschließt Ändern Sie das Telefonnummernformat und so weiter.
Thomas Bonini
1
@AndreasBonini: Ich würde diese Antwort wahrscheinlich geben, wenn ich nicht bei einem Unternehmen wie Google oder Facebook interviewt hätte. Obwohl Postgres zum Beispiel auch Versuche hat, wäre ich mir nicht sicher, ob diese den Datendurchsatz reduzieren, den Google benötigt.
LiKao
1
@LiKao: Denken Sie daran, dass das OP ausdrücklich "rund tausend Zahlen" angegeben hat
Thomas Bonini
@AndreasBonini: Richtig, könnte auch ein Test gewesen sein, bei dem der Befragte weiß, dass er solche Einschränkungen richtig interpretiert und die beste Lösung entsprechend auswählt.
LiKao
4
"effizient" in dieser Frage muss wirklich definiert werden - effizient auf welche Weise? Raum, Zeit, beides?
Matt B

Antworten:

36

Hier ist eine Verbesserung der Antwort von aix . Erwägen Sie die Verwendung von drei "Schichten" für die Datenstruktur: Die erste ist eine Konstante für die ersten fünf Ziffern (17 Bit); Von nun an hat jede Telefonnummer nur noch die verbleibenden fünf Ziffern. Wir betrachten diese verbleibenden fünf Ziffern als 17-Bit-Binärzahlen und speichern k dieser Bits mit einer Methode und 17 - k = m mit einer anderen Methode, wobei wir am Ende k bestimmen, um den erforderlichen Speicherplatz zu minimieren.

Wir sortieren zuerst die Telefonnummern (alle auf 5 Dezimalstellen reduziert). Dann zählen wir, wie viele Telefonnummern es gibt, für die die aus den ersten m Bits bestehende Binärzahl alle 0 ist, für wie viele Telefonnummern die ersten m Bits höchstens 0 ... 01 sind, für wie viele Telefonnummern die ersten m Bits sind höchstens 0 ... 10 usw. bis zur Anzahl der Telefonnummern, für die die ersten m Bits 1 ... 11 sind - diese letzte Anzahl ist 1000 (dezimal). Es gibt 2 ^ m solcher Zählungen und jede Zählung ist höchstens 1000. Wenn wir die letzte weglassen (weil wir wissen, dass es sowieso 1000 ist), können wir alle diese Zahlen in einem zusammenhängenden Block von (2 ^ m - 1) speichern. * 10 Bits. (10 Bit reichen aus, um eine Zahl unter 1024 zu speichern.)

Die letzten k Bits aller (reduzierten) Telefonnummern werden zusammenhängend im Speicher gespeichert; Wenn also k beispielsweise 7 ist, entsprechen die ersten 7 Bits dieses Speicherblocks (Bits 0 bis 6) den letzten 7 Bits der ersten (reduzierten) Telefonnummer, die Bits 7 bis 13 entsprechen den letzten 7 Bits der zweiten (reduzierten) Telefonnummer usw. Dies erfordert 1000 * k Bits für insgesamt 17 + (2 ^ (17 - k ) - 1) * 10 + 1000 * k , was sein Minimum von 11287 für k = 10 erreicht. Wir können also alle Telefonnummern in Ceil speichern ( 11287/8) = 1411 Bytes.

Zusätzlicher Platz kann gespart werden, indem beobachtet wird, dass keine unserer Zahlen mit z. B. 1111111 (binär) beginnen kann, da die niedrigste Zahl, die damit beginnt, 130048 ist und wir nur fünf Dezimalstellen haben. Dies ermöglicht es uns, einige Einträge aus dem ersten Speicherblock zu entfernen : Anstelle von 2 ^ m - 1 Zählungen benötigen wir nur Ceil (99999/2 ^ k ). Das heißt, die Formel wird

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

was erstaunlicherweise sein Minimum von 10997 sowohl für k = 9 als auch für k = 10 oder Ceil (10997/8) = 1375 Bytes erreicht.

Wenn wir wissen möchten, ob eine bestimmte Telefonnummer in unserem Set enthalten ist, prüfen wir zunächst, ob die ersten fünf Binärziffern mit den fünf von uns gespeicherten Ziffern übereinstimmen. Dann teilen wir die verbleibenden fünf Ziffern in die oberen m = 7 Bits (dh die m- Bit-Zahl M ) und die unteren k = 10 Bits (die Zahl K ) auf. Wir finden nun die Nummer a [M-1] reduzierter Telefonnummern, für die die ersten m Ziffern höchstens M - 1 sind, und die Nummer a [M] reduzierter Telefonnummern, für die die ersten m Ziffern höchstens M sind. beide aus dem ersten Bitblock. Wir prüfen nun zwischen dem a[M-1] -te und eine [M] -te Folge von k Bits im zweiten Speicherblock, um zu sehen, ob wir K finden ; Im schlimmsten Fall gibt es 1000 solcher Sequenzen. Wenn wir also die binäre Suche verwenden, können wir mit O-Operationen (log 1000) fertig werden.

Pseudo - Code für alle 1000 Nummern Druck folgt, wobei I die Zugriff K 'th k -Bit - Eintrag des ersten Speicherblock als ein [K] und der M -te m -Bit - Eintrag des zweiten Blocks des Speichers als b [M] (Beides würde einige Bitoperationen erfordern, deren Ausschreiben mühsam ist). Die ersten fünf Ziffern sind in der Nummer 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;

Vielleicht stimmt etwas mit dem Grenzfall für K = Ceil (99999/2 ^ k ) nicht, aber das ist leicht zu beheben.

Schließlich ist es aus entropischer Sicht nicht möglich, eine Teilmenge von 10 ^ 3 positiven ganzen Zahlen, die alle kleiner als 10 ^ 5 sind, in weniger als Ceil zu speichern (log [2] (Binomial (10 ^ 5, 10 ^ 3)). ) = 8073. Einschließlich der 17, die wir für die ersten 5 Ziffern benötigen, gibt es immer noch eine Lücke von 10997 - 8090 = 2907 Bits. Es ist eine interessante Herausforderung zu sehen, ob es bessere Lösungen gibt, bei denen Sie noch relativ effizient auf die Nummern zugreifen können!

Erik P.
quelle
4
Die Datenstruktur, die Sie hier beschreiben, ist eigentlich nur eine sehr effiziente Version von trie, die nur so wenig wie für die Indizierung benötigt und nur zwei Ebenen verwendet. In der Praxis wäre es schön zu sehen, ob dies einen Versuch mit mehr Levels schlagen kann, aber ich denke, dies hängt stark von der Verteilung der Nummern ab (in realen Live-Telefonnummern sind sie nicht vollständig zufällig, sondern nur fast).
LiKao
Hallo Erik, da du gesagt hast, dass du an anderen Alternativen interessiert bist, schau dir meine Lösung an. Es löst es in 8.580 Bit, was nur 490 Bit vom theoretischen Minimum abweicht. Es ist etwas ineffizient, einzelne Nummern nachzuschlagen, aber der Speicher ist sehr kompakt.
Briguy37
1
Ich würde annehmen, dass ein vernünftiger Interviewer die Antwort "a trie" anstelle von "einer komplexen maßgeschneiderten Datenbank" vorziehen würde. Wenn Sie Ihre 133t-Hacking-Fähigkeiten unter Beweis stellen möchten, können Sie hinzufügen: "Es wäre möglich, einen bestimmten Baumalgorithmus für diesen speziellen Fall zu erstellen, falls erforderlich."
KarlP
Hallo, können Sie bitte erklären, wie 5 Ziffern 17 Bits benötigen, um gespeichert zu werden?
Tushar Banne
@tushar Fünf Ziffern kodieren eine Zahl zwischen 00000 und 99999 einschließlich. Stellen Sie diese Zahl binär dar. 2 ^ 17 = 131072, also reichen 17 Bits dafür aus, 16 jedoch nicht.
Erik P.
43

Im Folgenden behandle ich die Zahlen als ganzzahlige Variablen (im Gegensatz zu Zeichenfolgen):

  1. Sortieren Sie die Zahlen.
  2. Teilen Sie jede Zahl in die ersten fünf Ziffern und die letzten fünf Ziffern auf.
  3. Die ersten fünf Ziffern sind für alle Zahlen gleich. Speichern Sie sie daher nur einmal. Dies erfordert 17 Bit Speicher.
  4. Speichern Sie die letzten fünf Ziffern jeder Nummer einzeln. Dies erfordert 17 Bits pro Nummer.

Um es noch einmal zusammenzufassen: Die ersten 17 Bits sind das gemeinsame Präfix, die nachfolgenden 1000 Gruppen von 17 Bits sind die letzten fünf Ziffern jeder Nummer, die in aufsteigender Reihenfolge gespeichert sind.

Insgesamt betrachten wir 2128 Bytes für die 1000 Nummern oder 17.017 Bits pro 10-stelliger Telefonnummer.

Suche ist O(log n)(binäre Suche) und vollständige Aufzählung ist O(n).

NPE
quelle
Ähm, wo ist die Raumkomplexität?
Aioobe
Zu viel Zeit zum Erstellen (O (log (n) * n k) (k ist die Länge) für die Sortierung im Vergleich zu O (n k) zum Erstellen eines Versuchs). Auch der Platz ist alles andere als optimal, da längere gemeinsame Präfixe einzeln gespeichert werden. Die Suchzeit ist auch nicht optimal. Bei solchen Zeichenfolgendaten kann man leicht die Länge der Zahlen vergessen, die die Suche dominiert. Das heißt, die binäre Suche ist O (log (n) * k), während ein Versuch nur O (k) benötigt. Sie können diese Ausdrücke reduzieren, wenn k konstant ist. Dies soll jedoch ein allgemeines Problem darstellen, wenn Sie über Datenstrukturen nachdenken, in denen Zeichenfolgen gespeichert sind.
LiKao
@LiKao: Wer hat etwas über Streicher gesagt? Ich beschäftige mich ausschließlich mit ganzzahligen Variablen, kist also irrelevant.
NPE
1
Ok, ich habe die Antwort dann falsch verstanden. Gemeinsame Teile werden jedoch nicht zusammen gespeichert, sodass der Punkt zur Raumeffizienz bestehen bleibt. Für 1000 5-stellige Zahlen gibt es eine ganze Reihe gängiger Präfixe, daher hilft es sehr, diese zu reduzieren. Auch bei Zahlen haben wir O (log (n)) gegen O (k) für Strings, was immer noch schneller ist.
LiKao
1
@Geek: 1001 Gruppen von 17 Bits sind 17017 Bits oder 2128 Bytes (mit einigen Änderungen).
NPE
22

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

Ich hatte einmal ein Interview, in dem sie nach Datenstrukturen fragten. Ich habe "Array" vergessen.

Mikhail
quelle
1
+1 das ist definitiv der richtige Weg. Ich habe diesen unter einem anderen Namen, einem Bibliotheksbaum oder einem lexikalischen Suchbaum oder so etwas gelernt, als ich Student war (wenn sich jemand an diesen alten Namen erinnert, sagen Sie es bitte).
Valmond
6
Dies erfüllt nicht die 4000-Byte-Anforderung. Allein für die Zeigerspeicherung benötigen Sie im schlimmsten Fall 1 Zeiger für die 1-4. Blätter zur nächsten Ebene, 10 Zeiger für die 5., 100 für die 6. und 1000 für die 7., 8. und 9. Ebene Dies ergibt mindestens 3114 verschiedene Speicherstellen, auf die Zeiger zeigen müssen, was bedeutet, dass Sie für jeden Zeiger mindestens 12 Bit benötigen. 12 * 3114 = 37368 Bits = 4671 Bytes> 4000 Bytes, und das zeigt nicht einmal, wie Sie den Wert jedes Blattes darstellen!
Briguy37
16

Ich würde wahrscheinlich in Betracht ziehen, eine komprimierte Version eines Trie zu verwenden (möglicherweise eine DAWG, wie von @Misha vorgeschlagen).

Das würde automatisch die Tatsache ausnutzen, dass sie alle ein gemeinsames Präfix haben.

Die Suche wird in konstanter Zeit durchgeführt, und das Drucken wird in linearer Zeit durchgeführt.

aioobe
quelle
Die Frage ist, wie die Daten am platzsparendsten gespeichert werden können. Würde es Ihnen etwas ausmachen, eine Schätzung darüber abzugeben, wie viel Speicherplatz diese Methode für die 1000 Telefonnummern benötigen würde? Vielen Dank.
NPE
Der Platz für den Versuch ist höchstens O (n * k), wobei n die Anzahl der Zeichenfolgen und k die Länge jeder Zeichenfolge ist. Unter Berücksichtigung der Tatsache, dass Sie keine 8-Bit-Zeichen benötigen, um Zahlen darzustellen, würde ich empfehlen, 4 hexadezimale Hexadezimalindizes und einen für das verbleibende Bit zu speichern. Auf diese Weise benötigen Sie maximal 17 Bit pro Nummer. Da Sie mit dieser Codierung in allen Fällen auf allen Ebenen zu Konflikten kommen, können Sie diese tatsächlich unterschreiten. In der Erwartung, dass wir 1000 Zahlen speichern, können wir bereits insgesamt 250 Bit für die Konflikte auf der ersten Ebene speichern. Testen Sie am besten die korrekte Codierung anhand von Beispieldaten.
LiKao
@LiKao, richtig, und wenn Sie zum Beispiel feststellen, dass 1000 Zahlen nicht mehr als 100 verschiedene letzte zwei Ziffern haben können, könnte der Versuch auf den letzten Ebenen erheblich reduziert werden.
Aioobe
@aioobe: Die Blätter könnten auf der letzten Ebene kollabiert werden, weil es keine Kinder gibt. Die Blätter auf der vorletzten Ebene benötigen jedoch 2 ^ 10 = 1024 Zustände (jede letzte Ziffer kann ein- oder ausgeschaltet sein), daher ist sie in diesem Fall nicht reduzierbar, da es nur 1000 Zahlen gibt. Dies bedeutet, dass die Anzahl der Worst-Case-Zeiger bei 3114 bleibt (siehe meinen Kommentar zu Mischas Antwort), während die benötigten Blätter auf 5 + 10 + 100 + 1000 + 1000 + 10 = 2125 gehen, wodurch die jeweils benötigten 12 Bytes nicht geändert werden Zeiger. Somit ergibt dies immer noch eine Versuchslösung von 4671 Bytes, wenn nur Zeiger allein berücksichtigt werden.
Briguy37
@ Briguy37, ich bin mir nicht sicher, ob ich dein Argument " Jede letzte Ziffer könnte ein- oder ausgeschaltet sein " bekomme . Alle Zahlen sind 10 Ziffern lang, oder?
Aioobe
15

Ich habe schon einmal von diesem Problem gehört (aber ohne die Annahme, dass die ersten 5 Ziffern gleich sind), und der einfachste Weg, dies zu tun, war Rice Coding :

1) Da die Reihenfolge keine Rolle spielt, können wir sie sortieren und nur Unterschiede zwischen aufeinanderfolgenden Werten speichern. In unserem Fall wären die durchschnittlichen Unterschiede 100.000 / 1000 = 100

2) Codieren Sie die Unterschiede mit Reiscodes (Basis 128 oder 64) oder sogar Golomb-Codes (Basis 100).

EDIT: Eine Schätzung für die Rice-Codierung mit Basis 128 (nicht weil dies die besten Ergebnisse liefern würde, sondern weil es einfacher zu berechnen ist):

Wir speichern den ersten Wert wie er ist (32 Bit).
Die restlichen 999 Werte sind Unterschiede (wir erwarten, dass sie klein sind, durchschnittlich 100) und enthalten:

unärer Wert value / 128(variable Anzahl von Bits + 1 Bit als Abschlusszeichen)
Binärwert für value % 128(7 Bits)

Wir müssen die Grenzen (nennen wir es VBL) für die Anzahl der variablen Bits irgendwie schätzen :
untere Grenze: Bedenken Sie, dass wir Glück haben und kein Unterschied größer ist als unsere Basis (in diesem Fall 128). Dies würde bedeuten, 0 zusätzliche Bits zu geben.
Obergrenze: Da alle Unterschiede, die kleiner als die Basis sind, im binären Teil der Zahl codiert werden, müssten wir maximal 100000/128 = 781,25 codieren (noch weniger, da wir nicht erwarten, dass die meisten Unterschiede Null sind ).

Das Ergebnis ist also 32 + 999 * (1 + 7) + variable (0..782) Bits = 1003 + variable (0..98) Bytes.

ruslik
quelle
Können Sie weitere Details zur Art und Weise der Codierung und zur endgültigen Größenberechnung angeben? 1101 Bytes oder 8808 Bits scheinen sehr nahe an der theoretischen Grenze von 8091 Bits zu liegen, daher bin ich sehr überrascht, dass es möglich ist, so etwas in der Praxis zu erreichen.
LiKao
Wäre es nicht 32 + 999 * (1 + 7 + variable(0..782))Bits? Jede der 999 Nummern benötigt eine Darstellung von value / 128.
Kirk Broadhurst
1
@ Kirk: Nein, wenn alle im Bereich von 5 Ziffern liegen. Dies liegt daran, dass wir erwarten würden, dass die Summe all dieser Unterschiede (denken Sie daran, wir codieren Unterschiede zwischen aufeinanderfolgenden Werten, nicht zwischen dem ersten und dem n-ten Wert) unter 100000 liegt (selbst im schlimmsten Fall)
ruslik
Sie benötigen 34 Bit anstelle von 32 Bit, um den ersten Wert darzustellen (9.999.999.999> 2 ^ 32 = 4.294.967.296). Die maximale Differenz wäre auch 00000 bis 99001, da die Zahlen eindeutig sind, wodurch 774 1 anstelle von 782 für Basis 128 hinzugefügt werden. Ihr Speicherbereich für 1.000 Zahlen für Basis 128 beträgt somit 8026-8800 Bit oder 1004-1100 Byte. Die 64-Bit-Basis bietet eine bessere Speicherkapazität mit Bereichen von 879 bis 1072 Byte.
Briguy37
1
@raisercostin: das hat Kirk gefragt. In Ihrem Beispiel können durch einmaliges Codieren der 20k-Differenz zwischen den ersten beiden Werten in Zukunft nur noch 80k des maximalen Bereichs auftreten. Dies wird 20k / 128 = 156 unäre Bits von maximal 782 (die 100k entsprechen)
verbrauchen
7

Dies ist ein bekanntes Problem aus Bentleys Programmierperlen.

Lösung: Entfernen Sie die ersten fünf Ziffern von den Zahlen, da sie für jede Zahl gleich sind. Verwenden Sie dann bitweise Operationen, um den verbleibenden 9999 möglichen Wert darzustellen. Sie benötigen nur 2 ^ 17 Bits, um die Zahlen darzustellen. Jedes Bit repräsentiert eine Zahl. Wenn das Bit gesetzt ist, befindet sich die Nummer im Telefonbuch.

Um alle Zahlen zu drucken, drucken Sie einfach alle Zahlen, bei denen das Bit gesetzt ist, verkettet mit dem Präfix. Um nach einer bestimmten Zahl zu suchen, führen Sie die erforderliche Bitarithmetik durch, um die bitweise Darstellung der Zahl zu überprüfen.

Sie können in O (1) nach einer Zahl suchen, und die Raumeffizienz ist aufgrund der Bitrepräsentation maximal.

HTH Chris.

Chris
quelle
3
Dies wäre ein guter Ansatz für einen dichten Satz von Zahlen. Leider ist das Set hier sehr spärlich: Es gibt nur 1.000 von 100.000 möglichen Zahlen. Dieser Ansatz würde daher im Durchschnitt 100 Bits pro Nummer erfordern. In meiner Antwort finden Sie eine Alternative, die nur ~ 17 Bit benötigt.
NPE
1
Wäre die Zeit, die zum Drucken aller Zahlen benötigt wird, nicht proportional zu 100.000 statt zu 1.000?
Aioobe
Wenn Sie die beiden Ideen kombinieren, erhalten Sie im Grunde sofort den Versuch. Die Verwendung eines Bitvektors mit 100.000 Einträgen ist eine Gesamtzuordnung und nimmt zu viel Platz ein. Die Suche nach O (log (n)) ist jedoch häufig zu langsam (abhängig von der Anzahl der Abfragen hier). Wenn Sie also eine Hierarchie von Bitsätzen für die Indizierung verwenden, werden maximal 17 Bits pro Nummer gespeichert, während Sie weiterhin die O (1) -Suche erhalten. So funktioniert der Versuch. Auch die Zeit zum Drucken ist in O (n) für den Versuch, den er vom sortierten Fall erbt.
LiKao
Dies ist nicht "der effizienteste platzsparende Weg, dies zu tun".
Jake Berger
5

Feste Speicherung von 1073 Bytes für 1.000 Nummern:

Das Grundformat dieser Speichermethode besteht darin, die ersten 5 Ziffern, eine Anzahl für jede Gruppe und den Versatz für jede Zahl in jeder Gruppe zu speichern.

Präfix:
Unser 5-stelliges Präfix nimmt die ersten 17 Bits ein .

Gruppierung:
Als nächstes müssen wir eine Gruppierung mit guter Größe für Zahlen herausfinden. Versuchen wir, ungefähr 1 Nummer pro Gruppe zu haben. Da wir wissen, dass ungefähr 1000 Nummern gespeichert werden müssen, teilen wir 99.999 in ungefähr 1000 Teile auf. Wenn wir die Gruppengröße als 100 wählen würden, würden Bits verschwendet. Versuchen wir also eine Gruppengröße von 128, die mit 7 Bits dargestellt werden kann. Dies gibt uns 782 Gruppen, mit denen wir arbeiten können.


Anzahl : Als nächstes müssen wir für jede der 782 Gruppen die Anzahl der Einträge in jeder Gruppe speichern. Eine 7-Bit-Zählung für jede Gruppe würde ergeben 7*782=5,474 bits, was sehr ineffizient ist, da die durchschnittliche dargestellte Anzahl aufgrund der Auswahl unserer Gruppen etwa 1 beträgt.

Daher haben wir stattdessen Zählungen variabler Größe mit führenden Einsen für jede Zahl in einer Gruppe, gefolgt von einer 0. Wenn wir also xZahlen in einer Gruppe hätten, hätten wir x 1'seine 0Zählung gefolgt , um die Zählung darzustellen. Wenn wir zum Beispiel 5 Zahlen in einer Gruppe hätten, würde die Anzahl durch dargestellt 111110. Wenn bei dieser Methode 1.000 Zahlen vorhanden sind, erhalten wir 1000 Einsen und 782 Nullen für insgesamt 1000 + 782 = 1.782 Bits für die Zählungen .

Versatz:
Zuletzt ist das Format jeder Zahl nur der 7-Bit-Versatz für jede Gruppe. Wenn beispielsweise 00000 und 00001 die einzigen Zahlen in der Gruppe 0-127 sind, wären die Bits für diese Gruppe 110 0000000 0000001. Unter der Annahme von 1.000 Zahlen gibt es 7.000 Bits für die Offsets .

Daher lautet unsere endgültige Zählung unter der Annahme von 1.000 Zahlen wie folgt:

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

Lassen Sie uns nun überprüfen, ob unsere Auswahl der Gruppengröße durch Aufrunden auf 128 Bit die beste Wahl für die Gruppengröße war. Wenn Sie xdie Anzahl der Bits für jede Gruppe auswählen, lautet die Formel für die Größe:

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

Das Minimieren dieser Gleichung für ganzzahlige Werte von xergibt x=6, was 8.580 Bits = 1.073 Bytes ergibt . Daher ist unser idealer Speicher wie folgt:

  • Gruppengröße: 2 ^ 6 = 64
  • Anzahl der Gruppen: 1.562
  • Gesamtspeicher:

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

Briguy37
quelle
1

Wenn man dies als rein theoretisches Problem betrachtet und die Implementierung unter Kontrolle lässt, besteht der effizienteste Weg darin, alle möglichen Sätze von 10000 letzten Ziffern in einer riesigen Indizierungstabelle zu indizieren. Angenommen, Sie haben genau 1000 Zahlen, würden Sie etwas mehr als 8000 Bits benötigen, um den aktuellen Satz eindeutig zu identifizieren. Es ist keine größere Komprimierung möglich, da Sie dann zwei Sätze haben würden, die mit demselben Status identifiziert werden.

Das Problem dabei ist, dass Sie jeden der 2 ^ 8000 Sätze in Ihrem Programm als Lut darstellen müssten und nicht einmal Google dazu in der Lage wäre.

Die Suche wäre O (1) und druckt alle Zahlen O (n). Die Einfügung wäre O (2 ^ 8000), was theoretisch O (1) ist, in der Praxis jedoch unbrauchbar ist.

In einem Interview würde ich diese Antwort nur geben, wenn ich sicher wäre, dass das Unternehmen jemanden sucht, der in der Lage ist, viel über den Tellerrand hinauszudenken. Andernfalls könnten Sie wie ein Theoretiker aussehen, der keine Bedenken in der realen Welt hat.

EDIT : Ok, hier ist eine "Implementierung".

Schritte zum Erstellen der Implementierung:

  1. Nehmen Sie ein konstantes Array mit einer Größe von 100 000 * (1000 wählen Sie 100 000) Bits. Ja, ich bin mir der Tatsache bewusst, dass dieses Array um mehrere Größenordnungen mehr Platz als Atome im Universum benötigt.
  2. Teilen Sie dieses große Array in Stücke von jeweils 100 000 auf.
  3. In jedem Chunk-Speicher ein Bit-Array für eine bestimmte Kombination der letzten fünf Ziffern.

Dies ist nicht das Programm, sondern eine Art Metaprogramm, das eine gigantische LUT erstellt, die jetzt in einem Programm verwendet werden kann. Konstante Programmdaten werden bei der Berechnung der Raumeffizienz normalerweise nicht berücksichtigt, daher ist uns dieses Array bei unseren endgültigen Berechnungen egal.

So verwenden Sie diese LUT:

  1. Wenn Ihnen jemand 1000 Zahlen gibt, speichern Sie die ersten fünf Ziffern separat.
  2. Finden Sie heraus, welcher der Blöcke Ihres Arrays zu diesem Satz passt.
  3. Speichern Sie die Nummer des Satzes in einer einzelnen 8074-Bit-Nummer (nennen Sie dies c).

Dies bedeutet, dass wir für die Speicherung nur 8091 Bit benötigen, was sich hier als optimale Codierung erwiesen hat. Das Finden des richtigen Blocks erfordert jedoch O (100 000 * (100 000 wählen 1000)), was nach mathematischen Regeln O (1) ist, in der Praxis jedoch immer länger dauert als die Zeit des Universums.

Die Suche ist jedoch einfach:

  1. Streifen der ersten fünf Ziffern (die verbleibende Nummer wird n 'genannt).
  2. Testen Sie, ob sie übereinstimmen
  3. Berechne i = c * 100000 + n '
  4. Überprüfen Sie, ob das Bit bei i in der LUT auf eins gesetzt ist

Das Drucken aller Zahlen ist ebenfalls einfach (und nimmt tatsächlich O (100000) = O (1) an, da Sie immer alle Bits des aktuellen Blocks überprüfen müssen, also habe ich dies oben falsch berechnet).

Ich würde dies nicht als "Implementierung" bezeichnen, da die Einschränkungen (Größe des Universums und Zeit, in der dieses Universum gelebt hat oder diese Erde existieren wird) offensichtlich missachtet werden. Theoretisch ist dies jedoch die optimale Lösung. Bei kleineren Problemen kann dies tatsächlich durchgeführt werden und wird manchmal auch durchgeführt. Zum Beispiel Sortiernetze sind ein Beispiel für diese Art der Codierung und können als letzten Schritt in rekursiven Sortieralgorithmen verwendet werden, um eine möglichst hohe Geschwindigkeit zu bekommen.

LiKao
quelle
1
Was ist der effizienteste platzsparende Weg, um dies zu tun?
Sven
1
Bei der Berechnung des Laufzeitraums kann dies leicht als die effizienteste platzsparende Methode erwiesen werden, da Sie jeden möglichen Status des Systems mit nur einer Nummer auflisten. Für dieses Problem kann es keine kleinere Codierung geben. Der Trick für diese Antwort ist, dass die Programmgröße bei den Berechnungen fast nie berücksichtigt wird (versuchen Sie, eine Antwort zu finden, die dies berücksichtigt, und Sie werden sehen, was ich meine). Für jedes Problem, dessen Größe begrenzt ist, können Sie immer alle Status auflisten, um die platzsparendste Methode zu erhalten.
LiKao
1

Dies entspricht dem Speichern von tausend nicht negativen Ganzzahlen mit jeweils weniger als 100.000. Wir können dazu so etwas wie eine arithmetische Codierung verwenden.

Letztendlich werden die Nummern in einer sortierten Liste gespeichert. Ich stelle fest, dass der erwartete Unterschied zwischen benachbarten Zahlen in der Liste 100.000 / 1000 = 100 beträgt, was in 7 Bits dargestellt werden kann. Es wird auch viele Fälle geben, in denen mehr als 7 Bits erforderlich sind. Eine einfache Möglichkeit, diese weniger häufigen Fälle darzustellen, besteht darin, das utf-8-Schema zu verwenden, bei dem ein Byte eine 7-Bit-Ganzzahl darstellt, sofern nicht das erste Bit gesetzt ist. In diesem Fall wird das nächste Byte gelesen, um eine 14-Bit-Ganzzahl zu erzeugen, es sei denn Das erste Bit wird gesetzt. In diesem Fall wird das nächste Byte gelesen, um eine 21-Bit-Ganzzahl darzustellen.

So kann mindestens die Hälfte der Unterschiede zwischen aufeinanderfolgenden ganzen Zahlen mit einem Byte dargestellt werden, und fast alle anderen erfordern zwei Bytes. Einige Zahlen, die durch größere Unterschiede als 16.384 getrennt sind, benötigen drei Bytes, von denen jedoch nicht mehr als 61 vorhanden sein können. Der durchschnittliche Speicher beträgt dann ungefähr 12 Bit pro Nummer oder etwas weniger oder höchstens 1500 Bytes.

Der Nachteil dieses Ansatzes ist, dass die Überprüfung der Existenz einer Zahl jetzt O (n) ist. Es wurde jedoch keine zeitliche Komplexitätsanforderung angegeben.

Nach dem Schreiben bemerkte ich, dass Ruslik bereits die oben genannte Differenzmethode vorgeschlagen hatte. Der einzige Unterschied ist das Codierungsschema. Meins ist wahrscheinlich einfacher, aber weniger effizient.

Crosbie
quelle
1

Nur um schnell nach einem Grund zu fragen, warum wir die Zahlen nicht in eine Basis 36 ändern möchten. Es spart vielleicht nicht so viel Platz, aber es würde sicher Zeit bei der Suche sparen, da Sie viel weniger als 10 Punkte betrachten werden. Oder ich würde sie je nach Gruppe in Dateien aufteilen. Also würde ich eine Datei (111) -222.txt benennen und dann nur Zahlen speichern, die in diese Gruppe passen, und sie dann in numerischer Reihenfolge durchsuchbar machen. Auf diese Weise kann ich immer chacken, um zu sehen, ob die Datei beendet wird. bevor ich eine größere Suche durchführe. oder um richtig zu sein, würde ich eine binäre Suche nach der Datei durchführen, um zu sehen, ob sie beendet wird. und eine weitere Bonary-Suche nach dem Inhalt der Datei

WojonsTech
quelle
0

Warum nicht einfach halten? Verwenden Sie ein Array von Strukturen.

Wir können also die ersten 5 Ziffern als Konstante speichern, vergessen Sie diese also vorerst.

65535 kann höchstens in einer 16-Bit-Nummer gespeichert werden, und die maximale Anzahl, die wir haben können, ist 99999, was mit der 17. Bit-Nummer mit einem Maximum von 131071 übereinstimmt.

Die Verwendung von 32-Bit-Datentypen ist eine Verschwendung, da wir nur 1 Bit dieser zusätzlichen 16-Bit-Daten benötigen. Daher können wir eine Struktur definieren, die einen Booleschen Wert (oder ein Zeichen) und eine 16-Bit-Zahl enthält.

Angenommen, C / C ++

typedef struct _number {

    uint16_t number;
    bool overflow;
}Number;

Diese Struktur nimmt nur 3 Bytes ein, und wir benötigen ein Array von 1000, also insgesamt 3000 Bytes. Wir haben den Gesamtraum um 25% reduziert!

Was das Speichern der Zahlen angeht, können wir einfach bitweise rechnen

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

Und das Gegenteil

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

Um alle zu drucken, können wir eine einfache Schleife über das Array verwenden. Das Abrufen einer bestimmten Nummer erfolgt natürlich in konstanter Zeit, da es sich um ein Array handelt.

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

Um nach einer Nummer zu suchen, benötigen wir ein sortiertes Array. Wenn die Zahlen gespeichert sind, sortieren Sie das Array (ich würde persönlich eine Zusammenführungssortierung wählen, O (nlogn)). Um jetzt zu suchen, würde ich einen Zusammenführungssortierungsansatz wählen. Teilen Sie das Array auf und sehen Sie, zwischen welchem ​​unsere Nummer liegt. Rufen Sie dann die Funktion nur für dieses Array auf. Führen Sie dies rekursiv durch, bis Sie eine Übereinstimmung haben und den Index zurückgeben. Andernfalls ist er nicht vorhanden und drucken Sie einen Fehlercode. Diese Suche wäre ziemlich schnell, und der schlimmste Fall ist immer noch besser als O (nlogn), da er absolut in kürzerer Zeit als die Zusammenführungssortierung ausgeführt wird (jedes Mal wird nur 1 Seite des Splits rekursiert, anstatt beide Seiten :)) ist O (nlogn).

jyore
quelle
0

Meine Lösung: bester Fall 7,025 Bit / Nummer, schlechtester Fall 14,193 Bit / Nummer, grober Durchschnitt 8,551 Bit / Nummer. Stream-codiert, kein wahlfreier Zugriff.

Noch bevor ich die Antwort von ruslik gelesen habe, habe ich sofort daran gedacht, den Unterschied zwischen den einzelnen Zahlen zu kodieren, da er klein ist und relativ konsistent sein sollte, aber die Lösung muss auch das Worst-Case-Szenario berücksichtigen können. Wir haben ein Leerzeichen von 100000 Zahlen, die nur 1000 Zahlen enthalten. In einem vollkommen einheitlichen Telefonbuch wäre jede Nummer um 100 größer als die vorherige Nummer:

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

In diesem Fall wäre kein Speicher erforderlich, um die Unterschiede zwischen den Zahlen zu codieren, da es sich um eine bekannte Konstante handelt. Leider können die Zahlen von den idealen Schritten von 100 abweichen. Ich würde die Differenz zum idealen Inkrement von 100 codieren, so dass ich, wenn sich zwei benachbarte Zahlen um 103 unterscheiden, die Zahl 3 codieren würde und wenn sich zwei benachbarte Zahlen um 92 unterscheiden, I. würde -8 codieren. Ich nenne das Delta aus einem idealen Inkrement von 100 die " Varianz ".

Die Varianz kann von -99 (dh zwei aufeinanderfolgende Nummern) bis 99000 reichen (das gesamte Telefonbuch besteht aus den Nummern 00000… 00999 und einer zusätzlichen am weitesten entfernten Nummer 99999), was einem Bereich von 99100 möglichen Werten entspricht.

Ich würde versuchen , einen minimalen Speicher zuzuweisen, um die häufigsten Unterschiede zu codieren und den Speicher zu erweitern, wenn ich auf größere Unterschiede stoße (wie bei ProtoBufvarint ). Ich verwende Blöcke mit sieben Bits, sechs für die Speicherung und ein zusätzliches Flag-Bit am Ende, um anzuzeigen, dass diese Varianz mit einem zusätzlichen Block nach dem aktuellen gespeichert wird, bis zu maximal drei Blöcken (was ein Maximum von ergibt 3 * 6 = 18 Speicherbits, die 262144 mögliche Werte sind, mehr als die Anzahl der möglichen Varianzen (99100). Jeder zusätzliche Block, der einem erhöhten Flag folgt, hat Bits von höherer Bedeutung, sodass der erste Block immer die Bits 0- hat. In 5 hat der optionale zweite Block die Bits 6-11 und der optionale dritte Block hat die Bits 12-17.

Ein einzelner Block bietet sechs Speicherbits, die 64 Werte aufnehmen können. Ich möchte die 64 kleinsten Abweichungen so zuordnen, dass sie in diesen einzelnen Block passen (dh Abweichungen von -32 bis +31), daher verwende ich die ProtoBuf-ZigZag-Codierung bis zu den Abweichungen von -99 bis +98 (da keine Notwendigkeit besteht) für eine negative Varianz über -99), an diesem Punkt wechsle ich zur regulären Codierung, versetzt um 98:  

Varianz | Codierter Wert
----------- + ----------------
    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
----------- | --------------- Ende von ZigZag
   100 | 198
   101 | 199
   ... | ...
  3996 | 4094
  3997 | 4095
----------- | --------------- 12 Bit
  3998 | 4096
  3999 | 4097
   ... | ...
 262045 | 262143
----------- | --------------- 18 Bit

Einige Beispiele dafür, wie Abweichungen als Bits codiert werden, einschließlich des Flags, um einen zusätzlichen Block anzuzeigen:

Varianz | Codierte Bits
----------- + ----------------
     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

Die ersten drei Nummern eines Beispieltelefonbuchs würden also wie folgt als Bitstrom codiert:

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

Im besten Fall ist das Telefonbuch etwas gleichmäßig verteilt und es gibt keine zwei Telefonnummern mit einer Varianz von mehr als 32, sodass 7 Bit pro Nummer plus 32 Bit für die Startnummer für insgesamt 32 + 7 * 999 verwendet werden = 7025 Bit .
Ein gemischtes Szenario , bei dem die Varianz von 800 Telefonnummern in einen Block passt (800 * 7 = 5600), 180 Nummern in jeweils zwei Blöcke passen (180 * 2 * 7 = 2520) und 19 Nummern in jeweils drei Blöcke passen (20 * 3) * 7 = 399) plus die anfänglichen 32 Bits ergeben 8551 Bits .
Im schlimmsten Fall passen 25 Zahlen in drei Blöcke (25 * 3 * 7 = 525 Bit) und die verbleibenden 974 Zahlen passen in zwei Blöcke (974 * 2 * 7 = 13636 Bit) plus 32 Bit für die erste Zahl für einen Flügel insgesamt14193 Bits .

   Anzahl der verschlüsselten Nummern |
 1 Stück | 2 Stücke | 3 Stücke | Gesamtbits
--------- + ---------- + ---------- + ------------
   999 | 0 | 0 | 7025
   800 | 180 | 19 | 8551
    0 | 974 | 25 | 14193

Ich sehe vier zusätzliche Optimierungen, die durchgeführt werden können, um den erforderlichen Speicherplatz weiter zu reduzieren:

  1. Der dritte Block benötigt nicht die vollen sieben Bits, er kann nur fünf Bits und ohne Flag-Bit sein.
  2. Es kann eine erste Übergabe der Zahlen geben, um die besten Größen für jeden Block zu berechnen. Vielleicht wäre es für ein bestimmtes Telefonbuch optimal, wenn der erste Block 5 + 1 Bits, der zweite 7 + 1 und der dritte 5 + 1 hat. Dies würde die Größe weiter auf ein Minimum von 6 * 999 + 32 = 6026 Bit plus zwei Sätze von drei Bits reduzieren, um die Größen der Blöcke 1 und 2 (die Größe von Block 3 ist der Rest der erforderlichen 16 Bits) für insgesamt zu speichern von 6032 Bits!
  3. Der gleiche anfängliche Durchgang kann ein besseres erwartetes Inkrement als das Standard-100 berechnen. Möglicherweise gibt es ein Telefonbuch, das von 55555-50000 beginnt und daher den halben Nummernbereich hat, sodass das erwartete Inkrement 50 betragen sollte. Oder es gibt ein nichtlineares Verteilung (Standardabweichung vielleicht) und ein anderes optimales erwartetes Inkrement können verwendet werden. Dies würde die typische Varianz verringern und möglicherweise die Verwendung eines noch kleineren ersten Blocks ermöglichen.
  4. Weitere Analysen können im ersten Durchgang durchgeführt werden, damit das Telefonbuch partitioniert werden kann, wobei jede Partition ihre eigenen erwarteten Inkrement- und Blockgrößenoptimierungen aufweist. Dies würde eine kleinere erste Blockgröße für bestimmte sehr einheitliche Teile des Telefonbuchs (Verringerung der Anzahl der verbrauchten Bits) und größere Blockgrößen für ungleichmäßige Teile (Verringerung der Anzahl der für Fortsetzungsflags verschwendeten Bits) ermöglichen.
Allon Guralnek
quelle
0

Die eigentliche Frage ist die Speicherung von fünfstelligen Telefonnummern.

Der Trick ist, dass Sie 17 Bit benötigen, um den Zahlenbereich von 0..99.999 zu speichern. Das Speichern von 17 Bit an herkömmlichen 8-Byte-Wortgrenzen ist jedoch ein Aufwand. Aus diesem Grund fragen sie, ob Sie in weniger als 4 KB arbeiten können, wenn Sie keine 32-Bit-Ganzzahlen verwenden.

Frage: Sind alle Zahlenkombinationen möglich?

Aufgrund der Art des Telefonsystems sind möglicherweise weniger als 65.000 Kombinationen möglich. Ich gehe davon aus, dass ja, weil wir über die letzten fünf Stellen in der Telefonnummer sprechen, im Gegensatz zu der Vorwahl oder den Austauschpräfixen.

Frage: Wird diese Liste statisch sein oder müssen Updates unterstützt werden?

Wenn es statisch ist, zählen Sie beim Auffüllen der Datenbank die Anzahl der Stellen <50.000 und die Anzahl der Stellen> = 50.000. Ordnen Sie zwei Arrays mit uint16geeigneter Länge zu: eines für die ganzen Zahlen unter 50.000 und eines für die höhere Menge. Wenn Sie Ganzzahlen im höheren Array speichern, subtrahieren Sie 50.000 und addieren Sie 50.000, wenn Sie Ganzzahlen aus diesem Array lesen. Jetzt haben Sie Ihre 1.000 Ganzzahlen in 2.000 8-Byte-Wörtern gespeichert.

Das Erstellen des Telefonbuchs erfordert zwei Eingabeverläufe, aber das Nachschlagen sollte im Durchschnitt in der Hälfte der Zeit erfolgen, als dies bei einem einzelnen Array der Fall wäre. Wenn die Suchzeit sehr wichtig wäre, könnten Sie mehr Arrays für kleinere Bereiche verwenden, aber ich denke, bei diesen Größen würde Ihre Leistungsgrenze die Arrays aus dem Speicher ziehen und 2k wird wahrscheinlich im CPU-Cache gespeichert, wenn Sie keinen Speicherplatz für irgendetwas registrieren, das Sie diese verwenden würden Tage.

Wenn es dynamisch ist , weisen Sie ein Array von etwa 1000 zu uint16und fügen Sie die Zahlen in sortierter Reihenfolge hinzu. Setzen Sie das erste Byte auf 50.001 und das zweite Byte auf einen geeigneten Nullwert wie NULL oder 65.000. Wenn Sie die Nummern speichern, speichern Sie sie in sortierter Reihenfolge. Wenn eine Zahl unter 50.001 liegt, speichern Sie sie vor dem 50.001-Marker. Wenn eine Zahl 50.001 oder höher ist, speichern Sie sie nach dem 50.001-Marker, subtrahieren Sie jedoch 50.000 vom gespeicherten Wert.

Ihr Array sieht ungefähr so ​​aus:

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

Wenn Sie also eine Nummer im Telefonbuch nachschlagen, durchlaufen Sie das Array. Wenn Sie den Wert 50.001 erreicht haben, fügen Sie Ihren Array-Werten 50.000 hinzu.

Dies macht Beilagen sehr teuer, aber das Nachschlagen ist einfach und Sie werden nicht viel mehr als 2k für die Speicherung ausgeben.

Dannyman
quelle