مرحباً بالخوارزميات › الترتيب 13 / 19 السابق التالي

الترتيب الجذري

قدّم القسم السابق الترتيب بالعد، وهو مناسب عندما يكون عدد العناصر nn كبيراً بينما يكون نطاق القيم mm صغيراً. لنفترض أننا نحتاج إلى ترتيب n=106n = 10^6 من أرقام الطلاب، حيث يتكوّن كل رقم منها من 8 خانات. عندئذ يكون نطاق القيم m=108m = 10^8 كبيراً جداً. فاستخدام الترتيب بالعد سيتطلب كمية كبيرة من الذاكرة، بينما يتجنّب الترتيب الجذري هذه المشكلة.

الترتيب الجذري (radix sort) قائم على الفكرة الجوهرية نفسها التي يقوم عليها الترتيب بالعد: فهو أيضاً يرتّب عبر عدّ مرات الظهور. وبناءً على ذلك، يستغل الترتيب الجذري علاقة الخانات بعضها ببعض، ويرتّب خانة واحدة في كل مرة للحصول على النتيجة النهائية.

سير الخوارزمية

باتخاذ بيانات أرقام الطلاب مثالاً، لنفترض أن أقل خانة هي الخانة 11 وأعلى خانة هي الخانة 88. ويوضح الشكل أدناه سير الترتيب الجذري.

  1. هيّئ الخانة k=1k = 1.
  2. نفّذ «الترتيب بالعد» على الخانة kk من أرقام الطلاب. وبعد اكتماله، ستكون البيانات مرتّبة من الأصغر إلى الأكبر وفقاً للخانة kk.
  3. زد kk بمقدار 11، ثم ارجع إلى الخطوة 2. وواصل التكرار حتى تُرتَّب جميع الخانات، وعندها تنتهي العملية.

سير خوارزمية الترتيب الجذري

لننظر الآن إلى الشيفرة. بالنسبة إلى عدد xx في الأساس dd، يمكن الحصول على خانته kk ورمزها xkx_k بالصيغة التالية:

xk=⌊xdk−1⌋ mod d x_k = \lfloor\frac{x}{d^{k-1}}\rfloor \bmod d

هنا، يشير الرمز ⌊a⌋\lfloor a \rfloor إلى تقريب العدد العشري العائم aa نحو الأسفل، ويشير  mod  d\bmod \: d إلى أخذ الباقي بالقياس dd. وبالنسبة إلى بيانات أرقام الطلاب، d=10d = 10 وk∈[1,8]k \in [1, 8].

وبالإضافة إلى ذلك، نحتاج إلى تعديل شيفرة الترتيب بالعد تعديلاً طفيفاً لتجعلها ترتّب استناداً إلى الخانة kk من العدد:

Go

/* الترتيب الجذري */
func radixSort(nums []int) {
	// احصل على أكبر عنصر في المصفوفة، لتحديد الحد الأقصى لعدد الخانات
	max := math.MinInt
	for _, num := range nums {
		if num > max {
			max = num
		}
	}
	// اجتز الخانات من الأقل إلى الأعلى
	for exp := 1; max >= exp; exp *= 10 {
		// نفّذ الترتيب بالعد على الخانة k من عناصر المصفوفة
		// k = 1 -> exp = 1
		// k = 2 -> exp = 10
		// أي exp = 10^(k-1)
		countingSortDigit(nums, exp)
	}
}

TypeScript

/* الترتيب الجذري */
function radixSort(nums: number[]): void {
    // احصل على أكبر عنصر في المصفوفة، لتحديد الحد الأقصى لعدد الخانات
    let m: number = Math.max(... nums);
    // اجتز الخانات من الأقل إلى الأعلى
    for (let exp = 1; exp <= m; exp *= 10) {
        // نفّذ الترتيب بالعد على الخانة k من عناصر المصفوفة
        // k = 1 -> exp = 1
        // k = 2 -> exp = 10
        // أي exp = 10^(k-1)
        countingSortDigit(nums, exp);
    }
}

لماذا نبدأ الترتيب من أقل خانة؟

في جولات الترتيب المتعاقبة، تتجاوز الجولة اللاحقة نتيجة الجولة السابقة. فمثلاً، إذا أسفرت الجولة الأولى عن a<ba < b لكن أسفرت الجولة الثانية عن a>ba > b، فإن نتيجة الجولة الثانية هي التي تسود. ولأن الخانات العليا ذات أولوية أعلى من الخانات الدنيا، ينبغي أن نرتّب الخانات الدنيا أولاً ثم الخانات العليا.

خصائص الخوارزمية

مقارنةً بالترتيب بالعد، يناسب الترتيب الجذري نطاقات قيم أكبر، لكنه لا يصلح إلا عندما يمكن تمثيل البيانات بعدد ثابت من الخانات ولا يكون عدد الخانات كبيراً جداً. فمثلاً، لا تناسب الأعداد العشرية العائمة الترتيب الجذري لأن عدد الخانات kk قد يكون كبيراً جداً، مما قد يؤدي إلى تعقيد زمني O(nk)≫O(n2)O(nk) \gg O(n^2).

  • التعقيد الزمني O(nk)O(nk)، وترتيب غير تكيّفي (non-adaptive): ليكن عدد العناصر nn، ولتُمثَّل القيم في الأساس dd، وليكن الحد الأقصى لعدد الخانات kk. يستغرق الترتيب بالعد لخانة واحدة زمن O(n+d)O(n + d)، لذا يستغرق ترتيب الخانات kk كلها زمن O((n+d)k)O((n + d)k). ومن الناحية العملية، يكون dd وkk صغيرين نسبياً عادةً، لذا يقترب التعقيد الزمني الإجمالي من O(n)O(n).
  • التعقيد المكاني O(n+d)O(n + d)، وترتيب ليس في المكان (non-in-place): مثل الترتيب بالعد، يحتاج الترتيب الجذري إلى المصفوفتين المساعدتين res وcounter بطولين nn وdd.
  • ترتيب مستقر: عندما يكون الترتيب بالعد مستقراً، يكون الترتيب الجذري مستقراً أيضاً؛ وعندما يكون الترتيب بالعد غير مستقر، لا يستطيع الترتيب الجذري ضمان نتائج ترتيب صحيحة.