Auf der Suche nach einer festgelegten Implementierung mit geringem Speicherbedarf

9

Ich suche die Implementierung des eingestellten Datentyps. Das heißt, wir müssen

  • Behalte eine dynamische Teilmenge S (der Größe n ) aus dem Universum U={0,1,2,3,,u1} der Größe u mit bei
  • Operationen insert(x)(ein Element xzu hinzufügen S) und find(x)(prüft, ob das Element xein Mitglied von S ).

Andere Operationen interessieren mich nicht. Zur Orientierung haben wir in Anwendungen, mit denen ich arbeite, u1010 .

Ich kenne Implementierungen, die beide Operationen in der Zeit bereitstellen O(1), daher mache ich mir hauptsächlich Sorgen um die Größe der Datenstruktur. Ich erwarte Milliarden von Einträgen, möchte aber vermeiden, so viel wie möglich zu tauschen.

Ich bin bereit, die Laufzeit bei Bedarf zu opfern. Die amortisierte Laufzeit von O(logn) kann ich zugeben; erwartete Laufzeiten oder Laufzeiten in ω(logn) sind nicht zulässig.

Eine Idee, die ich habe, ist, dass wenn S als eine Vereinigung von Bereichen dargestellt werden kann [xmin, xmax], wir in der Lage sein werden, Speichergröße mit dem Preis einer gewissen Leistungsminderung einzusparen. Es sind auch einige andere Datenmuster möglich, wie z [0, 2, 4, 6].

Könnten Sie mich bitte auf Datenstrukturen verweisen, die so etwas können?

HEKTO
quelle
Lassen Sie uns diese Diskussion im Chat fortsetzen
Raphael
Wie kommt die Anzahl der Elemente ins Bild? Dh was passiert, wenn ein Element eingefügt wird und es bereits n gibt ? nn
vonbrand
@vonbrand - das nist die Größe der Menge S. Sie kann mit jeder Menge zunehmen insertoder gleich bleiben, wenn sich das Element xbereits in der Menge befindet.
HEKTO
3
Können Sie eine geringe Wahrscheinlichkeit von Fehlalarmen akzeptieren? Wenn ja, könnte ein Bloom-Filter ideal sein: en.wikipedia.org/wiki/Bloom_filter
Joe
1
@AlekseyYakovlev, die falsch positive Rate eines Bloom-Filters hat nichts mit der Universumsgröße zu tun (nur mit der Anzahl der Hash-Funktionen , der Größe der Datenstruktur m und der Anzahl der Elemente n ), aber wenn n wirklich ist der Nähe von u (sagen wir u = n c für eine kleine konstante c ), werden Sie schwer zu tun , besser gedrückt als ein einfaches Bit - Vektor ich denke, mit nur c n insgesamt Bits des Raumes. kmnnuu=ncccn
Joe

Antworten:

8

Joes Antwort ist extrem gut und gibt Ihnen alle wichtigen Schlüsselwörter.

Sie sollten sich bewusst sein, dass sich die prägnante Datenstrukturforschung noch in einem frühen Stadium befindet und viele der Ergebnisse weitgehend theoretisch sind. Viele der vorgeschlagenen Datenstrukturen sind recht komplex zu implementieren, aber der größte Teil der Komplexität beruht auf der Tatsache, dass Sie die asymptotische Komplexität sowohl über die Universumsgröße als auch über die Anzahl der gespeicherten Elemente beibehalten müssen. Wenn eine davon relativ konstant ist, geht ein Großteil der Komplexität verloren.

