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)<c⋅f(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)<c⋅f(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 (hmn−m(n−m)2m
Wenn im Vergleich zu klein ist , zeigt Stirlings Näherung und ein wenig Arithmetik (Beweis ist eine Übung!) Folgendes:2m2n
(n−m)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