Menu
Coddy logo textTech
flag Ar iconالعربيةdown icon

التعاود (Recursion)

آخر تحديث

التعاود هو أن تستدعي الدالة نفسها على نسخة أصغر من المسألة نفسها، إلى أن تصل إلى حالة صغيرة بما يكفي للإجابة عنها مباشرة. تلك الحالة القابلة للإجابة مباشرة هي الحالة الأساسية، وكل دالة تعاودية تحتاج إلى واحدة: تستمر fib(n) في الانقسام إلى fib(n - 1) و fib(n - 2) حتى تبلغ fib(1) أو fib(0)، وهما تعيدان قيمتيهما مباشرة. يشغّل التصور أعلاه هذا بالضبط: اضغط على تشغيل وشاهد الاستدعاءات تتفرع في شجرة، وتبلغ الحالات الأساسية عند الأوراق، ثم تعيد قيمها إلى الأعلى مع الدمج في كل مستوى.

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

تعقيد الوقت والمساحة

لفيبوناتشي التعاودي الساذج المعروض أعلاه، وللإصلاحين المعياريين:

الأسلوبالوقتالمساحةملاحظات
التعاود الساذجO(2^n)O(n)تتضاعف شجرة الاستدعاءات في كل مستوى، والمساحة هي أعمق مكدس لا الشجرة كلها.
مع التحفيظ (memoization)O(n)O(n)تُحسب كل fib(k) مرة واحدة وتُخزَّن، فتتحول الأشجار الفرعية المكررة إلى عمليات بحث.
حلقة تكراريةO(n)O(1)متغيران متعاقبان يحلان محل المكدس تمامًا.
أي تعاود بشكل عامعدد الاستدعاءات × العمل في كل استدعاءO(max depth)يحمل المكدس إطارًا واحدًا لكل استدعاء بدأ ولم يعد بعد.

خطوة بخطوة

الخطوةما الذي يحدث
1يوضع الاستدعاء الأول fib(n) على مكدس الاستدعاءات.
2يحتاج إلى fib(n - 1)، فيوضع ذلك الاستدعاء على المكدس أيضًا، والأب ينتظر.
3تستمر الاستدعاءات في التداخل حتى يسأل أحدها عن n <= 1: تجيب الحالة الأساسية فورًا دون أي استدعاء أعمق.
4تعود قيمة الحالة الأساسية إلى أبيها، فيصبح بإمكانه بدء استدعائه الثاني fib(n - 2).
5عندما يعود كلا الابنين، يجمعهما الأب ويعود هو أيضًا، ويغادر إطاره المكدس.
6تتكرر العودة صعودًا في الشجرة حتى يخرج إطار الاستدعاء الأول حاملًا الجواب النهائي ويصبح المكدس فارغًا.

مثال محلول

حساب fib(4) بترتيب الاستدعاءات الدقيق، كما تعرضه الرسوم المتحركة:

الاستدعاءالمكدس في تلك اللحظةما يعيده
fib(4)fib(4)ينتظر الأبناء
fib(3)fib(4) > fib(3)ينتظر الأبناء
fib(2)fib(4) > fib(3) > fib(2)ينتظر الأبناء
fib(1)fib(4) > fib(3) > fib(2) > fib(1)1 (حالة أساسية)
fib(0)fib(4) > fib(3) > fib(2) > fib(0)0 (حالة أساسية)
fib(2) يجمعfib(4) > fib(3) > fib(2)1 + 0 = 1
fib(1)fib(4) > fib(3) > fib(1)1 (حالة أساسية)
fib(3) يجمعfib(4) > fib(3)1 + 1 = 2
fib(2) مرة أخرىfib(4) > fib(2)1، أُعيد حسابه من الصفر
fib(4) يجمعfib(4)2 + 1 = 3

متى تستخدم التعاود

