Memoization یک روش پیشرفته است که برای سرعت بخشیدن به برنامه های رایانه ای با نگه داشتن مقادیر تماس های عملکردی مجزا و بازگشت ورودی ذخیره شده در هنگام استفاده مجدد از همان عملکرد استفاده می شود.
در توسعه نرم افزار ، پیچیدگی زمان زمانی را که کامپیوتر برای اجرای یک الگوریتم می گیرد ، توصیف می کند.
در این آموزش ، ما از برنامه نویسی پویا برای حل دنباله فیبوناچی استفاده خواهیم کرد و در مورد پیچیدگی زمان راه حل های خود می آموزیم.
مشکلات برنامه نویسی پویا بیشتر با استفاده از دو روش حل می شود: جدول بندی (از پایین به بالا) و یادآوری (از بالا به پایین). در این آموزش ، ما بیشتر روی Memoization تمرکز خواهیم کرد.
پیش نیازها
برای درک مؤثر آموزش ، خواننده نیاز دارد:
- برخی از درک اساسی JavaScript.
- IDE مناسب مانند کد VS.
- آشنایی با رویکرد بازگشتی.
غذای اصلی
- اعداد فیبوناچی چیست؟
- پیچیدگی زمان چیست؟
- نحوه اجرای Memoization در JavaScript.
- نحوه استفاده از بازگشت برای حل سوالات برنامه نویسی پویا.
- درک کنید که چگونه پیچیدگی زمان با یادآوری کاهش می یابد.
برنامه نویسی پویا
هنگام شرکت در برنامه نویسی پویا ، باید:
- کد بازگشتی را بنویسید.
- مقدار بازده را که برای کاهش تماس های بازگشتی استفاده می شود ، به یاد بیاورید.
اجرای بازگشتی کلاسیک یک تابع فیبوناچی
1 همیشه شماره اول و دوم دنباله فیبوناچی است:
ما دو عدد قبلی را در دنباله اضافه می کنیم ، به عنوان مثال5+8 = 13 ، برای تولید عدد بعدی در دنباله. این تعداد باید موقعیت توالی را به عنوان مثال بدست آورد. 13 شماره هفتم فیبوناچی است.
مثال:
در کد فوق ، ما یک مقدار عددی را در دست می گیریم و تعداد دنباله فیبوناچی را برمی گردانیم.
به عبارت دیگر ، اگر مبلغ N کمتر از یا برابر با 2 باشد ، ما 1 را برمی گردیم.
عملکرد در کد فوق می تواند با افزودن ارقام 1 و 2 شماره 3 فیبوناچی را تعیین کند. سپس ، آن را اجرا می کند تا اطمینان حاصل شود که شماره های 1 و 2 1 هستند.
سپس آنها را اضافه کرده و آنها را به نمونه اصلی عملکرد باز می گرداند ، که اکنون 2 و 1 اضافه می کند.
بیایید اکنون کد خود را آزمایش کنیم تا کارایی آن را بررسی کنیم. ما فیبر پنج ، شش و هفت را محاسبه خواهیم کرد. خروجی باید 5 ، 8 و 13 باشد.
کد فوق نتایج مورد انتظار را نشان می دهد. حال ، بیایید تعداد بیشتری را به عملکرد FIB () اختصاص دهیم. ابتدا شماره فیبوناچی 70 را به شرح زیر تعیین خواهیم کرد:
به نظر می رسد که سه تماس اول فیبوناچی به خوبی کار کرده است. با این حال، تماس چهارم زمان زیادی می برد. بنابراین، برای نتایج سریعتر باید تابع فیبوناچی را اصلاح کنیم.
تجسم عملکرد فیب
ما از یک درخت بازگشتی برای تجسم مشکل خود استفاده خواهیم کرد. اما، ابتدا بیایید ببینیم وقتی روش fib را با 7 فراخوانی می کنیم چه اتفاقی می افتد.
ما درخت خود را از شماره 7 منشعب می کنیم. مرحله اول تفریق 1 (n-1) در شاخه چپ و 2 (n-2) در گره سمت راست است.
از همین منطق در سایر گره های ساختار استفاده کنید. کیس های پایه ما 2 و 1 هستند زیرا نمی توانیم بیشتر از این منشعب شویم.
در این صورت، ما کل درخت را می سازیم و هر زمان که سناریوی موردی اصلی داشته باشیم، شاخه های آن را متوقف می کنیم.
فراخوانی درخت بدون حفظ کردن
ما یک و دو را به عنوان موارد پایه در درخت خود خواهیم داشت. بنابراین، آنها 1 را به پدر و مادر برمی گردانند. سپس این دو مقدار را اضافه می کنیم تا عدد زیر را در دنباله پیدا کنیم.
توجه داشته باشید که تمام موارد پایه 1 را به والدین خود باز می گرداند.
اکنون مقادیر فرزندان چپ و راست را اضافه می کنیم تا زمانی که به بالای درخت خود برسیم. نتیجه نهایی 13 است.
پیچیدگی زمانی
همانطور که در بالا نشان داده شد، پیاده سازی بازگشتی کلاسیک معمولاً پیچیدگی زمانی 0(2^n) دارد. بنابراین، برای یافتن پیچیدگی زمانی الگوریتم خود، باید در نظر بگیریم که چند بار () fib را فراخوانی خواهیم کرد.
در سطح اول درخت، یک فراخوان داریم: fib 7. دو فراخوان در سطح بعدی (fib 6 و fib 5) و چهار در سطح قبلی وجود دارد.
هر بار که گره ما منشعب می شود، دو گره اضافی داریم، بنابراین 2*2*2 است.= 2^n. ساختن آن O(2^n) . توجه داشته باشید که معمولاً 2^n نیست.
می بینید که در سطح 5، یک گره وجود ندارد و در سطح 6، تنها یک گره وجود دارد. این بدان معنی است که درخت ما نامتقارن است.
حفظ کردن
0(2^n) پیچیدگی زمانی مطلوبی نیست. اگر فیب 70 را بخواهیم، 2^70 مرحله طول می کشد تا اجرا شود. بنابراین، ما یک عدد بزرگ به دست خواهیم آورد زیرا مراحل زیادی را طی می کند.
بیایید هر درخت فرعی مکرر را در ماهیت بازگشتی درخت خود جستجو کنیم. سپس، زیردرخت ها می توانند خود را کپی کنند.
برای پیاده سازی حافظه، باید یک زیردرخت تکراری بگیریم، از این محاسبات دوباره استفاده کنیم و سپس آنها را ذخیره کنیم. این اطلاعات ممکن است زمانی مفید باشد که نیاز به محاسبه مجدد جزئیات خاص داشته باشیم.
ما از یک شی جاوا اسکریپت برای به خاطر سپردن کد خود استفاده خواهیم کرد. کلیدهای شیء آرگومان تابع خواهد بود، در حالی که مقدار، مقدار بازگشتی ما خواهد بود.
ما باید چند آرگومان اختیاری را به تابع ()fib موجود خود اضافه کنیم. بنابراین، مانند شکل زیر، یک یادداشت را به یک شی خالی اختصاص می دهیم:
این بدان معناست که اگر تابع fib() را بدون ارسال آرگومان فراخوانی کنیم، به طور پیش فرض یک یادداشت ایجاد می شود.
یادداشت حاوی یک شی جاوا اسکریپت خالی خواهد بود. فرض می کنیم که یادداشت n را به عنوان کلید ذخیره می کند. مقادیر از تابع fib() بازگردانده می شوند.
ما باید وجود آرگومان n خود را در یادداشت خود با افزودن یک مورد پایه بررسی کنیم. اگر پیدا شد، میتوانیم مقدار ذخیرهشده را از آن یادداشت بازیابی کنیم و بنابراین، اجرا را به سرعت تمام کنیم.
ما از آرگومان اصلی n به عنوان یک کلید در یادداشت خود برای برگرداندن مقداری که با کلید یادداشت مرتبط است استفاده کردیم.
شرط (n در یادداشت) فقط برخی از نحو کلاسیک جاوا اسکریپت است. بنابراین ما در حال بررسی هستیم که آیا کلیدی در داخل شی جاوا اسکریپت وجود دارد یا خیر.
بیایید کل نتیجه خود را در یادداشت خود ذخیره کنیم. کلید ما n است. ما عملیات اولیه خود را تکمیل می کنیم و نتایج را در یادداشت برمی گردانیم. بنابراین، ما هیچ مقدار بازگشتی را تغییر نمی دهیم.
توجه داشته باشید که کلید دسترسی همیشه باید آرگومان شما باشد.
گام بعدی این است که یک شی یادداشت را به همه تماس های بازگشتی ارسال کنید:
ما فقط هر زمان که با یک تابع fib تماس سطح بالایی برقرار می کنیم، یک شی یادداشت سطح بالا دریافت می کنیم. این به این دلیل است که ما در آرگومان دوم همانطور که در یادداشت بازگشتی [n] نشان داده شده است، عبور نمی کنیم.
از آنجایی که ما آرگومان های دوم صریح را در فراخوانی های بازگشتی خود ارسال نمی کنیم، آنها همان شی یادداشت را دریافت می کنند و به نظر می رسد که توسط مرجع ارسال شده است.
در زیر کد کامل حفظ شده ما آمده است:
یک آرگومان جدید و یک مورد پایه در تغییرات کد بالا اضافه کردیم.
ما همچنین یک منطق ذخیره یادداشت را به صورت memo[n]=fib(n-1, memo) + fib(n-2, memo) اضافه کردیم. توجه داشته باشید که ما هیچ یک از منطق عملکردی را جایگزین نکردیم. بنابراین، این تغییرات باعث می شود تا زمان اجرای برنامه ما تا حد زیادی بهبود یابد.
اجرای درخت
در تصویر بالا، گرههای قرمز رنگ، نتیجهی یادداشتشده را برمیگردانند. الگوریتم یک بار برای هر مقدار از 0 تا n در صورت نادیده گرفتن فراخوانی می شود.
درخت فراخوانی برای شکل برجسته تر از fib(7) مانند تصویر بالا خواهد بود.
دنباله فیبوناچی - تحلیل پیچیدگی زمانی
در الگوریتم فوق، اگر n کمتر یا مساوی 1 باشد، n را برمی گردانیم یا دو فراخوانی بازگشتی برای محاسبه فیب n-1 و fib n-2 انجام می دهیم.
محاسبه پیچیدگی زمانی
برای محاسبه پیچیدگی زمانی دنباله فیبوناچی می توانید از فرمول های مختلفی استفاده کنید.
وقتی پیچیدگی زمانی برنامه ها را تحلیل می کنیم، فرض می کنیم که هر عملیات ساده یک واحد زمان می برد.
بنابراین ، اگر عملکرد فیبر () n را بیش از 1 بنامیم ، ابتدا مقایسه ای را با 1 در N انجام خواهیم داد
از آنجا که n از 1 بیشتر است ، به کنترل شرایط دیگر برنامه می رود. این زمانی است که ما دو تماس بازگشتی را انجام می دهیم و استدلال های N-1 و N-2 را منتقل می کنیم.
بیایید بگوییم زمان لازم برای محاسبه فیبر n t (n) است.
- O(n)=>الگوریتم زمان خطی FIB (تکرار)
در معادله بالا ، ما به دنبال کوچکترین مقدار گرد تا مقدار تخمینی هستیم.
- O(2^n)=>فیبر (بازگشت)-الگوریتم زمان طولانی
در معادله بالا ، ما به دنبال بزرگترین مقدار گرد تا مقدار تخمینی هستیم.
در یافتن راه حل معادله فوق ، ما مرز بالایی فیبوناچی را به عنوان o (2^n) بدست می آوریم.
این محدوده بالایی نیست. با این حال ، Fibonacci می تواند از نظر ریاضی به عنوان یک عملکرد بازگشتی خطی برای یافتن محدوده بالایی محکم نشان داده شود.
فرمول بازگشتی برای نوشتن اعداد فیبوناچی به صورت ریاضی
معادله بازگشتی یک عدد فیبوناچی t (n) = t (n-1)+t (n-2)+o (1) است. این امر به این دلیل است که زمان لازم برای محاسبه فیبر (N) برابر با مقدار زمانی است که ما برای محاسبه FIB (N-1) و FIB (N-2) می گیریم.
بنابراین ، ما همچنین باید زمان ثابت را در این مورد بگنجانیم. فیبوناچی اکنون به این صورت تعریف شده است:
محاسبه پیچیدگی زمان به یادگار
بیایید پیچیدگی زمان n را به عنوان t (n) ، از این رو t (n) = t (n-1) + t (n-2) انجام دهیم.
این امر به این دلیل است که F ( N-2) در هنگام محاسبه F (N - 1) در حافظه پنهان قرار دارد. بنابراین ، عملکرد f (n-2) 1 (خواندن از معادله ذخیره شده) ، از این رو t (n) = t (n-1) + 1 = t (n-2) + 2 =.= t (n-n) + n.
t (0) 1 به عنوان t (n) = o (n + 1) = o (n) در نظر گرفته می شود.
نتیجه
در یک توالی فیبوناچی ، نسبت دو عدد فیبوناچی پی در پی نزدیک به مقدار نسبت طلایی 1. 618034 است.
دو روش مختلف برای یافتن توالی فیبوناچی رابطه بازگشتی و روش نسبت طلایی است.
شما می توانید از دانش به دست آمده از این مقاله استفاده کنید تا برنامه خود را بهتر ساخت و در نتیجه پیچیدگی زمان را کاهش دهید.