A er af Python. til at gå, eller i JavaScript. associative arrayer i PHP, i C++. Hash-kort implementeres i stort set alle programmeringssprog på højt niveau. Og de er fantastiske! Hvem ønsker ikke at gemme og derefter få adgang til data i konstant tid? Uanset om du arbejder med store datasæt eller maling Leetcode-problemer, kommer denne datastruktur meget ofte til redning. Men hvad er de nøjagtigt, og hvordan fungerer de under kappen? I denne artikel vil vi forsøge at besvare disse spørgsmål. dict map Object Map Dictionary<TKey, TValue> Hvad er en Hash Map? På et højt niveau er et hashkort eller hashtabel en datastruktur, der implementerer en assosiativ array abstrakt datatype, en struktur, der kan kortlægge nøgler til værdier. Nøglen bruges til unikt at identificere en værdi i kortet, og værdien er de data, der gemmes. In fact, the average time complexity for these operations is O(1), which means that they can be performed in constant time! This feature makes hash maps probably the most used data structure in programming, however, there are some caveats to this, as we will see later. Den værste tidskompleksitet for disse operationer er O(n), som kan ske i visse scenarier, og jo mere vi ved om de interne, jo mere sandsynligt er vi at undgå disse scenarier. Ifølge den : Wikipedia artikel En hashtabel er en datastruktur, der implementerer en associativ array, også kaldet en ordbog eller blot et kort; en associativ array er en abstrakt datatype, der kortlægger nøgler til værdier. En hashtabel bruger en hashfunktion til at beregne et indeks, også kaldet en hash kode, i en række buckets eller slots, hvorfra den ønskede værdi kan findes. En hashtabel er en datastruktur, der implementerer en associativ array, også kaldet en ordbog eller blot et kort; en associativ array er en abstrakt datatype, der kortlægger nøgler til værdier. En hashtabel bruger en hashfunktion til at beregne et indeks, også kaldet en hash kode, i en række buckets eller slots, hvorfra den ønskede værdi kan findes. Så lad os tage et skridt tilbage og se på komponenterne i et hashkort. Hvad er en hash-funktion? En hashfunktion er en funktion, der tager en input (eller "nøgle") og normalt returnerer et heltal, der bruges til at indeksere dataene i hashkortet. En god hashfunktion har følgende egenskaber: Deterministisk: Den samme input vil altid producere den samme output. Uniform Distribution: Hashfunktionen skal fordele nøglerne ensartet på tværs af hashbordet for at minimere kollisioner. Hurtig beregning: Hashfunktionen skal være hurtig at beregne, selv for store input. Minimere kollisioner: Mængden af mulige nøgler er typisk meget større (ofte uendeligt) end mængden af hash-koder. Dette betyder, at forskellige nøgler kan producere den samme hash-kode. Et simpelt eksempel på en hashfunktion er moduloperationen, som tager en nøgle og returnerer resten, når den deles med størrelsen på hashtabellen. , hvilket betyder, at værdien forbundet med nøglen 23 vil blive gemt på indeks 3 i det underliggende array. Det betyder også, at vi har en kollision.I dette tilfælde vil begge nøgler kortlægge til samme indeks i arrayet. 23 % 10 = 3 33 % 10 = 3 Hvad er en bucket? En bucket er en slot i hashtabellen, hvor et nøgleværdipar er gemt. I tilfælde af en kollision, hvor to forskellige nøgler producerer den samme hash kode, kan bucket gemme flere nøgleværdipar. Dette gøres ofte ved hjælp af en linket liste eller en anden datastruktur til at håndtere kollisioner. Dette diagram illustrerer, hvordan alt dette fungerer: af Her kan vi se, hvordan hashfunktionen kortlægger nøgler til indekser i det underliggende array. Nøglerne 23 og 33 begge producerer den samme hashkode på 3, hvilket betyder, at de er gemt i samme bucket. Bucket kan derefter gemme begge nøgleværdipar, men når vi henter en værdi, skal vi tjekke nøglerne i bucket for at finde den rigtige. Dette er hvor tidskompleksiteten kan øges til O(n) i værste fald, hvis mange (eller endda alle) nøgler kolliderer og er gemt i samme bucket. Load faktor Belastningsfaktoren er et mål for, hvor fuld hashtabellen er. Den beregnes som antallet af elementer i hashtabellen divideret med antallet af buckets (eller slots) i det underliggende array. En højere belastningsfaktor betyder, at der er flere elementer i hashtabellen i forhold til antallet af buckets, hvilket kan føre til flere kollisioner og langsommere ydeevne. Collision Resolution Når to nøgler producerer den samme hash kode, har vi en kollision. : 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: I denne metode, når der opstår en kollision, søger hash-tabellen efter den næste tilgængelige slot i arrayet for at gemme det nye nøgle-værdi-par. Hvis der opstår en kollision, kontrollerer algoritmen det næste slot i arrayet, indtil det finder et tomt slot. Linear Probing Kompleksitet: Gennemsnitlig O(1), værste tilfælde O(n) på grund af primær klynger Fordele: Enkel implementering, god cache ydeevne for nærliggende adgang Ulemper: Primær clustering (konsekutivt besatte slots), ydeevneforringelse med clustering I stedet for at tjekke det næste slot, tjekker det slots på stigende afstande (1, 4, 9, osv.) fra det oprindelige indeks. Quadratic Probing Kompleksitet: Gennemsnitlig O(1), bedre end lineær sondning på grund af reduceret klynger Fordele: Reducerer primær clustering sammenlignet med lineær sondring, stadig cache-venlig Ulemper: Sekundær klassificering (nøgler med samme hash følger samme sondesekvens), kan ikke besøge alle slots : Bruger en anden hashfunktion til at bestemme trinstørrelsen for sondring. I modsætning til lineær sondring, der altid bevæger sig til næste slot, eller kvadratisk sondring, der bruger en fast sekvens, beregner dobbelt hash en unik trinstørrelse for hver nøgle. Hvor hvor Den anden hashfunktion skal returnere en værdi, der er forholdsvis primær i forhold til bordstørrelsen for at sikre, at alle slots kan besøges. og , så ville vi undersøge indekserne 7, 10, 13, 16 osv. 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 Hver af disse metoder har sine egne kompromisser med hensyn til kompleksitet, ydeevne og hukommelsesforbrug. Valget af kollisionsopløsningsstrategi kan have en betydelig indvirkning på hashkortets overordnede ydeevne. Her er et hurtigt resumé af fordelene og ulemperne ved hver kollisionsopløsningsmetode: 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 O(1) O(1) O(1) O(1) Worst-case Time Complexity O (n) O (n) O (n) O (n) Memory Overhead Højdepunkter (høje punkter) lavt lavt lavt Cache Performance fattige Godt Godt Moderat Implementation Complexity Enkelt Enkelt Moderat Komplekset Clustering Issues Ingen Primærklasse Sekundær cluster Minimum af Load Factor Tolerance Høj (> 1.0) Det er meget lavt (<0.7) Lavt gennemsnitligt (< 0,7) Den gennemsnitlige (<0.8) Deletion Complexity Enkelt Complex (tombstones) Komplekser (Tombstones og gravsten) Komplekser (Tombstones og gravsten) Space Efficiency lavere højere højere højere Performance Degradation Gradvis Hurtig ved høj belastning Moderate at high load Langsom ved høj belastning Hash Function Requirements En En En To af Best Use Cases Ukendte belastningsfaktorer, hyppige sletninger Cache-venlige applikationer, lav belastning Bedre end lineær, moderat belastning Høj ydeevne, forudsigelig belastning Eksempler fra den virkelige verden Programmeringssprogs implementeringer Disse implementeringer bruger ofte en kombination af de teknikker, der er beskrevet ovenfor for at levere effektiv ydeevne og håndtere kollisioner effektivt. Pythons dict bruger åben adressering med randomiseret sonding, rehashing, når belastningsfaktoren overstiger ca. 0,66. Java's HashMap bruger kæde med linkede lister (konvertering til afbalancerede træer for store buckets i Java 8+), rehashes ved 0,75 belastningsfaktor. C++'s unordered_map bruger typisk kæde, men implementeringerne kan variere. Databasesystemer Hash-kort bruges også i stor udstrækning i databaseindeksering. Mange databasesystemer bruger hashindekser til at fremskynde dataindsamling. Disse indekser giver mulighed for hurtige søgninger ved at hash de indekserede kolonner og gemme de resulterende nøgleværdipar i en hashtabel. Når en forespørgsel udføres, kan databasen hurtigt finde de relevante rækker ved at beregne hashnøglen og kigge den op i hashindeksen. Nogle populære databasesystemer, der bruger hash indeksering omfatter: PostgreSQL: Understøtter hashindekser, men de bruges ikke så almindeligt som B-tree-indekser. MongoDB: Bruger hashindekser til sharding og til at understøtte ligestillingsforespørgsler på hashede felter. Redis: Implementerer hashkort som en kerne datastruktur, der muliggør effektiv lagring og indhentning af nøgleværdipar. Disse implementeringer udnytter ofte de samme underliggende principper for hashing og kollisionsopløsning, der blev diskuteret tidligere, men de kan også indarbejde yderligere optimeringer, der er specifikke for databasekonteksten. Versionskontrol Versionskontrolsystemer som Git bruger hash-kort til effektivt at styre ændringer i filer og spore versioner. Hvert commit i Git er identificeret ved en SHA-1 hash af dets indhold, som tjener som en unik nøgle for commit-objektet. Dette giver Git mulighed for hurtigt at søge efter commits, grene og andre objekter i repositoriet. Git bruger ikke traditionel hash-tabelløsning, det er designet omkring antagelsen om, at kryptografiske hash-kollisioner ikke vil forekomme i praksis. At sætte det hele sammen: Hvordan implementering af viden betyder noget Forståelse af, hvordan hash-kort implementeres i dit valgte programmeringssprog, kan føre til betydelige ydeevneforbedringer i din kode. For eksempel har Python bruger åben adressering med optimeret stringhåndtering, forståelse af dette kan føre til meget bedre ydeevne. dict Dårlig implementering (kæmper mod Pythons dikt) 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 Indtast fuldskærmstilstand Udtast fuldskærmstilstand Problems: Flere hash-søgninger pr. ord (op til 3!) Open addressing gør nøgle-manglende checks dyre Udnytter ikke Pythons dict-optimeringer God implementering (Fungerer med 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 Indtast fuldskærmstilstand Udtast fuldskærmstilstand Why These Are Better: En enkelt hashoperation pr. ord i stedet for flere Udnytter Pythons strengoptimering - strengnøgler håndteres meget effektivt Arbejder med åben adressering - færre undersøgelsesoperationer er nødvendige Bruger indbyggede optimeringer som Counter, der er tilpasset Pythons implementering Præstationsforskel Den gode implementering er ofte 2-3 gange hurtigere, simpelthen ved at forstå og arbejde med Pythons dict implementering snarere end imod det! Typical Results: Konklusionen Hash-kort er blandt de mest grundlæggende og kraftfulde datastrukturer i datalogi, der giver næsten konstant adgang til data, hvilket gør dem uundværlige i moderne programmering.Gennem hele denne dybe dykning har vi undersøgt, hvordan de opnår deres bemærkelsesværdige O(1) gennemsnitlige ydeevne gennem smart brug af hashfunktioner, strategisk kollisionsopløsning og omhyggelig belastningsfaktorstyring. Den vigtigste indsigt er, at "magien" af hash-kort slet ikke er magisk - det er resultatet af veldesignede algoritmer og datastrukturer, der arbejder sammen. Key Takeaways: Hashfunktioner er fundamentet – de bestemmer, hvordan data er jævnt fordelt og direkte påvirker kollisionsrater Collision resolution strategier hver har forskellige kompromisser: kæde for enkelhed og robusthed, åben adressering for hukommelse effektivitet og cache ydeevne Load factor management gennem rehashing forhindrer performance degradation som hash-kort vokser Implementeringskendskab oversætter til reelle præstationsgevinster – at forstå, om dit sprog bruger kæde eller åben adressering kan gøre din kode 2-3 gange hurtigere Uanset om du optimerer et Python-script, debugger ydeevneproblemer i Java eller træffer arkitektoniske beslutninger for et databasesystem, giver denne forståelse af HashMap-internals dig værktøjerne til at træffe informerede valg. , der eller Du vil vide præcis, hvad der foregår under kappen, og hvordan du får mest muligt ud af disse utrolige datastrukturer. dict HashMap unordered_map Hash-kort er virkelig fantastiske – og nu ved du hvorfor!