CS210 - الأسبوع 13 - مرجع شامل
توفر الجداول التجزئة أداء وقت ثابت في الحالة المتوسطة لعمليات البحث، الإدراج، والحذف باستخدام دالة تجزئة لحساب فهرس مصفوفة مباشرة من المفتاح.
الخريطة هي هيكل بيانات يمثل مجموعة قابلة للبحث من أزواج المفتاح-القيمة، حيث:
get(key), put(key, value), remove(key)احفظ العناصر في جدول مفهرس بالمفتاح حيث يتم حساب الفهرس كدالة للمفتاح.
| السيناريو | الإستراتيجية | المشكلة |
|---|---|---|
| لا حدود للمساحة | دالة تجزئة تافهة مع المفتاح كفهرس | يهدر كميات هائلة من الذاكرة |
| لا حدود للزمن | بحث تسلسلي خلال جميع العناصر | بطيء جداً لمجموعات البيانات الكبيرة |
| العالم الحقيقي | التجزئة مع حل التصادم | يوازن المساحة والزمن بكفاءة |
قم بتشويش المفاتيح بشكل موحد لإنتاج فهرس جدول يكون:
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;
}
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.
هذا الافتراض حاسم لتحليل الأداء لكنه صعب التحقيق عملياً.
استخدم مصفوفة من M < N قائمة مرتبطة. كل موضع مصفوفة يشير إلى سلسلة من العناصر التي تتجزأ لذلك الفهرس.
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.
| العملية | الحالة المتوسطة | أسوأ حالة |
|---|---|---|
| بحث ناجح | ~3-5 فحوصات | N فحص |
| بحث فاشل | ~3-5 فحوصات | N فحص |
| إدراج | ~3-5 فحوصات | N فحص |
الهدف: حافظ على متوسط طول السلسلة N/M ≈ ثابت
عندما يحدث تصادم، افحص موضع المصفوفة التالي (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! هذا سيقطع البحث للعناصر التي تصادمت مع العنصر المحذوف.
الحل: بعد الحذف، يجب إعادة تجزئة جميع المفاتيح في نفس العنقود الذي يأتي بعد المفتاح المحذوف.
| التنفيذ | بحث | إدراج | حذف | مرتب؟ | واجهة المفتاح |
|---|---|---|---|---|---|
| بحث تسلسلي | 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() |
* تحت افتراض التجزئة الموحدة
تجزئة إلى موقعين وأدخل في السلسلة الأقصر.
النتيجة: يقلل الطول المتوقع لأطول سلسلة إلى 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
الاستخدامات: تخزين كلمات المرور، التواقيع الرقمية، البلوكشين
ملاحظة: مكلفة جداً للاستخدام العام في جداول الرموز
hashCode() صحيحة تستخدم جميع الحقول المهمةequals() و hashCode() متسقتان| حالة الاستخدام | الاختيار الأفضل | السبب |
|---|---|---|
| تحتاج عمليات مرتبة | شجرة الأحمر-أسود الثنائية | الجداول التجزئة لا تحافظ على الترتيب |
| بحث بسيط وسريع | جدول التجزئة (أي نوع) | وقت ثابت في الحالة المتوسطة |
| حمل غير متوقع | التسلسل المنفصل | يتدهور بلطف |
| ذاكرة محدودة | الفحص الخطي | لا حمل مؤشرات |
| كود حساس للذاكرة المخبأة | الفحص الخطي | محلية مرجع أفضل |
// قالب للأنواع المعرفة من المستخدم
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
الخوارزميات بواسطة روبرت سيدجويك وكيفين واين