រំលងទៅមាតិកា

ឧបករណ៍បង្ហាញ Hash Table

បញ្ចូល ស្វែងរក និងលុប keys រួចមើលដំណើរការ hash function, collision និងវិធីដោះស្រាយ។

Collision handling

Keys: 0 Load factor: 0 Probes or comparisons: 0
  • Checking
  • Inserted or found
  • Deleted
Step 1 of 1

hash(key) = key mod m          // numbershash(word): h = 0; for each char: h = (h × 31 + code) mod m // Separate chaining: each bucket holds a linked listinsert(key):  i = hash(key)  if key is already in buckets[i]: return  append key to buckets[i]search(key): walk the list buckets[hash(key)]delete(key): unlink key from buckets[hash(key)] // Linear probing: one key per slotinsert(key):  i = hash(key)  while table[i] holds another key    i = (i + 1) mod m  table[i] = keysearch(key):  i = hash(key); while table[i] is not empty    if table[i] = key: return i; i = (i + 1) mod m  return not founddelete(key): find it, then mark the slot "deleted"
Hash table operations
OperationAverageWorst case (all keys collide)
InsertO(1)O(n)
SearchO(1)O(n)
DeleteO(1)O(n)
SpaceO(n)O(n)

Hash table ស្វែងរក key មួយក្នុងរយៈពេលប្រហែលមួយជំហានប៉ុណ្ណោះ ដោយការគណនាកន្លែងដែលវាត្រូវរក្សាទុក។ ឧបករណ៍នេះបង្ហាញគ្រប់ជំហានទាំងអស់៖ អនុគមន៍ hash ត្រូវបានគណនាជាមួយ key របស់អ្នក (key mod m សម្រាប់លេខ និង polynomial hash សម្រាប់ពាក្យ), key ត្រូវបញ្ជូនទៅ bucket នោះ ហើយនៅពេល keys ពីរធ្លាក់លើ bucket តែមួយ អ្នកនឹងឃើញពីរបៀបដោះស្រាយ collision។

ប្ដូររវាង separate chaining (ដែល bucket នីមួយៗផ្ទុក linked list នៃ keys) និង linear probing (ដែល key ប៉ះទង្គិចត្រូវផ្លាស់ទីទៅប្រអប់ទំនេរបន្ទាប់)។ សាកល្បងលុប key ក្នុង linear probing ដើម្បីមើលមូលហេតុដែលវាបន្សល់ទុកសញ្ញាសម្គាល់ “deleted” (ហៅថា tombstone) ជំនួសឱ្យប្រអប់ទទេ។ Load factor បង្ហាញពីកម្រិតពេញនៃតារាង។

របៀបប្រើឧបករណ៍ ឧបករណ៍បង្ហាញ Hash Table

  1. ជ្រើសរើស Separate chaining ឬ Linear probing និងទំហំតារាង m។
  2. វាយបញ្ចូល key (លេខ ឬពាក្យ) រួចចុច Insert, Search ឬ Delete។
  3. មើលការគណនា hash នៅខាងលើ រួចតាមដានប្រអប់ដែលបានបន្លិច និងដំណើរការ probe ឬដើរតាម chain។
  4. បញ្ចូល key ដែលបង្ក collision (មានការផ្ដល់យោបល់) ហើយប្រៀបធៀបពីរបៀបដែលវិធីទាំងពីរដោះស្រាយវា។

ហេតុអ្វីប្រើ ឧបករណ៍បង្ហាញ Hash Table របស់ ToolOrca

  • ការបង្ហាញការគណនា hash function សម្រាប់គ្រប់ key
  • ម៉ូដ Separate chaining និង Linear probing
  • ចលនាបង្ហាញការដោះស្រាយ collision មួយជំហានម្តងៗ
  • សញ្ញា Tombstone សម្រាប់ការលុបក្នុង open addressing
  • បង្ហាញ Load factor និងចំនួនលើកធ្វើ probe
  • គាំទ្រ key ជាលេខ និងជា string
  • កូដជាភាសា C++, Java, Python និង JavaScript

របៀបដែល hash table ដំណើរការ

Hash table គឺជា array ដែលមាន m buckets បូករួមជាមួយ hash function ដែលបំលែង key ឱ្យទៅជាលេខ bucket។ សម្រាប់ចំនួនគត់ រូបមន្តសាមញ្ញគឺ h(k) = k mod m ហើយសម្រាប់ string គឺ h = (h × 31 + character code) mod m លើគ្រប់តួអក្សរ។ ការជ្រើសរើសទំហំតារាងជាចំនួនបឋម (prime number) ជួយបែងចែក keys ឱ្យរាយស្មើដៃល្អ។ ដើម្បីស្វែងរក key យើងគ្រាន់តែ hash វា រួចមើលតែក្នុង bucket នោះប៉ុណ្ណោះ ដែលធ្វើឱ្យការស្វែងរកជាមធ្យមចំណាយពេលត្រឹមតែ O(1)។

Keys ពីរផ្សេងគ្នាអាចផ្ដល់តម្លៃ hash ធ្លាក់លើ bucket តែមួយ ដែលហៅថា collision (ការប៉ះទង្គិច)។ ជាមួយ bucket ចំនួន m ការប៉ះទង្គិចគឺមិនអាចចៀសផុតបានឡើយនៅពេលអ្នករក្សាទុក keys ច្រើន (តាមទ្រឹស្ដី birthday paradox) ដូច្នេះ hash table ទាំងអស់សុទ្ធតែត្រូវការយុទ្ធសាស្ត្រដោះស្រាយ collision។

Separate Chaining ប្រៀបធៀបនឹង Linear Probing

