انتقل إلى المحتوى
Topos Uranos

ملخص

يمكن للدالة أن تستدعي نفسها: الاستدعاء الذاتي يحلّ مسألة بردّها إلى نسخة أصغر من المسألة نفسها. ينقل هذا الدرس إلى C++ تعريفات تراجعية من الرياضيات (المضروب، والقوة، ومجموع الأرقام)، لكلٍّ منها حالته الأساسية وحالته التراجعية؛ ويبيّن أن الاستقراء يضمن صحتها وأن مقياسًا متناقصًا يضمن توقّفها؛ ويتتبّع في مكدس الاستدعاءات الأطر التي تتراكم ثم تُزال، ويشرح متى ينفد المكدس. ثم يتناول حدّ نوع النتيجة، الذي يفيض قبل المكدس بكثير؛ ويقارن النسخة التراجعية بالنسخة التكرارية لفيبوناتشي بعدّ الاستدعاءات، ويختم بمعيار للاختيار بين الاستدعاء الذاتي والحلقة.

أهداف التعلم

  1. كتابة دالة تراجعية، بحالتها الأساسية وحالتها التراجعية، انطلاقًا من تعريف تراجعي.
  2. تعليل صحة دالة تراجعية وتوقّفها بالاستقراء على حجم المسألة.
  3. تتبّع تنفيذ دالة تراجعية في مكدس الاستدعاءات وشرح فيضان المكدس.
  4. مقارنة كلفة النسخة التراجعية والنسخة التكرارية لفيبوناتشي بعدّ استدعاءاتهما وعملياتهما.

التعريفات التراجعية

المضروب

مضروب العدد الطبيعي nn هو جداء الأعداد الطبيعية من 1 إلى nn: n!=1⋅2⋯nn! = 1 \cdot 2 \cdots n، مع الاصطلاح 0!=10! = 1. والنقاط في هذه الصيغة تخفي حلقة، وقد علّم الدرس الحلقات كتابتها. غير أن التعريف نفسه يقبل صورة أخرى لا تذكر أي حلقة: مضروب nn يساوي nn مضروبًا في مضروب n−1n - 1، ومضروب 0 يساوي 1.

0!=1,n!=n⋅(n−1)!(n≥1).0! = 1, \qquad n! = n \cdot (n - 1)! \quad (n \geq 1).

هذا تعريف تراجعي: يعرّف مضروب nn بواسطة مضروب عدد أصغر. وليس دائريًا، لأن كل تطبيق للقاعدة الثانية ينزل درجة واحدة، والسلّم ينتهي عند الأولى: 3!=3⋅2!=3⋅2⋅1!=3⋅2⋅1⋅0!=63! = 3 \cdot 2! = 3 \cdot 2 \cdot 1! = 3 \cdot 2 \cdot 1 \cdot 0! = 6. وللقاعدتين اسمان. الأولى هي الحالة الأساسية، التي تعطي النتيجة دون الإحالة إلى أي شيء آخر؛ والثانية هي الحالة التراجعية، التي تردّ المسألة إلى نسخة أصغر من المسألة نفسها.

تسمح C++ للدالة بأن تستدعي نفسها، وبذلك يُنقل التعريف حرفًا بحرف تقريبًا.

مثالالمضروب التراجعي
برنامج بلغة C++
#include <print>​// مضروب n.// الشرط المسبق: 0 <= n <= 12 (يُعلَّل الحدّ لاحقًا).// الشرط اللاحق: تُعيد n! = 1 · 2 · ... · n، مع 0! = 1.int factorial(int n){    if (n == 0) {        return 1;                   // الحالة الأساسية    }    return n * factorial(n - 1);    // الحالة التراجعية}​int main(){    for (int n{0}; n <= 6; ++n) {        std::println("{}! = {}", n, factorial(n));    }}
المخرجات
0! = 11! = 12! = 23! = 64! = 245! = 1206! = 720

