الفرز ليس سوى خوارزمية أخرى
في الصفحة السابقة رأيت أن المكتبة القياسية تأتي بخوارزميات جاهزة تعمل على أي نطاق عبر المُكرِّرات (iterators). الفرز هو ما ستلجأ إليه أكثر من غيره، وله صفحته الخاصة لأن به بعض الحواف الحادة: ترتيبات مخصصة، واستقرار، وقاعدة إن خالفتها أعطتك سلوكًا غير معرَّف بدلًا من إجابة خاطئة.
حصان العمل هنا هو std::sort من <algorithm>. تعطيه بداية النطاق ونهايته، فيعيد ترتيب العناصر في مكانها بترتيب تصاعدي:
لا تُصنع أي نسخة؛ المتجه نفسه يُعاد ترتيبه. خلف الكواليس، عادةً ما يكون std::sort خوارزمية introsort (فرز سريع يرتدّ إلى فرز الكومة)، ما يمنحك O(n log n) في المتوسط. وهذا في الغالب أسرع وأقل عرضة للأخطاء بكثير من كتابة فرز خاص بك.
ويعمل كذلك على مصفوفات C العادية؛ فأنت فقط تصف النطاق بالمؤشرات:
ترتيب مخصص باستخدام مقارن
افتراضيًا، يرتّب std::sort العناصر باستخدام operator<. لترتيبها بشكل مختلف، مرّر وسيطًا ثالثًا: مقارنًا يأخذ عنصرين ويُعيد true إذا كان ينبغي للأول أن يأتي قبل الثاني.
تعبير lambda هو الخيار الطبيعي. الترتيب التنازلي هو ببساطة a > b:
للحالة الشائعة وهي الترتيب التنازلي البسيط على الأنواع المضمّنة، توفّر المكتبة حتى مقارنًا جاهزًا، وهو greater<T>() من <functional>:
#include <functional>
sort(nums.begin(), nums.end(), greater<int>()); // مثل a > b
المقارن هو أيضًا الطريقة للفرز حسب شيء غير القيمة نفسها؛ مثل فرز السلاسل حسب الطول بدلًا من الترتيب الأبجدي:
استقبل معاملات المقارن عبر const& لأي شيء أكبر من بضعة بايتات (مثل string)؛ فنسخ كل عنصر في كل مقارنة هو هدر محض.
فرز البُنى (structs) حسب حقل
في البرامج الحقيقية، تفرز عادةً مجموعات من البُنى حسب أحد حقولها. ما على المقارن إلا الوصول إلى الحقل الذي يهمّك. هنا نفرز الأشخاص حسب العمر، الأصغر أولًا:
لاحظ أن Linus وDennis كلاهما في عمر 25. خرجا هنا بترتيبهما النسبي الأصلي، لكن std::sort لا يضمن ذلك. إذا كان الترتيب النسبي للعناصر المتساوية مهمًّا، فاستخدم std::stable_sort الذي يحافظ عليه (مقابل كلفة أداء بسيطة):
لفض التعادل عمدًا - مثلًا الفرز حسب العمر ثم أبجديًا حسب الاسم - قارن المفتاح الثانوي فقط عندما يتساوى المفتاحان الأساسيان. ويجعل std::tie هذا أنيقًا:
فخ الترتيب الضعيف الصارم
هذا هو أخطر خطأ منفرد في الفرز بلغة C++، لأنه لا يعطيك إجابة خاطئة، بل يعطيك سلوكًا غير معرَّف، وهو ما يعني غالبًا انهيارًا أو قراءة خارج الحدود.
يتطلب std::sort أن يُعرّف مقارنك ترتيبًا ضعيفًا صارمًا. القاعدة العملية: يجب أن يكون comp(x, x) مساويًا لـ false لأي عنصر x. بعبارة أخرى، لا يكون أي عنصر أبدًا "قبل" نفسه. وهذا بالضبط ما يمنحك إياه < و>، وبالضبط ما يخرقه <= و>=:
// خطأ: يُعيد true عندما يكون a == b، مخالفًا الترتيب الضعيف الصارم.
sort(v.begin(), v.end(), [](int a, int b) {
return a <= b; // سلوك غير معرَّف - قد ينهار مع بعض المدخلات
});
مع <=، يدّعي المقارن أن 5 تأتي قبل 5 أخرى، وهو ادعاء متناقض. عندئذٍ قد يدفع std::sort مؤشرًا إلى ما بعد نهاية النطاق. تبدو المدخلات الصغيرة أحيانًا وكأنها تعمل، وهذا ما يجعل هذا العيب مرعبًا؛ فقد يجتاز اختباراتك وينهار في الإنتاج. والإصلاح هو ببساطة <:
ومطبّ كلاسيكي ثانٍ: الفرز يُبطل صلاحية أي شيء يشير إلى داخل النطاق. فالمُكرِّرات والمؤشرات والفهارس التي حفظتها قبل الفرز لم تعد تشير إلى العنصر المنطقي نفسه بعده، لأن العناصر تحركت. أعِد اشتقاق أي موضع تحتاجه بعد الفرز، لا قبله أبدًا.
فرز جزء من نطاق
أحيانًا لا تحتاج إلى فرز كل شيء؛ تريد فقط الأوائل القليلة. فرز المتجه بأكمله لقراءة الثلاثة الأوائل هو إهدار. يرتّب std::partial_sort العناصر التي تطلبها فقط ويترك الباقي بترتيب غير محدَّد، وهو أرخص:
وإن كنت تحتاج فقط إلى العنصر الوحيد الذي سيقع في موضع معيّن - كالوسيط (median) - فإن std::nth_element يقوم بعمل أقل: يضع العنصر الصحيح في ذلك الفهرس، مع كل ما هو أصغر قبله وكل ما هو أكبر بعده، وكل ذلك في O(n) في المتوسط.
الجأ إلى هذه عندما يكون "الفرز الكامل" أكثر مما تحتاجه المسألة فعلًا؛ فهي توفّر وقتًا حقيقيًا مع البيانات الكبيرة.
التالي: القوالب (Templates)
هل لاحظت أن std::sort نفسه تعامل مع int وstring وبنية Person الخاصة بك، وأن greater<int>() كان بإمكانه أن يصبح greater<string>() بالسهولة نفسها؟ هذه العمومية ليست سحرًا؛ إنها القوالب (templates)، الآلية التي تتيح لقطعة واحدة من الشيفرة أن تعمل مع أي نوع يضعه المستدعي. في الصفحة التالية سنرى كيف تكتب دوالّك وأصنافك المعتمدة على القوالب، بحيث تصبح شيفرتك مستقلة عن النوع تمامًا كالخوارزميات التي كنت تستخدمها.
الأسئلة الشائعة
كيف تفرز متجهًا في C++؟
ضمّن <algorithm> واستدعِ sort(v.begin(), v.end()). هذا يفرز العناصر في مكانها بترتيب تصاعدي باستخدام operator<. لفرز مصفوفة، مرّر arr وarr + n (أو begin(arr) / end(arr)).
كيف تفرز بترتيب تنازلي في C++؟
مرّر مقارنًا يُعيد a > b: sort(v.begin(), v.end(), [](int a, int b){ return a > b; });. يمكنك أيضًا استخدام المقارن الجاهز sort(v.begin(), v.end(), greater<int>()); من <functional>.
لماذا يتسبب المقارن الخاص بي في انهيار std::sort في C++؟
يجب أن يكون المقارن الخاص بك ترتيبًا ضعيفًا صارمًا (strict weak ordering): عليه أن يُعيد false عندما يكون الوسيطان متساويين. استخدام <= أو >= (اللذين يُعيدان true للعناصر المتساوية) يخالف هذه القاعدة وهو سلوك غير معرَّف؛ إذ قد يقرأ std::sort خارج الحدود وينهار. قارن دائمًا باستخدام < أو >.