استخدمه عندماتجنّبه عندما
تكون المسألة ذاتية التشابه: الأشجار والبنى المتداخلة وفرّق تَسُدتعبّر حلقة بسيطة عن الشيء نفسه دون أطر مكدس
يكون العمق محدودًا ومعقولًا، مثل O(log n) في الترتيب بالدمجيمكن أن يبلغ العمق حجم المدخل على المدخلات الضخمة، فيهدد بفيضان المكدس
يحتاج التراجع (backtracking) إلى المكدس ليتذكر من أين يستأنفتتكرر المسائل الفرعية نفسها وأنت لا تخزّن نتائجها
تكون النسخة التعاودية أوضح للقراءة والتحقق بلا شكتكون داخل حلقة ساخنة يكون فيها عبء الاستدعاء ملموسًا في القياس

كود Recursion

تنفيذ نظيف وقابل للتشغيل لخوارزمية Recursion بلغات Python, JavaScript, Java, C++, C. اختر لغة، وانسخ الكود، أو افتحه محمّلًا مسبقًا في ساحة تجربة Coddy.

كود Recursion بلغة Python

Python
1calls = 02
3def fib(n, depth=0):4    global calls5    calls += 16    # Print the call with its depth so the recursion is visible7    print("  " * depth + f"fib({n})")8    if n <= 1:9        return n10    return fib(n - 1, depth + 1) + fib(n - 2, depth + 1)11
12
13print("fib(5) =", fib(5))14print("calls made:", calls)
شغّل هذا الكود في ساحة تجربة Python

الأسئلة الشائعة حول التعاود

ما هي الحالة الأساسية في التعاود؟
هي المدخل الصغير بما يكفي للإجابة عنه دون استدعاء تعاودي آخر. في fib(n) هي n <= 1، وتعيد n مباشرة. من دون حالة أساسية يمكن بلوغها لا تتوقف الاستدعاءات، ويستمر المكدس في النمو، ويتعطل البرنامج بفيضان المكدس.
ما هو مكدس الاستدعاءات ولماذا يهم؟
تحتفظ بيئة التشغيل بإطار واحد لكل استدعاء بدأ ولم يعد بعد، يحمل وسائطه ومتغيراته المحلية. عمق التعاود يساوي ارتفاع المكدس، لذا فإن تعاودًا يهبط n مستوى يستهلك ذاكرة O(n) حتى لو كان كل استدعاء لا يفعل شيئًا يُذكر. صف البطاقات أسفل الرسوم المتحركة يعرض هذا المكدس بالضبط وهو ينمو ثم ينحسر.
لماذا يستغرق فيبوناتشي التعاودي زمنًا أسيًا؟
لأن المسائل الفرعية نفسها تُحسب مرارًا وتكرارًا: في المثال المحلول أعلاه، تُقيَّم fib(2) مرتين داخل fib(4)، ويتضاعف هذا التكرار تقريبًا في كل مستوى، فينتج O(2^n) استدعاء. أما تخزين كل نتيجة عند حسابها أول مرة، وهو ما يسمى التحفيظ، فيطوي الشجرة إلى O(n).
هل التعاود أفضل من التكرار؟
لا أحدهما أفضل على الإطلاق. يمكن إعادة كتابة أي تعاود على شكل حلقة بمكدس صريح، وأي حلقة على شكل تعاود. يتفوق التعاود في وضوح القراءة للمسائل ذاتية التشابه مثل اجتياز الأشجار و البحث بالعمق أولاً، ويتفوق التكرار في الذاكرة وعبء الاستدعاء في المرور الخطي.
ما الذي يسبب فيضان المكدس في دالة تعاودية؟
إما حالة أساسية مفقودة أو لا يمكن بلوغها فلا تتوقف الاستدعاءات، وإما تعاود صحيح لكن عمقه ببساطة أكبر من حد المكدس في بيئة التشغيل، مثل التعاود مرة لكل عنصر على مدخل من ملايين العناصر. والعلاج هو ضمان بلوغ الحالة الأساسية، أو تقييد العمق، أو التحويل إلى التكرار.
ما الخوارزميات التعاودية بطبيعتها؟
خوارزميات الفرز بفرّق تَسُد مثل الترتيب بالدمج و quicksort، واجتياز الشجرة الثنائية والرسوم البيانية، والبحث الثنائي، وألغاز التراجع مثل مسألة الملكات N، وكل ما يُعرَّف فوق بنية متداخلة مثل JSON أو نظام الملفات.
Coddy programming languages illustration

أتقن الخوارزميات مع Coddy

ابدأ الآن