للدالة صورة التعريف بالضبط: if يعالج الحالة الأساسية وreturn يطبّق الحالة التراجعية. والاستدعاء factorial(n - 1) لا يختلف عن أي استدعاء آخر من الدرس الدوال: تُحسب قيمته الممرَّرة، ويُنشأ إطار جديد له n خاص به، وعند انتهائه تُضرب القيمة المُعادة في n الخاص بمن استدعى.

العقد ليس للزينة: مع n=−1n = -1 تستدعي الحالة التراجعية الدالة بـ −2-2، ثم بـ −3-3، ولا تبلغ 0 أبدًا؛ و13!13! لا يتسع له int. كثير من النصوص التمهيدية تكتب هذا المضروب بالحالة الأساسية if (n == 0 || n == 1)، وهي زائدة لكنها غير ضارة، دون أن تقول شيئًا عن الأعداد السالبة ولا عن حجم النتيجة: فلا يتوقف كودها مع n<0n < 0، ويعطي نتيجة لا معنى لها ابتداءً من 13!13!. وسيُفحص كلا العيبين في القسم الخاص به.

الحالة الأساسية والحالة التراجعية

لكل دالة تراجعية حالة أساسية واحدة أو أكثر، تُحلّ مباشرة، وحالة تراجعية واحدة أو أكثر، تستدعي الدالة نفسها بقيمة ممرَّرة أصغر بمعنى دقيق ما، وتركّب ما تُعيده.

القوة xnx^n، حيث nn عدد طبيعي، تُعرَّف بـ x0=1x^0 = 1 وxn=x⋅xn−1x^n = x \cdot x^{n - 1}: والحجم هنا هو الأس. ومجموع أرقام العدد الطبيعي nn (مجموع أرقام 2026 هو 2+0+2+6=102 + 0 + 2 + 6 = 10) يُعرَّف هكذا: إذا كان n<10n < 10، فالمجموع هو nn، لأن له رقمًا واحدًا؛ وإلا فهو مجموع أرقام ⌊n/10⌋\lfloor n / 10 \rfloor، أي nn بلا رقمه الأخير، مضافًا إليه ذلك الرقم الأخير، n mod 10n \bmod 10. والحجم هنا هو عدد الأرقام.

مثالالقوة ومجموع الأرقام
برنامج بلغة C++
#include <print>​// x مرفوعًا إلى القوة n.// الشرط المسبق: n >= 0.// الشرط اللاحق: تُعيد x^n، مع x^0 = 1.double power(double x, int n){    if (n == 0) {        return 1.0;    }    return x * power(x, n - 1);}​// مجموع الأرقام العشرية لـ n.// الشرط المسبق: n >= 0.// الشرط اللاحق: تُعيد مجموع أرقام n.int digitSum(int n){    if (n < 10) {        return n;    }    return digitSum(n / 10) + n % 10;}​int main(){    std::println("2^10 = {}", power(2.0, 10));    std::println("1.5^3 = {}", power(1.5, 3));    std::println("مجموع أرقام 2026: {}", digitSum(2026));}
المخرجات
2^10 = 10241.5^3 = 3.375مجموع أرقام 2026: 10

في digitSum، تحذف القسمة الصحيحة n / 10 الرقم الأخير، ويستخرجه n % 10، كما في تغيير الأساس في الدرس الخوارزميات العددية. ومع n=2026n = 2026 تطلب الدالة مجموع أرقام 202، الذي يطلب مجموع أرقام 20، الذي يطلب مجموع أرقام 2؛ وهذه حالة أساسية تُعيد 2، وتُركَّب النتائج عند العودة: 2+0=22 + 0 = 2، 2+2=42 + 2 = 4، 4+6=104 + 6 = 10.

فكتابة دالة تراجعية تقوم إذن على الإجابة عن سؤالين: أي الحالات تُحلّ بلا عون، وكيف يُحصل على حل حالة من حل حالة أصغر. فإذا كانت الإجابتان صحيحتين كانت الدالة صحيحة، والقسم الآتي يبرهن ذلك.