الگوریتم بازگشتی چیست؟ — آموزش با مثال و پیاده سازی

تصویر شاخص برای مقاله الگوریتم بازگشتی

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

فهرست مطالب این نوشته پنهان

مقدمه

الگوریتم‌های بازگشتی (Recursive Algorithms) یکی از مفاهیم بنیادین در علوم کامپیوتر هستند که قدرت حل مسائل پیچیده را با استفاده از تقسیم مسئله به زیرمسائل کوچک‌تر فراهم می‌کنند. این رویکرد، که بر پایه تقسیم و حل (Divide and Conquer) استوار است، در بسیاری از مسائل کاربرد دارد؛ از ساده‌ترین مسائل محاسباتی مانند محاسبه فاکتوریل گرفته تا مسائل پیچیده‌تر مانند پیمایش درخت‌ها و گراف‌ها.

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

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

الگوریتم بازگشتی چیست؟

الگوریتم بازگشتی روشی در برنامه نویسی است که در آن یک تابع برای حل یک مسئله، خودش را با ورودی‌های کوچک‌تر فراخوانی می‌کند. این فرآیند تا زمانی ادامه می‌یابد که مسئله به یک نقطه پایه (Base Case) برسد، جایی که دیگر نیازی به بازگشت نباشد و نتیجه مستقیماً قابل محاسبه باشد. در هر مرحله، مسئله اصلی به یک یا چند زیرمسئله‌ی کوچک‌تر شکسته می‌شود و پس از حل آن‌ها، پاسخ‌ها ترکیب شده و نتیجه نهایی ساخته می‌شود.

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

الگوریتم بازگشتی چیست؟

تعریف الگوریتم بازگشتی به زبان ساده

الگوریتم بازگشتی روشی در برنامه نویسی است که در آن یک تابع خودش را صدا می‌زند تا مسئله را حل کند. این کار تا زمانی ادامه پیدا می‌کند که به یک حالت ساده برسد که جواب آن مشخص است (شرط پایه). سپس، نتایج محاسبه‌شده در هر مرحله به عقب برمی‌گردند تا پاسخ نهایی ساخته شود.

به زبان ساده، بازگشت مثل این است که برای حل یک مسئله بزرگ، آن را به نسخه‌های کوچک‌تر خودش تقسیم کنیم و هر بار یک قدم جلو برویم تا به جواب برسیم. مثلا، اگر بخواهیم فاکتوریل یک عدد را حساب کنیم، می‌توانیم آن را به محاسبه فاکتوریل اعداد کوچک‌تر وابسته کنیم تا به ۱ برسیم و سپس جواب‌ها را با هم ترکیب کنیم.

ساختار یک الگوریتم بازگشتی

در الگوریتم‌های بازگشتی، وضعیت نهایی (Base Case) و مرحله تکرار (Recursive Case) دو بخش کلیدی هستند که تعیین می‌کنند الگوریتم چگونه کار کند و چه زمانی متوقف شود.

شرط پایه یا وضعیت نهایی (Base Case)

وضعیت نهایی (Base Case) در یک الگوریتم بازگشتی نقطه‌ای است که در آن تابع دیگر نیازی به فراخوانی خودش ندارد و به‌جای آن یک مقدار مشخص و ثابت را برمی‌گرداند. این شرط ضروری است تا الگوریتم بازگشتی در یک نقطه متوقف شود و از اجرای بی‌پایان جلوگیری کند. معمولاً وضعیت نهایی ساده‌ترین حالت مسئله است که پاسخ آن بدون نیاز به انجام محاسبات بیشتر مشخص است. نبود وضعیت نهایی در یک تابع بازگشتی باعث می‌شود که فراخوانی‌های نامحدود رخ داده و برنامه با خطای Stack Overflow متوقف شود.

گام بازگشتی یا مرحله تکرار (Recursive Case)

مرحله تکرار (Recursive Case) بخشی از الگوریتم بازگشتی است که در آن مسئله به نسخه‌های کوچک‌تر خود تجزیه شده و تابع، خودش را با ورودی جدید و تغییر‌یافته فراخوانی می‌کند. در این مرحله، ورودی تابع در هر فراخوانی به سمت وضعیت نهایی حرکت می‌کند تا در نهایت بازگشت متوقف شود. عملکرد صحیح مرحله تکرار وابسته به این است که در هر بار فراخوانی، ورودی تابع تغییر کند تا در نهایت به وضعیت نهایی برسد. این بخش هسته اصلی الگوریتم بازگشتی است که مسئله را به حل زیرمسائل وابسته می‌کند و سپس از طریق ترکیب نتایج آن‌ها، پاسخ نهایی ساخته می‌شود.

