📚 دليل دراسة الجداول التجزئة

CS210 - الأسبوع 13 - مرجع شامل

📖 جدول المحتويات

1. مقدمة للجداول التجزئة

🎯 المفهوم الرئيسي

توفر الجداول التجزئة أداء وقت ثابت في الحالة المتوسطة لعمليات البحث، الإدراج، والحذف باستخدام دالة تجزئة لحساب فهرس مصفوفة مباشرة من المفتاح.

ما هي الخريطة؟

الخريطة هي هيكل بيانات يمثل مجموعة قابلة للبحث من أزواج المفتاح-القيمة، حيث:

أمثلة من العالم الحقيقي

2. التجزئة: الخطة الأساسية

الفكرة الأساسية

احفظ العناصر في جدول مفهرس بالمفتاح حيث يتم حساب الفهرس كدالة للمفتاح.

🔑 المكونات الرئيسية

  1. دالة التجزئة: طريقة لحساب فهرس المصفوفة من المفتاح
  2. اختبار المساواة: طريقة للتحقق من تساوي مفتاحين
  3. حل التصادم: إستراتيجية للتعامل مع المفاتيح التي تتجزأ لنفس الفهرس

مقايضة المساحة-الزمن

السيناريو الإستراتيجية المشكلة
لا حدود للمساحة دالة تجزئة تافهة مع المفتاح كفهرس يهدر كميات هائلة من الذاكرة
لا حدود للزمن بحث تسلسلي خلال جميع العناصر بطيء جداً لمجموعات البيانات الكبيرة
العالم الحقيقي التجزئة مع حل التصادم يوازن المساحة والزمن بكفاءة

3. دوال التجزئة

⚡ الهدف المثالي

قم بتشويش المفاتيح بشكل موحد لإنتاج فهرس جدول يكون:

حساب رموز التجزئة

للأعداد الصحيحة

public int hashCode() { 
    return value; 
}

ببساطة أرجع قيمة العدد الصحيحة نفسها.

للبيانات المنطقية

public int hashCode() {
    if (value) return 1231;
    else return 1237;
}

استخدم عددين أوليين مختلفين لـ true و false.

للأعداد العشرية

public int hashCode() {
    long bits = doubleToLongBits(value);
    return (int) (bits ^ (bits >>> 32));
}

حول إلى تمثيل 64-بت وXOR النصفين.

للنصوص (طريقة هورنر)

public int hashCode() {
    int hash = 0;
    for (int i = 0; i < length(); i++)
        hash = s[i] + (31 * hash);
    return hash;
}
h = s[0]·31L-1 + s[1]·31L-2 + ... + s[L-2]·31 + s[L-1]

مثال تجزئة النص: "call"

hash = 99·31³ + 97·31² + 108·31 + 108
     = 108 + 31·(108 + 31·(97 + 31·99))
     = 3,045,982

التجزئة المعيارية

حول رمز التجزئة (أي عدد صحيح 32-بت) إلى فهرس جدول (0 إلى M-1):

❌ النسخة المعيبة

private int hash(Key key) {
    return key.hashCode() % M;
}

يمكن أن ترجع قيماً سالبة!

✅ النسخة الصحيحة

private int hash(Key key) {
    return (key.hashCode() & 0x7fffffff) % M;
}

تخفي بت الإشارة لضمان نتيجة موجبة

افتراض التجزئة الموحدة

الافتراض الأساسي: كل مفتاح متساوي الاحتمالية للتجزئة إلى عدد صحيح بين 0 و M-1.

هذا الافتراض حاسم لتحليل الأداء لكنه صعب التحقيق عملياً.

مشكلة عيد الميلاد وموازنة الحمل

4. التسلسل المنفصل

💡 الفكرة الأساسية

استخدم مصفوفة من M < N قائمة مرتبطة. كل موضع مصفوفة يشير إلى سلسلة من العناصر التي تتجزأ لذلك الفهرس.

كيف يعمل

  1. تجزئة: عين المفتاح لعدد صحيح i بين 0 و M-1
  2. إدراج: ضع في مقدمة السلسلة رقم i (إذا لم تكن موجودة بالفعل)
  3. بحث: ابحث فقط في السلسلة رقم i

تمثيل مرئي

Array Index          Linked List Chain
    0         →     [A] → [B] → [E] → null
    1         →     null
    2         →     [X] → [S] → null
    3         →     null
    4         →     [L] → [P] → null
    5         →     [M] → [H] → [C] → [R] → null
            

تنفيذ جافا

public class SeparateChainingHashST<Key, Value> {
    private int M = 97;  // عدد السلاسل
    private Node[] st = new Node[M];  // مصفوفة السلاسل
    
    private static class Node {
        private Object key;
        private Object val;
        private Node next;
    }
    
