Надоградите своје Питхон вештине: Испитивање речника

хеш табела (хеш мапа) је структура података која имплементира асоцијативни низ апстрактних врста података, структуру која може пресликати кључеве у вредности.

Ако мирише на Питхон dict, осећа се као dictи изгледа као један ... па, мора да је dict. Апсолутно! Ох, и setтакође ...

А?

Речници и скупови у Питхону имплементирани су помоћу хеш табеле. У почетку звучи застрашујуће, али како даље истражујемо, све би требало бити јасно.

објективан

Кроз овај чланак ћемо открити како се а dictимплементира у Питхон-у и направићемо сопствену имплементацију (једноставне). Чланак је подељен у три дела, а израда нашег прилагођеног речника одвија се у прва два:

  1. Разумевање шта су хеш табеле и како их користити
  2. Зароните у изворни код Питхона да бисте боље разумели како се имплементирају речници
  3. Истраживање разлика између речника и других структура података као што су листе и скупови

Шта је хеш табела?

Табела хеширања је структура која је дизајнирана за чување листе парова кључ / вредност, без угрожавања брзине и ефикасности манипулације и претраживања структуре.

Ефикасност хеш табеле изведена је из хеш функције - функције која израчунава индекс пара кључ / вредност - Што значи да можемо брзо уметати, претраживати и уклањати елементе јер знамо њихов индекс у меморијском низу.

Сложеност почиње када два наша кључа имају хеш исте вредности. Овај сценарио се назива хасх колизија . Постоји много различитих начина за решавање судара, али покрићемо само Питхонов начин. Нећемо улазити предубоко у објашњење наше хеш табеле ради одржавања овог чланка почетним и усредсређеним на Питхон.

Обавезно смо се умотали у концепт хеш столова пре него што кренемо даље. dictЗапочећемо са израдом костура за наш врло (врло) једноставан обичај који се састоји само од метода уметања и претраживања, користећи неке од Питхон-ових дундер метода. Морамо да иницијализујемо хеш табелу списком одређене величине и омогућимо претплату (знак []) за њу:

Сада наша листа хеш табела треба да садржи одређене структуре, од којих свака садржи кључ, вредност и хеш:

Основни пример

Мала компанија са 10 запослених жели да води евиденцију која садржи преостале боловања њихових запослених. Можемо користити следећу хеш функцију, тако да све може да стане у меморијски низ:

length of the employee's name % TABLE_SIZE

Дефинишимо нашу хеш функцију у класи Ентри:

Сада можемо иницијализовати низ од 10 елемената у нашој табели:

Чекати! Размислимо. Највероватније ћемо се позабавити неким хеш-колизијама. Ако имамо само 10 елемената, биће нам много теже пронаћи отворен простор након судара. Одлучимо да ће наша табела имати двоструку величину - 20 елемената! Обећавам, добро ће доћи у будућности.

Да бисмо брзо убацили сваког запосленог, следићемо логику:

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

Тако ће наша метода уметања изгледати следеће (још увек нема обраде хеш судара):

За претрагу у основи радимо исто:

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

Још нисмо завршили!

Руковање сударом Питхона

Питхон користи методу названу Опен Аддрессинг за руковање колизијама. Такође мења величину хеш табела када достигне одређену величину, али нећемо разговарати о том аспекту. Отворите дефиницију адресирања са Википедије:

У другој стратегији, која се назива отворено адресирање, сви уноси се чувају у самом низу сегмената. Када треба убацити нови унос, испитују се сегменти, почевши од утора за хеширање и настављајући у неком редоследу сонде , све док се не пронађе неузет слот. Када се тражи унос, сегменти се скенирају у истој секвенци, све док се не пронађе циљни запис или не пронађе неискоришћени утор низа, што указује да у табели не постоји такав кључ.

Испитајмо поступак дохвата вредности тако што ћемо keyпогледати изворни код Питхона (написан на Ц):

  1. Израчунај хеш од key
  2. Израчунајте indexставку према томе hash & maskгде mask = HASH_TABLE_SIZE-1(једноставним речима - узмите Н последњих битова из хеш-битова):
i = (size_t)hash & mask;

3. Ако је празно, вратите DKIX_EMPTYшто се на крају преводи у KeyError:

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

4. Ако није празно, упоредите кључеве и хешеве и поставите value_addrадресу на стварну адресу вредности ако је једнака:

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

и:

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

5. Ако није једнако, користите различите битове хеша (овде је објашњен алгоритам) и поново идите на корак 3:

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

Ево дијаграма који илуструје читав процес:

Процес уметања је прилично сличан - ако је пронађени слот празан, унос се убацује, ако није празан, упоређујемо кључ и хеш - ако је једнак, замењујемо вредност, а ако не настављамо потрагу за проналажењем ново место са perturbалгоритмом.

Посуђивање идеја од Питхона

Можемо позајмити Питхонову идеју упоређивања оба кључа и хеша сваког уноса са нашим објектом уноса (замењујући претходни метод):

Наша хеш-табела још увек нема никакво руковање сударима - применимо један! Као што смо раније видели, Питхон то чини упоређивањем уноса, а затим променом маске битова, али то ћемо учинити помоћу методе која се назива линеарно сондирање (која је облик отвореног адресирања, објашњено горе):

Када хеш функција изазове колизију пресликавањем новог кључа на ћелију хеш табеле која је већ заузета другим кључем, линеарно сондирање претражује табелу најближег следећег слободног места и тамо убацује нови кључ.

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

Редослед зависи од редоследа уметања

Редослед изреке зависи од историје уметања. Ако унос убацимо са одређеним хешом, а затим унос са истим хешом, други унос ће завршити на другом месту, ако бисмо га прво убацили.

Пре него што одеш…

Хвала за читање! Можете ме пратити на Медиум-у за више ових чланака или на ГитХуб-у због откривања неких супер репо-снимака :)

Ако вам се свидео овај чланак, држите притиснуто дугме за пљескање? да помогне другима да га пронађу. Што га дуже држите, дајете више пљеска!

И не устручавајте се да поделите своје мисли у коментарима испод или ме исправите ако нешто нисам у реду.

Додатна средства

  1. Пад хашева: основе хеш столова
  2. Моћни речник
  3. Увод у алгоритме