Wenn die Sammlung semi-statisch ist (dh Einfügungen sind selten oder zumindest mit geringem Volumen), lohnt es sich auf jeden Fall, eine einfach zu implementierende statische Datenstruktur (Sadakanes Sdarray ist eine gute Wahl) in Verbindung mit einem Update in Betracht zu ziehen Zwischenspeicher. Grundsätzlich zeichnen Sie Aktualisierungen in einer herkömmlichen Datenstruktur (z. B. B-Tree, Trie, Hash-Tabelle) auf und aktualisieren die "Haupt" -Datenstruktur regelmäßig in großen Mengen. Dies ist eine sehr beliebte Technik beim Abrufen von Informationen, da invertierte Indizes viele Vorteile für die Suche haben, aber an Ort und Stelle schwer zu aktualisieren sind. Wenn dies der Fall ist, lassen Sie es mich bitte in einem Kommentar wissen und ich werde diese Antwort ändern, um Ihnen einige Hinweise zu geben.

Wenn Einfügungen häufiger sind, empfehle ich prägnantes Hashing. Die Grundidee ist einfach genug, um sie hier zu erklären, also werde ich es tun.

nulog(un)+O(1)

Nun eine Terminologie:

  • Wenn Sie eine Datenstruktur haben, die die Daten speichern und Ihre Operationen in Speicherbits unterstützen kann, nennen wir dies eine implizite Datenstruktur.log(un)+O(1)
  • Wenn Sie eine Datenstruktur haben, die die Daten speichern und Ihre Operationen in Bits Platz, wir nennen dies eine kompakte Datenstruktur. Beachten Sie, dass dies in der Praxis bedeutet, dass der relative Overhead (relativ zum theoretischen Minimum) innerhalb einer Konstanten liegt. Dies kann 5% Overhead oder 10% Overhead oder 10-facher Overhead sein.log(un)+O(log(un))=(1+O(1))log(un)
  • Wenn Sie eine Datenstruktur haben, die die Daten speichern und Ihre Operationen in Bits Platz, wir nennen dies eine prägnante Datenstruktur.log(un)+o(log(un))=(1+o(1))log(un)

Der Unterschied zwischen prägnant und kompakt ist der Unterschied zwischen klein-oh und groß-oh. Den absoluten Wert für einen Moment ignorieren ...

  • c n 0 n > n 0 gg(n)=O(f(n)) bedeutet , dass es eine Konstante existiert und eine Anzahl so daß für alle , .cn0n>n0g(n)<cf(n)
  • c n 0 n > n 0 g ( n ) < cg(n)=o(f(n)) bedeutet , daß für alle Konstanten eine Zahl existiert so daß für alle , .cn0n>n0g(n)<cf(n)

Informell sind Big-Oh und Little-Oh beide "innerhalb eines konstanten Faktors", aber mit Big-Oh wird die Konstante für Sie ausgewählt (vom Algorithmus-Designer, dem CPU-Hersteller, den Gesetzen der Physik oder was auch immer), aber mit wenig -oh du wählst die Konstante selbst und sie kann so klein sein, wie du willst . Anders ausgedrückt, bei prägnanten Datenstrukturen wird der relative Overhead mit zunehmender Größe des Problems beliebig klein.

Natürlich muss die Größe des Problems möglicherweise sehr groß werden, um den gewünschten relativen Overhead zu realisieren, aber Sie können nicht alles haben.

OK, damit haben wir ein paar Zahlen zum Problem. Nehmen wir an, dass Schlüssel Bit-Ganzzahlen sind (die Universumsgröße beträgt also ), und wir möchten dieser Ganzzahlen speichern . Nehmen wir an, wir können eine idealisierte Hash-Tabelle mit voller Belegung und ohne Verschwendung auf magische Weise anordnen, sodass wir genau Hash-Slots benötigen .2 nn2n2m2m

Eine Suchoperation würde den Bit-Schlüssel hashen, Bits maskieren , um die Hash-Slots zu finden, und dann prüfen, ob der Wert in der Tabelle mit dem Schlüssel übereinstimmt. So weit, ist es gut.nm

Eine solche Hash-Tabelle verwendet Bits. Können wir es besser machen?n2m