ساختار یک الگوریتم بازگشتی

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

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

  • بازگشتی مستقیم
  • بازگشتی غیرمستقیم
  • بازگشتی دنباله‌دار

هر یک از این انواع بسته به نوع مسئله، میزان کارایی و میزان مصرف حافظه کاربردهای خاص خود را دارند. در ادامه به توضیح هر یک می‌پردازیم.

۱- الگوریتم بازگشتی مستقیم (Direct Recursion)

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

مثال محاسبه مجموع اعداد از ۱ تا n:

def sum_recursive(n):
    if n == 1:  # شرط پایه
        return 1
    return n + sum_recursive(n - 1)  # فراخوانی بازگشتی مستقیم

print(sum_recursive(5))

در این مثال، تابع sum_recursive مستقیماً خودش را صدا می‌زند تا زمانی که به مقدار پایه (n == 1) برسد. این نمونه‌ای از بازگشت مستقیم است، زیرا تابع تنها به خودش وابسته است و هیچ تابع دیگری را برای حل مسئله فراخوانی نمی‌کند.

اگر تابع sum_recursive(5) را صدا بزنیم، محاسبات به صورت زیر انجام می‌شود:

sum_recursive(5) = 5 + sum_recursive(4)
sum_recursive(4) = 4 + sum_recursive(3)
sum_recursive(3) = 3 + sum_recursive(2)
sum_recursive(2) = 2 + sum_recursive(1)
sum_recursive(1) = 1  # شرط پایه

خروجی:

۱۵

۲- الگوریتم بازگشتی غیرمستقیم (Indirect Recursion)

در بازگشت غیرمستقیم، یک تابع خودش را مستقیماً فراخوانی نمی‌کند، بلکه از طریق یک یا چند تابع دیگر این کار را انجام می‌دهد. به‌عبارت‌دیگر، تابع A تابع B را فراخوانی می‌کند و سپس تابع B دوباره تابع A را فراخوانی می‌کند. این نوع بازگشت معمولاً در مسائل منطقی و حل معادلات بازگشتی پیچیده استفاده می‌شود.

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

def is_even(n):
    if n == 0:
        return True
    return is_odd(n - 1)  # فراخوانی غیرمستقیم

def is_odd(n):
    if n == 0:
        return False
    return is_even(n - 1)  # فراخوانی غیرمستقیم

print(is_even(4))
print(is_odd(7))

در این مثال، دو تابع is_even(n) و is_odd(n) داریم که به طور غیرمستقیم یکدیگر را صدا می‌زنند تا تشخیص دهند که عدد داده‌شده زوج است یا فرد. برای مثال، اگر is_even(4) فراخوانی شود، اجرای کد به این صورت خواهد بود:

is_even(4) → is_odd(3) → is_even(2) → is_odd(1) → is_even(0) → True

خروجی:

True #(عدد ۴ زوج است)
True #(عدد ۷ فرد است)

۳- الگوریتم بازگشتی دنباله‌دار (Tail Recursion)

در این نوع بازگشت، فراخوانی بازگشتی آخرین عملیاتی است که در تابع انجام می‌شود. این یعنی نتیجه‌ی بازگشت دیگر نیازی به پردازش بیشتر ندارد و مستقیماً برگردانده می‌شود. این نوع بازگشت می‌تواند توسط برخی از کامپایلرها بهینه‌سازی شود و این بهینه‌سازی بازگشت دنباله‌دار (Tail Call Optimization – TCO) مصرف حافظه را کاهش می‌دهد تا از پر شدن پشته‌ی فراخوانی جلوگیری شود.

مثال محاسبه فاکتوریل با استفاده از یک مقدار کمکی:

def factorial_tail(n, acc=1):
    if n == 1:
        return acc  # شرط پایه
    return factorial_tail(n - 1, acc * n)  # بازگشت دنباله‌دار

print(factorial_tail(5))

این نسخه از محاسبه فاکتوریل بهینه‌تر است، زیرا مقدار acc در هر فراخوانی نگه‌داری می‌شود و نیازی به پردازش بیشتر بعد از بازگشت از فراخوانی‌های بازگشتی نیست.

اگر بخواهیم factorial_tail(5) را اجرا کنیم، داریم:

