توابع بازگشتی

به نظر می‌رسد ترجمه «بازگشت» بیشتر مناسب کلمه انگلیسی Return باشد تا کلمه Recursion ؛ به همین دلیل بعضی از اساتید عبارت «توابع خوداتکاء» را برای نامیدن این‌گونه توابع به کار می‌برند.
همچنین از آنجایی که ریشه‌ی این کلمه در زبان انگلیسی فعل Recur به معنای "اتفاق افتادن مجدد" می‌باشد ترجمه‌های مناسب دیگر می‌توانند "توابع بازوقوع" و "توابع بازفراخوانی شونده" باشند، که همگی قطعا بهتر از ترجمه‌ی مشهور و ضعیف آن(توابع بازگشتی) می‌باشند.
به هر حال، نگرش خوداتکاء یا بازگشتی در علم کامپیوتر یک روش فکر کردن برای حل مسائل است. در واقع خوداتکایی یا بازگشت یکی از ایده‌های اصلی علم کامپیوتر است. حل یک مسئله به روش خوداتکاء/بازگشتی بدین معناست که راه حل بستگی به مدل کوچکتری از صورت مسئله داشته باشد.
"قدرت بازگشت قطعا در این نهفته‌است که دسته‌ای نامتناهی از اشیا را بوسیله یک عبارت متناهی تعریف می‌کند. به معنای دیگر یک عدد نامتناهی در محاسبات می‌تواند بوسیله یک برنامه متناهی بازگشتی تعریف شود حتی اگر این برنامه هیچ تکرار واضحی نداشته باشد ".
اکثر زبان‌های برنامه نویسی سطح بالا عمل بازگشت را به این صورت تعریف می‌کنند که یک تابع خودش را فراخوانی کند. زبان‌های imperative ساختارهای حلقه‌ای مثل while یا for را برای انجام کارهای تکراری به کار می‌گیرند. بعضی از زبان‌های functional هیچ ساختار حلقه‌ای تعریف نمی‌کنند اما بر خود بازگشت تکیه دارند که کد را به‌طور مکرر فراخوانی کند. تئوری شماره‌پذیری ثابت کرده‌است که این زبان‌های فقط بازگشتی از نظر ریاضی برابر زبان‌های imperative هستند، یعنی اینکه مسائل را بدون نیاز به while و for حل می‌کنند.
الگوریتم‌های بازگشتی
در ریاضیات کاربردی و به خصوص کامپیوتر، مسائل فراوانی وجود دارد که حل آن‌ها را به سادگی می‌توان به صورت یک الگوریتم بازگشتی نشان داد. یک الگوریتم بازگشتی مانند یک تابع یا یک دنباله بازگشتی تعریف می‌شود فرمان‌های الگوریتم به‌طور مکرر و با پارامترهای مختلف اجرا می‌شوند تا به فرمان بنیادی الگوریتم برسیم. آنگاه تمام مقادیری را که محاسبه آن‌ها انجام نشده‌است را به صورت بازگشتی محاسبه می‌نماییم تا فرمان مورد نظر اجرا شود.
یک روش متداول برای آسان‌سازی مسائل این است که آن‌ها را به زیر مسائلی از همان نوع تقسیم بندی کنیم. این روش با نام گویشی کردن شناخته می‌شود. به عنوان یک تکنیک برنامه نویسی کامپیوتر، به این روش divide and conquer اطلاق می‌شود. و کلید راه حل تعداد زیادی از مسائل کامپیوتری مهم است و یک بخش اساسی می‌باشد.
تمام زبان‌های برنامه‌نویسی که امروز مورد استفاده‌اند تعریفی مستقیم از توابع بازگشتی در خود دارند. وقتی چنین تابعی فراخوانی می‌شود، کامپیوتر(برای اکثر زبان‌هایی که پشته دارند) یا خود کد زبان instanceهای مختلف تابع را (با فراخوانی پشته، هر چند روش‌های دیگر هم مورد استفاده قرار می‌گیرند). برعکس آن همه توابع بازگشتی می‌توانند به کمک پشته به یک تابع غیر بازگشتی تبدیل شوند. اکثر توابع و روش‌هایی که می‌توانند بوسیله کامپیوتر ارزشیابی شوند بدون استفاده از غیر بازگشتی کردن قابل بازگشتی شدن هستند.
برنامه‌نویسی بازگشتی
بعضا الگوریتم‌های بازگشتی را می‌توان به کمک زبان‌های برنامه‌نویسی پیاده‌سازی کرد که به این برنامه‌ها برنامه‌های بازگشتی اطلاق می‌شود. مانند الگوریتم‌های مرتب سازی، فاکتوریال، یافتن ب.م.م، و …. . در زیر به چند نمونه از برنامه‌های بازگشتی معروف اشاره می‌کنیم:
نمونه‌های برنامه‌های بازگشتی
فاکتوریال
محاسبه فاکتوریال مرسوم‌ترین مثال برنامه‌های بازگشتی می‌باشد.
تابع بازگشتی فاکتوریال :
\operatorname{fact(n) =
\begin{cases
1 - \mbox{if n = 0 \\
n \cdot \operatorname{fact(n-1) - \mbox{if n> 0 \\
\end{cases
شبه کد (بازگشتی):
function factorial is:
input : integer n such that n>= 0
output : n × (n-1) × (n-2) × … × 1
1. if n is 0, return 1
2. otherwise, return n × factorial(n-1)
end factorial
bn = nbn − 1
محاسبه رابطه بازگشتی برای n = 4:
b 4 = 4 * b 3
= 4 * 3 * b 2
= 4 * 3 * 2 * b 1
= 4 * 3 * 2 * 1 * b 0
= 4 * 3 * 2 * 1 * 1
= 4 * 3 * 2 * 1
= 4 * 3 * 2
= 4 * 6
= 24
این الگوریتم هم مانند الگوریتم‌های دیگر به صورت غیر بازگشتی هم نوشته می‌شود.
شبه کد (غیر بازگشتی):
function factorial is:
input : integer n such that n>= 0
output : n × (n-1) × (n-2) × … × 1
1. create new variable called running_total with a value of 1
2. begin loop
1. if n is 0, exit loop
2. set running_total to (running_total × n)
3. decrement n
4. repeat loop
3. return running_total
end factorial
فیبوناتچی
یکی دیگر از الگوریتم‌های بازگشتی متداول الگوریتم فیبوناتچی است.
تابع بازگشتی فیبوناتچی:
fib(n) =
\begin{cases
0 - \mbox{if n = 0 \\
1 - \mbox{if n = 1 \\
fib(n-1) + fib(n-2) - \mbox{if n>= 2 \\
\end{cases
شبه کد
function fib is:
input : integer n such that n>= 0
1. if n is 0, return 0
2. if n is 1, return 1
3. otherwise, return fib(n-1) + fib(n-2)
end fib
bn = bn-1 + bn-2
محاسبه رابطه بازگشتی برای n = 4:
b 4 = b 3 + b 2
= b 2 + b 1 + b 1 + b 0
= b 1 + b 0 + 1 + 1 + 0
= 1 + 0 + 1 + 1 + 0
= 3
شبه کد
function fib is:
input : integer Times such that Times>= 0, relative to TwoBack and OneBack
long TwoBack such that TwoBack = fib(x)
long OneBack such that OneBack = fib(x)
1. if n is 0, return TwoBack
2. if n is 1, return OneBack
3. if n is 2, return TwoBack + OneBack
4. otherwise, return fib(Times-1, OneBack, TwoBack + OneBack)
end fib
ب.م.م
محاسبه ب.م.م دو عدد نیز جزو مثال‌های معروف الگوریتم‌های بازگشتی می‌باشد که در زیر می‌توانید شرح خصوصیات آن را مشاهده کنید:
تابع بازگشتی محاسبه ب.م.م:
Function definition:
\gcd(x,y) =
\begin{cases
x - \mbox{if y = 0 \\
\gcd(y, \operatorname{remainder(x,y)) - \mbox{if x \ge y \mbox{ and y> 0 \\
\end{cases
شبه کد (بازگشتی):
function gcd is:
input : integer x, integer y such that x>= y and y> 0
1. if y is 0, return x
2. otherwise, return
end gcd
محاسبه رابطه بازگشتی برای x = 27 و y = 9:
gcd(27, 9) = gcd(9, 27 % 9)
= gcd(9, 0)
= 9
محاسبه رابطه بازگشتی برای x = 259 و y = 111:
gcd(259, 111) = gcd(111, 259 % 111)
= gcd(111, 37)
= gcd(37, 0)
= 37
شبه کد (غیر بازگشتی):
function gcd is:
input : integer x, integer y such that x>= y and y> 0
1. create new variable called remainder
2. begin loop
1. if y is zero, exit loop
2. set remainder to the remainder of x/y
3. set x to y
4. set y to remainder
5. repeat loop
3. return x
end gcd
برج‌های هانوی(برج برهما)
در محوطه معبدی در آسیای دور سه میله الماسی قرار داشت که یکی از آن‌ها حاوی تعدادی قرص طلایی بود. کاهنان معبد در تلاش بودند تا قرص‌های طلائی را از آن میله به یکی دیگر از میله‌ها تحت شرایطی انتقال دهند، و باور داشتند که با تمام شدن انتقال قرص‌ها عمر جهان نیز به پایان خواهد رسید! میله اولیه ۶۴ قرص داشت، که بر روی هم به‌طور نزولی بر اساس اندازه‌شان چیده شده‌بودند.
همانند شکل سه میله داریم. یکی از میله‌ها میله مبدا (A)، دیگری میله کمکی (B) و دیگری میله مقصد (C) است. هدف انتقال تمام دیسک‌ها از میله مبدا به میله مقصد با رعایت شرایط زیر است:
در هر زمان فقط یک دیسک را می‌توان جابجا نمود. نباید در هیچ زمانی دیسکی بر روی دیسک با اندازه کوچکتر قرار بگیرد.
هدف ما ارائه الگوریتمی است که کمترین توالی حرکت‌ها را برای انتقال دیسک‌ها به ما بدهد. مثلا اگر n=۲ باشد، توالی حرکت به صورت زیر است:
حل مسئله برج هانوی
دیسک ۱ را به میله B منتقل می‌کنیم.
دیسک ۲ را به میله C منتقل می‌کنیم.
دیسک ۱ را به میله C منتقل می‌کنیم.
توجه داشته باشید که بر اساس قانون اول نمی‌توان به غیر از بالاترین دیسک هر میله، به دیسک دیگری از آن دسترسی پیدا کرد.
حال سوال این است که آیا این مسئله به کمک تکنیک بازگشت قابل حل است؟ اصولا چه مسائلی را می‌توان بازگشتی حل نمود؟
برای اینکه مسئله‌ای بتواند با روش بازگشتی حل شود باید یک ویژگی اساسی داشته باشد. مسئله اصلی (مسئله‌ای که به ما داده می‌شود) قابل خرد شدن به زیر مسئله‌هایی از همان نوع مسئله اصلی باشد، به شرطی که اندازه زیر مسئله‌های ایجاد شده کمتر باشد. آنگاه می‌توان امیدوار بود که آن را به‌طور بازگشتی حل کرد! این ویژگی در مورد مسئله برج هانوی صدق می‌کند. ایده اصلی این است که توجهمان را به جای حرکت بالاترین دیسک، روی پایین‌ترین دیسک میله متمرکز کرده، و مراحل زیر را طی می‌کنیم:
n - ۱ دیسک بالایی را با شرایط ذکر شده و به کمک میله C به میله B منتقل می‌کنیم. بزرگترین دیسک را از میله مبدا به میله مقصد حرکت می‌دهیم. n - ۱ دیسک را که هم اکنون در میله B هستند با شرایط داده شده به میله مقصد انتقال می‌دهیم. می‌بینیم که توانستیم عملیات جابجا کردن n دیسک را به دو عملیات مشابه ولی با اندازه کمتر و یک عملیات ساده تقسیم کنیم. واضح است که جابجا کردن n - ۱ قرص راحتتر از جابجا نمودن n قرص است.
تابع بازگشتی برج‌های هانوی:
\operatorname{hanoi(n) =
\begin{cases
1 - \mbox{if n = 1 \\
2\cdot\operatorname{hanoi(n-1) + 1 - \mbox{if n> 1\\
\end{cases
رابطه بازگشتی برای هانوی:
محاسبه رابطه بازگشتی برای n = 4:
hanoi(4) = 2*hanoi(3) + 1
= 2*(2*hanoi(2) + 1) + 1
= 2*(2*(2*hanoi(1) + 1) + 1) + 1
= 2*(2*(2*1 + 1) + 1) + 1
= 2*(2*(3) + 1) + 1
= 2*(7) + 1
= 15
شبه کد (بازگشتی):
function hanoi is:
input : integer n, such that n>= 1
1. if n is 1 then return 1
2. return 2 * call hanoi(n-1) + 1
end hanoi
هر چند برای تمام توابع بازگشتی راه حل روشنی نمی‌توان یافت اما برج‌های هانوی از این قاعده مستثنی است:
راه حل مستقیم برای مسئله برج‌های هانوی:
h 1 = 1 = 2 1 - 1
h 2 = 3 = 2 2 - 1
h 3 = 7 = 2 3 - 1
h 4 = 15 = 2 4 - 1
h 5 = 31 = 2 5 - 1
h 6 = 63 = 2 6 - 1
h 7 = 127 = 2 7 - 1
In general:
h n = 2 n - 1, for all n>= 1
ساختمان داده‌های بازگشتی(ساخت یافته)
در اصطلاح کامپیوتری، ساختمان داده به روشهایی از ذخیره اطلاعات گفته می‌شود که برای استفاده بهینه از اطلاعات ذخیره شده اتخاذ می‌شود. غالبا انتخاب یک ساختمان داده موجب ایجاد الگوریتم (الخوارزمی)های متناسب با آن خواهد شد که این دو در کنار هم موجب افزایش سرعت انجام یک وظیفه یا کاهش مصرف حافظه برای پردازش داده می‌شود؛ سنگ بنای ساختمان‌های داده انواع داده و اشاره گرهای گوناگون است. که با توجه به چگونگی تعریف کاربرد آن‌ها در هر زبان برنامه نویسی پیاده‌سازی آن‌ها متفاوت خواهد بود.
فهرست پیوندی
در زیر می‌توانید تعریف ساده‌ای از گره فهرستهای پیوندی را مشاهده کنید. عنصر بعدی در ساختار گره اشاره گری است به یک ساختار گره.
struct node
int n; // some data
struct node *next; // pointer to another struct node
// LIST is simply a synonym for struct node * (aka syntactic sugar).
typedef struct node *LIST;
رویه‌هایی که لیست ساختمان داده را به وجود می‌آورند می‌توانند به صورت بازگشتی هم تعریف شوند.
void printList(LIST lst)
if (!isEmpty(lst)) // base case
printf("%d ", lst->n); // print integer followed by a space
printList(lst->next); // recursive call
درخت‌های دودویی
در زیر می‌توانید تعریف ساده‌ای از درخت‌های دودویی را مشاهده کنید. همانند گره‌ها برای لیست‌های الحاقی در اینجا نیز تعریف به صورت بازگشتی است. در اینجا دو اشاره گر داریم، یکی به زیر درخت سمت چپ و دیگری به زیر درخت سمت راست.
struct node
int n; // some data
struct node *left; // pointer to the left subtree
struct node *right; // point to the right subtree
// TREE is simply a synonym for struct node * (aka syntactic sugar).
typedef struct node *TREE;
void printTree(TREE t) {
if (!isEmpty(t)) { // base case
printTree(t->left); // go left
printf("%d ", t->n); // print the integer followed by a space
printTree(t->right); // go right
بازگشتی در مقابل غیر بازگشتی
درباره اینکه کدامیک سریعتر و بهینه‌تر است نمی‌توان نظر قطعی داد و بستگی به خود الگوریتم و دفعات اجرای آن دارد. اما در هر حال هر کدام برتری‌ها و کاستی‌های نسبت به دیگری دارد و در موارد خاصی الگوریتم‌ها فقط با یکی از این روش قابل پیاده سازی‌اند.
ترتیب فراخوانی توابع
تابع1
ترتیب فراخوانی توابع تاثیر به سزایی در اجرای برنامه دارد. به مثال زیر که با زبان C نوشته‌است توجه کنید.
void recursiveFunction(int num) {
if (num <5) {
printf("%d\n", num);
recursiveFunction(num + 1);
پرونده:RecursiveFunction1 execution.png
تابع2 معاوضه شده
void recursiveFunction(int num) {
if (num <5) {
recursiveFunction(num + 1);
printf("%d\n", num);
پرونده:RecursiveFunction2 execution.png
بازگشت مستقیم و غیرمستقیم
بازگشت مستقیم زمانی است که تابع خود را فراخوانی کند. بازگشت غیرمستقیم زمانی است که به‌طور مثال تابع الف تابع ب و تابع ب تابع ث و تابع ث نیز دوباره تابع الف را فراخوانی کند.