Angenommen, die Hash-Funktion ist invertierbar. Dann müssen wir nicht den gesamten Schlüssel in jedem Hash-Slot speichern. Die Position des Hash-Slots gibt Ihnen Bits des Hash-Werts. Wenn Sie also nur die verbleibenden Bits gespeichert haben , können Sie den Schlüssel aus diesen beiden Informationen (der Position des Hash-Slots und dem dort gespeicherten Wert) rekonstruieren. Sie würden also nur Speicherbits benötigen .m n - m (hmnm(nm)2m

Wenn im Vergleich zu klein ist , zeigt Stirlings Näherung und ein wenig Arithmetik (Beweis ist eine Übung!) Folgendes:2m2n

(nm)2m=log(2n2m)+o(log(2n2m))

Diese Datenstruktur ist also prägnant.

Es gibt jedoch zwei Fänge.

Der erste Haken ist das Konstruieren von "guten" invertierbaren Hash-Funktionen. Glücklicherweise ist dies viel einfacher als es aussieht; Kryptographen machen ständig invertierbare Funktionen, nur nennen sie sie "Chiffren". Sie können beispielsweise eine Hash-Funktion auf einem Feistel-Netzwerk basieren. Dies ist eine einfache Möglichkeit, um invertierbare Hash-Funktionen aus nicht invertierbaren Hash-Funktionen zu erstellen.

Der zweite Haken ist, dass echte Hash-Tabellen dank des Geburtstagsparadoxons nicht ideal sind. Sie möchten also eine anspruchsvollere Art von Hash-Tabelle verwenden, mit der Sie der vollen Belegung näher kommen, ohne dass etwas verschüttet wird. Kuckuck-Hashing ist dafür perfekt geeignet, da Sie damit theoretisch beliebig nahe am Ideal und in der Praxis ziemlich nahe kommen können.

Kuckuck-Hashing erfordert mehrere Hash-Funktionen, und es ist erforderlich, dass Werte in Hash-Slots markiert werden, mit denen die Hash-Funktion verwendet wurde. Wenn Sie beispielsweise vier Hash-Funktionen verwenden, müssen Sie in jedem Hash-Slot zwei zusätzliche Bits speichern. Dies ist immer noch prägnant, wenn wächst, daher ist es in der Praxis kein Problem und schlägt immer noch, ganze Schlüssel zu speichern.m

Vielleicht möchten Sie sich auch die Bäume von van Emde Boas ansehen.

MEHR GEDANKEN

Wenn irgendwo in der Nähe von , ist ungefähr Wenn Sie also (erneut) davon ausgehen, dass keine weitere Korrelation zwischen den Werten besteht, können Sie im Grunde keine ausführen besser als ein Bitvektor. Sie werden feststellen, dass die obige Hashing-Lösung in diesem Fall effektiv degeneriert (Sie speichern am Ende ein Bit pro Hash-Slot), aber es ist billiger, nur den Schlüssel als Adresse zu verwenden, als eine Hash-Funktion zu verwenden.n logu2log(un)u

Wenn sehr nahe an , empfiehlt Ihnen die gesamte prägnante Literatur zu Datenstrukturen, den Sinn des Wörterbuchs umzukehren. Speichern Sie die Werte, die im Set nicht vorkommen. Jetzt müssen Sie den Löschvorgang jedoch effektiv unterstützen, und um ein prägnantes Verhalten beizubehalten, müssen Sie auch in der Lage sein, die Datenstruktur zu verkleinern, wenn mehr Elemente "hinzugefügt" werden. Das Erweitern einer Hash-Tabelle ist eine gut verstandene Operation, das Kontrahieren jedoch nicht.unu

Pseudonym
quelle
Hallo, was den zweiten Absatz Ihrer Antwort betrifft - ich gehe davon aus, dass jeder Anruf insertvon einem Anruf findmit demselben Argument begleitet wird. Also, wenn die findzurückkehrt true, dann überspringen wir einfach die insert. Die Häufigkeit von findAnrufen ist also mehr als die Häufigkeit von insertAnrufen, auch wenn sie sich nnähern u, werden insertAnrufe sehr selten.
HEKTO
Aber Sie erwarten erhalten schließen schließlich? nun
Pseudonym
In der realen Welt wächst n, bis es u erreicht, aber wir können nicht vorhersagen, ob es passieren wird oder nicht. Die Datenstruktur sollte für jeden gut funktionierenn <= u
HEKTO
Richtig. Dann ist es fair zu sagen, dass wir keine einzige Datenstruktur kennen, die kurz ist (im obigen Sinne) und die dies über den gesamten Bereich von . Ich denke, Sie wollen eine spärliche Datenstruktur, wenn , und wechseln dann zu einer dichten (z. B. einem Bitvektor), wenn um , und dann zu einer spärlichen Datenstruktur mit einer invertierten Sinn, wenn in der Nähe von . n<unun<un nuu2nu
Pseudonym
5

Es hört sich so an, als ob Sie eine prägnante Datenstruktur für das dynamische Mitgliedschaftsproblem wünschen .

Denken Sie daran, dass eine prägnante Datenstruktur eine ist, bei der der Platzbedarf "nahe" an der informationstheoretischen Untergrenze liegt, im Gegensatz zu einer komprimierten Datenstruktur jedoch weiterhin effiziente Abfragen ermöglicht.

Das Mitgliedschaftsproblem ist genau das, was Sie in Ihrer Frage beschreiben:

Behalten Sie eine Teilmenge (der Größe ) aus dem Universum der Größe mit Operationen bei:n U = { 0 , 1 , 2 , 3 , , u - 1 } uSnU={0,1,2,3,,u1}u

  • find(x)(prüft, ob element xein Mitglied von ).S
  • insert(x)(füge ein Element xzu )S
  • delete(x)(entferne ein Element xaus )S

Wenn nur die findOperation unterstützt wird, ist dies das statische Mitgliedschaftsproblem. Wenn einer insertoder deletebeide unterstützt werden, aber nicht beide, wird dies als semidynamisch bezeichnet . Wenn alle drei Vorgänge unterstützt werden, wird dies als dynamisches Mitgliedschaftsproblem bezeichnet.

Technisch gesehen haben Sie wahrscheinlich nur nach einer Datenstruktur für das semidynamische Mitgliedschaftsproblem gefragt, aber ich kenne keine Datenstrukturen, die diese Einschränkung ausnutzen und auch Ihre anderen Anforderungen erfüllen. Ich habe jedoch die folgende Referenz:

In Satz 5.1 des Artikels Mitgliedschaft in konstanter Zeit und nahezu minimalem Raum geben Brodnik und Munro das folgende Ergebnis:

Es gibt eine Datenstruktur, die -Bits erfordert , die Suchen in konstanter Zeit und Einfügungen und Löschungen in konstanter erwarteter Amortisationszeit unterstützen.O(B)

Dabei ist die informationstheoretische Mindestanzahl der erforderlichen Bits.B=log(un)

Die Grundidee ist, dass sie das Universum rekursiv in Bereiche sorgfältig ausgewählter Größen aufteilen, sodass dies sogar so klingt, als ob die Techniken in der Richtung liegen, an die Sie denken.

Wenn Sie jedoch nach etwas suchen, das Sie tatsächlich implementieren können, weiß ich nicht, ob dies Ihre beste Wahl sein wird. Ich habe nur das Papier überflogen, und der Versuch, die Details zu erklären, geht weit über den Rahmen dieser Antwort hinaus. Sie parametrisieren ihre Lösung mit unterschiedlichen Strategien, abhängig von den relativen Größen von und . Und die dynamische Version der Datenstruktur wird nur im Papier skizziert.nun

Joe
quelle
1
Das Brodnik & Munro Paper Abstract sagt nichts über Beilagen aus. Aber ihr Ergebnis ist das, was wir erwarten können, oder? Wenn ja n = u/2, ist der benötigte Platz maximal.
HEKTO
@AlekseyYakovlev Sie erwähnen den dynamischen Fall nicht wirklich in der Zusammenfassung, aber der Satz, der sich mit dem dynamischen Fall befasst, wird in meiner Antwort zitiert (aus Abschnitt 5).
Joe