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

از ویکی‌پدیا، دانشنامهٔ آزاد
پرش به: ناوبری، جستجو


عمل بازگشت در علم کامپیوتر یک روش فکر کردن برای حل مسائل است. در واقع بازگشت یکی از ایده‌های اصلی علم کامپیوتر است.[۱] حل یک مسئله به روش بازگشتی بدین معناست که راه حل بستگی به مدل کوچکتری از صورت مسئله داشته باشد.[۲]

"قدرت بازگشت قطعاً در این نهفته‌است که دسته‌ای نامتناهی از اشیا را بوسیلهٔ یک عبارت متناهی تعریف می‌کند. به معنای دیگر یک عدد نا متناهی در محاسبات می‌تواند بوسیلهٔ یک برنامهٔ متناهی بازگشتی تعریف شود حتی اگر این برنامه هیچ تکرار واضحی نداشته باشد ".[۳]

اکثر زبان‌های برنامه نویسی سطح بالاعمل بازگشت را به این صورت تعریف می‌کنند که یک تابع خودش را فراخوانی کند. زبان‌های 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:
b4           = 4 * b3
= 4 * 3 * b2 = 4 * 3 * 2 * b1 = 4 * 3 * 2 * 1 * b0 = 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:
  b4            = b3 + b2
                = b2 + b1 + b1 + b0
                = b1 + b0 + 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 [ gcd( y, (remainder of x/y) ) ]


end gcd
\gcd(x,y) = \gcd(y, x % y)
\gcd(x,0) = x
محاسبهٔ رابطهٔ بازگشتی برای 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}

رابطهٔ بازگشتی برای هانوی:

h_n = 2h_{n-1}+1
h_1 = 1
محاسبهٔ رابطهٔ بازگشتی برای 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

هر چند برای تمام توابع بازگشتی راه حل روشنی نمی‌توان یافت اما برج‌های هانوی از این قاعده مستثنی است:

راه حل مستقیم برای مسئلهٔ برج‌های هانوی:

h1 = 1   = 21 - 1
h2 = 3   = 22 - 1
h3 = 7   = 23 - 1
h4 = 15  = 24 - 1
h5 = 31  = 25 - 1
h6 = 63  = 26 - 1
h7 = 127 = 27 - 1
In general:
hn = 2n - 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

بازگشت مستقیم و غیرمستقیم[ویرایش]

بازگشت مستقیم زمانی است که تابع خود را فراخوانی کند. بازگشت غیرمستقیم زمانی است که به طور مثال تابع الف تابع ب و تابع ب تابع ث و تابع ث نیز دوباره تابع الف را فراخوانی کند.

منابع[ویرایش]

  1. Epp, ‎Susanna. Discrete Mathematics with Applications. 2nd ed. 1995. 427. 
  2. Graham, ‎Ronald. Donald Knuth, Oren Patashnik. Concrete Mathematics. 1990. Chapter 1: Recurrent Problems. 
  3. Wirth, ‎Niklaus. Algorithms + Data Structures = Programs. Prentice-Hall, 1976. 126.