مقدمة عامة: لماذا Hash Tables؟ ولماذا هي مهمة؟
لو سألت أي مهندس أنظمة أو معماري برمجيات:
“ما هو أسرع هيكل بيانات للبحث؟”
الجواب 99% من الوقت: Hash Table.
لأنها ببساطة:
- أسرع من أي شجرة
- أسرع من أي قائمة
- أسرع من أي هيكل خطّي
- أسرع من أي index عادي
- أساس داخلي لكثير من البنى مثل:
- caches
- memory managers
- symbol tables
- compilers
- interpreters
- network routing tables
- database indexing
- distributed systems
- key-value stores مثل Redis
- أنظمة الذكاء الاصطناعي
- خوارزميات fingerprinting
ولأن معظم لغات البرمجة الحديثة تستخدم Hash Tables بشكل داخلي:
- Python → dict
- JavaScript → objects
- Java → HashMap
- C# → Dictionary
- Go → Maps
- PHP → Arrays (فعليًا Hash Table)
- C++ → unordered_map
يعني لو أتقنت Hash Tables؟
إنت فعليًا أتقنت واحدة من أقوى الهياكل الحاسوبية في العالم.
اليوم رح نفصل كل شيء من الصفر إلى أقصى نقطة ممكنة.
القسم الأول: المفهوم الأساسي للـ Hash Table
ما هي Hash Table؟
هي بنية بيانات مبنية على مفهوم التجزئة Hashing:
- عندك مفتاح (key)
- تطبق عليه دالة تجزئة (hash function)
- تنتج رقمًا
- الرقم يُحوَّل لمؤشر في جدول داخلي (bucket array)
- يتم تخزين القيمة في هذا المؤشر
تخيّل:
key = "Mohammad"
hash(key) = 981237642
index = hash % table_size
بهذا الشكل:
table[index] ← { key : value }
القسم الثاني: لماذا لا نستخدم المصفوفات العادية للبحث؟
لأن:
- البحث فيها O(n)
- ولو عندك مليون عنصر؟ الله يعينك
- ولو عندك 100 مليون عنصر؟ RIP بالأداء
بعكس Hash Table اللي بتعطيك:
- إدخال O(1)
- بحث O(1)
- حذف O(1)
تقريبًا، مش دائمًا—but on average.
القسم الثالث: مكونات Hash Table الداخلية
- Hash Function
- Bucket Array
- Collision Resolution Technique
- Load Factor
- Rehashing Mechanism
- Memory Model
- Key Equality Function
- Allocator / Memory Policy
كل نقطة منهم موضوع لحاله… بس رح نفصلها كلها هنا.
القسم الرابع: دالة التجزئة Hash Function (العقل المدبر)
دالة التجزئة هي قلب الهاش تابل.
جودتها تحدد:
- السرعة
- العشوائية
- مقاومة التصادم
- توزيع العناصر
- استهلاك الذاكرة
- وقت rehash
خصائص دالة تجزئة جيدة:
1) Deterministic
نفس المفتاح → نفس الهش دائمًا.
2) Uniformity
توزيع متساوٍ على buckets.
3) Speed
سريعة جدًا (O(1)).
4) Low Collisions
أقل قدر ممكن من التصادمات.
5) Avalanche Effect
اختلاف بسيط بالمفتاح يعطي اختلاف كبير بالهاش.
مثال متواضع لشرح الفكرة (ليس حقيقي):
int hashFunc(string key) {
long long h = 0;
for (char c : key)
h = (h * 37 + c);
return h;
}
C++ لا تستخدم هذه، لكنها تشرح المفهوم.
القسم الخامس: ماهي التصادمات Collisions
تصادم يعني:
hash("Ali") → 12
hash("Omar") → 12
اثنين راحوا لنفس المكان.
التصادم شيء طبيعي ومستحيل تتجنبه 100%.
لكن نتعامل معه بطرق ذكية.
القسم السادس: حل التصادمات (Collision Resolution)
1) Chaining (المستخدم في unordered_map)
كل bucket يحتوي Linked List:
bucket 5:
→ ("Ali", 20) → ("Omar", 50) → ("Lama", 60)
2) Open Addressing
أنواع:
- Linear Probing
- Quadratic Probing
- Double Hashing
لكن C++ ما تستخدمه في unordered_map.
القسم السابع: بنية unordered_map داخليًا
التركيب الحقيقي:
vector<list<pair<Key, Value>>> buckets;
أو شيء مشابه في التنفيذ الحقيقي.
كل bucket عبارة عن قائمة (linked list أو chain).
القسم الثامن: كيف يتم البحث بداخل unordered_map؟
- نحسب hash
- نحسب index
- نذهب للبكيت bucket
- نفحص قائمة العناصر داخلها
- نقارن المفاتيح بالعناصر الموجودة
- إن وجدنا القيمة → ممتاز
- إن لم نجدها → غير موجود
الزمن:
O(1) متوسط
O(n) أسوأ حالة عندما تتكدس العناصر في نفس bucket
لكن بفضل خوارزميات hashing الحديثة، الأسوأ نادر جدًا.
القسم التاسع: الـ Load Factor
Load factor = عدد العناصر ÷ عدد السلات (buckets)
مثال:
- 100 عنصر
- 10 buckets
load factor = 10
كل ما زاد، زادت التصادمات، وانخفض الأداء.
C++ عادة تعمل rehash تلقائي إذا:
load_factor ≥ 1.0 (تقريبًا)
القسم العاشر: Rehashing — إعادة بناء الجدول من الصفر
عندما يصبح الجدول “مزدحمًا”، يحدث الآتي:
- إنشاء table جديد أكبر
- إعادة توزيع كل العناصر حسب hash جديد
- نقل كل العناصر ضمن البنية الجديدة
عملية مكلفة لكنها نادرة.
C++ تتحكم بها تلقائيًا.
القسم الحادي عشر: استخدام unordered_map في C++
إنشاء map:
unordered_map<string, int> age;
الإضافة:
age["Mohammad"] = 19;
age["Omar"] = 30;
age.insert({"Sara", 25});
البحث:
if (age.find("Mohammad") != age.end()) {
cout << "Found";
}
الوصول للقيمة:
cout << age.at("Omar");
يفضل استخدام at بدل [] لتجنب إنشاء مفاتيح بدون قصد.
الحذف:
age.erase("Sara");
التكرار:
for (auto& p : age)
cout << p.first << " = " << p.second << endl;
الحجم:
cout << age.size();
فحص مفتاح:
if (age.count("Omar"))
cout << "exists";
القسم الثاني عشر: مقارنة map vs unordered_map
| الخاصية | map | unordered_map |
|---|---|---|
| الهيكل | Red-Black Tree | Hash Table |
| الترتيب | مرتب | غير مرتب |
| سرعة البحث | O(log n) | O(1) متوسط |
| الذاكرة | أقل | أكبر |
| الترتيب التلقائي | نعم | لا |
| الأسوأ حالة | ممتازة | ضعيفة جدًا |
متى تختار map؟
✔ عندما تحتاج ترتيب
✔ عندما تريد lower_bound
✔ عندما تحتاج traversal مرتب
متى تختار unordered_map؟
✔ عندما تريد أعلى سرعة ممكنة
✔ عندما تبحث كثيرًا
✔ عندما تخزن بيانات ضخمة
✔ عندما المفاتيح كبيرة (Strings)
القسم الثالث عشر: تطبيقات عملية ضخمة
1) بناء قاموس للكلمات Word Frequency
unordered_map<string, int> freq;
freq[word]++;
2) فهرس مستخدمين بسرعة خارقة
unordered_map<int, User*> users;
3) Hash Table داخل Compiler Symbol Table
unordered_map<string, VariableInfo> table;
4) Cache لعمليات مكلفة
unordered_map<string, string> cache;
5) تتبع زيارات IP في السيرفر (Rate Limiting)
unordered_map<string, int> visits;
6) تخزين graph adjacency list
unordered_map<int, vector<int>> graph;
القسم الرابع عشر: الأخطاء القاتلة عند استخدام unordered_map
❌ استخدام [] بدل at — إنشاء مفاتيح بلا قصد
❌ استخدام hash ضعيف لمفتاح ضخم (مثل vector أو string كبير)
❌ نسيان أن العنصر غير مرتب
❌ إعادة ترتيب الحاوية أثناء التكرار (rehash)
❌ استخدام unordered_map عندما necesitas ترتيب
❌ تخزين مؤشرات بدون delete (عند استخدام raw pointer)
القسم الخامس عشر: مثال عملاق يجمع كل المفاهيم
#include <iostream>
#include <unordered_map>
using namespace std;
int main() {
unordered_map<string, int> score;
// إضافة
score["Mohammad"] = 90;
score["Lama"] = 77;
score["Omar"] = 65;
score.insert({"Ali", 88});
// بحث
if (score.find("Mohammad") != score.end())
cout << "Mohammad score = " << score["Mohammad"] << endl;
// حذف
score.erase("Lama");
// طباعة جميع العناصر
cout << "All scores:\n";
for (auto& p : score) {
cout << p.first << " : " << p.second << endl;
}
// فحص وجود
if (score.count("Ali"))
cout << "Ali exists\n";
// عدد العناصر
cout << "Size: " << score.size() << endl;
}
الخلاصة النهائية — أقوى ملخص عربي للجداول التجزئية
اليوم فهمت:
✔ لماذا Hash Tables مهمة جداً
✔ كيف تعمل داخليًا
✔ كيف تعمل دوال التجزئة
✔ ما هو load factor
✔ كيف يتم rehash
✔ كيفية التعامل مع التصادمات
✔ الفرق بين map و unordered_map
✔ كيفية استخدام unordered_map بشكل احترافي
✔ تطبيقات واقعية ضخمة
✔ الأخطاء الفتاكة وكيف تتجنبها
✔ مثال ضخّم يجمع كل المفاهيم
✔ فهم معماري حقيقي للهياكل الداخلية
هذا الدرس حرفيًا أساس لأي مبرمج يريد الاحتراف في:
كتابة Compilers
هياكل البيانات
الخوارزميات
الأنظمة
الترجمة
الذكاء الاصطناعي
محركات الألعاب
بناء السيرفرات
اكتشاف المزيد من كود التطور
اشترك للحصول على أحدث التدوينات المرسلة إلى بريدك الإلكتروني.


