0Pricing
Coding Interview Prep · درس

آليات دوال التجزئة ومعالجة التصادمات

افهم كيفية إجراء Python لتجزئة الكائنات، وكيف يحل العنونة المفتوحة والتسلسل التصادمات، ولماذا قد يتدهور O(1) في الحالة المتوسطة إلى O(n)

آليات دوال التجزئة ومعالجة التصادمات درس مجاني في Coding Interview Prep على CoddyKit. هذا هو الدرس 1 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في Coding Interview Prep، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة Coding Interview Prep 4 دروس في المجموع.

ما هي خريطة التجزئة؟

تربط خريطة التجزئة (القاموس في Python) المفاتيح بالقيم باستخدام دالة تجزئة تحوّل أي مفتاح إلى فهرس صحيح في مصفوفة أساسية. توزّع دالة التجزئة المثالية المفاتيح بالتساوي على المصفوفة، مما يتيح البحث والإدراج والحذف بمتوسط تكلفة O(1). تُسمّى المصفوفة الأساسية جدول التجزئة أو مصفوفة الحاويات.

في Python، تمثل dict خريطة تجزئة محسّنة بدرجة كبيرة. يساعدكم فهم بنيتها الداخلية على تحليل السلوك في أسوأ الحالات واختيار المفاتيح المناسبة.

# Python dict is a hash map
hm = {}
hm['alice'] = 95
hm['bob']   = 87
hm['carol'] = 91

print(hm['alice'])          # O(1) lookup: 95
print('bob' in hm)          # O(1) membership: True
del hm['bob']               # O(1) deletion
print(hm)                   # {'alice': 95, 'carol': 91}

دوال التجزئة والطريقة __hash__

تستدعي Python الدالة __hash__(key) لحساب قيمة صحيحة من المفتاح، ثم تحسب باقي قسمة هذه القيمة على حجم الجدول لتحديد فهرس الحاوية. تمتلك الأنواع المضمّنة مثل int وstr وtuple تطبيقات تجزئة مضمّنة وسريعة. أما list وdict فليستا قابلتين للتجزئة، لأنهما قابلتان للتغيير، وتغييرهما سيبطل أي قيمة تجزئة مخزنة.

توزّع دالة التجزئة الجيدة المفاتيح بالتساوي، وتكون حتمية وسريعة الحساب. وتُغيّر Python قيمة تجزئة السلاسل النصية عشوائيًا بين عمليات التشغيل، وهي ميزة أمنية — استخدموا PYTHONHASHSEED=0 لتعطيل ذلك من أجل قابلية إعادة الإنتاج أثناء الاختبار.

# Built-in hash in Python
print(hash(42))           # integer hashes to itself (CPython)
print(hash('hello'))      # string hash (randomised per run)
print(hash((1, 2, 3)))    # tuple hash: depends on contents

# Unhashable types
try:
    hash([1, 2, 3])       # lists are mutable -> not hashable
except TypeError as e:
    print('Error:', e)

# Custom class: define __hash__ and __eq__
class Point:
    def __init__(self, x, y): self.x = x; self.y = y
    def __hash__(self): return hash((self.x, self.y))
    def __eq__(self, other): return self.x == other.x and self.y == other.y

points = {Point(1, 2): 'A', Point(3, 4): 'B'}
print(points[Point(1, 2)])  # 'A'

التصادمات: عندما تُجزَّأ مفتاحان إلى الحاوية نفسها

يحدث التصادم عندما ينتج مفتاحان مختلفان فهرس الحاوية نفسه. والتصادمات حتمية وفق مبدأ الحمام: عدد المفاتيح لا نهائي، بينما عدد الحاويات محدود. تتمثل الاستراتيجيتان القياسيتان لمعالجة ذلك في السلاسل والعنونة المفتوحة. تستخدم Python صيغةً من العنونة المفتوحة مع فحص شبه عشوائي.

تخزّن السلاسل قائمة مترابطة (أو مصفوفة ديناميكية) في كل حاوية؛ وتشكّل جميع المفاتيح التي تصطدم في تلك الحاوية سلسلةً واحدة. أما العنونة المفتوحة فتبحث عن الحاوية الفارغة التالية وفق تسلسل فحص محدد.

