الترتيب بالعد
الترتيب بالعد (counting sort) يرتّب عبر عدّ مرات ظهور العناصر، ويُطبَّق عادةً على مصفوفات الأعداد الصحيحة.
تنفيذ بسيط
لنبدأ بمثال بسيط. بمعطى مصفوفة nums طولها ، وجميع عناصرها «أعداد صحيحة غير سالبة»، يوضح الشكل أدناه التدفق الإجمالي للترتيب بالعد.
- اجتز المصفوفة لإيجاد أكبر عدد، ورمزه ، ثم أنشئ مصفوفة مساعدة
counterطولها . - استخدم
counterلعدّ عدد مرات ظهور كل عدد فيnums، حيث يخزّنcounter[num]عدد مرات ظهورnum. وهذا بسيط: اجتزnums(ورمز العدد الحالي بـnum) وزدcounter[num]بمقدار في كل مرة. - وبما أن فهارس
counterمرتّبة ترتيباً طبيعياً، فإن الأعداد تكون فعلياً مرتّبة بالفعل. بعد ذلك، اجتزcounterواكتب الأعداد مرة أخرى فيnumsبترتيب تصاعدي وفقاً لأعداد ظهورها.

والشيفرة كما يلي:
Go
/* الترتيب بالعد */
// تنفيذ بسيط، لا يصلح لترتيب الكائنات
func countingSortNaive(nums []int) {
// 1. عدّ أكبر عنصر m في المصفوفة
m := 0
for _, num := range nums {
if num > m {
m = num
}
}
// 2. عدّ مرات ظهور كل عدد
// يمثّل counter[num] عدد مرات ظهور num
counter := make([]int, m+1)
for _, num := range nums {
counter[num]++
}
// 3. اجتز counter، واكتب كل عنصر مرة أخرى في المصفوفة الأصلية nums
for i, num := 0, 0; num < m+1; num++ {
for j := 0; j < counter[num]; j++ {
nums[i] = num
i++
}
}
}
TypeScript
/* الترتيب بالعد */
// تنفيذ بسيط، لا يصلح لترتيب الكائنات
function countingSortNaive(nums: number[]): void {
// 1. عدّ أكبر عنصر m في المصفوفة
let m: number = Math.max(...nums);
// 2. عدّ مرات ظهور كل عدد
// يمثّل counter[num] عدد مرات ظهور num
const counter: number[] = new Array<number>(m + 1).fill(0);
for (const num of nums) {
counter[num]++;
}
// 3. اجتز counter، واكتب كل عنصر مرة أخرى في المصفوفة الأصلية nums
let i = 0;
for (let num = 0; num < m + 1; num++) {
for (let j = 0; j < counter[num]; j++, i++) {
nums[i] = num;
}
}
}
الصلة بين الترتيب بالعد وترتيب الدلاء
من منظور ترتيب الدلاء، يمكن النظر إلى كل فهرس في مصفوفة العدّ counter باعتباره دلو، ويمكن اعتبار عملية العدّ توزيعاً للعناصر على الدلاء المقابلة لها. وفي جوهر الأمر، الترتيب بالعد حالة خاصة من ترتيب الدلاء لبيانات الأعداد الصحيحة.
تنفيذ كامل
ربما لاحظ القراء المتنبّهون أن عملية الإدخال إذا كانت تتكوّن من كائنات، فإن الخطوة 3. أعلاه لم تعد صالحة. لنفترض أن الإدخال يتكوّن من كائنات منتجات ونريد ترتيبها حسب السعر (وهو متغير عضو في الصنف)؛ فإن الخوارزمية أعلاه لن تنتج إلا ترتيب الأسعار نفسها.
فكيف يمكننا الحصول على ترتيب البيانات الأصلية؟ نحسب أولاً المجاميع البادئة (prefix sums) لـcounter. وكما يوحي الاسم، فإن المجموع البادئ عند الفهرس ، أي prefix[i]، يساوي مجموع العناصر من الفهرس 0 حتى i:
وللمجموع البادئ تفسير واضح: prefix[num] - 1 يعطي فهرس آخر ظهور للعنصر num في المصفوفة الناتجة res. وهذه المعلومة بالغة الأهمية لأنها تخبرنا بالمكان الذي ينبغي أن يوضع فيه كل عنصر في المصفوفة الناتجة. بعد ذلك، نجتاز المصفوفة الأصلية nums بترتيب عكسي، ولكل عنصر num ننفّذ الخطوتين التاليتين.
- ضع
numعند الفهرسprefix[num] - 1في المصفوفةres. - قلّل المجموع البادئ
prefix[num]بمقدار للحصول على فهرس الموضع التالي لـnum.
وبعد اكتمال الاجتياز، تحتوي المصفوفة res على النتيجة المرتّبة، وأخيراً يُستخدم res لاستبدال المصفوفة الأصلية nums. ويوضح الشكل أدناه تدفق الترتيب بالعد الكامل.
ويُعرض تنفيذ الترتيب بالعد أدناه:
Go
/* الترتيب بالعد */
// تنفيذ كامل، يمكنه ترتيب الكائنات وهو ترتيب مستقر
func countingSort(nums []int) {
// 1. عدّ أكبر عنصر m في المصفوفة
m := 0
for _, num := range nums {
if num > m {
m = num
}
}
// 2. عدّ مرات ظهور كل عدد
// يمثّل counter[num] عدد مرات ظهور num
counter := make([]int, m+1)
for _, num := range nums {
counter[num]++
}
// 3. احسب المجموع البادئ لـcounter، وحوّل «عدد مرات الظهور» إلى «فهرس الذيل»
// counter[num]-1 هو آخر فهرس يظهر فيه num في res
for i := 0; i < m; i++ {
counter[i+1] += counter[i]
}
// 4. اجتز nums بترتيب عكسي، وضع كل عنصر في المصفوفة الناتجة res
// هيّئ المصفوفة res لتسجيل النتائج
n := len(nums)
res := make([]int, n)
for i := n - 1; i >= 0; i-- {
num := nums[i]
// ضع num عند الفهرس المقابل
res[counter[num]-1] = num
// قلّل المجموع البادئ بمقدار 1 للحصول على الفهرس التالي لوضع num
counter[num]--
}
// استخدم المصفوفة الناتجة res لاستبدال المصفوفة الأصلية nums
copy(nums, res)
}
TypeScript
/* الترتيب بالعد */
// تنفيذ كامل، يمكنه ترتيب الكائنات وهو ترتيب مستقر
function countingSort(nums: number[]): void {
// 1. عدّ أكبر عنصر m في المصفوفة
let m: number = Math.max(...nums);
// 2. عدّ مرات ظهور كل عدد
// يمثّل counter[num] عدد مرات ظهور num
const counter: number[] = new Array<number>(m + 1).fill(0);
for (const num of nums) {
counter[num]++;
}
// 3. احسب المجموع البادئ لـcounter، وحوّل «عدد مرات الظهور» إلى «فهرس الذيل»
// counter[num]-1 هو آخر فهرس يظهر فيه num في res
for (let i = 0; i < m; i++) {
counter[i + 1] += counter[i];
}
// 4. اجتز nums بترتيب عكسي، وضع كل عنصر في المصفوفة الناتجة res
// هيّئ المصفوفة res لتسجيل النتائج
const n = nums.length;
const res: number[] = new Array<number>(n);
for (let i = n - 1; i >= 0; i--) {
const num = nums[i];
res[counter[num] - 1] = num; // ضع num عند الفهرس المقابل
counter[num]--; // قلّل المجموع البادئ بمقدار 1 للحصول على الفهرس التالي لوضع num
}
// استخدم المصفوفة الناتجة res لاستبدال المصفوفة الأصلية nums
for (let i = 0; i < n; i++) {
nums[i] = res[i];
}
}
خصائص الخوارزمية
- التعقيد الزمني ، والترتيب بالعد غير تكيّفي (non-adaptive): يستغرق اجتياز
numsوcounterزمناً خطياً. وبشكل عام، عندما ، يقترب التعقيد الزمني من . - التعقيد المكاني ، وترتيب ليس في المكان (non-in-place): يستخدم المصفوفتين
resوcounterبطولين و على التوالي. - ترتيب مستقر: بما أن العناصر تُملأ في
resبترتيب «من اليمين إلى اليسار»، فإن اجتيازnumsبترتيب عكسي يمكن أن يمنع تغيّر المواضع النسبية للعناصر المتساوية، وبذلك يتحقق ترتيب مستقر. وفي الواقع، يمكن أن يؤدي اجتيازnumsبالترتيب الأمامي أيضاً إلى نتائج ترتيب صحيحة، لكن النتيجة ستكون غير مستقرة.
القيود
في هذه المرحلة، قد تظن أن الترتيب بالعد بالغ البراعة، لأنه يحقق ترتيباً فعّالاً بمجرد عدّ مرات الظهور. غير أن المتطلبات المسبقة لاستخدام الترتيب بالعد مقيّدة إلى حد كبير.
لا ينطبق الترتيب بالعد إلا على الأعداد الصحيحة غير السالبة. ولتطبيقه على أنواع أخرى من البيانات، عليك التأكد من إمكانية تحويلها إلى أعداد صحيحة غير سالبة دون تغيير الترتيب النسبي للعناصر. فمثلاً، بالنسبة إلى مصفوفة أعداد صحيحة تحتوي على أعداد سالبة، يمكنك أولاً إضافة ثابت إلى كل عدد لإزاحتها إلى النطاق غير السالب، ثم إزاحتها مرة أخرى بعد الترتيب.
يناسب الترتيب بالعد الحالات التي تحتوي على عناصر كثيرة لكن نطاق قيمها صغير. فمثلاً، في السيناريو أعلاه، لا يمكن أن يكون كبيراً جداً؛ وإلا فسيستهلك مساحة كبيرة جداً. وعندما ، يستغرق الترتيب بالعد زمن ، وقد يكون أبطأ من خوارزميات ترتيب بتعقيد زمني .