هياكل البيانات والخوارزميات / البنى المرتبة والاستدلال على الرسوم / الدرس 12
العودية والتقسيم والتغلب والاختيار
أثبت تناقص المسائل الفرعية، وميّز عمق الاستدعاء من العمل الكلي، ولا تحسب من الترتيب إلا ما تحتاجه المهمة.
في هذه الصفحة
أهداف الدرس
- يحتاج الاستدعاء العودي إلى مسألة صحيحة أصغر
- قد تحوي شجرة استدعاءات ضحلة عملا خطيا
- لا يحتاج اختيار الرتبة إلى ناتج مرتب كاملا
يحتاج الاستدعاء العودي إلى مسألة صحيحة أصغر
تفوض العودية مسألة فرعية إلى العقد نفسه. تحتاج الصحة إلى حالة أساس، ومقياس يتناقص حصرا، وقاعدة تجمع النتائج الفرعية الصحيحة لتنتج النتيجة الموعودة. ولا يُعد القول إن الدالة تستدعي نفسها برهانا على الانتهاء. يجد البرنامج الأول القيمة الكبرى في مجال نصف مفتوح غير فارغ دون نسخ الشرائح. يصغر كل مجال ابن حتى يبقى عنصر واحد، وتعطي مقارنة القيمتين الكبريين المعادتين القيمة الكبرى للمجال الأب. ويُرفض المدخل الفارغ لأن الكبرى غير معرّفة في هذا العقد العددي، بدلا من اختراع صفر يخطئ عندما تكون البيانات سالبة كلها.
def divide_max(values):
if not values:
raise ValueError("Maximum requires nonempty input")
def solve(left, right):
if right - left == 1:
return values[left]
middle = left + (right - left) // 2
a = solve(left, middle)
b = solve(middle, right)
return a if a >= b else b
return solve(0, len(values))
print(divide_max([-8, -2, -5, -9]))
print(divide_max([7]))
-2
7
يتقلص المجال دون تخصيص قوائم فرعية. ويوضح المدخل السالب كله لماذا تكون قيمة أساس صفرية اعتباطية خاطئة.
قد تحوي شجرة استدعاءات ضحلة عملا خطيا
لمثال القيمة الكبرى عمق استدعاء لوغاريتمي لأن المجالات متوازنة، لكنه يزور كل عنصر ويجمع n-1 زوجا؛ لذلك يكون العمل الكلي خطيا. عمق الاستدعاء وعدد الاستدعاءات كميتان مختلفتان. تستخدم إطارات الاستدعاء المساعدة O(log n) مساحة، دون المدخل. أما الدالة العودية البسيطة التي تزيل عنصرا واحدا فقط في كل استدعاء فلها عمق خطي، حتى لو بدت كل خطوة بسيطة. لا تَعِد Python بإزالة الاستدعاء الطرفي؛ ولذلك قد ينشئ استبدال حلقة بعودية طرفية اعتمادا على حد العودية. لا تغيّر حد العودية للعملية بوصفه إصلاحا روتينيا للخوارزمية.
لا يحتاج اختيار الرتبة إلى ناتج مرتب كاملا
إذا طلبت المهمة أصغر قيمة ذات الرتبة k بحسب فهرسة تبدأ من الصفر، فلا حاجة إلى ترتيب كل القيم الأخرى. تقسم Quickselect حول محور، وتتابع فقط في المنطقة الحاوية للرتبة k. يستخدم البرنامج الثاني ثلاث مناطق، فتشكل القيم المساوية للمحور كتلة مساواة، وتُحسم فورا رتبة تقع داخلها. قد يكون اختيار أول عنصر محورا بطريقة محددة سيئ التوازن جدا، فتبلغ كلفة الحالة الأسوأ التربيع؛ ولا تدّعي هذه الشيفرة عمدا زمنا خطيا متوقعا. تحتاج المحاور العشوائية أو المختارة بعناية إلى افتراضاتها وتحليلها الخاصين، ولا يوازن التقسيم الثلاثي تلقائيا مدخلات مرتبة ذات قيم مختلفة.
def select_k(values, k):
if not 0 <= k < len(values):
raise ValueError("k is outside the input")
work = list(values)
while True:
pivot = work[0]
lower = [value for value in work if value < pivot]
equal = [value for value in work if value == pivot]
higher = [value for value in work if value > pivot]
if k < len(lower):
work = lower
elif k < len(lower) + len(equal):
return pivot
else:
k -= len(lower) + len(equal)
work = higher
print(select_k([7, 2, 2, 9, 4], 2))
print(select_k([3, 3, 3], 1))
4
3
تنسخ الشيفرة القوائم وتقسمها، مع حفظ مدخل المستدعي. وهي خوارزمية اختيار تعليمية واضحة خارج الموضع، وليست تنفيذا محسنا يعمل في موضعه.
حلل كلف التقسيم والمناطق المستبعدة
يمسح كل تكرار للاختيار قائمة العمل الحالية عدة مرات، ويظل ذلك خطيا في حجمها الحالي مع افتراض مقارنات محدودة الكلفة. ينتج التقدم المتوازن مجموعا هندسيا للأحجام؛ أما إزالة محور مختلف واحد فقط في كل مرة فتنتج مجموعا تربيعيا. تضمن كتلة المساواة التقدم للقيم العادية المحدودة ذات الترتيب الكلي، لأن المحور نفسه ينتمي إليها. وقد تبطل NaN أو المقارنات غير المتسقة حجة التقسيم؛ لذلك يجب أن يستبعدها المجال المقبول أو يحدد معالجة خاصة لها. أقصى تخزين للقوائم خطي في حجم المدخل الحالي، لكن مجموع التخصيص والنسخ قد يكون أكبر كثيرا.
اختر خوارزمية أبسط عندما يناسب عقدها المهمة
لمدخلات قليلة صغيرة، يكون sorted(values)[k] غالبا أوضح تنفيذ للمراجعة، رغم أنه يحسب ترتيبا أكثر من المطلوب. ولإنتاج أكبر k قيم أثناء التدفق، تقدم كومة محدودة عقدا مختلفا للذاكرة والتحديث. ولإيجاد قيمة صغرى واحدة، يكون المسح الخطي أبسط من آليات الفرز أو الاختيار. تهم الأفضلية النظرية بالنسبة إلى المهمة والتمثيل ومخاطر الفشل فقط. لا تستبدل شيفرة تطبيق واضحة بتقسيم مخصص هش لمجرد اقتباس حد متوسط موات. أثبت هل يحتاج نمط العمل فعلا إلى الخوارزمية الأقوى، وهل يوافق مجالها العددي المقبول البيانات الفعلية.
اختبر مقاييس التناقص وحفظ الرتبة
قارن divide_max بـ max على جميع المصفوفات الصغيرة غير الفارغة، بما فيها البيانات السالبة كلها. وقارن كل رتبة صالحة في select_k بـ sorted لمصفوفات كثيرة التكرار، وقيم مرتبة بالفعل، وترتيب معكوس، وعنصر منفرد. افحص رفض الرتب غير الصالحة وبقاء المدخل دون تغيير. ومن خصائص الاختيار المفيدة ألا يزيد عدد القيم الأصغر حصرا من النتيجة على k، وأن يزيد عدد القيم التي لا تتجاوز النتيجة على k. تتعامل متراجحتا الرتبة هاتان مع التكرار دون طلب هوية فريدة للنسخة المختارة. تكمل الاختبارات حجتي التقسيم والانتهاء، ولا تستبدلهما.
الأخطاء الشائعة وإصلاحها
إعادة دالة كبرى عودية صفرا عندما يكون المدخل سالبا كله
- لماذا يحدث؟
- لا تنتمي القيمة المحايدة المختلقة إلى عقد إيجاد الكبرى.
- كيف تصلحه؟
- استخدم عنصرا فعليا في حالة الأساس، وارفض المدخل الفارغ.
- كيف تتجنب تكراره؟
- اختبر عنصرا سالبا منفردا ومصفوفة سالبة بالكامل.
وصف Quickselect بأنها خطية في الحالة الأسوأ مع أي محور
- لماذا يحدث؟
- أُغفلت افتراضات توازن اختيار المحور.
- كيف تصلحه؟
- اذكر الحالة الأسوأ الفعلية للاختيار المحدد، أو نفذ استراتيجية مبرهنة.
- كيف تتجنب تكراره؟
- أدرج بيانات مرتبة ذات قيم مختلفة في التحليل.
عدم تقلص الاختيار مع كثرة القيم المتكررة
- لماذا يحدث؟
- لم تُخصص للقيم المساوية للمحور منطقة تضمن التقدم.
- كيف تصلحه؟
- استخدم كتلة مساواة صحيحة، وعدّل الرتبة بعدد العناصر المستبعدة.
- كيف تتجنب تكراره؟
- اختبر مدخلات متساوية القيم كلها عند كل رتبة صالحة.
اختبر فهمك
1. لماذا تكون divide_max خطية رغم عمقها اللوغاريتمي؟
اعرض الحل المشروح
يقيس العمق أطول سلسلة استدعاءات نشطة فقط. وعبر شجرة العودية كاملة، تُزار كل عناصر المدخل وتُنفذ n-1 مقارنة لجمع النتائج. يعطي جمع العمل في كل العقد كلفة خطية، بينما لا ينشط معا سوى عدد لوغاريتمي من الإطارات عندما تكون التقسيمات متوازنة.
2. ماذا يحدث عندما تقع الرتبة k داخل كتلة المساواة مع المحور؟
اعرض الحل المشروح
تكون قيمة المحور هي الإجابة، لأن جميع القيم الأصغر تسبق تلك الكتلة، ولأن كل نسخة فيها تحمل القيمة نفسها. يمنع الرجوع الفوري تكرار تقسيم القيم المتساوية. ولا تحتاج النسخة المختارة إلى هوية فريدة عندما يطلب العقد رتبة قيمة فقط.
أهم ما تعلمته
- يحتاج الاستدعاء العودي إلى مسألة صحيحة أصغر
- اختبر مقاييس التناقص وحفظ الرتبة