إحنا اتكلمنا عن الـ Recursion قبل كده، وعرفنا إن أكتر حاجة محتاجين نفكر فيها هي إمتى نوقف الـ function عشان متدخلش في infinite loop.
الـ Recursion مش سحر، هو عبارة عن Stack (رصة فوق بعض). تخيلها زي “رصة أطباق”. كل ما تنادي Function، بتحط طبق جديد فوق الرصة. وعمرك ما هتعرف تشيل الطبق اللي تحت غير لما تخلص الطبق اللي فوق الأول.
الـ Recursion ببساطة هو إن الـ function بتنادي على نفسها بشكل متكرر لحد ما شرط معين يتحقق، وساعتها بتقف. الموضوع ده شبه الـ loop، طول ما الشرط متحقق، الـ loop بتكمل، وبنفس الطريقة، الـ function بتفضل تنادي على نفسها.
عشان نحل مشكلة باستخدام الـ Recursion، لازم شرطين يتحققوا:
- الـ Recursive Form: المشكلة لازم تكون مكتوبة بصيغة تسمح للـ
functionإنها تنادي على نفسها. - الـ Stopping Condition: لازم يكون فيه شرط وقوف (بيسموه
Base Case) عشان نضمن إن الـfunctionهتقف في وقت معين ومتكملش للأبد.
How does Recursion Work?
عشان نفهم الـ Recursion بيشتغل إزاي، لازم نفهم الأول إزاي الـ functions بتنادي على بعضها في البرمجة العادية (Synchronous programming).
لما function معينة (هنسميها F1) بتنادي على function تانية (هنسميها F2)، تنفيذ F1 بيقف مؤقتًا لحد ما F2 تخلص شغلها بالكامل وترجع. بعدها بس F1 بتقدر تكمل تنفيذ.
خلينا نشوف المثال ده عشان الصورة توضح:
using System;
namespace RecursionDemo
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Main Method Started");
fun1(4);
Console.WriteLine("Main Method Ended"); // This will be the last line to execute
Console.ReadKey();
}
static void fun1(int n)
{
Console.WriteLine("Fun1 Started");
fun2(3);
Console.WriteLine("Fun1 Ended");
}
static void fun2(int n)
{
Console.WriteLine("Fun2 Started");
fun3(2);
Console.WriteLine("Fun2 Ended");
}
static void fun3(int n)
{
Console.WriteLine("Fun3 Started");
fun4(1);
Console.WriteLine("Fun3 Ended");
}
static void fun4(int n)
{
Console.WriteLine("Fun4 Started");
Console.WriteLine("Fun4 Ended");
}
}
}الـ Output بتاع الكود ده هيكون كالآتي:
Main Method Started
Fun1 Started
Fun2 Started
Fun3 Started
Fun4 Started
Fun4 Ended
Fun3 Ended
Fun2 Ended
Fun1 Ended
Main Method Ended
زي ما إنت شايف، الـ functions بتتنفذ بالترتيب (Main → fun1 → fun2 → fun3 → fun4)، لكن بتنتهي بالعكس. الـ fun4 بتخلص الأول، وبعدها fun3، وهكذا لحد ما الـ Main تخلص في الآخر خالص.
النقطة المهمة هنا هي إن الـ function اللي بتنادي (caller) مش بتخلص إلا لما الـ function اللي تم استدعاؤها (called) تخلص تنفيذها الأول.
Tracing a Recursive Function
دلوقتي خلينا نشوف مثال على Recursive Function ونتبع خطوات تنفيذها. الـ function دي بتطبع أرقام بترتيب تنازلي.
using System;
namespace RecursionDemo
{
class Program
{
static void Main(string[] args)
{
int x = 3;
fun1(x);
Console.ReadKey();
}
static void fun1(int n)
{
// Base case: Stop when n is not greater than 0
if (n > 0)
{
Console.Write($"{n} "); // Execute action first
fun1(n - 1); // Then make the recursive call
}
}
}
}تتبع تنفيذ الكود ده بيكون زي شجرة (بيسموها Tracing Tree):
-
fun1(3):- الشرط
3 > 0صحيح. - هيطبع 3.
- هينادي على
fun1(2). (الـfunctionدي لسه مخلصتش).
- الشرط
-
fun1(2):- الشرط
2 > 0صحيح. - هيطبع 2.
- هينادي على
fun1(1). (الـfunctionدي كمان لسه مخلصتش).
- الشرط
-
fun1(1):- الشرط
1 > 0صحيح. - هيطبع 1.
- هينادي على
fun1(0).
- الشرط
-
fun1(0):- الشرط
0 > 0غلط. - الـ
functionمش هتعمل أي حاجة وهتنتهي.
- الشرط
لما fun1(0) بتخلص، التحكم بيرجع للي قبلها (fun1(1)). ولأن fun1(1) كانت خلصت شغلها قبل ما تنادي على fun1(0)، هي كمان بتنتهي. وهكذا، بترجع لـ fun1(2) وبعدها fun1(3) لحد ما نرجع للـ Main method.
الـ Output النهائي هيكون: 3 2 1.
Calling Phase vs. Returning Phase
الـ Recursion ليه مرحلتين أساسيتين:
- الـ Calling Phase: مرحلة الاستدعاء، وفيها الـ
functionبتفضل تنادي على نفسها. - الـ Returning Phase: مرحلة الرجوع، ودي بتبدأ لما نوصل للـ
base case، وساعتها التنفيذ بيبدأ يرجع للخلف.
لو عكسنا ترتيب الأوامر في المثال اللي فات، وخلينا الطباعة تحصل بعد الاستدعاء، هنشوف إزاي مرحلة الرجوع بتشتغل.
static void fun2(int n)
{
// Base case: Stop when n is not greater than 0
if (n > 0)
{
fun2(n - 1); // Make the recursive call first
Console.Write($"{n} "); // Then execute action on return
}
}لو نادينا على fun2(3)، التتبع هيكون كالآتي:
- الـ
fun2(3)بتنادي علىfun2(2). - الـ
fun2(2)بتنادي علىfun2(1). - الـ
fun2(1)بتنادي علىfun2(0). - الـ
fun2(0)بتوصل للـbase caseوتنتهي من غير ما تعمل حاجة. - التحكم بيرجع لـ
fun2(1)، اللي بتكمل شغلها وتطبع 1. - التحكم بيرجع لـ
fun2(2)، اللي بتكمل شغلها وتطبع 2. - التحكم بيرجع لـ
fun2(3)، اللي بتكمل شغلها وتطبع 3.
الـ Output هنا هيكون: 1 2 3. الطباعة حصلت في مرحلة الرجوع (Returning Phase).
Factorial Example
مثال مشهور جدًا على الـ Recursion هو حساب الـ Factorial بتاع رقم.
static int CalculateFactorial(int number)
{
// Base case: Factorial of 1 or 0 is 1
if (number <= 1)
return 1;
// Recursive step: number * factorial of (number - 1)
return number * CalculateFactorial(number - 1);
// The iterative alternative (using a loop)
// var factorial = 1;
// for (var i = 1; i <= number; i++)
// factorial *= i;
// return factorial;
}هنا الـ Base Case هو لما الرقم يوصل لـ 1. والعملية الحسابية (number * ...) بتحصل في مرحلة الرجوع (Returning Phase).
Hierarchical data
أكبر فايدة للـ Recursion بتظهر لما نتعامل مع بيانات متداخلة أو شجرية (hierarchical data).
زي مثلاً لما يكون عندك فولدر جوه فولدر جوه فولدر لعدد مستويات إنت مش عارفه مسبقًا. الـ Recursion هنا بيخليك تتعامل مع كل مستوى وتدخل للي بعده بشكل ديناميكي من غير ما تكون محدد العمق.
Ex 1: Print Directory Contents
المثال ده بيوضح إزاي نطبع كل الملفات والفولدرات اللي جوه path معين وكل الفولدرات الفرعية بتاعته.
static void PrintDirectoryContents(string dirPath, int level)
{
// Print all files in the current directory
foreach (var filePath in Directory.GetFiles(dirPath))
{
string fileName = new FileInfo(filePath).Name;
Console.WriteLine($"{new string('-', level)} {fileName}");
}
// Print all subdirectories in the current directory
foreach (var subDir in Directory.GetDirectories(dirPath))
{
string dirName = new DirectoryInfo(subDir).Name;
Console.WriteLine($"{new string('-', level)} {dirName}");
// Recursive call to process the subdirectory
PrintDirectoryContents(subDir, level + 1);
}
}الفائدة من Level إنها بتخليك تشوف المجلدات والملفات مترتبة وشكلها واضح زي الشجرة، فتعرف مين جوه مين ومين تبع مين بالظبط. من غير الـ
levelده، كل حاجة هتطبع على سطر واحد وبنفس المسافة، ومش هتعرف مين أصل ومين فرع.
Ex 2: Calculate Directory Size
المثال ده بيحسب المساحة الإجمالية لفولدر معين، شاملة كل الملفات والفولدرات الفرعية اللي جواه.
static long CalculateDirectorySize(string dirPath)
{
long size = 0;
// Add the size of all files in the current directory
foreach (var filePath in Directory.GetFiles(dirPath))
{
size += new FileInfo(filePath).Length;
}
// Recursively add the size of all subdirectories
foreach (var subDir in Directory.GetDirectories(dirPath))
{
size += CalculateDirectorySize(subDir);
}
return size;
}How Variables Work in Recursion
نقطة مهمة جدًا لازم نفهمها هي إن مع كل استدعاء لـ recursive function، بيتم إنشاء نسخة جديدة من كل الـ local variables بتاعتها. النسخ دي بتتحفظ في الـ Stack memory.
خلينا نشوف المثال ده عشان نفهم إزاي المتغيرات بتشتغل:
static int fun(int n)
{
if(n > 0)
{
// '+' operation happens during the returning phase
return fun(n - 1) + n;
}
return 0; // Base case
}
// In Main method:
// int result = fun(5);
// Console.WriteLine(result); // Output: 15اللي بيحصل هنا هو تجميع للأرقام من 1 لـ 5. عملية الجمع (+ n) بتحصل في مرحلة الرجوع.
ده رسم توضيحي بيشرح اللي بيحصل في الـ Stack:
graph TD A["fun(5)"] --> B["fun(4)"]; B --> C["fun(3)"]; C --> D["fun(2)"]; D --> E["fun(1)"]; E --> F["fun(0)"]; F -- "returns 0" --> E; E -- "returns 0 + 1 = 1" --> D; D -- "returns 1 + 2 = 3" --> C; C -- "returns 3 + 3 = 6" --> B; B -- "returns 6 + 4 = 10" --> A; A -- "returns 10 + 5 = 15" --> Result;
كل استدعاء (fun(5), fun(4), …) بيخلق activation record جديد في الـ stack مع نسخة خاصة بيه من المتغير n. عشان كده الـ Recursion بيستهلك ذاكرة أكتر من الـ loop العادية اللي بتستخدم متغير واحد بس.
Time Complexity of Recursive Functions
عشان نحسب الـ Time Complexity، بنفترض إن كل سطر كود بياخد وحدة زمنية واحدة (one unit of time).
لو بصينا على المثال بتاع fun1(n) اللي كان بيطبع الأرقام، هنلاقي إن جملة الطباعة Console.Write() بتتنفذ n مرة. يعني لو n=3، هتتنفذ 3 مرات. لو n=5، هتتنفذ 5 مرات.
ده معناه إن الوقت اللي الـ function بتاخده بيعتمد بشكل مباشر على قيمة n. عشان كده بنقول إن الـ Time Complexity بتاعتها هي O(n).
Advantages and Disadvantages
Advantages
- بيساعد في التعامل مع المعلومات المتعلقة باستدعاءات الـ
functionsبشكل منظم. - بيُستخدم في تقييم الـ
Stackوالتعامل مع تعبيرات زيprefix,postfix, وinfix. - بيخلي الكود أبسط وأوضح في الحالات المعقدة زي الـ
hierarchical data.
Disadvantages
- ممكن يكون بطيء بسبب كتر العمليات على الـ
Stack. - ممكن يسبب
Stack Overflowلو عمق الـRecursionكبير جدًا والـbase caseمتأخر. - ممكن يدخل في
infinite loopلو الـbase caseمش مكتوب صح.