factorial_tail(5, 1) → factorial_tail(4, 5) → factorial_tail(3, 20) 
→ factorial_tail(2, 60) → factorial_tail(1, 120) → ۱۲۰

خروجی:

۱۵۰

مزایا و معایب الگوریتم بازگشتی

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

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

  • خوانایی و سادگی پیاده‌سازی: الگوریتم‌های بازگشتی کد ساده و قابل‌فهمی دارند. در بسیاری از مسائل، مانند پیمایش درخت‌ها یا حل مسائل ترکیبیاتی، پیاده‌سازی بازگشتی بسیار طبیعی و شهودی است. به‌ویژه زمانی که مسئله به راحتی به زیرمسائل کوچکتر تقسیم می‌شود، کد بازگشتی می‌تواند بسیار ساده و خوانا باشد.
  • کاهش پیچیدگی کد: در مقایسه با روش‌های تکراری، الگوریتم‌های بازگشتی اغلب کد کمتری نیاز دارند. شما نیازی به استفاده از حلقه‌های پیچیده یا ساختارهای کنترلی ندارید و می‌توانید مستقیماً به حل مسئله بپردازید.
  • حل مسائل پیچیده: بسیاری از مسائل پیچیده، مانند حل معادلات بازگشتی یا جستجو در گراف‌ها، به‌طور طبیعی با الگوریتم‌های بازگشتی حل می‌شوند. این الگوریتم‌ها می‌توانند به‌طور مؤثر و آسان مشکلات بزرگتر را به زیرمسائل کوچکتر تقسیم کنند.

معایب الگوریتم بازگشتی

  • مشکل کارایی و مصرف حافظه: الگوریتم‌های بازگشتی معمولاً به میزان بیشتری از حافظه و زمان نیاز دارند، به‌ویژه اگر به‌طور مؤثر بهینه‌سازی نشده باشند. هر فراخوانی تابع جدید به پشته اضافه می‌شود، که می‌تواند منجر به مصرف زیاد حافظه شود. همچنین، بدون بهینه‌سازی، این الگوریتم‌ها ممکن است زمان اجرای بیشتری نسبت به روش‌های تکراری داشته باشند.
  • Stack Overflow: اگر تعداد فراخوانی‌های بازگشتی زیاد شود و به اندازه پشته‌ی سیستم برسد، خطای Stack Overflow رخ می‌دهد. این مشکل زمانی پیش می‌آید که تابع بازگشتی به‌طور نامحدود فراخوانی شود یا عمق بازگشت بیش‌ازحد پشته را پر کند. برای جلوگیری از این مشکل، باید مطمئن شویم که هر فراخوانی بازگشتی به سمت شرط پایه می‌رود و در نهایت متوقف می‌شود.
  • پیچیدگی تحلیل و دیباگ: در برخی موارد، الگوریتم‌های بازگشتی ممکن است به سختی قابل‌دیباگ یا تحلیل باشند، زیرا رفتار تابع به‌صورت بازگشتی و پیچیده است. در مسائل پیچیده‌تر، ممکن است درک کامل از نحوه‌ی اجرای هر مرحله مشکل باشد.

در نهایت، استفاده از الگوریتم بازگشتی باید با توجه به نیاز مسئله و محدودیت‌های سیستم انجام شود. در صورتی که کارایی و مصرف حافظه اهمیت زیادی داشته باشد، می‌توان از تکنیک‌هایی مانند Memoization یا Tail Recursion برای بهینه‌سازی الگوریتم‌های بازگشتی استفاده کرد.

مزایا و معایب الگوریتم بازگشتی

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

الگوریتم‌های بازگشتی در حل مسائل مختلف به‌ویژه در حوزه‌های ریاضی، جستجو و پیمایش، و مسائل ترکیبیاتی کاربرد فراوانی دارند. در ادامه به بررسی برخی از مهم‌ترین کاربردهای الگوریتم بازگشتی می‌پردازیم.

۱- حل مسائل ریاضی مانند فاکتوریل و فیبوناچی

الگوریتم بازگشتی در حل مسائل ساده‌ای مانند محاسبه فاکتوریل یک عدد یا دنباله‌ی فیبوناچی کاربرد دارند. این مسائل به‌طور طبیعی به زیرمسائل کوچکتر تقسیم می‌شوند که می‌توانند به راحتی با بازگشت حل شوند. برای مثال، در محاسبه فاکتوریل یک عدد n، تابع بازگشتی می‌تواند خود را با ورودی n-1 فراخوانی کند تا به‌تدریج به نتیجه برسد. همین‌طور در دنباله فیبوناچی، هر عدد با جمع دو عدد قبلی خودش محاسبه می‌شود.

