A ist Und in Python. und geht, oder in JavaScript. Associative Array in PHP, in C++. Hash-Karten werden in praktisch jeder Programmiersprache auf hohem Niveau implementiert. Und sie sind erstaunlich! Wer möchte keine Daten speichern und dann in ständiger Zeit auf Daten zugreifen? Ob Sie mit großen Datensätzen arbeiten oder Leetcode-Probleme schleifen, sehr oft kommt diese Datenstruktur zur Rettung. Aber was sind sie genau und wie funktionieren sie unter der Kappe? In diesem Artikel werden wir versuchen, diese Fragen zu beantworten. dict map Object Map Dictionary<TKey, TValue> Was ist eine Hash-Karte? Auf einer hohen Ebene ist eine Hash-Karte oder Hash-Tabelle eine Datenstruktur, die einen assoziativen Array-Abstraktdatentyp implementiert, eine Struktur, die Schlüssel zu Werten mappen kann. Der Schlüssel wird verwendet, um einen Wert in der Karte eindeutig zu identifizieren, und der Wert ist die Daten, die gespeichert werden. Tatsächlich ist die durchschnittliche Zeitkomplexität für diese Operationen O(1), was bedeutet, dass sie in ständiger Zeit ausgeführt werden können! Diese Funktion macht Hash-Karten wahrscheinlich die am häufigsten verwendete Datenstruktur in der Programmierung, es gibt jedoch einige Warnungen, wie wir später sehen werden. Die schlimmste Zeitkomplexität für diese Operationen ist O(n), was in bestimmten Szenarien passieren kann, und je mehr wir über die Interne wissen, desto wahrscheinlicher sind wir, diese Szenarien zu vermeiden. Nach den : Wikipedia Artikel Eine Hash-Tabelle ist eine Datenstruktur, die ein assoziatives Array implementiert, auch ein Wörterbuch oder einfach eine Karte genannt; ein assoziatives Array ist ein abstrakter Datentyp, der Schlüssel zu Werten mappt. Eine Hash-Tabelle verwendet eine Hash-Funktion, um einen Index, auch als Hash-Code bezeichnet, in eine Reihe von Buckets oder Slots zu berechnen, aus denen der gewünschte Wert gefunden werden kann. Eine Hash-Tabelle ist eine Datenstruktur, die ein assoziatives Array implementiert, auch ein Wörterbuch oder einfach eine Karte genannt; ein assoziatives Array ist ein abstrakter Datentyp, der Schlüssel zu Werten mappt. Eine Hash-Tabelle verwendet eine Hash-Funktion, um einen Index, auch als Hash-Code bezeichnet, in eine Reihe von Buckets oder Slots zu berechnen, aus denen der gewünschte Wert gefunden werden kann. Nehmen wir also einen Schritt zurück und schauen wir uns die Komponenten einer Hashkarte an. Was ist eine Hash-Funktion? Eine Hash-Funktion ist eine Funktion, die eine Eingabe (oder "Schlüssel") nimmt und in der Regel eine ganze Zahl zurückgibt, die verwendet wird, um die Daten in der Hash-Karte zu indexieren. Eine gute Hash-Funktion hat folgende Eigenschaften: Deterministisch: Die gleiche Eingabe führt immer zur gleichen Ausgabe. Die Hash-Funktion sollte die Schlüssel gleichmäßig über die Hash-Tabelle verteilen, um Kollisionen zu minimieren. Schnelle Berechnung: Die Hash-Funktion sollte auch bei großen Eingängen schnell berechnet werden. Minimieren von Kollisionen: Der Raum für mögliche Schlüssel ist in der Regel viel größer (oft unendlich) als der Raum für Hashcodes.Dies bedeutet, dass verschiedene Schlüssel den gleichen Hashcode erzeugen können.Während diese Kollisionen unvermeidlich sind, minimiert eine gute Hashfunktion die Wahrscheinlichkeit, dass zwei verschiedene Schlüssel den gleichen Hashcode erzeugen. Ein einfaches Beispiel für eine Hash-Funktion ist die Modulo-Operation, die einen Schlüssel nimmt und den Rest zurückgibt, wenn er durch die Größe der Hash-Tabelle geteilt wird. , was bedeutet, dass der mit dem Schlüssel 23 verbundene Wert am Index 3 im zugrunde liegenden Array gespeichert wird. auch, was bedeutet, dass wir eine Kollision haben. In diesem Fall würden beide Schlüssel auf den gleichen Index im Array mappen. 23 % 10 = 3 33 % 10 = 3 Was ist ein Bucket? Ein Bucket ist ein Slot in der Hash-Tabelle, wo ein Schlüsselwertpaar gespeichert wird. Im Falle einer Kollision, wo zwei verschiedene Schlüssel den gleichen Hash-Code erzeugen, kann der Bucket mehrere Schlüsselwertpaare speichern. Dieses Diagramm zeigt, wie das alles funktioniert: ist Hier können wir sehen, wie die Hash-Funktion die Schlüssel in die Indizes im zugrunde liegenden Array mappt. Die Schlüssel 23 und 33 beide produzieren den gleichen Hash-Code von 3, was bedeutet, dass sie in derselben Schublade gespeichert sind. Die Schublade kann dann beide Schlüssel-Wertpaare speichern, aber wenn wir einen Wert abrufen, müssen wir die Schlüssel in der Schublade überprüfen, um den richtigen zu finden. Dies ist, wo die Zeitkomplexität im schlimmsten Fall auf O(n) steigen kann, wenn viele (oder sogar alle) Schlüssel kollidieren und in derselben Schublade gespeichert werden. Load Faktor Der Lastfaktor ist ein Maß dafür, wie voll die Hash-Tabelle ist. Es wird als die Anzahl der Elemente in der Hash-Tabelle durch die Anzahl der Buckets (oder Slots) in der zugrunde liegenden Array geteilt berechnet. Ein höherer Lastfaktor bedeutet, dass es mehr Elemente in der Hash-Tabelle im Verhältnis zur Anzahl der Buckets gibt, was zu mehr Kollisionen und langsamerer Leistung führen kann. Kollision Resolution Wenn zwei Schlüssel den gleichen Hashcode erzeugen, haben wir eine Kollision. Es gibt mehrere Strategien, um Kollisionen in Hash-Karten zu bewältigen: : In this method, each bucket contains a linked list (or another data structure) of all key-value pairs that hash to the same index. When a collision occurs, the new key-value pair is simply added to the list in the appropriate bucket. This is the most common method for handling collisions. Chaining : Average O(1) for all operations, worst-case O(n) if all keys hash to the same bucket Complexity : Simple to implement, handles high load factors well, deletion is straightforward Pros : Extra memory overhead for pointers, poor cache performance due to scattered memory access Cons Open Addressing: In dieser Methode, wenn eine Kollision auftritt, sucht die Hash-Tabelle nach dem nächsten verfügbaren Slot im Array, um das neue Schlüssel-Wertpaar zu speichern. Wenn eine Kollision auftritt, überprüft der Algorithmus den nächsten Slot in der Array, bis er einen leeren findet. Linear Probing Komplexität: Durchschnitt O(1), am schlimmsten O(n) aufgrund der primären Clustering Vorteile: Einfache Implementierung, gute Cache-Leistung für nahe gelegene Zugriffe Nachteile: Primäres Clustering (nachfolgende besetzte Slots), Performance-Degradation mit Clustering : Anstatt den nächsten Slot zu überprüfen, überprüft er die Slots bei zunehmenden Entfernungen (1, 4, 9 usw.) vom ursprünglichen Index. Quadratic Probing Komplexität: Durchschnittliche O(1), besser als lineare Prüfung aufgrund reduzierter Clustering Vorteile: Reduziert primäre Clustering im Vergleich zu linearem Sondern, immer noch cache-freundlich Nachteile: Sekundäres Clustering (Schlüssel mit dem gleichen Hash folgen der gleichen Sondensequenz), kann nicht alle Slots besuchen : Verwendet eine zweite Hash-Funktion, um die Schrittgröße für das Prüfen zu bestimmen. Im Gegensatz zu linearem Prüfen, das sich immer zum nächsten Slot bewegt, oder quadratischem Prüfen, das eine feste Sequenz verwendet, berechnet Doppelhashing eine einzigartige Schrittgröße für jeden Schlüssel. Wo wo Die zweite Hash-Funktion muss einen Wert zurückgeben, der der Tabellengröße relativ primär entspricht, um sicherzustellen, dass alle Slots besucht werden können. und , dann würden wir Indizes 7, 10, 13, 16 usw. untersuchen. Double Hashing index = (hash1(key) + i * hash2(key)) % table_size i hash1(key) = 7 hash2(key) = 3 : Average O(1), best performance among open addressing methods Complexity : Minimizes clustering, uniform distribution of probe sequences, visits all slots when properly implemented Pros : More complex implementation, requires computing two hash functions, and slightly more overhead per operation Cons : If the load factor becomes too high, the hash table can be resized and all existing key-value pairs can be rehashed into the new table. This helps to maintain efficient performance as the number of elements grows. Rehashing : O(n) for the rehashing operation itself, but amortizes to O(1) per insertion over time Complexity : Maintains optimal performance by keeping the load factor low, prevents performance degradation Pros : Temporary performance spike during rehashing, requires additional memory during the resize operation Cons Jede dieser Methoden hat ihre eigenen Kompromisse in Bezug auf Komplexität, Leistung und Speichernutzung.Die Wahl der Kollisionsentscheidungsstrategie kann einen signifikanten Einfluss auf die Gesamtleistung der Hash-Karte haben. Hier ist eine kurze Zusammenfassung der Vor- und Nachteile jeder Kollisionslösungsmethode: Feature Chaining Linear Probing Quadratic Probing Double Hashing Average Time Complexity O(1) O(1) O(1) O(1) Worst-case Time Complexity O(n) O(n) O(n) O(n) Memory Overhead High (pointers) Low Low Low Cache Performance Poor Good Good Moderate Implementation Complexity Simple Simple Moderate Complex Clustering Issues None Primary clustering Secondary clustering Minimal Load Factor Tolerance High (>1.0) Low (<0.7) Low-Medium (<0.7) Medium (<0.8) Deletion Complexity Simple Complex (tombstones) Complex (tombstones) Complex (tombstones) Space Efficiency Lower Higher Higher Higher Performance Degradation Gradual Rapid at high load Moderate at high load Slow at high load Hash Function Requirements One One One Two Best Use Cases Unknown load factors, frequent deletions Cache-friendly applications, low load Better than linear, moderate load High performance, predictable load Average Time Complexity oder (1) oder (1) oder (1) oder (1) Worst-case Time Complexity oder n) oder n) oder n) oder n) Memory Overhead Höherer Wert (Pointers) niedriger niedriger niedriger Cache Performance arme gutes gutes Moderatorisch Implementation Complexity Einfache Einfache Moderatorisch Komplexe Clustering Issues Keiner Primärcluster Sekundärcluster Minimale Load Factor Tolerance Hoch (> 1,0) Niedrig (< 0,7) Niedrigdurchschnittliche (< 0,7) Durchschnittliche (< 0,8) Deletion Complexity Einfache Das Komplex (Tombstones) Complex (tombstones) Das Komplex (Tombstones) Space Efficiency niedriger höher höher höher Performance Degradation Graduell Schnell bei hoher Belastung Moderat bei hoher Belastung Langsam bei hoher Belastung Hash Function Requirements Einer Einer Einer Zwei Best Use Cases Unbekannte Lastfaktoren, häufige Löschungen Cache-freundliche Anwendungen, niedrige Ladung Besser als lineare, moderate Belastung Hohe Leistung, vorhersehbare Belastung Einige Beispiele aus der realen Welt Programmiersprachen Implementierung Viele Programmiersprachen haben eingebaute Hash-Karten.Diese Implementierungen verwenden oft eine Kombination der oben beschriebenen Techniken, um effiziente Leistung zu bieten und Kollisionen effektiv zu bewältigen. Pythons Dichtung verwendet offene Adressierung mit randomisiertem Sondern, wenn der Lastfaktor etwa 0,66 überschreitet. Java's HashMap verwendet Ketten mit verknüpften Listen (umgewandelt in ausgewogene Bäume für große Buckets in Java 8+), beschleunigt bei 0,75 Load-Faktor. In der Regel verwendet C++'s unordered_map Ketten, aber die Implementierungen können variieren. Datenbanksysteme Hash-Karten werden auch in der Datenbank-Indexierung weit verbreitet. Viele Datenbank-Systeme verwenden Hash-Indizes, um die Datenerfassung zu beschleunigen. Diese Indizes ermöglichen schnelle Suchanfragen, indem sie die indexierten Spalten hashen und die resultierenden Schlüsselwertepaare in einer Hash-Tabelle speichern.Wenn eine Abfrage ausgeführt wird, kann die Datenbank schnell die relevanten Zeilen finden, indem sie den Hash des Abfrage-Schlüssels berechnet und in den Hash-Index sucht. Einige beliebte Datenbanksysteme, die Hash-Indexierung verwenden, umfassen: PostgreSQL: Unterstützt Hash-Index, wird aber nicht so häufig verwendet wie B-Tree-Index. MongoDB: Verwendet Hash-Indizes zum Sharding und zur Unterstützung von Gleichheitsabfragen für Hash-Felder. Redis: Implementiert Hash-Karten als Kerndatenstruktur und ermöglicht eine effiziente Speicherung und Abrufung von Schlüsselwertpaaren. Diese Implementierungen nutzen oft die gleichen zugrunde liegenden Prinzipien des Hashings und der Kollisionslösung, die zuvor diskutiert wurden, aber sie können auch zusätzliche Optimierungen für den Datenbankkontext enthalten. Versionskontrolle Versionssteuerungssysteme wie Git verwenden Hash-Karten, um Dateiänderungen effizient zu verwalten und Versionen zu verfolgen. Jedes Kommit in Git wird durch einen SHA-1-Hash seiner Inhalte identifiziert, der als einzigartiger Schlüssel für das Kommit-Objekt dient. Dies ermöglicht es Git, schnell Kommitte, Zweige und andere Objekte im Repository zu suchen. Alles zusammenbringen: Wie die Umsetzung von Wissen zählt Das Verständnis, wie Hash-Karten in Ihrer gewählten Programmiersprache implementiert werden, kann zu signifikanten Leistungsverbesserungen in Ihrem Code führen. Zum Beispiel, seit Python verwendet offene Adressierung mit optimiertem String-Handling, das Verständnis kann zu viel besserer Leistung führen. dict Schlechte Implementierung (kämpft gegen Python's Dict) def count_words_bad(text): word_counts = {} words = text.split() for word in words: # This is inefficient with open addressing! if word in word_counts: # First lookup word_counts[word] += 1 # Second lookup + assignment else: word_counts[word] = 1 # Third lookup + assignment return word_counts Vollbildmodus Eintritt Vollbildmodus Eintritt Vollbildmodus Problems: Mehrere Hash-Lookups pro Wort (bis zu 3!) Open addressing macht schlüsselfehlende checks teuer Nutzt keine Python-Dict-Optimierungen Gute Implementierung (wirkt mit Python's Dict) from collections import defaultdict, Counter def count_words_good_v1(text): # defaultdict eliminates key existence checks word_counts = defaultdict(int) words = text.split() for word in words: word_counts[word] += 1 # Single operation! return dict(word_counts) def count_words_good_v2(text): # Counter is optimized specifically for Python's dict implementation words = text.split() return Counter(words) def count_words_good_v3(text): # dict.get() with default avoids the membership test word_counts = {} words = text.split() for word in words: word_counts[word] = word_counts.get(word, 0) + 1 # Single lookup return word_counts Vollbildmodus Eintritt Vollbildmodus Eintritt Vollbildmodus Why These Are Better: Eine Hash-Operation pro Wort anstelle von mehreren Nutzt die String-Optimierung von Python – String-Tasten werden sehr effizient bearbeitet Arbeitet mit offenem Adressen - weniger Durchsuchungsoperationen erforderlich Verwendet eingebaute Optimierungen wie Counter, die für die Implementierung von Python angepasst sind Leistungsunterschied Die gute Implementierung ist oft 2-3x schneller, einfach durch das Verständnis und die Arbeit mit der Python-Dict-Implementierung und nicht gegen sie! Typical Results: Schlussfolgerung Hash-Karten gehören zu den grundlegendsten und leistungsstärksten Datenstrukturen in der Informatik und bieten nahezu konstanten Zeitzugang zu Daten, wodurch sie in der modernen Programmierung unentbehrlich sind. Die wichtigste Erkenntnis ist, dass die "Magie" von Hash-Karten überhaupt keine Magie ist - es ist das Ergebnis von gut gestalteten Algorithmen und Datenstrukturen, die zusammenarbeiten. Key Takeaways: Hash-Funktionen sind die Grundlage – sie bestimmen, wie Daten gleichmäßig verteilt sind und die Kollisionsraten direkt beeinflussen. Jede Kollisionserlösungsstrategie hat unterschiedliche Kompromisse: Ketten für Einfachheit und Robustheit, offene Adressierung für Speichereffizienz und Cache-Leistung Load-Faktor-Management durch Rehashing verhindert Leistungsabbau, wenn Hash-Karten wachsen Implementierungswissen führt zu echten Leistungsgewinnen – das Verständnis, ob Ihre Sprache Ketten- oder offene Adressierung verwendet, kann Ihren Code 2-3-mal schneller machen Egal, ob Sie ein Python-Script optimieren, Leistungsprobleme in Java debuggen oder architektonische Entscheidungen für ein Datenbankensystem treffen, dieses Verständnis von HashMap internals gibt Ihnen die Werkzeuge, um fundierte Entscheidungen zu treffen. , der oder Sie werden genau wissen, was unter der Kappe passiert und wie Sie diese unglaublichen Datenstrukturen optimal nutzen können. dict HashMap unordered_map Hash-Karten sind wirklich großartig – und jetzt weißt du warum!