it-swarm.com.de

Was sind die Vorteile der linearen Prüfung gegenüber der separaten Verkettung oder umgekehrt bei der Implementierung von Hash-Tabellen?

Ich habe Algorithmen aufgefrischt und diese beiden Methoden zur Implementierung von Hash-Tabellen überprüft. Es scheint, dass sie weitgehend ähnliche Leistungsmerkmale und Speicheranforderungen haben.

Ich kann mir einige Nachteile der linearen Prüfung vorstellen - nämlich, dass das Erweitern des Arrays teuer sein könnte (aber dies wird getan, höchstens 2 log N-mal? Wahrscheinlich keine große Sache) und dass das Verwalten von Löschungen etwas schwieriger ist . Aber ich gehe davon aus, dass es auch Vorteile gibt, oder dass es in Lehrbüchern nicht als praktikable Implementierungsmethode neben der offensichtlicheren Implementierung dargestellt wird.

Warum sollten Sie einen über den anderen wählen?

8
Casey

Bei linearer Prüfung (oder wirklich jeder Prüfung) muss eine Löschung "weich" sein. Dies bedeutet, dass Sie einen Dummy-Wert (oft als Grabstein bezeichnet) eingeben müssen, der mit nichts übereinstimmt, nach dem der Benutzer suchen könnte. Oder Sie müssten jedes Mal aufwärmen. Ein erneutes Aufwärmen, wenn sich zu viele Grabsteine ​​bilden, wird immer noch empfohlen oder eine Strategie, um den Friedhof zu defragmentieren.

Eine separate Verkettung (jeder Bucket ist ein Zeiger auf eine verknüpfte Werteliste) hat den Nachteil, dass Sie am Ende eine verknüpfte Liste mit allen Cache-bezogenen Problemen durchsuchen.

Ein weiterer Vorteil der Prüfmethode besteht darin, dass alle Werte im selben Array gespeichert sind. Dies macht das Kopieren beim Schreiben sehr einfach, indem nur das Array kopiert wird. Wenn Sie sicher sein können, dass das Original nicht über eine Klasseninvariante geändert wird, ist eine Momentaufnahme O(1)] und kann ohne Sperren durchgeführt werden.

9
ratchet freak

Schauen Sie sich diese großartige Antwort an:

https://stackoverflow.com/questions/23821764/why-do-we-use-linear-probing-in-hash-tables-when-there-is-separate-chaining-link

Zitat hier:

Ich bin überrascht, dass Sie gesehen haben, dass verkettetes Hashing schneller ist als lineares Testen - in der Praxis ist lineares Testen normalerweise erheblich schneller als Verketten. In der Tat ist das der Hauptgrund, warum es verwendet wird.

Obwohl verkettetes Hashing theoretisch großartig ist und lineares Testen einige bekannte theoretische Schwächen aufweist (wie die Notwendigkeit einer Fünf-Wege-Unabhängigkeit in der Hash-Funktion, um O(1) erwartete Lookups) in der Praxis zu gewährleisten Die lineare Prüfung ist aufgrund der Referenzlokalität in der Regel erheblich schneller. Insbesondere ist es schneller, auf eine Reihe von Elementen in einem Array zuzugreifen, als Zeigern in einer verknüpften Liste zu folgen, sodass die lineare Prüfung das verkettete Hashing tendenziell übertrifft, selbst wenn sie untersucht werden muss mehr Elemente.

Es gibt andere Siege beim verketteten Hashing. Zum Beispiel erfordern Einfügungen in eine lineare Prüftabelle keine neuen Zuordnungen (es sei denn, Sie bereiten die Tabelle erneut auf). In Anwendungen wie Netzwerkroutern, in denen der Speicher knapp ist, ist es daher gut zu wissen, dass nach dem Einrichten der Tabelle Die Elemente können ohne das Risiko eines Malloc-Fehlers darin platziert werden.

4
bmpasini

Ich werde mit einer voreingenommenen Antwort einspringen, bei der ich tatsächlich lieber getrennte Verkettung mit einfach verknüpften Listen und finde es einfacher, um mit ihnen Leistung zu erzielen (ich sage sie nicht sind optimal, nur einfacher für meine Anwendungsfälle), so widersprüchlich das klingt.

Natürlich ist das theoretische Optimum immer noch eine Hash-Tabelle ohne jegliche Kollisionen oder eine Sondierungstechnik mit minimaler Clusterbildung. Die separate Verkettungslösung muss sich jedoch überhaupt nicht mit Clustering-Problemen befassen.

