القاموس والمجموعة: جداول التجزئة
السر وراء سرعة dict و set: حساب مكان العنصر بدل البحث عنه. ⏱ نحو 35 دقيقة.
- تشرح فكرة جدول التجزئة ودالة التجزئة.
- تبني جدول تجزئة بسيطًا وتعالج التصادم بالسلاسل.
- تحفظ تعقيد عمليات
dictوsetفي المتوسط والأسوأ. - تستخدم القاموس والمجموعة للعدّ وحذف التكرار وعمليات المجموعات.
١كيف يجد القاموس المفتاح في خطوة واحدة؟
القاموس dict والمجموعة set مبنيان على جدول التجزئة Hash Table: مصفوفة خانات، ودالة تحوّل المفتاح إلى رقم خانة. فبدل البحث في كل العناصر، نحسب رقم الخانة ونذهب إليها مباشرة.
# دالة التجزئة تحوّل أي قيمة غير قابلة للتعديل إلى عدد صحيحprint(hash(441001))print(hash(441001) % 8) # رقم الخانة في جدول من 8 خاناتprint(hash((24, 39)) == hash((24, 39)))
النصوص تُجزَّأ بقيمة عشوائية تتغير كل مرة يُشغَّل فيها بايثون (لأسباب أمنية)، لذلك لا نطبع hash لنص هنا. المهم: القيمة نفسها تعطي التجزئة نفسها أثناء تشغيل البرنامج.
٢جدول تجزئة نبنيه بأيدينا
جدول بسيط من 8 خانات، كل خانة قائمة صغيرة تحمل أزواج (مفتاح، قيمة). وحين يقع مفتاحان في الخانة نفسها (تصادم Collision) نضعهما معًا في قائمتها، وتسمى هذه الطريقة السلاسل Chaining:
class HashTable: def __init__(self, size=8): self.buckets = [[] for _ in range(size)] def _index(self, key): return sum(ord(ch) for ch in str(key)) % len(self.buckets) # دالة تجزئة بسيطة def put(self, key, value): b = self.buckets[self._index(key)] for pair in b: if pair[0] == key: pair[1] = value # تحديث return b.append([key, value]) def get(self, key): for k, v in self.buckets[self._index(key)]: if k == key: return v return None phones = HashTable()phones.put("أحمد", "0551")phones.put("سارة", "0552")phones.put("خالد", "0553")print(phones.get("سارة"), phones.get("فهد"))for i, b in enumerate(phones.buckets): if b: print(i, b)
٣التعقيد: O(1) في المتوسط
| العملية | المتوسط | الأسوأ | متى الأسوأ؟ |
|---|---|---|---|
البحث d[k] و k in d | O(1) | O(n) | كل المفاتيح في خانة واحدة (دالة تجزئة سيئة) |
| الإضافة والتعديل | O(1) | O(n) | كذلك |
| الحذف | O(1) | O(n) | كذلك |
بايثون يستخدم دالة تجزئة جيدة جدًّا، ويكبّر الجدول تلقائيًّا حين يمتلئ (مثل المصفوفة الديناميكية)، فتبقى العمليات O(1) عمليًّا.
٤استخدامات قوية
# 1) العدّ: كم مرة ظهرت كل كلمة؟ O(n)text = "قهوة شاي قهوة ماء قهوة شاي"count = {}for w in text.split(): count[w] = count.get(w, 0) + 1print(count) # 2) حذف التكرار مع حفظ الترتيبseen, unique = set(), []for w in text.split(): if w not in seen: seen.add(w) unique.append(w)print(unique) # 3) عمليات المجموعاتa = {"أحمد", "سارة", "خالد"} # مسجلون في 101b = {"سارة", "منى", "خالد"} # مسجلون في 121print(sorted(a & b), sorted(a - b), len(a | b))
from collections import Counterprint(Counter("قهوة شاي قهوة ماء قهوة شاي".split()).most_common(2))
توقّع وتحقّق
متوسط تعقيد k in d:
وقوع مفتاحين في الخانة نفسها:
Collision.أيها لا يصلح مفتاحًا للقاموس؟
list قابلة للتعديل.الطلاب المسجلون في المقررين معًا:
الخلاصة
- جدول التجزئة: المفتاح ←
hash← رقم خانة، فالبحثO(1)في المتوسط. - التصادم: مفتاحان في خانة واحدة، ويُحل بالسلاسل.
- المفاتيح يجب أن تكون
hashable: رقم ونص وtuple، لاlist. - العدّ بـ
getأوCounter، والتكرار بـset، و & | - للمجموعات.
تحدٍّ برمجي: أول حرف لا يتكرر
متوسطاكتب دالة first_unique(text) تُرجع أول حرف لا يتكرر في النص، أو None إن لم يوجد، بتعقيد O(n): مرور للعدّ ثم مرور للبحث.
- المرور الأول: عدّ كل حرف في قاموس.
- المرور الثاني: أول حرف عدده 1.
- اطبع النتيجة لكل نص:
"سلسبيل"و"مرمر"و"تراكيب".
ابدأ من هذا الكود، واضغط ▶ شغّل هنا لتكتب حلّك وتجرّبه مباشرة:
def first_unique(text): pass
💡 تلميح 1
count.get(ch, 0) + 1💡 تلميح 2
text.count(ch) لكل حرف لصار الحل O(n²).حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.
def first_unique(text): count = {} for ch in text: count[ch] = count.get(ch, 0) + 1 for ch in text: if count[ch] == 1: return ch return None for t in ["سلسبيل", "مرمر", "تراكيب"]: print(t, first_unique(t))