A. Der Wert ist ein int kleiner als die Größe der Hash-Tabelle. Daher ist der Wert ein eigener Hash, daher gibt es keine Hash-Tabelle. Aber wenn es so wäre, wäre es O (1) und immer noch ineffizient.
Dies ist ein Fall, in dem Sie die Schlüssel trivial unterschiedlichen Buckets zuordnen können, sodass ein Array eine bessere Wahl für die Datenstruktur zu sein scheint als eine Hash-Tabelle. Die Ineffizienzen wachsen jedoch nicht mit der Tabellengröße.
(Möglicherweise verwenden Sie weiterhin eine Hash-Tabelle, da Sie nicht darauf vertrauen, dass die Ints während der Entwicklung des Programms kleiner als die Tabellengröße bleiben. Sie möchten den Code möglicherweise wiederverwendbar machen, wenn diese Beziehung nicht besteht oder Sie dies einfach nicht tun möchten, dass Menschen, die den Code lesen / pflegen, geistige Anstrengungen verschwenden müssen, um die Beziehung zu verstehen und aufrechtzuerhalten).
B. Sie müssen einen Hash des Werts berechnen. In dieser Situation ist die Reihenfolge O (n) für die Größe der Daten, die nachgeschlagen werden. Die Suche könnte O (1) sein, nachdem Sie O (n) gearbeitet haben, aber das kommt in meinen Augen immer noch zu O (n).
Wir müssen zwischen der Größe des Schlüssels (z. B. in Bytes) und der Größe der Anzahl der Schlüssel unterscheiden, die in der Hash-Tabelle gespeichert sind. Behauptungen, dass Hash-Tabellen O (1) -Operationen bereitstellen, bedeuten, dass Operationen (Einfügen / Löschen / Suchen) nicht weiter verlangsamt werden, da die Anzahl der Schlüssel von Hunderten auf Tausende auf Millionen auf Milliarden steigt (zumindest nicht, wenn alle Daten vorliegen Der Zugriff / die Aktualisierung erfolgt in einem ebenso schnellen Speicher, sei es, dass RAM- oder Festplatten-Cache-Effekte ins Spiel kommen können, aber selbst die Kosten eines Cache-Fehlschlags im ungünstigsten Fall sind in der Regel ein konstantes Vielfaches der Best-Case-Treffer.
Stellen Sie sich ein Telefonbuch vor: Sie haben vielleicht Namen, die ziemlich lang sind, aber ob das Buch 100 Namen oder 10 Millionen hat, die durchschnittliche Namenslänge wird ziemlich konsistent sein und der schlimmste Fall in der Geschichte ...
Der Guinness-Weltrekord für den längsten Namen, den jemals jemand verwendet hat, wurde von Adolph Blaine Charles David Earl Frederick Gerald Hubert Irvin John Kenneth Lloyd Martin Nero Oliver Paul Quincy Randolph Sherman Thomas Uncas Victor William Xerxes Yancy Wolfeschlegelsteinhausenbergerdorff, Senior, aufgestellt
... wc
sagt mir, dass das 215 Zeichen sind - das ist keine harte Obergrenze für die Schlüssellänge, aber wir müssen uns keine Sorgen machen, dass es massiv mehr gibt.
Dies gilt für die meisten realen Hash-Tabellen: Die durchschnittliche Schlüssellänge wächst nicht mit der Anzahl der verwendeten Schlüssel. Es gibt Ausnahmen, zum Beispiel kann eine Schlüsselerstellungsroutine Zeichenfolgen zurückgeben, in die inkrementierende Ganzzahlen eingebettet sind, aber selbst dann erhöhen Sie jedes Mal, wenn Sie die Anzahl der Schlüssel um eine Größenordnung erhöhen, die Schlüssellänge nur um 1 Zeichen: Dies ist nicht signifikant.
Es ist auch möglich, einen Hash aus einer Menge von Schlüsseldaten fester Größe zu erstellen. Zum Beispiel wird Microsoft Visual C ++ mit einer Standardbibliotheksimplementierung ausgeliefert std::hash<std::string>
, die einen Hash erstellt, der nur zehn Bytes enthält, die gleichmäßig entlang der Zeichenfolge verteilt sind. Wenn die Zeichenfolgen also nur bei anderen Indizes variieren, kommt es zu Kollisionen (und damit in der Praxis zu Nicht-O (1) -Verhalten auf der Suchseite nach der Kollision), aber die Zeit zum Erstellen des Hashs hat eine harte Obergrenze.
Und wenn Sie keinen perfekten Hash oder eine große Hash-Tabelle haben, gibt es wahrscheinlich mehrere Artikel pro Eimer. Es entwickelt sich also sowieso irgendwann zu einer kleinen linearen Suche.
Im Allgemeinen stimmt das, aber das Tolle an Hash-Tabellen ist, dass die Anzahl der Schlüssel, die während dieser "kleinen linearen Suche" besucht wurden, - für den separaten Verkettungsansatz bei Kollisionen - eine Funktion des Lastfaktors der Hash-Tabelle (Verhältnis von Schlüsseln zu Buckets) ist.
Bei einem Auslastungsfaktor von 1,0 beträgt die durchschnittliche Länge dieser linearen Suchvorgänge unabhängig von der Anzahl der Schlüssel durchschnittlich ~ 1,58 (siehe meine Antwort hier ). Für geschlossenes Hashing ist es etwas komplizierter, aber nicht viel schlimmer, wenn der Lastfaktor nicht zu hoch ist.
Dies ist technisch richtig, da die Hash-Funktion nicht alle Informationen im Schlüssel verwenden muss und daher eine konstante Zeit sein kann, und weil eine ausreichend große Tabelle Kollisionen auf eine nahezu konstante Zeit reduzieren kann.
Diese Art verfehlt den Punkt. Jede Art von assoziativer Datenstruktur muss letztendlich manchmal Operationen über jeden Teil des Schlüssels ausführen (Ungleichheit kann manchmal nur aus einem Teil des Schlüssels bestimmt werden, aber Gleichheit erfordert im Allgemeinen, dass jedes Bit berücksichtigt wird). Zumindest kann es den Schlüssel einmal hashen und den Hash-Wert speichern. Wenn es eine ausreichend starke Hash-Funktion verwendet - z. B. 64-Bit-MD5 -, wird möglicherweise sogar die Möglichkeit ignoriert, dass zwei Schlüssel denselben Wert haben (ein Unternehmen) Ich habe genau das für die verteilte Datenbank getan: Die Zeit für die Hash-Generierung war im Vergleich zu WAN-weiten Netzwerkübertragungen immer noch unbedeutend. Es macht also nicht allzu viel Sinn, sich über die Kosten für die Verarbeitung des Schlüssels Gedanken zu machen: Dies ist mit dem Speichern von Schlüsseln unabhängig von der Datenstruktur verbunden, und wie oben erwähnt - nicht.
Bei Hash-Tabellen, die groß genug sind, um Kollisionen zu reduzieren, fehlt auch der Punkt. Für eine separate Verkettung haben Sie bei jedem Lastfaktor immer noch eine konstante durchschnittliche Kollisionskettenlänge - sie ist nur höher, wenn der Lastfaktor höher ist, und diese Beziehung ist nicht linear. Der SO-Benutzer Hans kommentiert meine Antwort auch wie oben verlinkt :
Die durchschnittliche Schaufellänge, die von nicht leeren Schaufeln abhängig ist, ist ein besseres Maß für die Effizienz. Es ist a / (1-e ^ {- a}) [wobei a der Lastfaktor ist, e 2,71828 ist ...]
Der Ladefaktor allein bestimmt also die durchschnittliche Anzahl kollidierender Schlüssel, die Sie beim Einfügen / Löschen / Suchen durchsuchen müssen. Bei einer getrennten Verkettung ist es nicht nur konstant, wenn der Lastfaktor niedrig ist, sondern immer konstant. Für die offene Adressierung hat Ihr Anspruch jedoch eine gewisse Gültigkeit: Einige kollidierende Elemente werden in alternative Buckets umgeleitet und können dann die Operationen auf anderen Schlüsseln stören, sodass bei höheren Lastfaktoren (insbesondere> .8 oder .9) die Kollisionskettenlänge dramatischer schlechter wird.
In der Praxis ist dies der Fall, da es im Laufe der Zeit nur funktioniert, solange die Hash-Funktion und die Tabellengröße so gewählt werden, dass Kollisionen minimiert werden, obwohl dies häufig bedeutet, dass keine Hash-Funktion mit konstanter Zeit verwendet wird.
Nun, die Tabellengröße sollte zu einem vernünftigen Auslastungsfaktor führen, wenn man zwischen engem Hashing oder getrennter Verkettung wählt. Aber auch wenn die Hash-Funktion etwas schwach ist und die Schlüssel nicht sehr zufällig sind, hilft eine Primzahl von Buckets oft, sie zu reduzieren Kollisionen ebenfalls (wird hash-value % table-size
dann so umbrochen, dass Änderungen nur zu einem oder zwei höherwertigen Bits im Hash-Wert immer noch in Eimer aufgelöst werden, die pseudozufällig über verschiedene Teile der Hash-Tabelle verteilt sind).