Opgrader dine Python-færdigheder: Undersøgelse af ordbogen

en hash-tabel (hash-kort) er en datastruktur, der implementerer en associerende matrix abstrakt datatype, en struktur, der kan kortlægge nøgler til værdier.

Hvis det lugter som en Python dict, føles som en dictog ligner en ... ja, det må være en dict. Absolut! Åh, og setogså ...

Hvad?

Ordbøger og sæt i Python implementeres ved hjælp af en hash-tabel. Det lyder måske skræmmende i starten, men når vi undersøger nærmere, bør alt være klart.

Objektiv

I hele denne artikel vil vi opdage, hvordan a dictimplementeres i Python, og vi vil bygge vores egen implementering af (en simpel). Artiklen er opdelt i tre dele, og opbygningen af ​​vores brugerdefinerede ordbog finder sted i de to første:

  1. Forståelse af, hvad hash-tabeller er, og hvordan man bruger dem
  2. Dykke ned i Pythons kildekode for bedre at forstå, hvordan ordbøger implementeres
  3. Undersøg forskelle mellem ordbogen og andre datastrukturer såsom lister og sæt

Hvad er et hashbord?

En hash-tabel er en struktur, der er designet til at gemme en liste med nøgleværdipar uden at gå på kompromis med hastighed og effektivitet ved manipulation og søgning i strukturen.

Effektiviteten af ​​hash-tabellen er afledt af hash-funktionen - en funktion, der beregner indekset for nøgleværdiparet - Det betyder, at vi hurtigt kan indsætte, søge og fjerne elementer, da vi kender deres indeks i hukommelsesarrayet.

Kompleksiteten begynder, når to af vores nøgler hash til den samme værdi. Dette scenario kaldes en hash-kollision . Der er mange forskellige måder at håndtere en kollision på, men vi dækker kun Pythons måde. Vi går ikke for dybt med vores hash-tabel forklaring for at holde denne artikel nybegyndervenlig og Python-fokuseret.

Lad os sørge for, at vi har pakket hovedet omkring begrebet hash-tabeller, inden vi går videre. Vi starter med at oprette skeletter til vores meget (meget) enkle brugerdefinerede, dictder kun består af indsættelses- og søgemetoder ved hjælp af nogle af Pythons dundermetoder. Vi bliver nødt til at initialisere hash-tabellen med en liste over en bestemt størrelse og aktivere abonnement ([] -tegn) for den:

Nu skal vores hash-tabel liste indeholde specifikke strukturer, der hver indeholder en nøgle, en værdi og en hash:

Grundlæggende eksempel

En lille virksomhed med 10 ansatte ønsker at føre registre, der indeholder deres medarbejders resterende sygedage. Vi kan bruge følgende hash-funktion, så alt kan passe ind i hukommelsesarrayet:

length of the employee's name % TABLE_SIZE

Lad os definere vores hash-funktion i indgangsklassen:

Nu kan vi initialisere et 10-element array i vores tabel:

Vente! Lad os tænke over det. Vi vil sandsynligvis tackle nogle hash-kollisioner. Hvis vi kun har 10 elementer, vil det være meget sværere for os at finde et åbent rum efter en kollision. Lad os beslutte, at vores bord vil have dobbelt størrelse - 20 elementer! Det vil være praktisk i fremtiden, lover jeg.

For hurtigt at indsætte hver medarbejder følger vi logikken:

