→ חזרו אחורה

פרק 10 - רקורסיה

⏱ 15 דקות קריאה

עד עכשיו פתרנו בעיות חוזרות באמצעות לולאות (for ו-while). יש דרך נוספת, אלגנטית לפעמים יותר, לגרום לתוכנית לחזור על פעולה: לגרום למתודה לקרוא לעצמה. הכלי הזה נקרא רקורסיה (Recursion), והוא מופיע גם בשאלות שדורשות מעקב אחר קריאות פונקציה.

1. מהי רקורסיה?

רקורסיה היא מצב שבו מתודה קוראת לעצמה במהלך הריצה שלה, כדי לפתור גרסה קטנה יותר של אותה בעיה. כל קריאה כזו מקרבת אותנו לפתרון, עד שמגיעים למקרה הכי פשוט שאפשר לפתור בלי קריאה נוספת.

אנלוגיה: תארו לעצמכם בובות מטריושקה - כל בובה מכילה בתוכה בובה קצת יותר קטנה, וכן הלאה, עד לבובה הכי קטנה שאין בתוכה כלום. הבובה הכי קטנה היא כמו תנאי העצירה של הרקורסיה - השלב שבו מפסיקים להיכנס פנימה ומתחילים לצאת החוצה.

2. שני חלקים חובה: תנאי עצירה וקריאה רקורסיבית

לכל פונקציה רקורסיבית תקינה חייבים להיות שני חלקים: תנאי עצירה (Base Case) - מקרה פשוט שבו הפונקציה מחזירה תשובה ישירות, בלי לקרוא לעצמה שוב; וקריאה רקורסיבית (Recursive Case) - קריאה לאותה פונקציה עם קלט קטן או פשוט יותר, שמתקדמת לכיוון תנאי העצירה.

Java

static void countDown(int n) {
    if (n == 0) {
        System.out.println("Done");
        return;
    }
    System.out.println(n);
    countDown(n - 1);
}
            

תנאי העצירה כאן הוא n == 0 - כשמגיעים אליו, הפונקציה מדפיסה Done וחוזרת, בלי לקרוא לעצמה שוב. בכל קריאה אחרת, הפונקציה מדפיסה את n וקוראת לעצמה עם n - 1, כלומר מתקדמת צעד אחד לכיוון תנאי העצירה.

קריאה ל-countDown(3) תדפיס לפי הסדר: 3, 2, 1, ואז Done - כל קריאה מטפלת במספר אחד ומעבירה את השאר לקריאה הבאה.

3. עקבו אחרי הקריאות - סכום מ-1 עד n

דוגמה קלאסית נוספת: פונקציה שמחשבת את סכום כל המספרים השלמים מ-1 עד n.

Java

static int sum(int n) {
    if (n == 0) {
        return 0;
    }
    return n + sum(n - 1);
}
            

sum(4) לא יכולה לחשב תשובה מיידית, אז היא קוראת ל-sum(3) וממתינה לתשובה שלה. sum(3) קוראת ל-sum(2), שקוראת ל-sum(1), שקוראת ל-sum(0).

sum(0) מגיעה לתנאי העצירה ומחזירה 0 - בלי קריאה נוספת. עכשיו הקריאות "נפתחות" בסדר הפוך: sum(1) מחזירה 1+0=1, sum(2) מחזירה 2+1=3, sum(3) מחזירה 3+3=6, ו-sum(4) מחזירה 4+6=10.

4. מחסנית הקריאות (Call Stack)

כל קריאה לפונקציה, כולל קריאה רקורסיבית, נשמרת בזיכרון במבנה שנקרא מחסנית הקריאות, עד שהיא מקבלת תשובה וחוזרת. זו הסיבה שרקורסיה בלי תנאי עצירה תקין לא רק "נתקעת" - היא ממלאת את הזיכרון הזמין וגורמת לשגיאת ריצה בשם StackOverflowError.

כל שכבה במחסנית הקריאות "ממתינה" לתשובה מהקריאה הפנימית ממנה, בדיוק כמו שאי אפשר לסגור בובת מטריושקה חיצונית לפני שסוגרים את כל הבובות שבתוכה.