۲- جستجوی دودویی (Binary Search)

جستجوی دودویی یک الگوریتم بسیار مؤثر برای پیدا کردن یک عنصر در یک آرایه مرتب شده است. این الگوریتم به‌صورت بازگشتی کار می‌کند و در هر مرحله آرایه را به دو بخش تقسیم کرده و جستجو را در نیمی از آرایه ادامه می‌دهد. با کاهش اندازه مسئله در هر مرحله، جستجوی دودویی بسیار سریع و کارآمد می‌شود و پیچیدگی زمانی آن برابر با O(logn) است.

۳- پیمایش درخت‌ها و گراف‌ها

الگوریتم بازگشتی در پیمایش درخت‌ها و گراف‌ها به‌ویژه در روش جستجوی عمق اول (DFS) بسیار مفید هستند. در این الگوریتم، ابتدا از یک راس شروع می‌کنیم و به سراغ همسایه‌های آن می‌رویم تا جایی که تمام مسیرهای ممکن طی شوند. این فرآیند به‌صورت بازگشتی انجام می‌شود تا تمام رئوس و یال‌های گراف یا درخت پیمایش شوند. این الگوریتم در بسیاری از مسائل مانند پیدا کردن مسیرها، تشخیص حلقه‌ها و مسائل مشابه استفاده می‌شود.

۴- حل مسئله برج‌های هانوی و مسائل ترکیبیاتی

مسئله برج‌های هانوی یک مسأله کلاسیک است که می‌تواند به‌راحتی با الگوریتم بازگشتی حل شود. در این مسئله، سه برج داریم و باید دیسک‌ها را از یک برج به برج دیگر انتقال دهیم، با این شرط که در هر بار تنها یک دیسک منتقل شود و دیسک بزرگتر هرگز روی دیسک کوچکتر قرار نگیرد. این مسئله به‌راحتی با بازگشت حل می‌شود، زیرا می‌توان آن را به زیرمسئله‌های کوچکتر تقسیم کرد.
همچنین در مسائل ترکیبیاتی مانند شمارش حالت‌ها، تولید ترتیب‌ها یا انتخاب زیرمجموعه‌ها، الگوریتم‌های بازگشتی به‌طور گسترده‌ای استفاده می‌شوند.

این کاربردها نشان می‌دهند که الگوریتم‌های بازگشتی به‌ویژه در مسائلی که به صورت طبیعی به تقسیم و حل زیرمسائل نیاز دارند، بسیار قدرتمند و کارآمد هستند.

مثال محاسبه فاکتوریل با الگوریتم بازگشتی

فاکتوریل یک عدد n به صورت ضرب همه اعداد از ۱ تا n تعریف می‌شود. برای مثال، ۵! به‌صورت زیر محاسبه می‌شود:

۱۲۰=۵×۴×۳×۲×۱=!۵

الگوریتم بازگشتی برای محاسبه فاکتوریل با استفاده از تقسیم مسئله به زیرمسائل کوچکتر و فراخوانی خود تابع برای مقادیر کوچکتر تا رسیدن به شرط پایه انجام می‌شود. مرحله تکرار در هر فراخوانی به زیرمسئله‌ای کوچک‌تر انجام می‌شود و وضعیت نهایی زمانی است که به n=1 برسیم و محاسبه متوقف می‌شود.

محاسبه فاکتوریل

  • مراحل محاسبه فاکتوریل به صورت بازگشتی:

ابتدا بررسی می‌کنیم که آیا عدد ورودی برابر ۰ یا ۱ است. اگر بله، فاکتوریل برابر ۱ است (شرط پایه). اگر عدد ورودی بیشتر از ۱ باشد، فاکتوریل آن عدد برابر با حاصل‌ضرب آن عدد و فاکتوریل عدد قبلی (n−۱) است. این فرآیند به صورت بازگشتی ادامه می‌یابد تا به شرط پایه برسیم.

  • مرحله تکرار:

در الگوریتم بازگشتی برای محاسبه فاکتوریل، مرحله تکرار از طریق فراخوانی‌های متوالی خود تابع انجام می‌شود. هر بار که تابع factorial(n) برای یک عدد n>1 فراخوانی می‌شود، تابع دوباره خود را برای n−۱ فراخوانی می‌کند. این مرحله تکرار تا زمانی که به شرط پایه (یعنی n=1 یا n=0) برسیم ادامه می‌یابد.

  • وضعیت نهایی:

