Menu
flag Ar iconالعربيةdown icon

‏C++ unordered_map: شرح البحث السريع في جدول التجزئة

تعلَّم std::unordered_map في C++ - شقيق map المعتمِد على جدول التجزئة الذي يمنحك إدراجًا وبحثًا بمتوسط O(1). يغطي العمليات الأساسية، ومأزق الإدراج التلقائي عبر []، والفرق بين count وfind، ومتى تختاره بدلًا من map المرتَّبة.

تحتوي هذه الصفحة على محررات قابلة للتشغيل - حرّر، شغّل، وشاهد النتيجة فوراً.

عندما لا يهمّ الترتيب

لقد رأيت للتوّ std::map، الذي يبقي مفاتيحه مرتَّبة باستخدام شجرة متوازنة ويمنحك عمليات بتعقيد O(log n). لكن للترتيب كلفة، وكثيرًا ما لا يهمّك بأيّ ترتيب تخرج المفاتيح - كلّ ما تريده هو أن تسأل "هل هذا المفتاح موجود، وما قيمته؟" بأسرع ما يمكن.

لهذا وُجد std::unordered_map. إنه جدول تجزئة: يمرِّر كلّ مفتاح عبر دالة تجزئة ليقرّر مكان تخزينه، ما يجعل الإدراج والبحث والحذف بمتوسط O(1) بدلًا من O(log n). المقابل أنّ ترتيب المرور غير محدَّد - تخرج المفاتيح بالترتيب الذي تصادف أن تحفظها به السلال (buckets).

الواجهة شبه مطابقة لواجهة map عن قصد - غالبًا ما يمكنك استبدال أحدهما بالآخر بتغيير النوع فقط. ضمِّن <unordered_map> (لا <map>)، وتخرج المفاتيح غير مرتَّبة.

الإدراج والتحديث

هناك عدّة طرق لإدخال المُدخلات، وهي لا تتصرّف جميعها بالطريقة نفسها. الطريقتان الأكثر استخدامًا هما operator[] وinsert:

الفرق الجوهري: ‏operator[] يكتب فوق قيمة موجودة، بينما insert يترك المفتاح الموجود مسبقًا دون مساس. إن كنت على C++17، فإنّ insert_or_assign(key, value) يمنحك دلالة "اضبط هذا مهما كان"، وtry_emplace(key, args...) يُنشئ القيمة في مكانها فقط عندما يكون المفتاح جديدًا - وهو مفيد للقيم المكلفة في الإنشاء.

مأزق operator[]: يُدرِج عند القراءة

هذا أكثر أخطاء unordered_map شيوعًا، لذا يحظى بقسم خاصّ به. ‏m[key] ليس قراءة محضة. إن كان المفتاح مفقودًا، فإنه يُنشئه بالقيمة الافتراضية ويُدرِجه (يصبح int قيمته 0، ويصبح string قيمته "")، ثم يُعيد مرجعًا. لذا فإنّ شيفرة تبدو كأنها بحث تُكبِّر الخريطة بهدوء:

أنشأ seen["y"] المُدخل "y" -> 0 لمجرّد ذكره. لاختبار الوجود دون تعديل الخريطة، استخدم count (يُعيد 0 أو 1) أو find:

قاعدة عامة: استخدم [] فقط عندما تنوي الإنشاء أو التحديث. أمّا للقراءة المحضة، فاستخدم count أو contains (C++20) أو find.

‏find وat: عمليات بحث آمنة

يُعيد find مكرِّرًا (iterator) إلى المُدخل، أو end() إن كان المفتاح غير موجود. لا يُدرِج أبدًا، ويتيح لك الوصول إلى المفتاح والقيمة معًا عبر it->first وit->second في عملية بحث واحدة:

عندما تعلم أنّ المفتاح ينبغي أن يكون موجودًا وتريد فشلًا صريحًا إن لم يكن كذلك، استخدم at. على عكس []، لا يُدرِج at - بل يطرح std::out_of_range لمفتاح مفقود:

int p = prices.at("pen");   // سليم
int q = prices.at("hat");   // يطرح std::out_of_range - المفتاح غير موجود

إذن لديك ثلاثة أساليب للبحث: ‏[] (يُدرِج)، وat (يطرح استثناءً)، وfind/count (يُبلغان عن الوجود دون المساس بالخريطة). اختر الأسلوب الذي يطابق نمط فشله ما تقصده.

المرور على العناصر والحذف