Die von mir verwendete Datendarstellung ruft jedoch keine separate Speicherzuordnung pro Knoten auf. Hier ist es in C:

struct Bucket
{
    int head;
};

struct BucketNode
{
    int next;
    int element;
};

struct HashTable
{
    // Array of buckets, pre-allocated in advance.
    struct Bucket* buckets;

    // Array of nodes, pre-allocated assuming the client knows
    // how many nodes he's going to insert in advance. Otherwise
    // realloc using a similar strategy as std::vector in C++.
    struct BucketNode* nodes;

    // Number of bucket heads.
    int num_buckets;

    // Number of nodes inserted so far.
    int num_nodes;
};

Die Buckets sind nur 32-Bit-Indizes (ich verwende in Wirklichkeit nicht einmal eine Struktur) und die Knoten sind nur zwei 32-Bit-Indizes. Oft brauche ich nicht einmal den element Index, da die Knoten oft parallel zu dem Array von Elementen gespeichert werden, die in die Tabelle eingefügt werden sollen, wodurch der Overhead der Hash-Tabelle auf 32 Bit pro Bucket und 32 reduziert wird -Bits pro eingefügtem Element. Die reale Version, die ich öfter benutze, sieht folgendermaßen aus:

struct HashTable
{
    // Array of head indices. The indices point to entries in the 
    // second array below.
    int* buckets;

    // Array of next indices parallel to the elements to insert.
    int* next_indices;

    // Number of bucket heads.
    int num_buckets;
};

Auch wenn sich die räumliche Lokalität verschlechtert, kann ich leicht einen Nachbearbeitungsdurchlauf durchführen, bei dem ich eine neue Hash-Tabelle erstelle, bei der jeder Bucket-Knoten an den anderen angrenzt (triviale Kopierfunktion, die nur einen linearen Durchlauf durch die Hash-Tabelle durchführt und eine neue erstellt - Aufgrund der Art und Weise, in der die Hash-Tabelle durchlaufen wird, landet die Kopie mit allen benachbarten Knoten in einem aneinander angrenzenden Bucket.

Was Sondierungstechniken betrifft, hat dies den Vorteil, dass die räumliche Lokalität bereits von Anfang an vorhanden ist, ohne Speicherpools oder ein Backing-Array, wie ich sie verwende, und sie haben auch nicht den 32-Bit-Overhead pro Bucket und Knoten, aber dann Möglicherweise müssen Sie sich mit Clustering-Problemen befassen, die sich bei vielen Kollisionen auf bösartige Weise ansammeln können.

Ich finde, dass Clustering von Natur aus Kopfschmerzen ist, die bei vielen Kollisionen viel Analyse erfordern. Der Vorteil dieser Lösung ist, dass ich ohne eine derart gründliche Analyse und Prüfung oft beim ersten Mal ein anständiges Ergebnis erzielen kann. Auch wenn die Größe der Tabelle implizit von selbst geändert wird, habe ich Fälle erlebt, in denen solche Designs die Speichernutzung in einer Weise in die Luft jagten, die weit über das hinausging, was diese grundlegende Lösung, die 32 Bit pro Bucket und 32 Bit pro Knoten erfordert, ausführen würde auch im schlimmsten Fall. Es ist eine Lösung, die verhindert, dass es zu schlecht wird, selbst wenn es eine Reihe von Kollisionen gibt.

Der größte Teil meiner Codebasis dreht sich um Datenstrukturen, die Indizes speichern und häufig Indizes parallel zum Array der einzufügenden Elemente speichern. Dies verringert die Speichergröße, vermeidet überflüssige tiefe Kopien der einzufügenden Elemente und macht es sehr einfach, über die Speichernutzung nachzudenken. Abgesehen davon profitiere ich in meinem Fall eher von vorhersehbarer Leistung als von optimaler Leistung. Ein Algorithmus, der in vielen gängigen Szenarien optimal ist, aber im schlimmsten Fall eine schreckliche Leistung erbringen kann, ist für mich oft weniger vorzuziehen als einer, der die ganze Zeit einigermaßen gut funktioniert und nicht dazu führt, dass die Bildraten zu unvorhersehbaren Zeiten stottern, und so auch ich neigen dazu, diese Art von Lösungen zu bevorzugen.

1
user204677