Ich habe einige interessante Behauptungen zu SO re Java-Hashmaps und deren Suchzeit gesehen O(1)
. Kann jemand erklären, warum das so ist? Sofern sich diese Hashmaps nicht wesentlich von den Hashing-Algorithmen unterscheiden, für die ich mich entschieden habe, muss immer ein Datensatz vorhanden sein, der Kollisionen enthält.
In diesem Fall wäre die Suche O(n)
eher als O(1)
.
Kann jemand erklären , ob sie sind O (1) , und wenn ja, wie sie dies erreichen?
java
hashmap
big-o
time-complexity
paxdiablo
quelle
quelle
Antworten:
Ein besonderes Merkmal einer HashMap ist, dass ihr Verhalten im Gegensatz zu beispielsweise ausgeglichenen Bäumen wahrscheinlich ist. In diesen Fällen ist es normalerweise am hilfreichsten, über die Komplexität im Hinblick auf die Wahrscheinlichkeit des Eintretens eines Worst-Case-Ereignisses zu sprechen. Bei einer Hash-Karte ist dies natürlich der Fall bei einer Kollision in Bezug darauf, wie voll die Karte gerade ist. Eine Kollision ist ziemlich einfach abzuschätzen.
Bei einer Hash-Karte mit nur einer bescheidenen Anzahl von Elementen ist es daher ziemlich wahrscheinlich, dass mindestens eine Kollision auftritt. Mit der Big O-Notation können wir etwas überzeugenderes tun. Beachten Sie, dass für jede beliebige feste Konstante k gilt.
Mit dieser Funktion können wir die Leistung der Hash-Map verbessern. Wir könnten stattdessen über die Wahrscheinlichkeit von höchstens 2 Kollisionen nachdenken.
Das ist viel niedriger. Da die Kosten für die Behandlung einer zusätzlichen Kollision für die Leistung von Big O nicht relevant sind, haben wir einen Weg gefunden, die Leistung zu verbessern, ohne den Algorithmus tatsächlich zu ändern! Wir können dies verallgemeinern
Und jetzt können wir eine beliebige Anzahl von Kollisionen ignorieren und am Ende eine verschwindend geringe Wahrscheinlichkeit für mehr Kollisionen haben, als wir berücksichtigen. Sie könnten die Wahrscheinlichkeit auf ein beliebig kleines Niveau bringen, indem Sie das richtige k auswählen, ohne die tatsächliche Implementierung des Algorithmus zu ändern.
Wir sprechen darüber, indem wir sagen, dass die Hash-Map mit hoher Wahrscheinlichkeit O (1) -Zugriff hat
quelle
Sie scheinen das Worst-Case-Verhalten mit der durchschnittlichen (erwarteten) Laufzeit zu verwechseln. Ersteres ist zwar O (n) für Hash-Tabellen im Allgemeinen (dh es wird kein perfektes Hashing verwendet), dies ist jedoch in der Praxis selten relevant.
Jede zuverlässige Implementierung einer Hash-Tabelle in Verbindung mit einem halbwegs anständigen Hash hat eine Abrufleistung von O (1) mit einem sehr kleinen Faktor (tatsächlich 2) im erwarteten Fall innerhalb eines sehr engen Varianzspielraums.
quelle
In Java verwendet HashMap Hashcode, um einen Bucket zu finden. Jeder Bucket ist eine Liste von Elementen, die sich in diesem Bucket befinden. Die Elemente werden gescannt und zum Vergleich gleich verwendet. Beim Hinzufügen von Elementen wird die Größe der HashMap geändert, sobald ein bestimmter Lastprozentsatz erreicht ist.
Manchmal muss es also mit einigen Elementen verglichen werden, aber im Allgemeinen ist es viel näher an O (1) als an O (n). Aus praktischen Gründen ist das alles, was Sie wissen müssen.
quelle
Denken Sie daran, dass o (1) nicht bedeutet, dass bei jeder Suche nur ein einzelnes Element untersucht wird. Dies bedeutet, dass die durchschnittliche Anzahl der überprüften Elemente für die Anzahl der Elemente im Container konstant bleibt. Wenn also durchschnittlich 4 Vergleiche erforderlich sind, um einen Artikel in einem Container mit 100 Artikeln zu finden, sollten durchschnittlich 4 Vergleiche erforderlich sein, um einen Artikel in einem Container mit 10000 Artikeln zu finden, und für eine beliebige andere Anzahl von Artikeln (es gibt immer einen ein bisschen Varianz, insbesondere um die Punkte, an denen die Hash-Tabelle erneut aufbereitet wird, und wenn es eine sehr kleine Anzahl von Elementen gibt).
Kollisionen verhindern also nicht, dass der Container o (1) -Operationen ausführt, solange die durchschnittliche Anzahl von Schlüsseln pro Bucket innerhalb einer festen Grenze bleibt.
quelle
Ich weiß, dass dies eine alte Frage ist, aber es gibt tatsächlich eine neue Antwort darauf.
Sie haben Recht, dass eine Hash-Karte nicht wirklich ist
O(1)
streng genommen ist, denn da die Anzahl der Elemente beliebig groß wird, können Sie möglicherweise nicht in konstanter Zeit suchen (und die O-Notation wird in Zahlen definiert, die dies können willkürlich groß werden).Daraus folgt jedoch nicht, dass die Echtzeitkomplexität ist
O(n)
da es keine Regel gibt, die besagt, dass die Buckets als lineare Liste implementiert werden müssen.Tatsächlich implementiert Java 8 die Buckets,
TreeMaps
sobald sie einen Schwellenwert überschreiten, der die tatsächliche Zeit angibtO(log n)
.quelle
Wenn die Anzahl der Buckets (nennen Sie es b) konstant gehalten wird (der übliche Fall), ist die Suche tatsächlich O (n).
Wenn n groß wird, beträgt die Anzahl der Elemente in jedem Bucket durchschnittlich n / b. Wenn die Kollisionsauflösung auf eine der üblichen Arten erfolgt (z. B. verknüpfte Liste), lautet die Suche O (n / b) = O (n).
In der O-Notation geht es darum, was passiert, wenn n immer größer wird. Es kann irreführend sein, wenn es auf bestimmte Algorithmen angewendet wird, und Hash-Tabellen sind ein typisches Beispiel. Wir wählen die Anzahl der Eimer basierend auf der Anzahl der Elemente, mit denen wir uns befassen möchten. Wenn n ungefähr die gleiche Größe wie b hat, ist die Suche ungefähr zeitlich konstant, aber wir können es nicht O (1) nennen, da O als Grenze als n → ∞ definiert ist.
quelle
O(1+n/k)
wok
ist die Anzahl der Eimer?Wenn Implementierung setzt
k = n/alpha
dann ist esO(1+alpha) = O(1)
daalpha
eine Konstante ist.quelle
Wir haben festgestellt, dass sich die Standardbeschreibung der Hash-Tabellensuche mit O (1) auf die erwartete Durchschnittszeit bezieht, nicht auf die strikte Leistung im ungünstigsten Fall. Für eine Hash-Tabelle, die Kollisionen mit Verkettung auflöst (wie Javas Hashmap), ist dies technisch gesehen O (1 + α) mit einer guten Hash-Funktion , wobei α der Lastfaktor der Tabelle ist. Immer noch konstant, solange die Anzahl der Objekte, die Sie speichern, nicht mehr als ein konstanter Faktor ist, der größer als die Tabellengröße ist.
Es wurde auch erklärt, dass es streng genommen möglich ist, Eingaben zu erstellen, die O ( n ) -Suchen für jede deterministische Hash-Funktion erfordern . Es ist aber auch interessant, die im schlimmsten Fall erwartete Zeit zu berücksichtigen , die sich von der durchschnittlichen Suchzeit unterscheidet. Bei Verwendung der Verkettung ist dies O (1 + die Länge der längsten Kette), zum Beispiel Θ (log n / log log n ), wenn α = 1 ist.
Wenn Sie an theoretischen Methoden interessiert sind, um Worst-Case-Lookups mit konstanter Zeit zu erzielen, können Sie sich über dynamisches perfektes Hashing informieren , das Kollisionen rekursiv mit einer anderen Hash-Tabelle auflöst!
quelle
Es ist nur O (1), wenn Ihre Hashing-Funktion sehr gut ist. Die Implementierung der Java-Hash-Tabelle schützt nicht vor fehlerhaften Hash-Funktionen.
Ob Sie die Tabelle beim Hinzufügen von Elementen vergrößern müssen oder nicht, ist für die Frage nicht relevant, da es um die Suchzeit geht.
quelle
Elemente in der HashMap werden als Array verknüpfter Listen (Knoten) gespeichert. Jede verknüpfte Liste im Array repräsentiert einen Bucket für den eindeutigen Hashwert eines oder mehrerer Schlüssel.
Beim Hinzufügen eines Eintrags in der HashMap wird der Hashcode des Schlüssels verwendet, um die Position des Buckets im Array zu bestimmen.
Hier steht das & für den bitweisen UND-Operator.
Beispielsweise:
100 & "ABC".hashCode() = 64 (location of the bucket for the key "ABC")
Während des Abrufvorgangs wird auf dieselbe Weise die Position des Buckets für den Schlüssel bestimmt. Im besten Fall hat jeder Schlüssel einen eindeutigen Hashcode und führt zu einem eindeutigen Bucket für jeden Schlüssel. In diesem Fall verbringt die Methode get nur Zeit, um die Position des Buckets zu bestimmen und den Wert abzurufen, der konstant O (1) ist.
Im schlimmsten Fall haben alle Schlüssel denselben Hashcode und werden in demselben Bucket gespeichert. Dies führt dazu, dass die gesamte Liste durchlaufen wird, was zu O (n) führt.
Im Fall von Java 8 wird der Bucket der verknüpften Liste durch eine TreeMap ersetzt, wenn die Größe auf mehr als 8 ansteigt. Dies reduziert die Sucheffizienz im ungünstigsten Fall auf O (log n).
quelle
Dies gilt grundsätzlich für die meisten Hash-Tabellen-Implementierungen in den meisten Programmiersprachen, da sich der Algorithmus selbst nicht wirklich ändert.
Wenn in der Tabelle keine Kollisionen vorhanden sind, müssen Sie nur eine einzige Suche durchführen, daher beträgt die Laufzeit O (1). Wenn Kollisionen vorliegen, müssen Sie mehr als eine Suche durchführen, wodurch die Leistung in Richtung O (n) verringert wird.
quelle
Dies hängt vom gewählten Algorithmus ab, um Kollisionen zu vermeiden. Wenn Ihre Implementierung eine separate Verkettung verwendet, tritt das Worst-Case-Szenario auf, in dem jedes Datenelement auf denselben Wert gehasht wird (z. B. schlechte Auswahl der Hash-Funktion). In diesem Fall unterscheidet sich die Datensuche nicht von einer linearen Suche in einer verknüpften Liste, dh O (n). Die Wahrscheinlichkeit, dass dies geschieht, ist jedoch vernachlässigbar und die besten und durchschnittlichen Nachschlagefälle bleiben konstant, dh O (1).
quelle
Abgesehen von Akademikern sollte aus praktischer Sicht akzeptiert werden, dass HashMaps keine Auswirkungen auf die Leistung hat (sofern Ihr Profiler Ihnen nichts anderes sagt).
quelle
Nur im theoretischen Fall, wenn Hashcodes immer unterschiedlich sind und der Bucket für jeden Hashcode ebenfalls unterschiedlich ist, existiert O (1). Andernfalls ist die Reihenfolge konstant, dh beim Inkrementieren der Hashmap bleibt die Suchreihenfolge konstant.
quelle
Natürlich hängt die Leistung der Hashmap von der Qualität der Funktion hashCode () für das angegebene Objekt ab. Wenn die Funktion jedoch so implementiert wird, dass die Wahrscheinlichkeit von Kollisionen sehr gering ist, hat sie eine sehr gute Leistung (dies ist nicht in jedem möglichen Fall streng O (1), aber in den meisten Fällen Fällen).
Die Standardimplementierung in der Oracle-JRE besteht beispielsweise darin, eine Zufallszahl zu verwenden (die in der Objektinstanz gespeichert ist, damit sie sich nicht ändert - aber auch die voreingenommene Sperrung deaktiviert, aber das ist eine andere Diskussion), damit die Wahrscheinlichkeit von Kollisionen besteht sehr niedrig.
quelle
hashCode % tableSize
die es durchaus zu Kollisionen kommen kann. Sie können die 32-Bit-Version nicht vollständig nutzen. Das ist der Sinn von Hash-Tabellen ... Sie reduzieren einen großen Indizierungsbereich auf einen kleinen.