ما معنى Set في الخوارزميات وهياكل البيانات؟ دليل عملي مبسّط

شروحات تقنية

في هياكل البيانات، المجموعة (Set) هي بنية تُخزِّن عناصر فريدة لا تتكرر، من دون أن تهتم بترتيبها بالضرورة. بعبارة مبسّطة: لو حاولت إضافة عنصر موجود مسبقًا، فلن يُضاف مرة ثانية، وتبقى المجموعة محتفظة بنسخة واحدة فقط من كل قيمة. هذا التعريف مستمدّ مباشرةً من مفهوم المجموعة في الرياضيات، وهو ما يجعل الـ Set الأداة المثالية كلما احتجت إلى «قيم بلا تكرار» أو إلى فحص سريع جدًا لوجود عنصر من عدمه.

الفكرة الأساسية: التفرّد وفحص العضوية

الـ Set يقوم على قاعدتين تميّزانه عن باقي الهياكل:

  • التفرّد (Uniqueness): كل قيمة تظهر مرة واحدة فقط. إضافة قيمة مكرّرة عملية «صامتة» لا تغيّر شيئًا.
  • فحص العضوية السريع (Membership): السؤال «هل هذا العنصر موجود؟» يُجاب عنه بسرعة عالية جدًا، وهذا هو السبب الأول لاستخدام الـ Set في كثير من الخوارزميات.

لاحظ أن الـ Set التقليدي لا يدعم الفهرسة؛ لا يمكنك قول «أعطني العنصر رقم 3» كما في المصفوفة، لأن العناصر غير مرتّبة منطقيًا. أنت تسأل عن الوجود، لا عن الموضع.

متى تستخدم Set؟ حالات عملية

  • إزالة التكرارات: تمرير قائمة فيها قيم مكرّرة إلى Set يترك لك القيم الفريدة فقط في سطر واحد.
  • تتبّع ما تمت زيارته: في خوارزميات الرسوم البيانية (مثل BFS وDFS) نستخدم Set لتخزين العُقد التي زُرناها ومنع الدوران في حلقة لا نهائية.
  • عمليات المجموعات الرياضية: الاتحاد (Union)، والتقاطع (Intersection)، والفرق (Difference) بين مجموعتين، وهي جاهزة ومباشرة في معظم اللغات.
  • الكشف عن التكرار بكفاءة: بدل مقارنة كل عنصر بكل العناصر (وهو بطيء جدًا)، تضيفها إلى Set وتكتشف التكرار لحظة حدوثه.

كيف يعمل Set من الداخل؟

الـ Set مفهوم مجرّد، وله تنفيذان شائعان يحدّدان أداءه وسلوكه:

  1. مجموعة التجزئة (Hash Set): تعتمد على دالة تجزئة (Hash) تحوّل كل عنصر إلى موقع في الذاكرة. هذا يمنحها سرعة ممتازة، لكن العناصر تبقى غير مرتّبة. هذا هو النوع الأكثر شيوعًا.
  2. المجموعة الشجرية (Tree Set): تُبنى على شجرة بحث ثنائية متوازنة، فتُبقي العناصر مرتّبة تلقائيًا، مقابل أداء أبطأ قليلًا.

نقطة دقيقة كثيرًا ما تُغفَل: أداء الـ Hash Set يوصف بأنه O(1) في المتوسط، وليس مضمونًا دائمًا. عند حدوث تصادمات كثيرة في التجزئة قد يتدهور الأداء نظريًا إلى O(n). في الممارسة العملية يبقى سريعًا جدًا، لكن من الأمانة معرفة أن الـ O(1) هنا «متوسط» لا «مضمون».

العمليات الأساسية وأداؤها

العمليةHash Set (متوسط)Tree Set
الإضافة (add)O(1)O(log n)
البحث/العضوية (contains)O(1)O(log n)
الحذف (remove)O(1)O(log n)
الحفاظ على الترتيبلانعم (مرتّب)

