Hash-Tabellen sollen unter Verwendung von beispielsweise einfacher Verkettung und Verdoppelung bei einer bestimmten Kapazität amortisiert werden.
Dies setzt jedoch voraus, dass die Längen der Elemente konstant sind. Um den Hash eines Elements zu berechnen, muss das Element durchlaufen werden, wobei Zeit benötigt wird, wobei die Länge ist.l
Um jedoch zwischen Elementen zu unterscheiden, müssen die Elemente eine Länge von mindestens Bits haben. Andernfalls werden sie nach dem Pigeonhole-Prinzip nicht unterschieden. Die Hash-Funktion, die Elementbits durchläuft , benötigt Zeit.lg n lg n Θ ( lg n )
Können wir stattdessen sagen, dass die Geschwindigkeit einer Hash-Tabelle unter Berücksichtigung einer vernünftigen Hash-Funktion, die alle Teile der Eingabe verwendet, tatsächlich ? Warum sind Hash-Tabellen in der Praxis dann effizient zum Speichern von Elementen variabler Länge wie Zeichenfolgen und großen Ganzzahlen?
quelle
Antworten:
Die Geschichte, dass Hash-Tabellen amortisiert werden istΘ(1)
eine Lüge,eine übermäßige Vereinfachung.Dies gilt nur, wenn:k
c
r
- Die Menge der zu hashenden Daten pro Element im Vergleich zur Anzahl der K eys trivial ist und die Geschwindigkeit des Hashing eines K ey schnell ist - . - Die Anzahl der C ollisions ist klein - c . - Wir nicht berücksichtigen Zeit benötigt , um R die Hash - Tabelle ESIZE - r .
Große Zeichenfolgen für HashΘ(k)
Θ(k) O(1) Ω(k) O(k) Ω(k) .
Wenn die erste Annahme falsch ist, steigt die Laufzeit auf . Dies gilt definitiv für große Saiten, aber für große Saiten hätte ein einfacher Vergleich auch eine Laufzeit von Θ ( k ) . Ein Hash ist also nicht asymptotisch langsamer, obwohl das Hashing immer langsamer ist als ein einfacher Vergleich, da der Vergleich ein frühes Opt-out hat, also O ( 1 ) , Ω ( k ) und das Hashing immer den vollen String O ( k ) hashen muss. Ω ( k )
Beachten Sie, dass Ganzzahlen sehr langsam wachsen. 8 Bytes können Werte bis zu speichern ; 8 Bytes sind eine triviale Menge an Hash. Wenn Sie Bigint speichern möchten, stellen Sie sich diese einfach als Zeichenfolgen vor.1018
Langsamer Hash-AlgorithmusΘ(1)
Wenn der Hashing-Betrag im Vergleich zur Speicherung der Daten nicht trivial ist, wird die Annahme offensichtlich unhaltbar. Sofern kein kryptografischer Hash verwendet wird, sollte dies kein Problem sein.
Entscheidend ist , dass > > k . Solange dies gilt, ist Θ ( 1 ) eine faire Aussage.n >> k Θ(1)
Viele KollisionenO(log(n))
Wenn die Hashing-Funktion schlecht ist oder die Hash-Tabelle klein ist oder die Größe der Hash-Tabelle unangenehm ist, treten häufig Kollisionen auf und die Laufzeit geht auf . Die Hashing-Funktion sollte so gewählt werden, dass Kollisionen selten sind und dennoch so schnell wie möglich. Wenn Sie Zweifel haben, entscheiden Sie sich für weniger Kollisionen auf Kosten eines langsameren Hashing. Als Faustregel gilt, dass die Hashing-Tabelle immer zu weniger als 75% gefüllt sein sollte. Und die Größe der Hashing-Tabelle sollte keine Korrelation mit der Hashing-Funktion haben. Oft ist die Größe der Hashing-Tabelle (relativ) prim.
Ändern der Größe der Hash-Tabelle
Da eine fast vollständige Hash-Tabelle zu viele Kollisionen verursacht und eine große (leere) Hash-Tabelle Platzverschwendung darstellt, können Sie bei vielen Implementierungen die Hash-Tabelle nach Bedarf vergrößern (und verkleinern!).
Das Erweitern einer Tabelle kann eine vollständige Kopie aller Elemente (und möglicherweise eine Umbildung) umfassen, da der Speicher aus Leistungsgründen kontinuierlich sein muss.
Nur in pathologischen Fällen ist die Größenänderung der Hash-Tabelle ein Problem, sodass die (kostspieligen, aber seltenen) Größenänderungen über viele Aufrufe hinweg abgeschrieben werden.
LaufzeitΘ(kcr)
k c r Θ(1)
Die tatsächliche Laufzeit einer Hash-Tabelle ist also . Es wird angenommen, dass jedes von k , c , r im Durchschnitt eine (kleine) Konstante in der amortisierten Laufzeit ist, und daher sagen wir, dass Θ ( 1 ) eine faire Aussage ist.
Um auf Ihre Fragen zurückzukommen
Bitte entschuldigen Sie die Umschreibung. Ich habe versucht, verschiedene Bedeutungen zu extrahieren. Sie können gerne Kommentare abgeben, wenn ich einige verpasst habe
Sie scheinen besorgt über die Länge der Ausgabe der Hash-Funktion zu sein. Nennen wir dies ( n wird im Allgemeinen als die Anzahl der zu hashenden Elemente angesehen). m ist l o g ( n ), da m einen Eintrag in der Hash-Tabelle eindeutig identifizieren muss. Dies bedeutet, dass m sehr langsam wächst. Bei 64 Bit nimmt die Anzahl der Hash-Tabelleneinträge einen beträchtlichen Teil des weltweit verfügbaren RAM ein. Mit 128 Bit wird der verfügbare Festplattenspeicher auf dem Planeten Erde weit überschritten. Das Erstellen eines 128-Bit-Hashs ist nicht viel schwieriger als das Erstellen eines 32-Bit-Hashs. Nein , die Zeit zum Erstellen eines Hashs ist nicht O (m n m log(n)
O(m) O(log(n))
(oder O ( l o g ( n ) ), wenn Sie so wollen).
Die Hash-Funktion durchläuft jedoch keine Bits von Elementen. Pro Punkt (!!) geht es nur durch Daten. Auch die Länge der Eingabe (k) hat keine Beziehung zur Anzahl der Elemente. Dies ist wichtig, da einige Nicht-Hashing-Algorithmen viele Elemente in der Sammlung untersuchen müssen, um ein (nicht) übereinstimmendes Element zu finden. In der Hash-Tabelle werden durchschnittlich nur 1 oder 2 Vergleiche pro betrachtetem Element durchgeführt, bevor eine Schlussfolgerung gezogen wird. O ( k )log(n)
O(k)
Da unabhängig von der Länge der Eingabe ( ) die Länge der Ausgabe ( ) immer gleich ist, sind Kollisionen selten und die Suchzeit konstant. Wenn jedoch die Schlüssellänge Vergleich zur Anzahl der Elemente in der Hash-Tabelle ( ) groß wird, ändert sich die Geschichte ...m k nk m
k n
Hash-Tabellen sind für sehr große Zeichenfolgen nicht sehr effizient .
Wenn (dh die Größe der Eingabe ist im Vergleich zur Anzahl der Elemente in der Hash-Tabelle ziemlich groß), können wir nicht mehr sagen, dass der Hash eine konstante Laufzeit hat, sondern auf eine Laufzeit von wechseln muss allem, weil es kein frühes Aus gibt. Sie müssen den vollständigen Schlüssel hashen. Wenn Sie nur eine begrenzte Anzahl von Elementen speichern, ist es möglicherweise viel besser, einen sortierten Speicher zu verwenden, da Sie beim Vergleich von deaktivieren können, sobald ein Unterschied festgestellt wird. n > > k Θ ( k ) , k 1 ≠ k 2not n>>k Θ(k) k1 ≠ k2
Wenn Sie jedoch Ihre Daten kennen, können Sie festlegen, dass nicht der vollständige Schlüssel, sondern nur der (bekannte oder angenommene) flüchtige Teil davon gehasht wird. Dabei wird die Eigenschaft wiederhergestellt, während die Kollisionen in Schach gehalten werden.Θ(1)
Versteckte KonstantenΘ(1)
Wie jeder wissen sollte, bedeutet einfach, dass die Zeit pro verarbeitetem Element eine Konstante ist. Diese Konstante ist für das Hashing viel größer als für den einfachen Vergleich. Bei kleinen Tabellen ist eine binäre Suche schneller als eine Hash-Suche, da beispielsweise 10 binäre Vergleiche sehr wohl schneller sind als ein einzelner Hash. Für kleine Datensätze sollten Alternativen zu Hash-Tabellen in Betracht gezogen werden. Bei großen Datenmengen leuchten Hash-Tabellen wirklich.
quelle
Beginnen wir mit einer einfacheren Frage. Betrachten Sie die vielleicht einfachste Datenstruktur, die es gibt, ein Array . Stellen wir uns der Vollständigkeit halber eine Reihe von ganzen Zahlen vor. Wie lange dauert die Operation ? Die Antwort hängt vom Berechnungsmodell ab. Hierbei sind zwei Modelle relevant: das RAM-Modell (das häufiger verwendet wird) und das Bitmodell (das einfacher zu erklären ist).A[i]=A[j]
In dem Bit - Modell , ein basisches Operation mit Bits kostet . Wenn also die ganzen Zahlen Bits breit sind, wird die Operation ungefähr 2 .N w A [ i ] = A [ j ] 2 wN N w A[i]=A[j] 2w
Im RAM-Modell ist die Basisdateneinheit kein Bit, sondern ein Wort (manchmal auch als Maschinenwort bezeichnet ). Ein Wort ist eine Ganzzahl mit der Breite , wobei die Größe der Eingaben (in Bits) ist. Eine grundlegende Operation mit Worten kostet . In den meisten Fällen haben die benötigten Ganzzahlen, wenn Sie ein ganzzahliges Array haben, die Breite , sodass die Operation kostet .n N N O ( log n ) A [ i ] = A [ j ] O ( 1 )logn n N N O(logn) A[i]=A[j] O(1)
Wie ich oben sagte, analysieren wir normalerweise Algorithmen unter Verwendung des RAM-Modells. Die einzige häufige Ausnahme ist die Ganzzahlarithmetik, insbesondere die Ganzzahlmultiplikation, die häufig in Bezug auf die Anzahl der Bitoperationen analysiert wird.
Warum verwenden wir das RAM-Modell? Da es mehr Vorhersagekraft hat (gegenüber der Realität). Die Annahme, dass die Eingabegröße in der Größe eines Maschinenworts höchstens exponentiell ist, ist normalerweise gerechtfertigt, insbesondere für moderne 64-Bit-Prozessoren, und Operationen an Maschinenwörtern benötigen in tatsächlichen CPUs eine konstante Zeit.
Hash-Tabellen sind kompliziertere Datenstrukturen und umfassen drei Typen: den Schlüsseltyp, den Hash-Typ und den Werttyp. Aus Sicht des Wertetyps ist eine Hash-Tabelle nur ein verherrlichtes Array. Lassen Sie uns diesen Aspekt ignorieren. Es kann immer angenommen werden, dass der Hash-Typ aus einer kleinen Anzahl von Maschinenwörtern besteht. Der Schlüsseltyp erfüllt eine spezielle Eigenschaft: Er ist hashbar , was bedeutet, dass er eine Hash-Operation hat, die (mindestens) eine deterministische Funktion ist (eine Funktion, die immer den gleichen Wert zurückgibt ).
Wir können jetzt Ihre Frage beantworten: Wie lange dauert es, einen Schlüssel zu hashen? Die Antwort hängt vom Berechnungsmodell ab. Diesmal haben wir drei gemeinsame Modelle: die beiden früheren und das Orakelmodell.
Im Orakelmodell nehmen wir an, dass uns die Hash-Funktion von einem "Orakel" gegeben wird, das den Hash eines beliebigen Schlüssels in konstanter Zeit berechnen kann.
Im RAM-Modell und im Bitmodell ist die Hash-Funktion eine tatsächliche Funktion, und die zeitliche Komplexität der Hash-Tabelle hängt von der zeitlichen Komplexität der Hash-Funktion ab. Hash-Funktionen, die für Hash-Tabellen (und nicht für kryptografische Zwecke) verwendet werden, sind normalerweise sehr schnell und benötigen lineare Zeit für die Eingabe. Das heißt, wenn der Schlüsseltyp eine Länge von Bits (im Bitmodell) oder Wörtern (im RAM-Modell) hat, benötigt die Hash-Funktion die Zeit . Wenn eine Konstante ist, benötigt die Hash-Funktion eine konstante Zeit.N O ( N ) N.N N O(N) N
Wenn wir die Laufzeit von Hash-Tabellen-Algorithmen analysieren, verwenden wir normalerweise implizit das Orakelmodell. Dies wird oft in einer anderen Sprache ausgedrückt: Wir sagen einfach, dass wir die Anzahl der Aufrufe der Hash-Funktion zählen. Dies ist sinnvoll, da normalerweise Anwendungen der Hash-Funktion der dominierende Begriff in der Laufzeit von Hash-Tabellen-Algorithmen sind. Um die tatsächliche Zeitkomplexität zu analysieren, müssen Sie lediglich die Anzahl der Hash-Aufrufe mit der Laufzeit multiplizieren der Hash-Funktion.
Bei der Analyse der Laufzeit eines Algorithmus unter Verwendung einer Hash-Tabelle als Datenstruktur interessiert uns häufig die tatsächliche Laufzeit, normalerweise das RAM-Modell. Eine Möglichkeit besteht darin, das zu tun, was im vorhergehenden Absatz vorgeschlagen wurde, nämlich die Laufzeit von Hash-Tabellenoperationen (angegeben als Anzahl der Aufrufe von Hash-Funktionen) mit der Laufzeit der Hash-Funktion zu multiplizieren.
Dies ist jedoch nicht gut genug, wenn die Tasten unterschiedliche Längen haben. Stellen Sie sich zum Beispiel vor, wir haben Schlüssel der Größe , und wir berechnen den Hash von jedem von ihnen einmal. Die tatsächliche Zeitkomplexität beträgt , aber die obige Berechnung ergibt nur . Wenn dies in einer Anwendung der Fall ist, können wir dies auf Ad-hoc-Basis berücksichtigen, indem wir eine verfeinerte Analyse der Komplexität der zugrunde liegenden Hash-Tabelle verwenden. O ( 2 m ) O ( m 2 m )1,2,4,…,2m O(2m) O(m2m)
quelle