Ich suche die Implementierung des eingestellten Datentyps. Das heißt, wir müssen
- Behalte eine dynamische Teilmenge (der Größe ) aus dem Universum der Größe mit bei
- Operationen
insert(x)
(ein Elementx
zu hinzufügen ) undfind(x)
(prüft, ob das Elementx
ein Mitglied von ).
Andere Operationen interessieren mich nicht. Zur Orientierung haben wir in Anwendungen, mit denen ich arbeite, .
Ich kenne Implementierungen, die beide Operationen in der Zeit bereitstellen , 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 kann ich zugeben; erwartete Laufzeiten oder Laufzeiten in sind nicht zulässig.
Eine Idee, die ich habe, ist, dass wenn 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?
n
ist die Größe der Menge S. Sie kann mit jeder Menge zunehmeninsert
oder gleich bleiben, wenn sich das Elementx
bereits in der Menge befindet.Antworten:
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.
Nun eine Terminologie:
Der Unterschied zwischen prägnant und kompakt ist der Unterschied zwischen klein-oh und groß-oh. Den absoluten Wert für einen Moment ignorieren ...
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 nn 2n 2m 2m
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.n m
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 (h m n−m (n−m)2m
Wenn im Vergleich zu klein ist , zeigt Stirlings Näherung und ein wenig Arithmetik (Beweis ist eine Übung!) Folgendes:2m 2n
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 logu2 log(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.un u
quelle
insert
von einem Anruffind
mit demselben Argument begleitet wird. Also, wenn diefind
zurückkehrttrue
, dann überspringen wir einfach dieinsert
. Die Häufigkeit vonfind
Anrufen ist also mehr als die Häufigkeit voninsert
Anrufen, auch wenn sie sichn
nähernu
, werdeninsert
Anrufe sehr selten.n <= u
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:
find(x)
(prüft, ob elementx
ein Mitglied von ).insert(x)
(füge ein Elementx
zu )delete(x)
(entferne ein Elementx
aus )Wenn nur die
find
Operation unterstützt wird, ist dies das statische Mitgliedschaftsproblem. Wenn einerinsert
oderdelete
beide 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:
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.nu n
quelle
n = u/2
, ist der benötigte Platz maximal.