5. רקורסיה מול לולאה

כל בעיה שאפשר לפתור עם רקורסיה אפשר גם לפתור עם לולאה, ולהפך - זו בעיקר שאלה של איזה פתרון קריא וטבעי יותר לבעיה הספציפית. בעיות שמתפרקות באופן טבעי לגרסה קטנה יותר של עצמן (כמו סכום, עצרת, ובהמשך הדרך - עצים ורשימות מקושרות) לרוב קריאות יותר כרקורסיה. כשחשוב לחסוך בזיכרון או בזמן ריצה, פתרון עם לולאה לרוב עדיף.

6. דוגמה נוספת - סדרת פיבונאצי

לפעמים פונקציה רקורסיבית קוראת לעצמה יותר מפעם אחת. דוגמה מוכרת היא סדרת פיבונאצי, שבה כל איבר הוא סכום שני האיברים שלפניו:

Java

static int fib(int n) {
    if (n <= 1) {
        return n;
    }
    return fib(n - 1) + fib(n - 2);
}
            

כאן יש שני מקרים לתנאי העצירה יחד (n == 0 ו-n == 1), ושתי קריאות רקורסיביות בכל שלב - כל קריאה מתפצלת לשתי קריאות קטנות יותר, עד שמגיעים לתנאי העצירה.

7. טעויות נפוצות

שכחתם תנאי עצירה: בלי if שמחזיר ערך ישירות, הפונקציה תקרא לעצמה לנצח, עד ל-StackOverflowError.

הקלט לא מתקדם לכיוון תנאי העצירה: אם הקריאה הרקורסיבית לא מקרבת את הקלט לתנאי העצירה (למשל קריאה ל-sum(n) במקום ל-sum(n - 1)), הרקורסיה לעולם לא תיעצר.

תנאי עצירה במקום הלא נכון: בדיקה של n == 0 כשהמקרה הפשוט ביותר הוא בעצם n == 1 (או להפך) עלולה לדלג על מקרה תקין או לגרום לתוצאה שגויה.

🎓 זווית הבגרות

איך זה מופיע בבגרות: רקורסיה מופיעה בעיקר כשאלת מעקב ("מה מחזירה הקריאה...?"), ובהמשך הדרך היא הבסיס לעבודה עם עצים ורשימות מקושרות (שאלון 899271).

סוג שאלה טיפוסי: מעקב אחרי קריאות רקורסיביות מקוננות (בדיוק כמו sum או fib למעלה) וחיזוי הערך המוחזר בסוף.

טעות נפוצה: לקרוא לפונקציה הרקורסיבית בלי להשתמש בערך שהיא מחזירה - למשל לכתוב רק fib(n - 1); במקום return fib(n - 1) + fib(n - 2);.

מה לזכור למבחן: קודם כתבו את תנאי העצירה, ורק אז את הקריאה הרקורסיבית - וודאו שהקלט בקריאה תמיד קטן/פשוט יותר מהקלט המקורי.

8. תרגול

תרגול קריאה פשוט: מה יודפס למסך על ידי הקוד הבא? (רקורסיה עם קריאה אחת בכל שלב)

Java
static int sum(int n) {
    if (n == 0) {
        return 0;
    }
    return n + sum(n - 1);
}

public static void main(String[] args) {
    System.out.println(sum(3));
}

ותרגול קצת יותר מתקדם - רקורסיה עם שתי קריאות בכל שלב. מה יודפס למסך?

Java
static int fib(int n) {
    if (n <= 1) {
        return n;
    }
    return fib(n - 1) + fib(n - 2);
}

public static void main(String[] args) {
    System.out.println(fib(5));
}

כתבו את שורת הקוד שקוראת לפונקציה countDown באופן רקורסיבי, עם הפרמטר n - 1.

כתבו את שורת ה-return בפונקציה sum, שמחזירה את n ועוד הקריאה הרקורסיבית ל-sum(n - 1).

כתבו את שורת ה-return בפונקציה fib, שמחזירה את סכום שתי הקריאות הרקורסיביות fib(n - 1) ו-fib(n - 2).

פרק 9 - מבוא למחלקות ועצמים פרק 11 - צמתים מקושרים