# Simplified chaining hash map
class ChainingHashMap:
    def __init__(self, capacity=8):
        self.capacity = capacity
        self.buckets  = [[] for _ in range(capacity)]

    def _idx(self, key):
        return hash(key) % self.capacity

    def put(self, key, val):
        bucket = self.buckets[self._idx(key)]
        for i, (k, v) in enumerate(bucket):
            if k == key:
                bucket[i] = (key, val)
                return
        bucket.append((key, val))

    def get(self, key):
        for k, v in self.buckets[self._idx(key)]:
            if k == key:
                return v
        return None

hm = ChainingHashMap()
hm.put('a', 1); hm.put('b', 2)
print(hm.get('a'))  # 1
print(hm.get('c'))  # None

العنونة المفتوحة: الفحص الخطي

في الفحص الخطي، عندما يحدث تصادم عند الفهرس i، تفحص الخريطة الخانات i+1 ثم i+2 وهكذا، مع الالتفاف إلى بداية المصفوفة، حتى تعثر على خانة فارغة. ويجب أن يتبع البحث تسلسل الفحص نفسه للعثور على المفتاح. ويتطلب الحذف استخدام علامة tombstone بدلًا من إفراغ الخانة، حتى لا تنقطع سلسلة الفحص.

يمثل التكتل العيب الرئيسي: فعندما تتكوّن كتلة من الخانات الممتلئة، تؤدي عمليات الإدراج اللاحقة في تلك المنطقة إلى توسيع الكتلة، مما يخفض الأداء تدريجيًا إلى O(n).

class LinearProbingHashMap:
    DELETED = object()  # tombstone sentinel

    def __init__(self, capacity=8):
        self.capacity = capacity
        self.keys  = [None] * capacity
        self.vals  = [None] * capacity
        self.size  = 0

    def _probe(self, key):
        idx = hash(key) % self.capacity
        while self.keys[idx] is not None and self.keys[idx] != key:
            idx = (idx + 1) % self.capacity
        return idx

    def put(self, key, val):
        idx = self._probe(key)
        if self.keys[idx] is None:
            self.size += 1
        self.keys[idx] = key
        self.vals[idx] = val

    def get(self, key):
        idx = self._probe(key)
        if self.keys[idx] == key:
            return self.vals[idx]
        return None

hm = LinearProbingHashMap()
hm.put('x', 10); hm.put('y', 20)
print(hm.get('x'))  # 10

معامل الامتلاء وإعادة التحجيم

معامل الامتلاء هو نسبة العناصر المخزنة إلى السعة الإجمالية: α = n/m. ومع ازدياد α، يزداد احتمال التصادم ويتراجع الأداء. تعيد Python تحجيم dict (فتضاعف السعة) عندما يتجاوز معامل الامتلاء نحو 2/3. وتتضمن إعادة التحجيم إعادة تجزئة جميع العناصر الموجودة في الجدول الأكبر، وهي عملية بتكلفة O(n) تحدث على فترات متباعدة، مما يحافظ على تكلفة إدراج مُهَدرَجة مقدارها O(1).

import sys

d = {}
prev_size = sys.getsizeof(d)
for i in range(30):
    d[i] = i
    new_size = sys.getsizeof(d)
    if new_size != prev_size:
        print(f'Resized at n={i+1}: {prev_size} -> {new_size} bytes')
        prev_size = new_size

O(1) في المتوسط مقابل O(n) في أسوأ الحالات

عند استخدام دالة تجزئة جيدة، تكون التصادمات نادرة، ويظل طول السلسلة المتوقع ثابتًا بغض النظر عن n. ولذلك تكون تكلفة البحث والإدراج والحذف في الحالة المتوسطة O(1). لكن سيناريو أسوأ حالة — مثل إدخال عدائي متعمد يضع جميع المفاتيح في الحاوية نفسها — يخفض تكلفة جميع العمليات إلى O(n). وتخفف بذرة التجزئة العشوائية في Python من هذا الهجوم، لكنها لا تلغي أسوأ حالة من الناحية النظرية.

عند التحليل في المقابلة، قولوا: «O(1) في المتوسط، وO(n) في أسوأ الحالات بسبب التصادمات».

# Python randomised hash seed prevents worst-case hash-flooding
import os
print('PYTHONHASHSEED:', os.environ.get('PYTHONHASHSEED', 'random'))
# By default Python randomises the hash of strings each run
# This prevents an attacker from crafting keys that all collide
# To reproduce results in testing: PYTHONHASHSEED=0 python script.py

