الفرق بين المراجعتين لصفحة: «Algorithms/Recursion»

من موسوعة حسوب
أنشأ الصفحة ب'<noinclude>{{DISPLAYTITLE:التعاود}}</noinclude> تسمى العملية التي تستدعي الدالة فيها نفسها استدعاءً مباشرًا أ...'
 
لا ملخص تعديل
 
(3 مراجعات متوسطة بواسطة نفس المستخدم غير معروضة)
سطر 1: سطر 1:
<noinclude>{{DISPLAYTITLE:التعاود}}</noinclude>
<noinclude>{{DISPLAYTITLE:التعاود}}</noinclude>
تسمى العملية التي تستدعي الدالة فيها نفسها استدعاءً مباشرًا أو غير مباشر بالتعاود recursion، وتسمى هذه الدالة بالدالة التعاودية recursive function. يمكن إيجاد الحلول لبعض المشاكل بسهولة كبيرة باستخدام خوارزمية التعاود، منها على سبيل المثال: أبراج هانوي Towers of Hanoi، الترتيب الداخلي Inorder، والترتيب القبلي Preorder، والترتيب البعدي Postorder في عمليات التنقل عبر عناصر أشجار البيانات Tree Traversals، وخوارزمية البحث بالعمق أولًا في الرسوم البيانية وغيرها.
تسمى العملية التي تستدعي الدالة فيها نفسها استدعاءً مباشرًا أو غير مباشر بالتعاود recursion، وتسمى هذه الدالة بالدالة التعاودية recursive function. يمكن إيجاد الحلول لبعض المسائل بسهولة كبيرة باستخدام خوارزمية التعاود، منها على سبيل المثال: [[Algorithms/Towers of Hanoi|أبراج هانوي Towers of Hanoi]]، [[Algorithms/binary trees#.D8.A7.D9.84.D8.AA.D9.86.D9.82.D9.84 .D8.A7.D9.84.D9.88.D8.B3.D8.B7.D9.8A|التنقل الوسطي Inorder]]، و<nowiki/>[[Algorithms/binary trees#.D8.A7.D9.84.D8.AA.D9.86.D9.82.D9.84 .D8.A7.D9.84.D9.82.D8.A8.D9.84.D9.8A|التنقل القبلي Preorder]]، و<nowiki/>[[Algorithms/binary trees#.D8.A7.D9.84.D8.AA.D9.86.D9.82.D9.84 .D8.A7.D9.84.D8.A8.D8.B9.D8.AF.D9.8A|التنقل البعدي Postorder]] في عمليات التنقل عبر عناصر أشجار البيانات Tree Traversals، و<nowiki/>[[Algorithms/graphs#.D8.A7.D9.84.D8.A8.D8.AD.D8.AB .D8.A8.D8.A7.D9.84.D8.B9.D9.85.D9.82 .D8.A3.D9.88.D9.84.D9.8B.D8.A7 .D9.81.D9.8A .D8.A7.D9.84.D8.B1.D8.B3.D9.88.D9.85 .D8.A7.D9.84.D8.A8.D9.8A.D8.A7.D9.86.D9.8A.D8.A9|خوارزمية البحث بالعمق أولًا]] في [[Algorithms/graphs|الرسوم البيانية]] وغيرها.


تقدم البرامج التعاودية حلًّا للحالة الأساس Base Case ويُعرض الحل للمشكلة الأكبر على هيئة مشاكل أصغر.
تقدم البرامج التعاودية حلًّا للحالة الأساس Base Case ويُعرض الحل للمسألة الأكبر على هيئة مسائل أصغر.


<source lang="c++">int fact(int n)
<source lang="c++">int fact(int n)
سطر 13: سطر 13:
الحالة الأساس في المثال السابق هي <code>n <= 1</code>، ويمكن إيجاد الحل لأرقام أكبر عن طريق تحويل القيم إلى قيم أصغر إلى حين الوصول إلى الحالة الأساس.
الحالة الأساس في المثال السابق هي <code>n <= 1</code>، ويمكن إيجاد الحل لأرقام أكبر عن طريق تحويل القيم إلى قيم أصغر إلى حين الوصول إلى الحالة الأساس.


== استخدام التعاود لحل المشاكل ==
== استخدام التعاود لحل المسائل ==


تتلخّص فكرة التعاود في تمثيل المشكلة على هيئة مشكلة أو مشاكل أصغر، وإضافة حالة واحدة أو حالات أساس متعددة لإيقاف التعاود. فعلى سبيل المثال، يمكن حساب مضروب العدد <code>n</code> إن كان مضروب العدد <code>n-1</code> معروفًا، فتكون الحالة الأساس لعملية إيجاد المضروب هي <code>n = 0</code>؛ لذا نعيد القيمة <code>1</code> عندما تصبح <code>n=0</code>.
تتلخّص فكرة التعاود في تمثيل المسألة على هيئة مسألة أو مسائل أصغر، وإضافة حالة واحدة أو حالات أساس متعددة لإيقاف التعاود. فعلى سبيل المثال، يمكن حساب مضروب العدد <code>n</code> إن كان مضروب العدد <code>n-1</code> معروفًا، فتكون الحالة الأساس لعملية إيجاد المضروب هي <code>n = 0</code>؛ لذا نعيد القيمة <code>1</code> عندما تصبح <code>n=0</code>.


== حالة فيضان الكدس ==
== حالة فيضان الكدس ==


إن لم تصل الدالة إلى الحالة الأساس أو إن كانت الحالة الأساس غير معرفة، فإنّ ذلك سيؤدي إلى حدوث حالة تسمى بفيضان الكدس Stack Overflow. مثال:
إن لم تصل الدالة إلى الحالة الأساس أو إن كانت الحالة الأساس غير معرفة، فإنّ ذلك سيؤدي إلى حدوث حالة تسمى بفيضان الكدس Stack Overflow. مثال:


<source lang="c++">int fact(int n)
<source lang="c++">int fact(int n)
سطر 34: سطر 32:
يؤدي استدعاء الدالة <code>fact(10)‎</code> إلى استدعاء الدوال <code>fact(9)‎</code> و <code>fact(8)‎</code> و <code>fact(7)‎</code> وهكذا، ولن تصل الدالة إلى العدد <code>100</code>، وهذا يعني عدم الوصول إلى الحالة الأساس، وإن استنزفت الذاكرة جرّاء استدعاء هذه الدوال في مكدس استدعاء الدوال، فإنّ ذلك سيتسبب في حدوث خطأ فيضان المكدس.
يؤدي استدعاء الدالة <code>fact(10)‎</code> إلى استدعاء الدوال <code>fact(9)‎</code> و <code>fact(8)‎</code> و <code>fact(7)‎</code> وهكذا، ولن تصل الدالة إلى العدد <code>100</code>، وهذا يعني عدم الوصول إلى الحالة الأساس، وإن استنزفت الذاكرة جرّاء استدعاء هذه الدوال في مكدس استدعاء الدوال، فإنّ ذلك سيتسبب في حدوث خطأ فيضان المكدس.


= التعاود المباشر والتعاود غير المباشر =
== التعاود المباشر والتعاود غير المباشر ==


نقول أن الدالة تعاودية مباشرة إذا كانت تستدعي نفسها. ونقول أن الدالة تعاودية غير مباشرة إذا كانت الدالة تستدعي دالة أخرى وكانت تلك الدالة الأخرى تستدعي الدالة الأولى استدعاءً مباشرًا أو غير مباشر.  
نقول أن الدالة تعاودية مباشرة إذا كانت تستدعي نفسها. ونقول أن الدالة تعاودية غير مباشرة إذا كانت الدالة تستدعي دالة أخرى وكانت تلك الدالة الأخرى تستدعي الدالة الأولى استدعاءً مباشرًا أو غير مباشر.  
سطر 151: سطر 149:
يمكن كتابة الدالة أعلاه بطريقة تجعلها دالة تعاودية مذيلة. يمكن استخدام معامل واحد أو أكثر وتجميع قيمة المضروب في المعامل الثاني، وعندما تصل قيمة <code>n</code> إلى <code>0</code>، تعيد الدالة القيمة المجمّعة.
يمكن كتابة الدالة أعلاه بطريقة تجعلها دالة تعاودية مذيلة. يمكن استخدام معامل واحد أو أكثر وتجميع قيمة المضروب في المعامل الثاني، وعندما تصل قيمة <code>n</code> إلى <code>0</code>، تعيد الدالة القيمة المجمّعة.


* C++:
* C++:


<source lang="c++">#include<iostream>  
<source lang="c++">#include<iostream>  
سطر 293: سطر 291:
== مساوئ التعاود ومميزاته ==
== مساوئ التعاود ومميزاته ==


يمكن حلّ المشاكل بالطريقتين التكرارية iterative والتعاودية؛ بمعنى أنّه يمكن تحويل أي برنامج مكتوب بطريقة تعاودية إلى برنامج مكتوب بطريقة تكرارية، والعكس صحيح كذلك. ولكن البرنامج التعاودي يستهلك مساحة أكبر من البرنامج التكراري، وذلك لأنّ جميع الدوال ستبقى في المكدس إلى حين الوصول إلى الحالة الأساس. كذلك يتطلب البرنامج التعاودي زمنًا أطول في التنفيذ وذلك بسبب استدعاء الدالة مرة بعد أخرى ثم العودة إلى البداية من جديد.
يمكن حلّ المسائل بالطريقتين التكرارية iterative والتعاودية؛ بمعنى أنّه يمكن تحويل أي برنامج مكتوب بطريقة تعاودية إلى برنامج مكتوب بطريقة تكرارية، والعكس صحيح كذلك. ولكن البرنامج التعاودي يستهلك مساحة أكبر من البرنامج التكراري، وذلك لأنّ جميع الدوال ستبقى في المكدس إلى حين الوصول إلى الحالة الأساس. كذلك يتطلب البرنامج التعاودي زمنًا أطول في التنفيذ وذلك بسبب استدعاء الدالة مرة بعد أخرى ثم العودة إلى البداية من جديد.
 
ولكن تقدّم الطريقة التعاودية أسلوبًا بسيطًا وواضحًا لكتابة الشيفرة. تكون بعض المسائل مسائل تعاودية في الأصل، كما هو الحال في عملية التنقل عبر عناصر الشجرة tree traversal، وخوارزمية [[Algorithms/Towers of Hanoi|أبراج هانوي]]، وغيرها. ويُفضل استخدام الشيفرة التعاودية لحلّ مثل هذه المسائل. ويجدر التنبيه إلى أنّ بالإمكان حل هذه المسائل باستخدام الطريقة التكرارية وبالاستعانة [[Algorithms/stacks|بمكدس البيانات]].
 
== [[Algorithms/Towers of Hanoi|مسألة برج هانوي]] ==
خوارزمية برج هانوي Tower of Hanoi هي مسألة رياضية يستخدم فيها ثلاثة قضبان و عدد معين (ليكن <code>n</code>) من الأقراص، والهدف هو تحريك كدس الأقراص من قضيب إلى آخر  باتباع بعض القواعد المحددة.


ولكن تقدّم الطريقة التعاودية أسلوبًا بسيطًا وواضحًا لكتابة الشيفرة. تكون بعض المشاكل مشاكل تعاودية في الأصل، كما هو الحال في عملية التنقل عبر عناصر الشجرة tree traversal، وبرج هانوي، وغيرها. ويُفضل استخدام الشيفرة التعاودية لحلّ مثل هذه المشاكل. ويجدر التنبيه إلى أنّ بالإمكان حل هذه المشاكل باستخدام الطريقة التكرارية وبالاستعانة بمكدس البيانات.
== [[Algorithms/multiply no operators|إيجاد حاصل ضرب عددين دون استخدام العوامل الحسابية]] ==
المطلوب في هذه المسألة هو إيجاد حاصل ضرب عددين صحيحين دون استخدام عوامل الضرب والقسمة وعوامل الأعداد الثنائية ودون استخدام أي نوع من الحلقات التكرارية.


== مصادر ==
== مصادر ==

المراجعة الحالية بتاريخ 16:57، 2 ديسمبر 2019

تسمى العملية التي تستدعي الدالة فيها نفسها استدعاءً مباشرًا أو غير مباشر بالتعاود recursion، وتسمى هذه الدالة بالدالة التعاودية recursive function. يمكن إيجاد الحلول لبعض المسائل بسهولة كبيرة باستخدام خوارزمية التعاود، منها على سبيل المثال: أبراج هانوي Towers of Hanoi، التنقل الوسطي Inorder، والتنقل القبلي Preorder، والتنقل البعدي Postorder في عمليات التنقل عبر عناصر أشجار البيانات Tree Traversals، وخوارزمية البحث بالعمق أولًا في الرسوم البيانية وغيرها.

تقدم البرامج التعاودية حلًّا للحالة الأساس Base Case ويُعرض الحل للمسألة الأكبر على هيئة مسائل أصغر.

int fact(int n)
{
    if (n < = 1) // الحالة الأساس
        return 1;
    else    
        return n*fact(n-1);    
}

الحالة الأساس في المثال السابق هي n <= 1، ويمكن إيجاد الحل لأرقام أكبر عن طريق تحويل القيم إلى قيم أصغر إلى حين الوصول إلى الحالة الأساس.

استخدام التعاود لحل المسائل

تتلخّص فكرة التعاود في تمثيل المسألة على هيئة مسألة أو مسائل أصغر، وإضافة حالة واحدة أو حالات أساس متعددة لإيقاف التعاود. فعلى سبيل المثال، يمكن حساب مضروب العدد n إن كان مضروب العدد n-1 معروفًا، فتكون الحالة الأساس لعملية إيجاد المضروب هي n = 0؛ لذا نعيد القيمة 1 عندما تصبح n=0.

حالة فيضان الكدس

إن لم تصل الدالة إلى الحالة الأساس أو إن كانت الحالة الأساس غير معرفة، فإنّ ذلك سيؤدي إلى حدوث حالة تسمى بفيضان الكدس Stack Overflow. مثال:

int fact(int n)
{
    // حالة أساس خاطئة (قد تتسبب في حدوث فيضان الكدس)
    if (n == 100) 
        return 1;

    else
        return n*fact(n-1);
}

يؤدي استدعاء الدالة fact(10)‎ إلى استدعاء الدوال fact(9)‎ و fact(8)‎ و fact(7)‎ وهكذا، ولن تصل الدالة إلى العدد 100، وهذا يعني عدم الوصول إلى الحالة الأساس، وإن استنزفت الذاكرة جرّاء استدعاء هذه الدوال في مكدس استدعاء الدوال، فإنّ ذلك سيتسبب في حدوث خطأ فيضان المكدس.

التعاود المباشر والتعاود غير المباشر

نقول أن الدالة تعاودية مباشرة إذا كانت تستدعي نفسها. ونقول أن الدالة تعاودية غير مباشرة إذا كانت الدالة تستدعي دالة أخرى وكانت تلك الدالة الأخرى تستدعي الدالة الأولى استدعاءً مباشرًا أو غير مباشر.

مثال:

// دالة تعاودية مباشرة
void directRecFun()
{
    // Some code....

    directRecFun();

    // Some code...
}

// دالة تعاودية غير مباشرة
void indirectRecFun1()
{
    // Some code...

    indirectRecFun2();

    // Some code...
}


// دالة تعاودية غير مباشرة
void indirectRecFun2()
{
    // Some code...

    indirectRecFun1();

    // Some code...
}

الدالة التعاودية المذيلة وغير المذيلة

تكون الدالة التعاودية مذيلة tail-recursion إن كان الاستدعاء التعاودي هو آخر ما تنفّذه الدالة.

void print(int n) 
{ 
	if (n < 0) return; 
	cout << " " << n; 

	// آخر عبارة تنفّذها الدالة هي الاستدعاء التعاودي
	print(n-1); 
}

تعدّ الدوال التعاودية المذيّلة أفضل من تلك غير المذيلة، وذلك لإمكانية تحسين التعاود المذيّل بواسطة المصرّف compiler. وتقوم الفكرة على مبدأ بسيط مفاده أنّه لمّا كان الاستدعاء التعاودي في نهاية الدالة، فهذا يعني أنّه لم يبق شيء يمكن القيام به في هذه الدالة؛ ولا حاجة حينئذٍ لحفظ إطار التعاود الحالي current function's stack frame الخاص بالدالة.

تحويل الدالة التعاودية غير المذيلة إلى مذيلة

تحسب الدالة التالية مضروب العدد المعطى للدالة وليكن n. هذه الدالة دالة تعاودية غير مذيلة. صحيح أنّها قد تبدوا للوهلة الأولى مذيلة، ولكن إمعان النظر فيها يكشف عن أن الاستدعاء fact(n)‎ يستخدم القيمة المعادة بواسطة الاستدعاء fact(n-1)‎، وهذا يعني أنّ الاستدعاء fact(n-1)‎ ليس آخر ما تقوم به الدالة fact(n)‎.

#include<iostream> 
using namespace std; 

// fact(n) يستخدم الاستدعاء
// fact(n-1) القيمة المعادة بواسطة الاستدعاء 
// fact(n-1)‎ وهذا يعني أنّ الاستدعاء
// fact(n)‎ ليس آخر ما تقوم به الدالة

unsigned int fact(unsigned int n) 
{ 
	if (n == 0) return 1; 

	return n*fact(n-1); 
} 

// اختبار الدالة السابقة 
int main() 
{ 
	cout << fact(5); 
	return 0; 
}
  • بايثون:
# fact(n) يستخدم الاستدعاء
# fact(n-1) القيمة المعادة بواسطة الاستدعاء 
# fact(n-1)‎ وهذا يعني أنّ الاستدعاء
# fact(n)‎ ليس آخر ما تقوم به الدالة

def fact(n): 

	if (n == 0): 
		return 1

	return n * fact(n-1) 

# اختبار الدالة السابقة
print(fact(5))
  • جافا:
class GFG { 
	
	// fact(n) يستخدم الاستدعاء
	// fact(n-1) القيمة المعادة بواسطة الاستدعاء 
	// fact(n-1)‎ وهذا يعني أنّ الاستدعاء
	// fact(n)‎ ليس آخر ما تقوم به الدالة

	static int fact(int n) 
	{ 
		if (n == 0) return 1; 
	
		return n*fact(n-1); 
	} 
	
	// اختبار الدالة السابقة
	public static void main(String[] args) 
	{ 
		System.out.println(fact(5)); 
	} 
}

يمكن كتابة الدالة أعلاه بطريقة تجعلها دالة تعاودية مذيلة. يمكن استخدام معامل واحد أو أكثر وتجميع قيمة المضروب في المعامل الثاني، وعندما تصل قيمة n إلى 0، تعيد الدالة القيمة المجمّعة.

  • C++‎:
#include<iostream> 
using namespace std; 

unsigned factTR(unsigned int n, unsigned int a) 
{ 
	if (n == 0) return a; 

	return factTR(n-1, n*a); 
} 

// facTR دالة تغليف للدالة 
unsigned int fact(unsigned int n) 
{ 
return factTR(n, 1); 
} 

// اختبار الدوال السابقة
int main() 
{ 
	cout << fact(5); 
	return 0; 
}
  • بايثون:
def factTR(n, a): 

	if (n == 0): 
		return a 

	return factTR(n - 1, n * a) 

# facTR دالة تغليف للدالة  
def fact(n): 
	return factTR(n, 1) 

# اختبار الدالة السابقة 
print(fact(5))
  • جافا:
class GFG { 
	
	static int factTR(int n, int a) 
	{ 
		if (n == 0) 
			return a; 
	
		return factTR(n - 1, n * a); 
	} 
	
	// factTR دالة تغليف للدالة
	static int fact(int n) 
	{ 
		return factTR(n, 1); 
	} 

	// اختبار الدالة السابقة
	static public void main (String[] args) 
	{ 
		System.out.println(fact(5)); 
	} 
}

حجز الذاكرة للدوال التعاودية

عند استدعاء الدالة من الدالة الرئيسية main()‎ يُحجز مقدار من الذاكرة في المكدس، وعندما تستدعي الدالة التعاودية نفسها يُحجز مقدار آخر من الذاكر فوق المقدار المحجوز أصلًا للدالة المستدعية وتُنشئ نسخة مختلفة من المتغيرات المحلية لكل استدعاء. وعند الوصول إلى الحالة الأساس، تعيد الدالة قيمتها إلى الدالة التي استدعتها، ويتم التخلص من الذاكرة المحجوزة وتستمر العملية.

أمثلة

تعرض الأمثلة التالية طريقة إنشاء الدوال التعاودية في عدد من لغات البرمجة:

#include<bits/stdc++.h> 
using namespace std; 

void printFun(int test) 
{ 
	if (test < 1) 
		return; 
	else
	{ 
		cout << test << " "; 
		printFun(test-1); // العبارة 2 
		cout << test << " "; 
		return; 
	} 
} 

int main() 
{ 
	int test = 3; 
	printFun(test); 
}
  • بايثون:
def printFun(test): 

	if (test < 1): 
		return
	else: 
	
		print( test,end = " ") 
		printFun(test-1) # العبارة 2 
		print( test,end = " ") 
		return
	

test = 3
printFun(test)
  • جافا:
class GFG{ 
static void printFun(int test) 
{ 
	if (test < 1) 
		return; 
	else
	{ 
		System.out.printf("%d ",test); 
		printFun(test-1); // العبارة 2 
		System.out.printf("%d ",test); 
		return; 
	} 
} 

public static void main(String[] args) 
{ 
	int test = 3; 
	printFun(test); 
} 
}

يُحجز مقدار من الذاكرة عند استدعاء الدالة printFun(3)‎ ويهيّئ المتغيّر المحلي test ليأخذ القيمة 3 ويضاف التعبيران 1 و 4 إلى المكدس، وتطبع الدالة القيمة 3 في البداية. في العبارة 2، تستدعى الدالة printFun(2)‎ ويحجز مقدار من الذاكرة لهذه الدالة ويهيئ المتغير المحلي testإلى القيمة 12 وتضاف العبارتان 1 و 4 إلى المكدس. وبنفس الطريقة تستدعي الدالة printFun(2)‎ الدالة printFun(1)‎ وتستدعي الأخيرة الدالة prinFun(0)‎.

تصل الدالة printFun(0)‎ في عملها إلى عبارة if الشرطية وتعود إلى الدالة printFun(1)‎ ثم تنفّذ بقية العبارات في الدالة printFun(1)‎ لتعود الدالة بعدها إلى الدالة printFun(2)‎ وهكذا. تطبع الدالة في المخرجات الأعداد من 3 إلى 1 ثم تطبع الأعداد 1 إلى 3.

مساوئ التعاود ومميزاته

يمكن حلّ المسائل بالطريقتين التكرارية iterative والتعاودية؛ بمعنى أنّه يمكن تحويل أي برنامج مكتوب بطريقة تعاودية إلى برنامج مكتوب بطريقة تكرارية، والعكس صحيح كذلك. ولكن البرنامج التعاودي يستهلك مساحة أكبر من البرنامج التكراري، وذلك لأنّ جميع الدوال ستبقى في المكدس إلى حين الوصول إلى الحالة الأساس. كذلك يتطلب البرنامج التعاودي زمنًا أطول في التنفيذ وذلك بسبب استدعاء الدالة مرة بعد أخرى ثم العودة إلى البداية من جديد.

ولكن تقدّم الطريقة التعاودية أسلوبًا بسيطًا وواضحًا لكتابة الشيفرة. تكون بعض المسائل مسائل تعاودية في الأصل، كما هو الحال في عملية التنقل عبر عناصر الشجرة tree traversal، وخوارزمية أبراج هانوي، وغيرها. ويُفضل استخدام الشيفرة التعاودية لحلّ مثل هذه المسائل. ويجدر التنبيه إلى أنّ بالإمكان حل هذه المسائل باستخدام الطريقة التكرارية وبالاستعانة بمكدس البيانات.

مسألة برج هانوي

خوارزمية برج هانوي Tower of Hanoi هي مسألة رياضية يستخدم فيها ثلاثة قضبان و عدد معين (ليكن n) من الأقراص، والهدف هو تحريك كدس الأقراص من قضيب إلى آخر  باتباع بعض القواعد المحددة.

إيجاد حاصل ضرب عددين دون استخدام العوامل الحسابية

المطلوب في هذه المسألة هو إيجاد حاصل ضرب عددين صحيحين دون استخدام عوامل الضرب والقسمة وعوامل الأعداد الثنائية ودون استخدام أي نوع من الحلقات التكرارية.

مصادر

  • صفحة Recursion في توثيق الخوارزميات في موقع GeeksforGeeks.
  • صفحة Tail Recursion في توثيق الخوارزميات في موقع GeeksforGeeks.