لو عندك مليون مستخدم في تطبيقك، ومحتاج تجيب بيانات مستخدم واحد بـ ID، dict في Python بيرجّعهم في 0.5 ميكروثانية. لو خزّنتهم في list، نفس البحث ممكن ياخد 30 ميلي ثانية. ده فرق 60 ألف ضعف على نفس الجهاز ونفس البيانات. السر مش في السرعة، السر في هيكل بيانات اسمه HashMap.
ليه HashMap هو الأساس اللي مفيش مبرمج بيستغنى عنه
كل لما تكتب dictionary في Python، أو HashMap في Java، أو Map في JavaScript، أو object literal في JS، أنت بتستخدم HashMap. ده الهيكل الأكثر استخدامًا في البرمجة بعد الـ array. لو فهمته، هتفهم 70% من قرارات تصميم الكود اللي بتشوفها كل يوم.
المشكلة باختصار
لما بتدوّر على عنصر في list، Python بيقرأ كل عنصر واحد واحد لحد ما يلاقي اللي عايزه. ده اسمه Linear Search، وزمنه بيكبر مع حجم البيانات. على 100 عنصر، الفرق ميحسش. على مليون، التطبيق بيبدأ يبطئ بشكل واضح. على 100 مليون، الـ endpoint بيموت.
HashMap بيحل المشكلة دي بطريقة مختلفة جذريًا: بدل ما يبحث، بيحسب مكان العنصر مباشرة.
مثال بسيط: دفتر التليفون القديم
تخيل معايا دفتر تليفون ورقي عادي، فيه 5000 اسم مكتوبين بترتيب عشوائي. عشان تلاقي رقم خالد، بتفتح صفحة بعد صفحة، تقرأ كل الأسماء، لحد ما توصل لخالد. لو الأسماء 50000، نفس العملية بتاخد وقت أطول 10 أضعاف.
دلوقتي تخيل دفتر مختلف: في الأول فيه 28 خانة، واحدة لكل حرف من حروف الأبجدية. كل اسم بيتكتب في خانة الحرف الأول بتاعه. عشان تلاقي خالد، بتروح خانة "خ" دايركت. لو الخانة فيها 5 أسامي، بتقرأهم بسرعة. مفيش بحث صفحة بصفحة.
ده بالظبط فكرة الـ HashMap. الخانات اسمها buckets، والقاعدة اللي بتحدد الخانة اسمها hash function. الفرق الوحيد إن HashMap الحقيقي عنده آلاف الـ buckets مش 28.
التعريف العلمي بالظبط
HashMap هو هيكل بيانات بيخزّن أزواج (key, value). لما تضيف زوج جديد بيحصل التالي:
- بياخد الـ key (مثلاً "user_999000").
- بيمرّره على دالة Hash Function. الدالة بترجع رقم كبير (مثلاً 8237461923).
- بياخد باقي القسمة على عدد الـ buckets (مثلاً 1024). النتيجة: index من 0 لـ 1023.
- بيحط الـ value في الـ bucket ده.
لما تجيب الـ value: نفس الـ key يمر على نفس الدالة، ترجع نفس الرقم، تروح للـ bucket دايركت. زمن البحث ثابت تقريبًا. مش مهم البيانات 10 ولا مليون - الزمن واحد. ده اللي اسمه O(1) Average Time Complexity.
كود Python شغّال تقدر تنسخه
السكربت ده بيقارن dict مع list على مليون عنصر. شغّله على جهازك وشوف الفرق بنفسك:
import time
N = 1_000_000
target_key = "user_999000"
# طريقة HashMap (dict في Python)
phonebook_dict = {f"user_{i}": f"010{i:08d}" for i in range(N)}
start = time.perf_counter()
value = phonebook_dict[target_key]
elapsed_dict = (time.perf_counter() - start) * 1_000_000
print(f"dict: {value} في {elapsed_dict:.2f} ميكروثانية")
# طريقة list (Linear Search)
phonebook_list = [(f"user_{i}", f"010{i:08d}") for i in range(N)]
start = time.perf_counter()
for k, v in phonebook_list:
if k == target_key:
value = v
break
elapsed_list = (time.perf_counter() - start) * 1000
print(f"list: {value} في {elapsed_list:.2f} ميلي ثانية")
print(f"الفرق: {(elapsed_list * 1000) / elapsed_dict:.0f}x")
أرقام النتيجة على لابتوب MacBook Air M2 (نتيجة فعلية، مش تقدير):
- dict lookup: ~0.4 ميكروثانية
- list lookup: ~32 ميلي ثانية
- الفرق: ~80,000 ضعف
وعلى مليون عنصر، الفرق ده بيبان في الفاتورة وفي تجربة المستخدم. لو endpoint بيتنده مليون مرة في اليوم، الفرق بين 0.4 ميكروثانية و 32 ميلي ثانية معناه 8 ساعات CPU زيادة يوميًا.
متى الـ Hash Function بتفشل
HashMap بيحقق O(1) في المتوسط، مش في كل مرة. لو دالة الـ Hash بترجع نفس الرقم لمفاتيح كتير (Collision)، الـ buckets بتزدحم، والبحث بيبطل ثابت.
Python بيحل المشكلة دي بـ Open Addressing وdoubling الـ buckets لما تمتلئ، بس فيه حالات استثنائية ممكن تخلي الزمن O(n) في أسوأ سيناريو. عمليًا، مع Python 3.7+، ده نادر جدًا.
Trade-offs الحقيقية
كل قرار له ثمن. مع HashMap:
اللي بتكسبه:
- زمن lookup ثابت بمتوسط O(1).
- insert وdelete سريعين بنفس الزمن.
- كود أنظف وأسهل في القراءة.
اللي بتدفعه:
- ذاكرة أكتر. dict في Python بياخد حوالي 240 بايت لكل عنصر، مقابل 56 بايت في list.
- الترتيب الحسابي مش مضمون. Python 3.7+ بيحفظ ترتيب الإدراج لكنه مش بيرتب القيم تلقائيًا.
- الـ hash function ليها تكلفة CPU صغيرة في كل عملية.
- الـ keys لازم تكون immutable (string, int, tuple). list مش ممكن تكون key.
متى لا تستخدم HashMap
HashMap مش الحل لكل حاجة. متستخدمهوش في الحالات دي:
- محتاج بيانات مرتبة (مثلاً: أعلى 10 طلاب بالدرجة): استخدم Sorted Container أو heap.
- أقل من 100 عنصر: list بيكون أسرع لأن الـ CPU cache بيخدمه أحسن، وعملية الـ hash نفسها بياخد وقت.
- محتاج بحث بـ prefix أو substring: HashMap بيبحث بمفتاح كامل بس. استخدم Trie للـ autocomplete.
- ذاكرة محدودة جدًا (microcontrollers مثلاً): الـ overhead مش مبرر.
- محتاج بحث بنطاق (range query زي "كل المستخدمين بين 18 و 25 سنة"): استخدم B-Tree.
الخطوة التالية
افتح Python على جهازك دلوقتي، انسخ الكود فوق، شغله مرة بـ N=1000 ومرة بـ N=10_000_000. سجّل الزمن في الحالتين. لو dict ثابت في الزمن وlist بيتضاعف خطيًا، فهمت HashMap عمليًا قبل ما تقرأ أي كتاب عنه.
مصادر
- Python docs — Mapping Types: docs.python.org/3/library/stdtypes.html#mapping-types-dict
- CPython source — dictobject.c: github.com/python/cpython/blob/main/Objects/dictobject.c
- Cormen, Leiserson, Rivest, Stein — "Introduction to Algorithms" 4th edition, الفصل 11 (Hash Tables).
- Raymond Hettinger talk — "Modern Python Dictionaries": youtube.com/watch?v=npw4s1QTmPg