وضعیت نهایی زمانی است که n=1 یا n=0 برسیم. در این مرحله، محاسبه فاکتوریل متوقف می‌شود و مقدار ۱ باز می‌گردد (شرط پایه). از این نقطه به بعد، نتایج بازگشتی که در فراخوانی‌های قبلی محاسبه شده‌اند، به ترتیب بازگشتی باز می‌گردند و حاصل‌ضرب آن‌ها به نتیجه نهایی فاکتوریل تبدیل می‌شود.

در ادامه پیاده سازی محاسبه فاکتوریل با استفاده از الگوریتم بازگشتی در چند زبان برنامه نویسی مهم آورده شده است.

پیاده سازی محاسبه فاکتوریل با الگوریتم بازگشتی در پایتون

در قطعه کد زیر، نحوه پیاده سازی محاسبه فاکتوریل با استفاده از الگوریتم بازگشتی با زبان پایتون آورده شده است.

def fact(n):
  
    # BASE CONDITION
    if n == 0:
        return 1
    return n * fact(n - 1)

print("Factorial of 5 : ", fact(5))

پیاده سازی محاسبه فاکتوریل با الگوریتم بازگشتی در سی شارپ

در قطعه کد زیر، نحوه پیاده سازی محاسبه فاکتوریل با استفاده از الگوریتم بازگشتی با زبان سی شارپ آورده شده است.

using System;

class Program {
    static int Fact(int n) {
      
        // BASE CONDITION
        if (n == 0)
            return 1;
        
        return n * Fact(n - 1);
    }

    static void Main() {
        Console.WriteLine("Factorial of 5 : " + Fact(5));
    }
}

پیاده سازی محاسبه فاکتوریل با الگوریتم بازگشتی در سی پلاس پلاس

در قطعه کد زیر، نحوه پیاده سازی محاسبه فاکتوریل با استفاده از الگوریتم بازگشتی با زبان سی پلاس پلاس آورده شده است.

#include <iostream>
using namespace std;

int fact(int n)
{
    // BASE CONDITION
    if (n == 0)
        return 1;
  
    return n * fact(n - 1);
}

int main()
{
    cout << "Factorial of 5 : " << fact(5);
    return 0;
}

پیاده سازی محاسبه فاکتوریل با الگوریتم بازگشتی در جاوا

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

public class GfG {
    public static int fact(int n) {
      
        // BASE CONDITION
        if (n == 0)
            return 1;
      
        return n * fact(n - 1);
    }

    public static void main(String[] args) {
        System.out.println("Factorial of 5 : " + fact(5));
    }
}

خروجی قطعه کد محاسبه فاکتوریل

پس از اجرای کدهای بالا خروجی قطعه کد به صورت زیر خواهد بود:

Factorial of 5 : 120

مثال دنباله فیبوناچی با الگوریتم بازگشتی

الگوریتم بازگشتی برای محاسبه دنباله فیبوناچی به‌طور طبیعی از رابطه بازگشتی استفاده می‌کند و مسئله را به مسائل کوچکتر تقسیم می‌کند. به همین دلیل، این الگوریتم بسیار ساده است، اما برای مقادیر بزرگ n کارایی چندان خوبی ندارد، زیرا تعداد زیادی فراخوانی‌های تکراری انجام می‌شود.

دنباله فیبوناچی یک دنباله عددی است که در آن هر عدد از جمع دو عدد قبلی خود به دست می‌آید. این دنباله از دو عدد اول ۰ و ۱ شروع می‌شود. دنباله فیبوناچی در بسیاری از مسائل ریاضی و علوم کامپیوتر کاربرد دارد. یکی از کاربردهای مشهور آن در مدل‌سازی رشد جمعیت خرگوش‌ها در مدل‌های جمعیتی است.

دنباله فیبوناچی

  • مراحل دنباله فیبوناچی به صورت بازگشتی:

الگوریتم بازگشتی برای محاسبه دنباله فیبوناچی در هر مرحله به دو زیرمسئله تقسیم می‌شود: محاسبه F(n−۱) و F(n−۲).

این فرآیند تا رسیدن به شرایط پایه ادامه می‌یابد و سپس نتایج محاسبات به عقب برگشته و محاسبات نهایی انجام می‌شود. وضعیت نهایی زمانی است که به F(0) یا F(1) برسیم و مراحل تکرار به اتمام می‌رسند.

  • مرحله تکرار:

در الگوریتم بازگشتی برای محاسبه دنباله فیبوناچی، مرحله تکرار به این صورت است که در هر مرحله، تابع بازگشتی برای محاسبه F(n) فراخوانی می‌شود تا زمانی که به شرایط پایه F(0)=0 یا F(1)=1 برسیم. در هر فراخوانی بازگشتی، دو زیرمسئله محاسبه می‌شوند: F(n−۱) و F(n−۲).

این فراخوانی‌ها ادامه می‌یابد تا به شرایط پایه برسیم. در واقع، در هر مرحله، الگوریتم دو فراخوانی بازگشتی انجام می‌دهد (یکی برای F(n−۱) و دیگری برای F(n−۲))، که این روند تا رسیدن به شرایط پایه تکرار می‌شود.

  • وضعیت نهایی:

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

در ادامه پیاده سازی دنباله فیبوناچی با استفاده از الگوریتم بازگشتی در چند زبان برنامه نویسی مهم آورده شده است.

پیاده سازی دنباله فیبوناچی با الگوریتم بازگشتی در پایتون

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

# Python code to implement Fibonacci series

# Function for fibonacci
def fib(n):

    # Stop condition
    if (n == 0):
        return 0

    # Stop condition
    if (n == 1 or n == 2):
        return 1

    # Recursion function
    else:
        return (fib(n - 1) + fib(n - 2))


# Driver Code

# Initialize variable n.
n = 5;
print("Fibonacci series of 5 numbers is :",end=" ")

# for loop to print the fibonacci series.
for i in range(0,n): 
    print(fib(i),end=" ")

پیاده سازی دنباله فیبوناچی با الگوریتم بازگشتی در سی شارپ

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

using System;

public class GFG
{

  // Function for fibonacci
  static int fib(int n)
  {

    // Stop condition
    if (n == 0)
      return 0;

    // Stop condition
    if (n == 1 || n == 2)
      return 1;

    // Recursion function
    else
      return (fib(n - 1) + fib(n - 2));
  }

  // Driver Code
  static public void Main ()
  {

    // Initialize variable n.
    int n = 5;
    Console.Write("Fibonacci series of 5 numbers is: ");

    // for loop to print the fibonacci series.
    for (int i = 0; i < n; i++) 
    {
      Console.Write(fib(i) + " ");
    }
  }
}

پیاده سازی دنباله فیبوناچی با الگوریتم بازگشتی در سی پلاس پلاس

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

// C++ code to implement Fibonacci series
#include <bits/stdc++.h>
using namespace std;

// Function for fibonacci

int fib(int n)
{
    // Stop condition
    if (n == 0)
        return 0;

    // Stop condition
    if (n == 1 || n == 2)
        return 1;

    // Recursion function
    else
        return (fib(n - 1) + fib(n - 2));
}

// Driver Code
int main()
{
    // Initialize variable n.
    int n = 5;
    cout<<"Fibonacci series of 5 numbers is: ";

    // for loop to print the fibonacci series.
    for (int i = 0; i < n; i++) 
    {
        cout<<fib(i)<<" ";
    }
    return 0;
}

پیاده سازی دنباله فیبوناچی با الگوریتم بازگشتی در جاوا

در قطعه کد زیر، نحوه پیاده سازی دنباله فیبوناچی با استفاده از الگوریتم بازگشتی با زبان جاوا آورده شده است.

// Java code to implement Fibonacci series
import java.util.*;

class GFG
{

// Function for fibonacci
static int fib(int n)
{
    // Stop condition
    if (n == 0)
        return 0;

    // Stop condition
    if (n == 1 || n == 2)
        return 1;

    // Recursion function
    else
        return (fib(n - 1) + fib(n - 2));
}

// Driver Code
public static void main(String []args)
{
  
    // Initialize variable n.
    int n = 5;
    System.out.print("Fibonacci series of 5 numbers is: ");

    // for loop to print the fibonacci series.
    for (int i = 0; i < n; i++) 
    {
        System.out.print(fib(i)+" ");
    }
}
}

خروجی قطعه کد دنباله فیبوناچی

پس از اجرای کدهای بالا خروجی قطعه کد به صورت زیر خواهد بود:

Fibonacci series of 5 numbers is: 0 1 1 2 3

مثال حل مسئله برج هانوی با الگوریتم بازگشتی

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

  1. در هر مرحله فقط یک دیسک می‌تواند جابجا شود.
  2. دیسک بزرگتر هیچ‌گاه نمی‌تواند روی دیسک کوچکتر قرار گیرد.
  3. می‌توان دیسک‌ها را فقط بین دو میله مجاور جابجا کرد.

