بازگشت (علوم رایانه)

بازگشت (به انگلیسی: Recursion) در علوم رایانه، روشی برای حل یک مسئله است که در آن راه‌حل مسئله به راه‌حل هایی در نمونه های کوچکتر از همان مساله وابسته می باشد[1].

به نظر می‌رسد ترجمه «بازگشت» بیشتر مناسب کلمه انگلیسی Return باشد تا کلمه Recursion ؛ به همین دلیل بعضی از اساتید عبارت «توابع خوداتکاء» را برای نامیدن این‌گونه توابع به کار می‌برند. همچنین از آنجایی که ریشهٔ این کلمه در زبان انگلیسی فعل Recur به معنای "اتفاق افتادن مجدد" می‌باشد ترجمه‌های مناسب دیگر می توانند "توابع بازوقوع" و "توابع بازفراخوانی شونده" باشند، که همگی قطعا بهتر از ترجمهٔ مشهور و ضعیف آن(توابع بازگشتی) می‌باشند. به هر حال، نگرش خوداتکاء یا بازگشتی در علم کامپیوتر یک روش فکر کردن برای حل مسائل است. در واقع خوداتکایی یا بازگشت یکی از ایده‌های اصلی علم کامپیوتر است.[2] حل یک مسئله به روش خوداتکاء/بازگشتی بدین معناست که راه حل بستگی به مدل کوچکتری از صورت مسئله داشته باشد.[3]

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

اکثر زبان‌های برنامه نویسی سطح بالا عمل بازگشت را به این صورت تعریف می‌کنند که یک تابع خودش را فراخوانی کند. زبان‌های دستوری ساختارهای حلقه‌ای مثل while یا for را برای انجام کارهای تکراری به کار می‌گیرند. بعضی از زبان‌های تابعی هیچ ساختار حلقه‌ای تعریف نمی‌کنند اما بر خود بازگشت تکیه دارند که کد را به‌طور مکرر فراخوانی کند. نظریهٔ رایانش‌پذیری ثابت کرده‌است که این زبان‌های فقط بازگشتی از نظر ریاضی برابر زبان‌های دستوری هستند، یعنی اینکه مسائل را بدون نیاز به while و for حل می‌کنند.

الگوریتم‌های بازگشتی

در ریاضیات کاربردی و به خصوص کامپیوتر، مسائل فراوانی وجود دارد که حل آن‌ها را به سادگی می‌توان به صورت یک الگوریتم بازگشتی نشان داد. یک الگوریتم بازگشتی مانند یک تابع یا یک دنباله بازگشتی تعریف می‌شود فرمان‌های الگوریتم به‌طور مکرر و با پارامترهای مختلف اجرا می‌شوند تا به فرمان بنیادی الگوریتم برسیم. آنگاه تمام مقادیری را که محاسبهٔ آن‌ها انجام نشده‌است را به صورت بازگشتی محاسبه می‌نماییم تا فرمان مورد نظر اجرا شود. یک روش متداول برای آسان‌سازی مسائل این است که آن‌ها را به زیر مسائلی از همان نوع تقسیم بندی کنیم. این روش با نام گویشی کردن شناخته می‌شود. به عنوان یک تکنیک برنامه نویسی کامپیوتر، به این روش تقسیم و غلبه (به انگلیسی: Divide and Conquer) اطلاق می‌شود. و کلید راه حل تعداد زیادی از مسائل کامپیوتری مهم است و یک بخش اساسی می‌باشد. تمام زبان‌های برنامه‌نویسی که امروز مورد استفاده‌اند تعریفی مستقیم از توابع بازگشتی در خود دارند. وقتی چنین تابعی فراخوانی می‌شود، کامپیوتر(برای اکثر زبان‌هایی که پشته دارند) یا خود کد زبان instanceهای مختلف تابع را (با فراخوانی پشته، هر چند روش‌های دیگر هم مورد استفاده قرار می‌گیرند). برعکس آن همهٔ توابع بازگشتی می‌توانند به کمک پشته به یک تابع غیر بازگشتی تبدیل شوند. اکثر توابع و روش‌هایی که می‌توانند بوسیلهٔ کامپیوتر ارزشیابی شوند بدون استفاده از غیر بازگشتی کردن قابل بازگشتی شدن هستند.