    private int hash(Key key) {
        return (key.hashCode() & 0x7fffffff) % M;
    }
    
    public Value get(Key key) {
        int i = hash(key);
        for (Node x = st[i]; x != null; x = x.next)
            if (key.equals(x.key))
                return (Value) x.val;
        return null;
    }
    
    public void put(Key key, Value val) {
        int i = hash(key);
        for (Node x = st[i]; x != null; x = x.next)
            if (keys[i].equals(key)) { 
                x.val = val; 
                return; 
            }
        st[i] = new Node(key, val, st[i]);
    }
}

تحليل الأداء

✅ افتراض

تحت افتراض التجزئة الموحدة، الاحتمال أن عدد المفاتيح في قائمة يكون ضمن عامل ثابت من N/M قريب جداً من 1.

متوسط # الفحوصات = N/M
العملية الحالة المتوسطة أسوأ حالة
بحث ناجح ~3-5 فحوصات N فحص
بحث فاشل ~3-5 فحوصات N فحص
إدراج ~3-5 فحوصات N فحص

إستراتيجية تغيير الحجم

الهدف: حافظ على متوسط طول السلسلة N/M ≈ ثابت

5. الفحص الخطي

💡 الفكرة الأساسية (عنونة مفتوحة)

عندما يحدث تصادم، افحص موضع المصفوفة التالي (i+1, i+2, ...) حتى تجد فتحة فارغة.

كيف يعمل

  1. تجزئة: عين المفتاح لعدد صحيح i بين 0 و M-1
  2. إدراج: ضع في فهرس الجدول i إذا كان حراً؛ إذا لا، جرب i+1, i+2, إلخ.
  3. بحث: ابحث في فهرس الجدول i؛ إذا كان مشغولاً ولكن لا تطابق، جرب i+1, i+2, إلخ.

أساسي: حجم المصفوفة M يجب أن يكون أكبر من عدد أزواج المفتاح-القيمة N.

مثال مرئي

Index:  0   1   2   3   4   5   6   7   8   9   10  11  12  13  14  15
       [P] [M] [ ] [ ] [A] [C] [S] [H] [L] [ ] [E] [ ] [ ] [ ] [R] [X]

Insert K: hash(K) = 5
          Position 5 occupied → try 6 → occupied → try 7 → occupied
          → try 8 → occupied → try 9 → EMPTY! Insert at 9
            

تنفيذ جافا

public class LinearProbingHashST<Key, Value> {
    private int M = 30001;
    private Value[] vals = (Value[]) new Object[M];
    private Key[] keys = (Key[]) new Object[M];
    
    private int hash(Key key) {
        return (key.hashCode() & 0x7fffffff) % M;
    }
    
    public Value get(Key key) {
        for (int i = hash(key); keys[i] != null; i = (i+1) % M)
            if (key.equals(keys[i]))
                return vals[i];
        return null;
    }
    
    public void put(Key key, Value val) {
        int i;
        for (i = hash(key); keys[i] != null; i = (i+1) % M)
            if (keys[i].equals(key))
                break;
        keys[i] = key;
        vals[i] = val;
    }
}

مشكلة وقوف كنوث

تشبيه: سيارات تصل إلى شارع ذو اتجاه واحد به M مكان وقوف. كل واحدة تريد مكان عشوائي i؛ إذا كان مشغولاً، جرب i+1, i+2, إلخ.

تحليل الأداء

بحث ناجح: ~½(1 + 1/(1-α))

بحث فاشل/إدراج: ~½(1 + 1/(1-α)²)

حيث α = N/M (عامل الحمل)

عامل الحمل α بحث ناجح بحث فاشل
½ ~1.5 ~2.5
~2.0 ~5.0
¾ ~3.0 ~8.5

⚙️ الاختيار النموذجي

احتفظ بـ α = N/M ≈ ½ لعمليات وقت ثابت

الحذف في الفحص الخطي

⚠️ مشكلة أساسية

لا يمكن حذف إدخال ببساطة وتعيينه لـ null! هذا سيقطع البحث للعناصر التي تصادمت مع العنصر المحذوف.

الحل: بعد الحذف، يجب إعادة تجزئة جميع المفاتيح في نفس العنقود الذي يأتي بعد المفتاح المحذوف.

6. مقارنة الأداء

التسلسل المنفصل مقابل الفحص الخطي

✅ التسلسل المنفصل

  • الأداء يتدهور بلطف
  • أقل حساسية لدوال التجزئة المصممة بشكل سيء
  • حذف سهل
  • يمكن تجاوز عامل حمل 1

✅ الفحص الخطي

  • مساحة مهدرة أقل (لا مؤشرات)
  • أداء مخبأ أفضل
  • تنفيذ أبسط
  • أسرع عندما يكون عامل الحمل منخفضاً