مسئله برج هانوی

برای حل این مسئله به‌صورت بازگشتی، یک روش تقسیم و غلبه داریم. در هر مرحله باید n دیسک را از میله اول به میله سوم منتقل کنیم. این کار با استفاده از یک میله کمکی انجام می‌شود.

  • مراحل حل مسئله برج هانوی به صورت بازگشتی:

الگوریتم بازگشتی به شرح زیر است:

– حالت پایه: اگر فقط یک دیسک باقی مانده باشد، آن را به میله مقصد منتقل کنید.

– مرحله بازگشتی: ابتدا n−۱ دیسک را از میله اول به میله کمکی منتقل کنید. سپس دیسک بزرگترین را از میله اول به میله مقصد منتقل کنید. در نهایت n−۱ دیسک را از میله کمکی به میله مقصد منتقل کنید. این فرآیند برای هر تعداد دیسک تکرار می‌شود.

  • مرحله تکرار:

در الگوریتم بازگشتی برج هانوی، برای حل مسئله، در هر مرحله یک زیرمسئله بزرگتر به دو زیرمسئله کوچکتر تقسیم می‌شود. برای انتقال n دیسک از میله اول به میله مقصد، ابتدا n−۱ دیسک را از میله اول به میله کمکی منتقل می‌کنیم (با استفاده از میله مقصد به عنوان میله کمکی)، سپس دیسک بزرگترین (دیسک n) را از میله اول به میله مقصد منتقل می‌کنیم.

در نهایت دوباره n−۱ دیسک را از میله کمکی به میله مقصد منتقل می‌کنیم (با استفاده از میله اول به عنوان میله کمکی). در این فرآیند، فراخوانی‌های بازگشتی به‌طور متوالی انجام می‌شوند تا زمانی که به شرط پایه برسیم که در آن فقط یک دیسک باقی‌مانده باشد. هر فراخوانی بازگشتی یک مرحله از جابجایی دیسک‌ها را به زیرمسئله‌های کوچکتر تقسیم می‌کند.

  • وضعیت نهایی:

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

در ادامه پیاده سازی حل مسئله برج هانوی با استفاده از الگوریتم بازگشتی در چند زبان برنامه نویسی مهم آورده شده است.

پیاده سازی حل مسئله برج هانوی با الگوریتم بازگشتی در پایتون

در قطعه کد زیر، نحوه پیاده سازی حل مسئله برج هانوی با استفاده از الگوریتم بازگشتی با زبان پایتون آورده شده است.

# Recursive Python function to solve tower of hanoi


def TowerOfHanoi(n, from_rod, to_rod, aux_rod):
    if n == 0:
        return
    TowerOfHanoi(n-1, from_rod, aux_rod, to_rod)
    print("Move disk", n, "from rod", from_rod, "to rod", to_rod)
    TowerOfHanoi(n-1, aux_rod, to_rod, from_rod)


# Driver code
N = 3

# A, C, B are the name of rods
TowerOfHanoi(N, 'A', 'C', 'B')

پیاده سازی حل مسئله برج هانوی با الگوریتم بازگشتی در سی شارپ

در قطعه کد زیر، نحوه پیاده سازی حل مسئله برج هانوی با استفاده از الگوریتم بازگشتی با زبان سی شارپ آورده شده است.

// C# recursive program to solve tower of hanoi puzzle
using System;
class GFG {
    static void towerOfHanoi(int n, char from_rod,
                             char to_rod, char aux_rod)
    {
        if (n == 0) {
            return;
        }
        towerOfHanoi(n - 1, from_rod, aux_rod, to_rod);
        Console.WriteLine("Move disk " + n + " from rod "
                          + from_rod + " to rod " + to_rod);
        towerOfHanoi(n - 1, aux_rod, to_rod, from_rod);
    }

    //  Driver method
    public static void Main(String[] args)
    {
        int N = 3;

        // A, B and C are names of rods
        towerOfHanoi(N, 'A', 'C', 'B');
    }
}

پیاده سازی حل مسئله برج هانوی با الگوریتم بازگشتی در سی پلاس پلاس

در قطعه کد زیر، نحوه پیاده سازی حل مسئله برج هانوی با استفاده از الگوریتم بازگشتی با زبان پلاس پلاس آورده شده است.

// C++ recursive function to
// solve tower of hanoi puzzle
#include <bits/stdc++.h>
using namespace std;