Python dict مقابل defaultdict مقابل Counter

توفر Python ثلاث صيغ من خرائط التجزئة تجدر معرفتها. يُعد dict الخريطة العامة؛ ويؤدي الوصول إلى مفتاح مفقود فيه إلى رفع الاستثناء KeyError. أما defaultdict(factory) فيعيد قيمة افتراضية عند الوصول إلى مفتاح مفقود، وهو مفيد لجمع القوائم أو إجراء العدّ. وCounter فئة فرعية متخصصة لعدّ الكائنات القابلة للتجزئة، كما تدعم العمليات الحسابية بين العدادات.

from collections import defaultdict, Counter

# defaultdict for grouping
groups = defaultdict(list)
for word in ['apple', 'ant', 'banana', 'bee', 'avocado']:
    groups[word[0]].append(word)
print(dict(groups))
# {'a': ['apple','ant','avocado'], 'b': ['banana','bee']}

# Counter for frequency
c = Counter('abracadabra')
print(c.most_common(3))  # [('a',5),('b',2),('r',2)]
print(c['a'] - Counter('aa')['a'])  # counter subtraction

خريطة التجزئة مقابل مجموعة التجزئة

تخزّن مجموعة التجزئة المفاتيح فقط، من دون قيم مرتبطة بها، وتدعم اختبار العضوية والإدراج والحذف بتكلفة O(1). تمثل set في Python مجموعة تجزئة. استخدموا المجموعة عندما تحتاجون فقط إلى الإجابة عن سؤال «هل هذا العنصر موجود؟» من دون تخزين بيانات مرتبطة به. واستخدموا القاموس عندما تحتاجون إلى ربط قيم، مثل الأعداد أو النتائج، بالمفاتيح.

# set for membership testing
visited = set()
for node in [1, 3, 5, 3, 7, 1]:
    if node not in visited:
        print('New node:', node)
        visited.add(node)

# Set operations: union, intersection, difference
A = {1, 2, 3, 4}
B = {3, 4, 5, 6}
print('Union:', A | B)         # {1,2,3,4,5,6}
print('Intersection:', A & B)  # {3,4}
print('Difference:', A - B)    # {1,2}

تنفيذ خريطة تجزئة من الصفر (نسخة المقابلة)

قد يطلب منكم القائمون على المقابلة أحيانًا تنفيذ خريطة تجزئة أساسية. وتشمل المكونات الرئيسية: مصفوفة ثابتة الحجم من الحاويات، ويُفضَّل أن تكون سعتها 16 أو 1024؛ وقائمة من أزواج (key, value) في كل حاوية لمعالجة التصادم بالسلاسل؛ ودالة تجزئة، باستخدام hash % capacity في Python؛ وإعادة التحجيم عندما يتجاوز معامل الامتلاء 0.7. إن ذكر إعادة التحجيم ومعامل الامتلاء استباقيًا يبرهن على عمق معرفتكم.

class HashMap:
    def __init__(self, capacity=16):
        self.capacity = capacity
        self.size     = 0
        self.buckets  = [[] for _ in range(capacity)]

    def _hash(self, key):
        return hash(key) % self.capacity

    def put(self, key, val):
        b = self.buckets[self._hash(key)]
        for i, (k, v) in enumerate(b):
            if k == key:
                b[i] = (key, val)
                return
        b.append((key, val))
        self.size += 1
        if self.size / self.capacity > 0.7:
            self._resize()

    def get(self, key, default=None):
        for k, v in self.buckets[self._hash(key)]:
            if k == key:
                return v
        return default

    def _resize(self):
        old = self.buckets
        self.capacity *= 2
        self.buckets = [[] for _ in range(self.capacity)]
        self.size = 0
        for bucket in old:
            for k, v in bucket:
                self.put(k, v)

hm = HashMap()
for i in range(20):
    hm.put(i, i * 2)
print(hm.get(10))   # 20
print(hm.capacity)  # should have resized

عندما تفشل خرائط التجزئة: المفاتيح غير القابلة للتجزئة