مقارنة تنفيذات جدول الرموز الكاملة

التنفيذ بحث إدراج حذف مرتب؟ واجهة المفتاح
بحث تسلسلي N N N لا equals()
بحث ثنائي lg N N N نعم compareTo()
شجرة بحث ثنائية 1.39 lg N 1.39 lg N √N نعم compareTo()
شجرة الأحمر-أسود الثنائية 1.0 lg N 1.0 lg N 1.0 lg N نعم compareTo()
التسلسل المنفصل 3-5* 3-5* 3-5* لا equals(), hashCode()
الفحص الخطي 3-5* 3-5* 3-5* لا equals(), hashCode()

* تحت افتراض التجزئة الموحدة

7. مواضيع متقدمة

متغيرات دوال التجزئة

تجزئة فحص مزدوج

تجزئة إلى موقعين وأدخل في السلسلة الأقصر.

النتيجة: يقلل الطول المتوقع لأطول سلسلة إلى log log N

تجزئة مزدوجة

استخدم الفحص الخطي لكن تخطى مقدار متغير (ليس فقط 1) كل مرة.

h(x) = h1(x) + j·h2(x)  حيث j = 0, 1, 2, ...

المزايا: يزيل التكتل بشكل فعال، يمكن للجدول أن يكون ممتلئاً تقريباً

تجزئة الوقواق

تجزئة المفتاح لموقعين. إذا كان كلاهما مشغولاً، أزح مفتاحاً واحداً وأعد إدراجه.

النتيجة: وقت أسوأ حالة ثابت للبحث!

هجمات التعقيد الخوارزمي

🚨 مخاوف أمنية

الخصم الخبيث الذي يعرف دالة التجزئة الخاصة بك يمكنه تصميم مدخلات تتجزأ جميعها لنفس الدلو، مسبباً تدهور الأداء إلى O(N).

أمثلة:

تصادم تجزئة النصوص في جافا

بسبب دالة التجزئة الأساسية 31 في جافا، من الممكن توليد 2N نص بطول 2N تتجزأ جميعها لنفس القيمة!

"Aa" and "BB" both hash to 2112
"AaAa", "AaBB", "BBAa", "BBBB" all hash to -540425984
            

دوال التجزئة أحادية الاتجاه

دوال التجزئة التشفيرية

دوال تجزئة خاصة حيث من غير المجدي حسابياً:

أمثلة: SHA-256, SHA-3, BLAKE3

الاستخدامات: تخزين كلمات المرور، التواقيع الرقمية، البلوكشين

ملاحظة: مكلفة جداً للاستخدام العام في جداول الرموز

8. الملخص وأفضل الممارسات

🎯 النقاط الرئيسية

أفضل الممارسات

✅ افعل:

❌ لا تفعل:

متى تستخدم ماذا

حالة الاستخدام الاختيار الأفضل السبب
تحتاج عمليات مرتبة شجرة الأحمر-أسود الثنائية الجداول التجزئة لا تحافظ على الترتيب
بحث بسيط وسريع جدول التجزئة (أي نوع) وقت ثابت في الحالة المتوسطة
حمل غير متوقع التسلسل المنفصل يتدهور بلطف
ذاكرة محدودة الفحص الخطي لا حمل مؤشرات
كود حساس للذاكرة المخبأة الفحص الخطي محلية مرجع أفضل

المطبات الشائعة

  1. فيض رمز التجزئة: نسيان تخفي بت الإشارة يمكن أن يسبب فهارس سالبة
  2. دوال تجزئة سيئة: استخدام جزء فقط من المفتاح يؤدي للتكتل
  3. equals/hashCode غير متسقتان: إذا كان a.equals(b) لكن a.hashCode() ≠ b.hashCode()، الجدول ينكسر
  4. تجاهل عامل الحمل: ترك الجدول يمتلئ كثيراً يتدهور الأداء
  5. الأمن: استخدام دوال تجزئة قابلة للتنبؤ في بيئات خصومية

مرجع سريع: تنفيذ رمز التجزئة

// قالب للأنواع المعرفة من المستخدم
public int hashCode() {
    int hash = 17;  // ثابت غير صفري
    hash = 31 * hash + field1.hashCode();  // نوع مرجعي
    hash = 31 * hash + ((Integer) field2).hashCode();  // بدائي
    hash = 31 * hash + field3.hashCode();  // نوع مرجعي
    return hash;
}

🎓 أنت جاهز!

لديك الآن فهم شامل للجداول التجزئة. تدرب على تنفيذ كل من التسلسل المنفصل والفحص الخطي لترسيخ هذه المفاهيم.

دليل الدراسة من مواد CS210 الأسبوع 13

الخوارزميات بواسطة روبرت سيدجويك وكيفين واين