void towerOfHanoi(int n, char from_rod, char to_rod,
                  char aux_rod)
{
    if (n == 0) {
        return;
    }
    towerOfHanoi(n - 1, from_rod, aux_rod, to_rod);
    cout << "Move disk " << n << " from rod " << from_rod
         << " to rod " << to_rod << endl;
    towerOfHanoi(n - 1, aux_rod, to_rod, from_rod);
}

// Driver code
int main()
{
    int N = 3;

    // A, B and C are names of rods
    towerOfHanoi(N, 'A', 'C', 'B');
    return 0;
}

پیاده سازی حل مسئله برج هانوی با الگوریتم بازگشتی در جاوا

در قطعه کد زیر، نحوه پیاده سازی حل مسئله برج هانوی با استفاده از الگوریتم بازگشتی با زبان جاوا آورده شده است.

// JAVA recursive function to
// solve tower of hanoi puzzle
import java.io.*;
import java.math.*;
import java.util.*;
class GFG {
    static void towerOfHanoi(int n, char from_rod,
                             char to_rod, char aux_rod)
    {
        if (n == 0) {
            return;
        }
        towerOfHanoi(n - 1, from_rod, aux_rod, to_rod);
        System.out.println("Move disk " + n + " from rod "
                           + from_rod + " to rod "
                           + to_rod);
        towerOfHanoi(n - 1, aux_rod, to_rod, from_rod);
    }

    // Driver code
    public static void main(String args[])
    {
        int N = 3;

        // A, B and C are names of rods
        towerOfHanoi(N, 'A', 'C', 'B');
    }
}

خروجی قطعه کد حل مسئله برج هانوی

پس از اجرای کدهای بالا خروجی قطعه کد به صورت زیر خواهد بود:

Move disk 1 from rod A to rod C
Move disk 2 from rod A to rod B
Move disk 1 from rod C to rod B
Move disk 3 from rod A to rod C
Move disk 1 from rod B to rod A
Move disk 2 from rod B to rod C
Move disk 1 from rod A to rod C

کاربردهای پیشرفته الگوریتم بازگشتی

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

  • هوش مصنوعی و یادگیری ماشین: الگوریتم‌های بازگشتی مانند شبکه‌های عصبی بازگشتی (RNN) و LSTM در پردازش داده‌های توالی‌ای (مثل تحلیل زبان طبیعی و پیش‌بینی سری‌های زمانی) استفاده می‌شوند تا وابستگی‌های بلندمدت در داده‌ها را یاد بگیرند.
  • پردازش تصویر و گرافیک کامپیوتری: در پردازش تصویر، از الگوریتم‌های بازگشتی برای تشخیص الگو، تقسیم‌بندی و تحلیل تصاویر استفاده می‌شود. همچنین در گرافیک کامپیوتری، برای مدل‌سازی اشیاء سه‌بعدی و شبیه‌سازی حرکت نور در صحنه‌ها به‌کار می‌روند.
  • طراحی سیستم‌ها و تجزیه مسائل پیچیده: در طراحی سیستم‌های پیچیده و مدل‌سازی ساختارهای سلسله‌مراتبی یا گرافی، الگوریتم‌های بازگشتی برای تحلیل روابط و حل مسائل پیچیده کاربرد دارند. این کاربردها در سیستم‌های توزیع‌شده و شبیه‌سازی‌های علمی رایج است.

نتیجه گیری

الگوریتم بازگشتی ابزارهای قدرتمندی در دنیای برنامه نویسی و حل مسائل پیچیده هستند. این الگوریتم‌ها به‌ویژه در مسائلی که می‌توانند به زیرمسائل کوچکتر تقسیم شوند، بسیار کارآمد و شهودی هستند. برای بهره‌برداری بهینه از این الگوریتم، باید به چالش‌هایی مانند مصرف بالای حافظه و زمان اجرای طولانی توجه کرد.

توانایی این الگوریتم‌ها در مدل‌سازی مسائل به‌صورت سلسله‌مراتبی و تقسیم وظایف به بخش‌های کوچکتر، همچنان آن‌ها را به ابزاری ضروری برای حل چالش‌های پیشرفته در علوم کامپیوتر تبدیل کرده است.

میزان رضایتمندی
لطفاً میزان رضایت خودتان را از این مطلب با دادن امتیاز اعلام کنید.
[ امتیاز میانگین 0 از 0 نفر ]
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع و مراجع:
geeksforgeeks geeksforgeeks medium

دیدگاه‌ خود را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

پیمایش به بالا