برنامه‌نویسی بازگشتی

بعضاً الگوریتم‌های بازگشتی را می‌توان به کمک زبان‌های برنامه‌نویسی پیاده‌سازی کرد که به این برنامه‌ها برنامه‌های بازگشتی اطلاق می‌شود. مانند الگوریتم‌های مرتب سازی، فاکتوریل، یافتن ب.م.م، و .... . در زیر به چند نمونه از برنامه‌های بازگشتی معروف اشاره می‌کنیم:

فاکتوریل

یک مثال متداول برای برنامه‌های بازگشتی، محاسبهٔ فاکتوریل برای اعداد طبیعی است:

شبه کد (بازگشتی):
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

این تابع می‌تواند به شکل یک رابطه‌ی بازگشتی نیز نوشته شود:

محاسبهٔ رابطهٔ بازگشتی برای 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

فیبوناچی

یکی دیگر از الگوریتم‌های بازگشتی متداول الگوریتم فیبوناچی است. تابع بازگشتی فیبوناچی:

شبه کد
function fib is:
input: integer n such that n > 0


    1. if n is 1, return 1
    2. if n is 2, return 1
    3. otherwise, return [ fib(n-1) + fib(n-2) ]


end fib

و می‌توان این تابع را با رابطهٔ بازگشتی زیر نشان داد:

محاسبهٔ رابطهٔ بازگشتی برای n = 5:
  b5            = b4 + b3
                = b3 + b2 + b2 + b1
                = b2 + b1 + 1 + 1 + 1
                = 1 + 1 + 3
                = 5

ب.م.م

الگوریتم اقلیدس که بزرگترین مقسوم‌علیه مشترک (ب.م.م) دو عدد را محاسبه می‌کند، می‌تواند به صورت بازگشتی نوشته شود. تابع به شکل زیر تعریف می‌شود:


شبه کد (بازگشتی):
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

رابطهٔ بازگشتی ب.م.م به شکل زیر تعریف می‌شود، در این رابطه منظور از ، مقدار باقیمانده‌ی حاصل از است:

محاسبهٔ رابطهٔ بازگشتی برای 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 قرص است. تابع بازگشتی برج‌های هانوی:

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

محاسبهٔ رابطهٔ بازگشتی برای 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
        }
}

بازگشتی در مقابل غیر بازگشتی

دربارهٔ اینکه کدامیک سریعتر و بهینه تر است نمی‌توان نظر قطعی داد و بستگی به خود الگوریتم و دفعات اجرای آن دارد. اما در هر حال هر کدام برتری‌ها و کاستی‌های نسبت به دیگری دارد و در موارد خاصی الگوریتم‌ها فقط با یکی از این روش قابل پیاده سازی‌اند.

ترتیب فراخوانی توابع

تابع ۱

ترتیب فراخوانی توابع تأثیر به سزایی در اجرای برنامه دارد. به مثال زیر که با زبان C نوشته است، توجه کنید.

void recursiveFunction(int num) {
   if (num <5) {
      printf("%d\n", num);
      recursiveFunction(num + 1);
   }
}

تابع ۲ با خطوط جابجا شده

void recursiveFunction(int num) {
   if (num <5) {
      recursiveFunction(num + 1);
      printf("%d\n", num);
   }
}

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

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

منابع

  • ریاضیات گسسته و الگوریتم‌ها نوشته دکتر علی بهفروز و مهندس محمد ایزدی
  1. Graham, Ronald; Knuth, Donald; Patashnik, Oren (1990). "1: Recurrent Problems". Concrete Mathematics. ISBN 0-201-55802-5.
  2. Epp, Susanna (1995), Discrete Mathematics with Applications (2nd ed.), p. 427
  3. Graham, Ronald (1990), Concrete Mathematics, Donald Knuth, Oren Patashnik, p. Chapter 1: Recurrent Problems
  4. Wirth, Niklaus (1976), Algorithms + Data Structures = Programs, Prentice-Hall, p. 126
This article is issued from Wikipedia. The text is licensed under Creative Commons - Attribution - Sharealike. Additional terms may apply for the media files.