لا يمكن استخدام الكائنات القابلة للتجزئة إلا بوصفها مفاتيح في القواميس. في Python، يكون الكائن قابلًا للتجزئة إذا كان يمتلك طريقة __hash__ وطريقة __eq__، وكانت قيمة التجزئة الخاصة به لا تتغير طوال فترة حياته. أما القوائم والمجموعات والقواميس فقابلة للتغيير، ولذلك لا يمكن تجزئتها. وتُعد الصفوف ومجموعات frozenset بدائل قابلة للتجزئة للقوائم والمجموعات عند استخدامها مفاتيح.

من الأخطاء الشائعة في المقابلات: يتطلب تجميع الكلمات المتناظرة استخدام tuple مرتبة، وليس list مرتبة، بوصفها مفتاح dict.

from collections import defaultdict

def groupAnagrams(strs):
    groups = defaultdict(list)
    for s in strs:
        key = tuple(sorted(s))  # tuple is hashable; list is not
        groups[key].append(s)
    return list(groups.values())

print(groupAnagrams(['eat','tea','tan','ate','nat','bat']))
# [['eat','tea','ate'], ['tan','nat'], ['bat']]

ملخص: تعقيد خريطة التجزئة

توفر خرائط التجزئة تكلفة O(1) في الحالة المتوسطة للإدراج والحذف والبحث، وهي أساس كثير من الحلول المثلى في المقابلات. وتتمثل الافتراضات الرئيسية في أن دالة التجزئة الجيدة توزّع المفاتيح بالتساوي، وأن يظل معامل الامتلاء محدودًا (وتحافظ إعادة التحجيم على ذلك)، وأن تكون كائنات المفاتيح ثابتة وقابلة للتجزئة. عند تحقق هذه الافتراضات، تحوّل خرائط التجزئة عمليات البحث الخطي بتكلفة O(n) إلى عمليات بحث بتكلفة O(1)، مما يتيح حلولًا مثل two-sum بتكلفة O(n) بدلًا من O(n²).

اختبار سريع

اختبروا مدى فهمكم لمفاهيم Data Structures & Algorithms — Coding Interview Prep التي تناولها هذا الدرس.

مراجعة الدرس

تعلّمتم في هذا الدرس أن خريطة التجزئة تربط المفاتيح بفهارس الحاويات باستخدام دالة تجزئة، وتحقق عمليات بتكلفة O(1) في الحالة المتوسطة، وأن التصادمات تُعالج بالسلاسل، أي قائمة مترابطة لكل حاوية، أو بالعنونة المفتوحة، أي الفحص بحثًا عن الخانة الفارغة التالية، وأن الكائنات الثابتة والقابلة للتجزئة فقط يمكن استخدامها مفاتيح في القواميس — استخدموا الصفوف بدلًا من القوائم عند الحاجة إلى مفتاح يمثل تسلسلًا. ننتقل بعد ذلك إلى حل مسألة two-sum وصيغها المتعددة في المقابلات.

الأسئلة الشائعة

هل درس «آليات دوال التجزئة ومعالجة التصادمات» مجاني؟

نعم — نص درس «آليات دوال التجزئة ومعالجة التصادمات» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة Coding Interview Prep، انتقل إلى CoddyKit PRO. تتضمن دورة Coding Interview Prep 4 دروس في المجموع.

ماذا ستتعلم في «آليات دوال التجزئة ومعالجة التصادمات»؟

افهم كيفية إجراء Python لتجزئة الكائنات، وكيف يحل العنونة المفتوحة والتسلسل التصادمات، ولماذا قد يتدهور O(1) في الحالة المتوسطة إلى O(n) تتمرن على Coding Interview Prep مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.

هل أحتاج إلى خبرة سابقة لأبدأ Coding Interview Prep؟

لا تُشترط خبرة سابقة. Coding Interview Prep على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 1 من أصل 4.

كم من الوقت يستغرق درس «آليات دوال التجزئة ومعالجة التصادمات»؟

معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.

هل يمكنني كتابة وتشغيل أكواد في درس Coding Interview Prep هذا؟

نعم. كل درس في Coding Interview Prep يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.

جميع الدروس في هذه الدورة

  1. آليات دوال التجزئة ومعالجة التصادمات
  2. Two-Sum ومتغيراته العديدة
  3. عدّ التكرارات والتجميع
  4. أطول تسلسل متتالٍ وذاكرة LRU المؤقتة
← العودة إلى Coding Interview Prep