Separate chaining រក្សាទុក linked list តូចមួយក្នុង bucket នីមួយៗ។ Keys ដែលប៉ះទង្គិចគ្នានឹងត្រូវបន្ថែមទៅចុង list ហើយការស្វែងរកត្រូវដើរតែក្នុង list នោះប៉ុណ្ណោះ។ វាដំណើរការបានល្អទោះបីតារាងផ្ទុក keys ច្រើនជាងចំនួន bucket ក៏ដោយ ហើយការលុបក៏មានលក្ខណៈសាមញ្ញ។ HashMap របស់ Java និង unordered_map របស់ C++ ប្រើប្រាស់ chaining។

Linear probing (ទម្រង់មួយនៃ open addressing) រក្សាទុក key មួយក្នុងមួយប្រអប់។ ប្រសិនបើប្រអប់គោលដៅមានគេយកហើយ វានឹងសាកល្បងប្រអប់បន្ទាប់គឺ (i + 1) mod m រហូតដល់រកឃើញប្រអប់ទំនេរ។ ការស្វែងរកក៏ដើរតាមផ្លូវនេះដូចគ្នា ហើយឈប់នៅពេលជួបប្រអប់ទទេ។ ការលុបត្រូវតែបន្សល់ទុក tombstone បើពុំនោះទេការស្វែងរកលើកក្រោយអាចនឹងឈប់លឿនពេក។ វាដំណើរការលឿនជាមួយ cache របស់ម៉ាស៊ីន ប៉ុន្តែប្រឈមនឹងបញ្ហាកកស្ទះ (clustering) នៅពេលតារាងចាប់ផ្តើមពេញ ហេតុនេះគេត្រូវពង្រីកទំហំតារាងមុនពេលវាពេញ។ dict របស់ Python ប្រើប្រាស់ open addressing។

Load factor និងការប្តូរទំហំ (Resizing)

Load factor គឺស្មើ n / m ដែលជាចំនួន keys ចែកនឹងចំនួន buckets។ នៅពេលដែលវាកើនឡើង chains នឹងកាន់តែវែង ហើយការ probe នឹងកាន់តែយឺត។ Hash table ជាក់ស្ដែងនឹងពង្រីកទំហំ (ជាទូទៅកើនឡើងទ្វេដង និង rehash គ្រប់ key ឡើងវិញ) នៅពេល load factor កើនលើសប្រហែល 0.75 សម្រាប់ chaining ឬ 0.5 ទៅ 0.7 សម្រាប់ probing ដើម្បីរក្សាប្រតិបត្តិការឱ្យនៅត្រឹម O(1) ជាមធ្យម។

គ្រប់ visualizer នៅលើគេហទំព័រនេះដំណើរការផ្ទាល់ក្នុង browser របស់អ្នកជាមួយប៊ូតុងបញ្ជាដូចគ្នា៖ ចាក់ (Play), ផ្អាក (Pause), ទៅមុខមួយជំហាន, ថយក្រោយមួយជំហាន និងរបារលៃល្បឿន។ អ្នកក៏អាចអូសរបារកំណត់ទីតាំងដើម្បីលោតទៅជំហានណាមួយ ឬចុចលើផ្ទាំងគំនូរហើយប្រើប្រាស់ Space bar និងគ្រាប់ចុចព្រួញបានផងដែរ។

សំណួរដែលសួរញឹកញាប់

តើអ្វីទៅជា hash collision?

គឺជាពេលដែល keys ពីរខុសគ្នា ត្រូវបាន hash function គណនាឃើញតម្លៃដូចគ្នា និងចង្អុលទៅ bucket តែមួយ។ ឧទាហរណ៍៖ ប្រសិនបើ m = 11 keys 3 និង 14 ទាំងពីរគឺផ្ដល់លទ្ធផល 3 mod 11 = 14 mod 11 = 3។

តើ chaining ឬ linear probing មួយណាប្រសើរជាង?

Chaining មានភាពសាមញ្ញជាង និងទ្រទ្រង់ load factor ខ្ពស់បានល្អ។ Linear probing ប្រើប្រាស់ memory តិចជាង និងដំណើរការលឿនជាងដោយសារ CPU cache ដរាបណាតារាងត្រូវបានរក្សាឱ្យនៅទំនេរក្រោម 70%។

ហេតុអ្វី linear probing ត្រូវការ tombstone?

ការស្វែងរកនឹងឈប់នៅពេលវាជួបប្រអប់ទទេ។ ប្រសិនបើការលុបគ្រាន់តែធ្វើឱ្យប្រអប់នោះទទេ keys ដែលនៅខាងមុខតាមផ្លូវ probe នឹងរកមិនឃើញឡើយ។ Tombstone បញ្ជាក់ថា “សូមបន្តស្វែងរកទៅមុខទៀត”។

តើ hash table ចំណាយពេលយូរបំផុត (worst-case) ប៉ុន្មាន?

O(n) នៅពេលដែល keys ទាំងអស់ប៉ះទង្គិចចូលក្នុង bucket តែមួយ ឬកកស្ទះជាជួរវែង។ Hash function ល្អ និងការ resize ទំហំតារាងបានត្រឹមត្រូវ ជួយរក្សាពេលជាមធ្យមឱ្យនៅត្រឹម O(1)។

ហេតុអ្វីបានជាទំហំតារាងគួរតែជាចំនួនបឋម (Prime Number)?

ជាមួយរូបមន្ត k mod m ប្រសិនបើ keys ចែកដាច់នឹងកត្តារួមរបស់ m (ឧទាហរណ៍ keys គូទាំងអស់ជាមួយ m ជាលេខគូ) វានឹងកកកុញតែក្នុង bucket មួយចំនួនប៉ុណ្ណោះ។ ការប្រើចំនួនបឋម m ជួយបំបែកទិន្នន័យឱ្យរាយបានស្មើល្អ។