Recursion (רקורסיה)
עודכן לאחרונה
רקורסיה היא פונקציה שקוראת לעצמה על גרסה קטנה יותר של אותה בעיה, עד שהיא מגיעה למקרה קטן מספיק כדי לענות עליו ישירות. המקרה הזה, שאפשר לענות עליו ישירות, הוא מקרה הבסיס (תנאי העצירה), וכל פונקציה רקורסיבית צריכה אחד כזה: fib(n) ממשיכה להתפצל ל-fib(n - 1) ול-fib(n - 2) עד שהיא מגיעה ל-fib(1) או ל-fib(0), שפשוט מחזירות את עצמן. ההדמיה למעלה מריצה בדיוק את זה: לחצו על הפעלה וראו את הקריאות מתפצלות לעץ, מגיעות למקרי הבסיס בעלים, ואז מחזירות את הערכים שלהן למעלה ומשתלבות בכל רמה.
הדבר השני שהאנימציה מראה הוא מחסנית הקריאות: כל קריאה שהתחילה ועוד לא חזרה. המחסנית גדלה ככל שהקריאות מעמיקות, מגיעה לשיא בעומק הרקורסיה, ומתפרקת כשהתוצאות חוזרות, ולכן רקורסיה עמוקה עלולה לגרום לגלישת מחסנית, ואילו לולאה איטרטיבית אף פעם לא מגדילה את המחסנית. אותה צורת קריאות מניעה את החיפוש לעומק, את מיון המיזוג ואת רוב הפעולות על עץ בינארי.
סיבוכיות זמן וזיכרון
עבור פיבונאצ'י רקורסיבי נאיבי שמוצג למעלה, ושני התיקונים הסטנדרטיים:
| גישה | זמן | זיכרון | הערות |
|---|---|---|---|
| רקורסיה נאיבית | O(2^n) | O(n) | עץ הקריאות מוכפל בכל רמה; הזיכרון הוא המחסנית העמוקה ביותר, לא העץ כולו. |
| עם memoization | O(n) | O(n) | כל fib(k) מחושב פעם אחת ונשמר במטמון; תתי עצים חוזרים הופכים לחיפושים. |
| לולאה איטרטיבית | O(n) | O(1) | שני משתנים מתגלגלים מחליפים את המחסנית לגמרי. |
| כל רקורסיה, באופן כללי | קריאות × עבודה לכל קריאה | O(max depth) | המחסנית מחזיקה מסגרת אחת לכל קריאה שהתחילה ולא חזרה. |
צעד אחר צעד
| צעד | מה קורה |
|---|---|
| 1 | הקריאה הראשונה fib(n) נכנסת למחסנית הקריאות. |
| 2 | היא צריכה את fib(n - 1), ולכן גם הקריאה הזו נכנסת למחסנית; ההורה מחכה. |
| 3 | הקריאות ממשיכות להתקנן עד שאחת מהן שואלת על n <= 1: מקרה הבסיס עונה מיד, בלי קריאה עמוקה יותר. |
| 4 | הערך של מקרה הבסיס חוזר להורה שלו, שיכול עכשיו להתחיל את הקריאה השנייה שלו, fib(n - 2). |
| 5 | כששני הילדים חזרו, ההורה מחבר אותם וחוזר גם הוא; המסגרת שלו יוצאת מהמחסנית. |
| 6 | החזרות נמשכות במעלה העץ עד שהמסגרת של הקריאה הראשונה יוצאת עם התשובה הסופית והמחסנית ריקה. |
דוגמה מפורטת
חישוב fib(4) לפי סדר הקריאות המדויק, כמו שהאנימציה מציגה אותו:
| קריאה | המחסנית באותו רגע | מחזירה |
|---|---|---|
fib(4) | fib(4) | מחכה לילדים |
fib(3) | fib(4) > fib(3) | מחכה לילדים |
fib(2) | fib(4) > fib(3) > fib(2) | מחכה לילדים |
fib(1) | fib(4) > fib(3) > fib(2) > fib(1) | 1 (מקרה בסיס) |
fib(0) | fib(4) > fib(3) > fib(2) > fib(0) | 0 (מקרה בסיס) |
fib(2) משלבת | fib(4) > fib(3) > fib(2) | 1 + 0 = 1 |
fib(1) | fib(4) > fib(3) > fib(1) | 1 (מקרה בסיס) |
fib(3) משלבת | fib(4) > fib(3) | 1 + 1 = 2 |
fib(2) שוב | fib(4) > fib(2) | 1, מחושב מחדש מאפס |
fib(4) משלבת | fib(4) | 2 + 1 = 3 |
מתי להשתמש ברקורסיה
| השתמשו בה כאשר | הימנעו ממנה כאשר |
|---|---|
| הבעיה דומה לעצמה: עצים, מבנים מקוננים, הפרד ומשול | לולאה פשוטה מבטאת את אותו דבר בלי מסגרות מחסנית |
העומק חסום וצנוע, כמו O(log n) במיון מיזוג | העומק יכול להגיע לגודל הקלט על קלטים ענקיים, עם סיכון לגלישת מחסנית |
| backtracking צריך את המחסנית כדי לזכור מאיפה להמשיך | אותן תת בעיות חוזרות ואתם לא שומרים אותן במטמון |
| הגרסה הרקורסיבית בבירור קלה יותר לקריאה ולאימות | אתם בתוך לולאה חמה שבה תקורת הקריאות משפיעה באופן מדיד |
קוד Recursion
מימוש נקי של Recursion שאפשר להריץ, ב-Python, JavaScript, Java, C++, C. בחרו שפה, העתיקו את הקוד, או פתחו אותו טעון מראש בעורך האונליין של Coddy.
קוד Recursion ב-Python
1calls = 02
3def fib(n, depth=0):4 global calls5 calls += 16 # Print the call with its depth so the recursion is visible7 print(" " * depth + f"fib({n})")8 if n <= 1:9 return n10 return fib(n - 1, depth + 1) + fib(n - 2, depth + 1)11
12
13print("fib(5) =", fib(5))14print("calls made:", calls)קוד Recursion ב-JavaScript
1let calls = 0;2
3function fib(n, depth = 0) {4 calls += 1;5 // Print the call with its depth so the recursion is visible6 console.log(' '.repeat(depth) + `fib(${n})`);7 if (n <= 1) return n;8 return fib(n - 1, depth + 1) + fib(n - 2, depth + 1);9}10
11console.log('fib(5) =', fib(5));12console.log('calls made:', calls);קוד Recursion ב-Java
1public class Main {2 static int calls = 0;3
4 static int fib(int n, int depth) {5 calls++;6 // Print the call with its depth so the recursion is visible7 System.out.println(" ".repeat(depth) + "fib(" + n + ")");8 if (n <= 1) return n;9 return fib(n - 1, depth + 1) + fib(n - 2, depth + 1);10 }11
12 public static void main(String[] args) {13 System.out.println("fib(5) = " + fib(5, 0));14 System.out.println("calls made: " + calls);15 }16}קוד Recursion ב-C++
1#include <iostream>2#include <string>3
4int calls = 0;5
6int fib(int n, int depth) {7 calls++;8 // Print the call with its depth so the recursion is visible9 std::cout << std::string(depth * 2, ' ') << "fib(" << n << ")\n";10 if (n <= 1) return n;11 return fib(n - 1, depth + 1) + fib(n - 2, depth + 1);12}13
14int main() {15 int result = fib(5, 0);16 std::cout << "fib(5) = " << result << "\n";17 std::cout << "calls made: " << calls << "\n";18 return 0;19}קוד Recursion ב-C
1#include <stdio.h>2
3int calls = 0;4
5int fib(int n, int depth) {6 calls++;7 /* Print the call with its depth so the recursion is visible */8 printf("%*sfib(%d)\n", depth * 2, "", n);9 if (n <= 1) return n;10 return fib(n - 1, depth + 1) + fib(n - 2, depth + 1);11}12
13int main(void) {14 printf("fib(5) = %d\n", fib(5, 0));15 printf("calls made: %d\n", calls);16 return 0;17}שאלות נפוצות על רקורסיה
מהו מקרה בסיס ברקורסיה?
fib(n) הוא n <= 1, שמחזיר את n ישירות. בלי מקרה בסיס שאפשר להגיע אליו, הקריאות לא נעצרות, המחסנית ממשיכה לגדול, והתוכנית קורסת עם גלישת מחסנית.מהי מחסנית הקריאות ולמה היא חשובה?
n רמות משתמשת ב-O(n) זיכרון גם אם כל קריאה כמעט לא עושה עבודה. שורת התגיות מתחת לאנימציה מראה בדיוק את המחסנית הזו גדלה ומתפרקת.למה פיבונאצ'י רקורסיבי לוקח זמן אקספוננציאלי?
fib(2) מחושב פעמיים בתוך fib(4), והכפילות מוכפלת בערך בכל רמה, מה שנותן O(2^n) קריאות. שמירה של כל תוצאה במטמון בפעם הראשונה שהיא מחושבת, מה שנקרא memoization, מכווצת את העץ ל-O(n).