القاعدة العملية: اختر Hash Set عندما تريد أقصى سرعة ولا يهمّك الترتيب، واختر Tree Set عندما تحتاج إلى المرور على العناصر بترتيب مُصنَّف.

Set أم List؟ الفرق الجوهري

كثير من المبتدئين يخلط بينهما، والفارق حاسم عند اختيار الهيكل المناسب:

المعيارSet (مجموعة)List (قائمة)
التكرارغير مسموحمسموح
الترتيبغير مضمون عادةًمُحافَظ عليه
الفهرسة بالرقملانعم
فحص «هل موجود؟»سريع جدًا O(1)بطيء O(n)
الاستخدام النموذجيقيم فريدة وفحص وجودتسلسل مرتّب قابل للتكرار

الخلاصة: إذا كنت ستسأل كثيرًا «هل هذا العنصر موجود؟» ولا تحتاج تكرارًا ولا ترتيبًا، فالـ Set أسرع بمراحل. أما إذا كان الترتيب أو التكرار أو الوصول بالرقم مهمًّا، فاستخدم القائمة.

Set في أشهر لغات البرمجة

الفكرة واحدة، لكن التفاصيل تختلف، وأهمها سلوك الترتيب:

  • Python: النوع set قابل للتعديل، وfrozenset نسخة ثابتة غير قابلة للتعديل تصلح كمفتاح في قاموس. ترتيب العناصر غير مضمون.
  • JavaScript: النوع Set (بحرف كبير) يتميّز بأنه يحافظ على ترتيب الإدخال عند المرور عليه، وهذا سلوك مختلف عن بايثون فانتبه له.
  • Java: لديك HashSet (بلا ترتيب)، وLinkedHashSet (يحافظ على ترتيب الإدخال)، وTreeSet (مرتّب تصاعديًا).
  • C++: std::set مبنية على شجرة ومرتّبة، بينما std::unordered_set مبنية على التجزئة وأسرع لكن بلا ترتيب.

أخطاء شائعة تجنّبها

  • افتراض أن الترتيب محفوظ: لا تعتمد على ترتيب عناصر الـ Set إلا إذا كانت اللغة تضمنه صراحةً (مثل Set في JavaScript أو LinkedHashSet في Java).
  • تخزين عناصر غير قابلة للتجزئة: في بايثون لا يمكنك وضع قائمة (list) داخل set لأنها قابلة للتعديل؛ استخدم tuple بدلًا منها.
  • الخلط بين Set وMap/Dictionary: الـ Set يخزّن قيمًا مفردة، بينما الخريطة (Map) تخزّن أزواج «مفتاح وقيمة». إذا احتجت أن تربط كل عنصر بمعلومة إضافية، فأنت تريد Map لا Set.

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

هل الـ Set دائمًا أسرع من القائمة؟ ليس دائمًا. هو أسرع بكثير في فحص «هل العنصر موجود؟»، لكنه لا يدعم الوصول بالرقم، وإنشاؤه يستهلك ذاكرة أكثر قليلًا بسبب بنية التجزئة. الأفضلية تعتمد على العملية التي تكررها أكثر.

لماذا لا يمكنني الوصول إلى عنصر بالفهرس مثل mySet[0]؟ لأن الـ Set غير مرتّب منطقيًا ولا يملك مفهوم «الموضع». إن احتجت الفهرسة، حوّله إلى قائمة أولًا أو استخدم قائمة من البداية.

ما الفرق بين set وfrozenset في بايثون؟ set قابل للتعديل (تضيف وتحذف منه)، أما frozenset فثابت لا يتغيّر بعد إنشائه، ما يجعله صالحًا للاستخدام كمفتاح في قاموس أو كعنصر داخل مجموعة أخرى.

هل يمكن للـ Set أن يحتوي أنواعًا مختلطة؟ في اللغات الديناميكية مثل بايثون وجافاسكربت نعم، بشرط أن تكون العناصر قابلة للتجزئة. لكن الأنظف عمليًا أن تبقي عناصر المجموعة من نوع واحد متجانس لتسهيل التعامل معها.