يا جماعة الخير، خلوني أحكيلكم قصة صارت معي ومع فريقي قبل كم سنة. كنا بنشتغل على نظام ضخم لإدارة المستخدمين، نظام فيه مئات الآلاف، وقربنا نوصل للمليون مستخدم. كل شيء كان ماشي تمام، لحد ما بدأت الشكاوى توصلنا من قسم الدعم الفني: “النظام بطيء”، “صفحة المستخدم بتطول لَتفتح”، “العملاء زهقوا!”.
لما فحصنا الموضوع، كانت الصدمة. عملية بسيطة زي البحث عن مستخدم عن طريق رقم جواله أو إيميله كانت بتاخذ ثواني طويلة! تخيلوا، ثواني في عالم البرمجة تعتبر دهرًا. قعدنا نحلل الكود، وإذ بالمصيبة: كنا مخزنين بيانات المستخدمين في قائمة (List) عادية، وكل مرة بنبحث عن مستخدم، كنا بنعمل حلقة (loop) تمشي على كل المستخدمين واحد واحد لحد ما تلاقي اللي بدنا ياه. بالزبط زي اللي بدور على إبرة في كومة قش. هذا الأسلوب اسمه “البحث الخطي” (Linear Search)، وهو أبسط أنواع البحث، وأبطأها على الإطلاق.
كان الوضع محبط، والمدير مش رح يرحمنا. في اجتماع “ولعت” فيه الدنيا، الكل كان متوتر، وصرت أحكي لحالي “يا أبو عمر، شو هالحكي؟ معقول بعد كل هالسنين خبرة نوقع بهيك غلطة؟”. وقتها، لمعت في بالي فكرة بسيطة لكنها عبقرية، زي اللي بتذكر وصفة قديمة كانت جدته تستعملها. وقفت وحكيتلهم: “يا جماعة، الحل بسيط… جداول التجزئة”.
ما هو الجحيم الذي كنا فيه؟ (مشكلة البحث الخطي)
قبل ما أحكيلكم عن المنقذ، خلوني أوضحلكم حجم المشكلة اللي كنا فيها. لما يكون عندك مليون مستخدم، والبحث عن مستخدم واحد يتطلب المرور عليهم كلهم في أسوأ الحالات، فهذا يعني مليون عملية مقارنة!
هذا ما يعرف في علم الخوارزميات بـ O(n) أو (Order of n)، حيث ‘n’ هو عدد العناصر. كلما زاد عدد المستخدمين (n)، زاد وقت البحث بشكل خطي ومباشر. وهذا تمامًا ما كان يحدث معنا، كلما سجل مستخدم جديد، أصبح النظام أبطأ قليلًا، حتى وصلنا إلى نقطة الانهيار.
المنقذ الذي لم نتوقعه: ما هي جداول التجزئة (Hash Tables)؟
جدول التجزئة، أو ما يعرف بالـ Hash Map أو Dictionary في كثير من لغات البرمجة، هو هيكل بيانات (Data Structure) عبقري بكل معنى الكلمة. فكرته الأساسية تتلخص في جملة واحدة: “بدلًا من أن تبحث عن المعلومة، اذهب إليها مباشرة!”.
تخيل أن لديك خزانة ملفات ضخمة (مكتبة)، بدلًا من أن تبحث في كل الأدراج، هناك موظف (دالة التجزئة) تخبره باسم الملف الذي تريده (المفتاح)، وهو فورًا يخبرك برقم الدرج (الفهرس) الذي يوجد به الملف. عملية شبه فورية!
المكونات الأساسية لجدول التجزئة
لفهم السحر، يجب أن نعرف المكونات الثلاثة الرئيسية:
- المفتاح (Key): هو المعرّف الفريد للبيانات التي تريد تخزينها. في قصتنا، كان المفتاح هو رقم جوال المستخدم أو بريده الإلكتروني.
- دالة التجزئة (Hash Function): هي القلب النابض للنظام. هذه الدالة تأخذ المفتاح (Key) كمدخل، وتقوم بعملية حسابية “سحرية” لتنتج رقمًا صحيحًا فريدًا يسمى “الهاش” أو “الفهرس” (Index).
- المصفوفة (Array of Buckets): هي هيكل البيانات الأساسي الذي يتم فيه تخزين البيانات الفعلية. دالة التجزئة تخبرنا في أي “سطل” (Bucket) أو خانة في هذه المصفوفة يجب أن نضع بياناتنا أو نجدها.
كيف تعمل هذه “الخزعبلات” على أرض الواقع؟ (مثال عملي)
لنأخذ مثالًا بسيطًا. نريد تخزين معلومات مستخدمين بناءً على أسمائهم:
- نريد تخزين بيانات المستخدم “علي”.
- نمرر المفتاح “علي” إلى دالة التجزئة:
hash("علي"). - لنفترض أن الدالة أرجعت الرقم
5. - نقوم بتخزين بيانات “علي” في الخانة رقم
5في المصفوفة الداخلية.
الآن، عندما نريد استرجاع بيانات “علي”، لا نبحث في كل المصفوفة! بل نكرر نفس العملية:
- نحسب
hash("علي")مرة أخرى، وبالطبع سنحصل على نفس النتيجة:5. - نذهب مباشرة إلى الخانة رقم
5ونجد بيانات “علي” بانتظارنا.
هذه العملية، في أفضل وأغلب الحالات، تأخذ وقتًا ثابتًا، بغض النظر عن عدد المستخدمين! وهذا ما يعرف بـ O(1) أو (Constant Time). هذا هو الفرق بين ثوانٍ طويلة وجزء من الثانية. إنه الفرق بين نظام فاشل ونظام ناجح.
في لغة بايثون، هذا المفهوم مدمج بشكل جميل جدًا من خلال القواميس (Dictionaries):
# إنشاء قاموس (وهو تطبيق لجدول التجزئة)
users = {}
# إضافة مستخدمين جدد (عملية الإضافة O(1) في المتوسط)
users["abu_omar"] = {"name": "أبو عمر", "specialty": "AI"}
users["salma"] = {"name": "سلمى", "specialty": "Frontend"}
users["khalid"] = {"name": "خالد", "specialty": "DevOps"}
# البحث عن مستخدم (عملية البحث O(1) في المتوسط)
user_data = users["abu_omar"]
print(user_data) # سيطبع بيانات أبو عمر فورًا
# في قصتنا، كنا سنفعل شيئًا كهذا:
# user_by_email = {}
# user_by_email["test@example.com"] = { ... بيانات المستخدم ... }
# ...
# والمبرمج يبحث هكذا:
# current_user = user_by_email["test@example.com"]
“ولكن ماذا لو…؟” (معالجة التصادمات – Collisions)
قد يسأل سائل ذكي: “يا أبو عمر، ماذا لو أعطت دالة التجزئة نفس الفهرس (Index) لمفتاحين مختلفين؟”. مثلًا، hash("علي") أعطت 5، وhash("سامي") أعطت أيضًا 5! هذا سؤال ممتاز، وهذه المشكلة تسمى “التصادم” (Collision).
هذا أمر وارد وطبيعي في جداول التجزئة، والعبقرية تكمن أيضًا في كيفية حل هذه المشكلة. هناك طريقتان مشهورتان:
1. السلسلة المنفصلة (Separate Chaining)
هذه هي الطريقة الأكثر شيوعًا. بدلًا من أن تحتوي الخانة رقم 5 على قيمة واحدة، فإنها تحتوي على قائمة صغيرة (مثل Linked List). عندما يحدث تصادم، ببساطة نضيف العنصر الجديد إلى نهاية هذه القائمة الصغيرة. فعندما نبحث عن “سامي”، نذهب إلى الخانة 5، نجد قائمة صغيرة فيها “علي” و”سامي”، ثم نبحث في هذه القائمة الصغيرة فقط، وليس في المليون عنصر!
2. العنونة المفتوحة (Open Addressing)
فكرة هذه الطريقة هي: “إذا وجدت مكاني محجوزًا، سأبحث عن أول مكان فارغ قريب وأجلس فيه”. فعندما يأتي “سامي” ويجد الخانة 5 محجوزة بـ “علي”، يقوم بالبحث في الخانة 6، ثم 7، وهكذا حتى يجد مكانًا فارغًا. عملية البحث تتبع نفس المنطق.
نصائح أبو عمر الذهبية للتعامل مع جداول التجزئة
بعد سنين من التعامل مع هياكل البيانات هذه، اسمحوا لي أن أقدم لكم خلاصة الخبرة:
نصيحة 1: لا تخترع العجلة من جديد!
كل لغات البرمجة الحديثة (Python, JavaScript, Java, C#, Go, Rust…) توفر تطبيقات ممتازة ومحسّنة لجداول التجزئة. استخدمها! في بايثون هيdict، في جافاسكريبت هيMapأوObject، في جافا هيHashMap. لا تضيع وقتك في بناء واحدة من الصفر إلا إذا كان لغرض التعلم.
نصيحة 2: افهم مفتاحك (Key)
جودة جدول التجزئة من جودة دالة التجزئة. عندما تستخدم أنواع بيانات بسيطة (نصوص، أرقام) كمفاتيح، فاللغة تتكفل بكل شيء. لكن إذا استخدمت كائنات معقدة (Custom Objects) كمفتاح، قد تحتاج في بعض اللغات (مثل جافا) إلى تعريف دالتيhashCode()وequals()بنفسك لضمان عمل الجدول بشكل صحيح.
نصيحة 3: اعرف متى لا تستخدمها
جداول التجزئة ليست الحل لكل شيء. عيبها الرئيسي أنها غير مرتبة. إذا كنت تحتاج إلى بياناتك مرتبة أبجديًا أو رقميًا بشكل دائم، أو تحتاج إلى إيجاد “أكبر” أو “أصغر” عنصر بسرعة، فربما هياكل بيانات أخرى مثل الأشجار الثنائية المتوازنة (Balanced Binary Search Trees) تكون خيارًا أفضل.
الخلاصة: من كومة القش إلى الفهرس المنظم 🗂️
في مشروعنا، استغرق الأمر منا يومين فقط لإعادة كتابة أجزاء البحث في النظام لاستخدام جداول التجزئة بدلًا من القوائم الخطية. النتيجة؟ العملية التي كانت تستغرق 3-5 ثوانٍ أصبحت تستغرق 10-20 ميللي ثانية. نعم، قرأتها صحيح، ميللي ثانية! لقد أنقذنا المشروع، وأسعدنا العملاء، وبدا الأمر وكأنه سحر.
الدرس المستفاد ليس فقط عن جداول التجزئة، بل عن أهمية اختيار هيكل البيانات الصحيح للمشكلة الصحيحة. فهم الخوارزميات وهياكل البيانات ليس مجرد ترف أكاديمي، بل هو السلاح السري للمبرمج المحترف الذي يميزه عن غيره. فلا تستهينوا أبدًا بالأساسيات، فهي التي تبني الأنظمة العظيمة. 😉