array[length of the employee's name % 20] = employee_remaining_sick_days

Så vores indsættelsesmetode vil se ud som følger (ingen hash-kollisionshåndtering endnu):

Til søgning gør vi stort set det samme:

array[length of the employee's first name % 20] 

Vi er ikke færdige endnu!

Python kollisionshåndtering

Python bruger en metode kaldet Åben adressering til håndtering af kollisioner. Det ændrer også størrelsen på hash-tabellerne, når den når en bestemt størrelse, men vi diskuterer ikke dette aspekt. Åben adresseringsdefinition fra Wikipedia:

I en anden strategi, kaldet åben adressering, gemmes alle posteringer i selve bucket-arrayet. Når en ny post skal indsættes, undersøges spandene, startende med hashed-to-spalten og fortsætter i en eller anden probesekvens , indtil der findes en ledig spalte. Når du søger efter en post, scannes skovlene i samme rækkefølge, indtil enten måleregistreringen findes, eller der findes en ubrugt matrixplads, hvilket indikerer, at der ikke er nogen sådan nøgle i tabellen.

Lad os undersøge processen med at hente en værdi keyved at se på Python-kildekoden (skrevet i C):

  1. Beregn hash af key
  2. Beregn indexelementets element efter hash & maskhvor mask = HASH_TABLE_SIZE-1(i enkle vendinger - tag N sidste bits fra hashbitene):
i = (size_t)hash & mask;

3. Hvis den er tom, skal du returnere, DKIX_EMPTYhvilket til sidst oversættes til en KeyError:

if (ix == DKIX_EMPTY) { *value_addr = NULL; return ix;}

4. Hvis det ikke er tomt, skal du sammenligne nøgler og hashes og indstille value_addradressen til den aktuelle værdi-adresse, hvis den er lig:

if (ep->me_key == key) { *value_addr = ep->me_value; return ix;}

og:

if (dk == mp->ma_keys && ep->me_key == startkey) { if (cmp > 0) { *value_addr = ep->me_value; return ix; }}

5. Hvis det ikke er lige, skal du bruge forskellige bit af hash (algoritme forklaret her) og gå til trin 3 igen:

perturb >>= PERTURB_SHIFT;i = (i*5 + perturb + 1) & mask;

Her er et diagram, der illustrerer hele processen:

Indsættelsesprocessen er ret ens - hvis den fundne plads er tom, indsættes posten, hvis den ikke er tom, sammenligner vi nøglen og hashen - hvis den er lige, erstatter vi værdien, og hvis ikke fortsætter vi vores søgen efter at finde et nyt sted med perturbalgoritmen.

Låne ideer fra Python

Vi kan låne Pythons idé om at sammenligne både nøgler og hashes for hver post til vores entry-objekt (erstatter den tidligere metode):

Vores hash-tabel har stadig ikke nogen kollisionshåndtering - lad os implementere en! Som vi så tidligere, gør Python det ved at sammenligne poster og derefter ændre bitmasken, men vi vil gøre det ved hjælp af en metode kaldet lineær sondering (som er en form for åben adressering, forklaret ovenfor):

Når hash-funktionen forårsager en kollision ved at kortlægge en ny nøgle til en celle i hash-tabellen, der allerede er optaget af en anden nøgle, søger lineær sondering i tabellen for den nærmeste følgende gratis placering og indsætter den nye nøgle der.

So what we’re going to do is to move forward until we find an open space. If you recall, we implemented our table with double the size (20 elements and not 10) — This is where it comes handy. When we move forward, our search of an open space will be much quicker because there’s more room!

But we have a problem. What if someone evil tries to insert the 11th element? We need to raise an error (we won’t be dealing with table resizing in this article). We can keep a counter of filled entries in our table:

Now let’s implement the same in our searching method:

The full code can be found here.

Now the company can safely store sick days for each employee:

Python Set

Going back to the beginning of the article, set and dict in Python are implemented very similarly, with set using only key and hash inside each record, as can be seen in the source code:

typedef struct { PyObject *key; Py_hash_t hash; /* Cached hash code of the key */} setentry;

As opposed to dict, that holds a value:

typedef struct { /* Cached hash code of me_key. */ Py_hash_t me_hash; PyObject *me_key; PyObject *me_value; /* This field is only meaningful for combined tables */} PyDictKeyEntry;

Performance and Order

Time comparison

I think it’s now clear that a dict is much much faster than a list (and takes way more memory space), in terms of searching, inserting (at a specific place) and deleting. Let's validate that assumption with some code (I am running the code on a 2017 MacBook Pro):

And the following is the test code (once for the dict and once for the list, replacing d):

The results are, well, pretty much what we expected..

dict: 0.015382766723632812 seconds

list:55.5544171333313 seconds

Ordren afhænger af indsætningsrækkefølgen

Diktens rækkefølge afhænger af historien om indsættelse. Hvis vi indsætter en post med en bestemt hash, og derefter en post med den samme hash, ender den anden post på et andet sted, så hvis vi først indsætter den.

Før du går…

Tak for læsningen! Du kan følge mig på Medium for flere af disse artikler eller på GitHub for at finde nogle seje repos :)

Hvis du nød denne artikel, skal du holde klappeknappen nede? for at hjælpe andre med at finde det. Jo længere du holder det, jo flere klapper giver du!

Og tøv ikke med at dele dine tanker i kommentarerne nedenfor, eller korriger mig, hvis jeg fik noget galt.

Yderligere ressourcer

  1. Hash Crash: Grundlæggende om Hash-tabeller
  2. The Mighty Dictionary
  3. Introduktion til algoritmer