تُعدّ حلقة for المعتمِدة على نطاق مع الربط البنيوي (structured bindings) الطريقة النظيفة للمرور على كلّ مُدخل. تذكَّر أنّ الترتيب اعتباطيّ - لا تعتمد عليه أبدًا:

لإزالة مُدخل، يأخذ erase مفتاحًا مباشرةً ويُعيد عدد ما حُذف (0 أو 1). مأزق مهمّ: الحذف أثناء المرور في unordered_map يُبطِل مكرِّر العنصر المحذوف وحده فقط، لذا استخدم القيمة المُعادة من erase(it) للتقدّم بأمان:

كتابة الأسلوب wins.erase(it++) أو وضع ++it بعد erase(it) بسيط هو الفخّ الكلاسيكي للمكرِّر المعلَّق - المكرِّر المحذوف ميّت، فخذ دائمًا المكرِّر الذي يُعيده erase.

‏map أم unordered_map؟

كلاهما يخزِّن أزواج مفتاح-قيمة بواجهة متشابهة، لذا يتلخّص الاختيار في ما تحتاجه:

// std::map            -> مفاتيح مرتَّبة، O(log n)، استعلامات على نطاق (lower_bound)
// std::unordered_map  -> بلا ترتيب،     O(1) متوسط، أسرع بحث بسيط

اِلجأ إلى unordered_map عندما تحتاج فقط إلى بحث سريع بالمفتاح ويكون الترتيب غير ذي صلة (عدّ تكرارات الكلمات، التخزين المؤقت، إزالة التكرار). اختر map عندما تحتاج إلى المفاتيح بترتيب مُرتَّب، أو تريد المرور بترتيب، أو تُجري استعلامات على نطاق. تحذيران بشأن unordered_map: تعقيده O(1) هو متوسط - وقد تُدهوره دالة تجزئة سيئة - ونوع المفتاح المخصَّص يحتاج إلى تخصيص تجزئة (specialization) أو كائن دالة تجزئة (functor)، بينما لا يحتاج map إلا إلى operator<.

التالي: Set

لقد رأيت الآن كلا نوعَي حاوية المفتاح-القيمة. لكن أحيانًا لا تحتاج إلى قيمة على الإطلاق - يهمّك فقط هل الشيء موجود، مثل مجموعة من الوسوم الفريدة أو المعرّفات التي زُرتها. تاليًا سننظر في std::set (وقريبه المعتمِد على التجزئة unordered_set)، اللذين يخزّنان المفاتيح وحدها ويُبقيانها فريدة تلقائيًّا.

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

ما الفرق بين map وunordered_map في C++؟

std::map شجرة ثنائية متوازنة: تبقى المفاتيح مرتَّبة وتكون العمليات بتعقيد O(log n). أمّا std::unordered_map فهو جدول تجزئة: المفاتيح بلا ترتيب محدّد، لكن الإدراج والبحث بمتوسط O(1). استخدم unordered_map عندما تحتاج فقط إلى بحث سريع بالمفتاح ولا يهمّك الترتيب؛ واستخدم map عندما تحتاج إلى مرور مرتَّب أو استعلامات على نطاق.

هل يُدرِج unordered_map[] مفتاحًا إن لم يكن موجودًا؟

نعم. ‏m[key] يُنشئ بالقيمة الافتراضية ويُدرِج المفتاح إن كان مفقودًا، ثم يُعيد مرجعًا إليه. هذا يعني أنّ حتى تعبيرًا يبدو كقراءة مثل if (m[key] == ...) يُكبِّر الخريطة بصمت. للتحقّق من وجود مفتاح دون إدراجه، استخدم m.count(key) أو m.find(key) بدلًا من ذلك.

هل unordered_map أسرع دائمًا من map في C++؟

لا. هو O(1) في المتوسط، لكن للتجزئة كلفة إضافية، وقد تؤدّي دالة تجزئة سيئة (أو مفاتيح معادية) إلى تدهور البحث إلى O(n). في الخرائط الصغيرة قد تجعل العوامل الثابتة وسلوك الذاكرة المؤقتة الأسوأ خريطة map مرتَّبة بالسرعة نفسها أو أسرع. قِس الأداء إن كان مهمًّا، لكن اِلجأ إلى unordered_map افتراضيًا عندما تحتاج فقط إلى البحث بالمفتاح.

Coddy programming languages illustration

تعلّم البرمجة مع Coddy

ابدأ الآن