Tingkatkan kemahiran Python anda: Meneliti Kamus

jadual hash (peta hash) adalah struktur data yang menerapkan jenis data abstrak array asosiatif, struktur yang dapat memetakan kunci nilai.

Sekiranya berbau seperti Python dict, terasa seperti dict, dan kelihatan seperti ... Pokoknya, ia mesti menjadi dict. Tentunya! Oh, dan setjuga ...

Hah?

Kamus dan set dalam Python dilaksanakan menggunakan jadual hash. Mungkin terdengar menakutkan pada mulanya, tetapi ketika kami menyiasat lebih lanjut, semuanya harus jelas.

Objektif

Sepanjang artikel ini, kita akan mengetahui bagaimana a dictdilaksanakan di Python, dan kita akan membina pelaksanaan sendiri (yang sederhana). Artikel ini terbahagi kepada tiga bahagian, dan membina kamus khusus kami berlaku dalam dua bahagian pertama:

  1. Memahami apa itu jadual hash dan bagaimana menggunakannya
  2. Menyelami kod sumber Python untuk lebih memahami bagaimana kamus dilaksanakan
  3. Meneroka perbezaan antara kamus dan struktur data lain seperti senarai dan set

Apakah jadual hash?

Jadual hash adalah struktur yang dirancang untuk menyimpan senarai pasangan nilai-kunci, tanpa menjejaskan kelajuan dan kecekapan memanipulasi dan mencari struktur.

Keberkesanan jadual hash berasal dari fungsi hash - fungsi yang mengira indeks pasangan nilai-kunci - Bererti kita dapat memasukkan, mencari dan mengeluarkan elemen dengan cepat kerana kita mengetahui indeksnya dalam array memori.

Kerumitan bermula apabila dua kunci kami mempunyai nilai yang sama. Senario ini dipanggil perlanggaran hash . Terdapat banyak cara untuk menangani perlanggaran, tetapi kita hanya akan merangkumi cara Python. Kami tidak akan terlalu mendalam dengan penjelasan jadual hash kami untuk memastikan artikel ini mesra pemula dan berfokus pada Python.

Mari pastikan kita memikirkan konsep jadual hash sebelum meneruskan. Kita akan mulakan dengan membuat kerangka untuk kebiasaan kita yang sangat sederhana dictyang hanya terdiri daripada kaedah penyisipan dan carian, dengan menggunakan beberapa kaedah dunder Python. Kita perlu menginisialisasi jadual hash dengan senarai ukuran tertentu, dan mengaktifkan langganan ([] tanda) untuknya:

Sekarang, senarai jadual hash kami perlu memuat struktur tertentu, masing-masing mengandungi kunci, nilai dan hash:

Contoh Asas

Sebuah syarikat kecil yang mempunyai 10 pekerja ingin menyimpan rekod yang mengandungi pekerja mereka yang masih sakit. Kita boleh menggunakan fungsi hash berikut, sehingga semuanya dapat masuk dalam array memori:

length of the employee's name % TABLE_SIZE

Mari tentukan fungsi hash kami di kelas Entry:

Sekarang kita dapat memulakan 10 elemen array dalam jadual kita:

Tunggu! Mari fikirkannya. Kami kemungkinan besar akan mengatasi beberapa perlanggaran hash. Sekiranya kita hanya mempunyai 10 elemen, akan menjadi lebih sukar bagi kita untuk mencari ruang terbuka selepas perlanggaran. Mari kita memutuskan bahawa jadual kita akan mempunyai dua kali ganda ukuran - 20 elemen! Saya akan berjanji pada masa akan datang.

Untuk memasukkan setiap pekerja dengan cepat, kami akan mengikuti logiknya:

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

Oleh itu, kaedah penyisipan kami akan kelihatan seperti berikut (belum ada pengendalian perlanggaran hash):

Untuk mencari, pada dasarnya kami melakukan perkara yang sama:

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

Kami belum selesai!

Pengendalian perlanggaran Python

Python menggunakan kaedah yang disebut Open Address untuk menangani perlanggaran. Ini juga mengubah ukuran jadual hash ketika mencapai ukuran tertentu, tetapi kita tidak akan membincangkan aspek itu. Buka definisi Pengalamatan dari Wikipedia:

Dalam strategi lain, yang disebut pengalamatan terbuka, semua catatan masuk disimpan dalam array baldi itu sendiri. Apabila entri baru harus dimasukkan, baldi diperiksa, dimulai dengan slot hash-to dan diteruskan dalam beberapa urutan probe , hingga slot yang tidak dijumpai dijumpai. Semasa mencari entri, baldi diimbas dalam urutan yang sama, sehingga sama ada rekod sasaran dijumpai, atau slot array yang tidak digunakan dijumpai, yang menunjukkan bahawa tidak ada kunci seperti itu di dalam jadual.

Mari kita periksa proses mendapatkan nilai dengan key, dengan melihat kod sumber Python (ditulis dalam C):

  1. Hitung hash bagi key
  2. Hitung indexitem dengan hash & maskmana mask = HASH_TABLE_SIZE-1(secara sederhana - ambil N bit terakhir dari bit hash):
i = (size_t)hash & mask;

3. Sekiranya kosong, kembalikan DKIX_EMPTYyang akhirnya akan menjadi KeyError:

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

4. Sekiranya tidak kosong, bandingkan kunci & hash dan tetapkan value_addralamat ke alamat nilai sebenar jika sama:

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

dan:

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

5. Sekiranya tidak sama, gunakan bit hash yang berbeza (algoritma dijelaskan di sini) dan pergi ke langkah 3 sekali lagi:

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

Berikut adalah gambarajah untuk menggambarkan keseluruhan proses:

Proses penyisipan hampir serupa - jika slot yang dijumpai kosong, entri sedang dimasukkan, jika tidak kosong maka kita membandingkan kunci dan hash - jika sama, kita ganti nilainya, dan jika tidak, kita teruskan usaha mencari tempat baru dengan perturbalgoritma.

Meminjam idea dari Python

Kami boleh meminjam idea Python untuk membandingkan kedua kunci dan hash setiap entri ke objek entri kami (menggantikan kaedah sebelumnya):

Jadual hash kami masih tidak mempunyai pengendalian perlanggaran - mari kita laksanakan! Seperti yang kita lihat sebelumnya, Python melakukannya dengan membandingkan entri dan kemudian mengubah topeng bit, tetapi kita akan melakukannya dengan menggunakan kaedah yang disebut linear probing (yang merupakan bentuk pengalamatan terbuka, yang dijelaskan di atas):

Apabila fungsi hash menyebabkan perlanggaran dengan memetakan kunci baru ke sel tabel hash yang sudah ditempati oleh kunci lain, penyelidikan linear mencari jadual untuk lokasi bebas berikut yang terdekat dan memasukkan kunci baru di sana.

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

Order depends on insertion order

The order of the dict depends on the history of insertion. If we insert an entry with a specific hash, and afterwards an entry with the same hash, the second entry is going to end up in a different place then if we were to insert it first.

Before you go…

Thanks for reading! You can follow me on Medium for more of these articles, or on GitHub for discovering some cool repos :)

If you enjoyed this article, please hold down the clap button ? to help others find it. The longer you hold it, the more claps you give!

And do not hesitate to share your thoughts in the comments below, or correct me if I got something wrong.

Additional resources

  1. Hash Crash: The Basics of Hash Tables
  2. The Mighty Dictionary
  3. Introduction to Algorithms