Menu
Coddy logo textTech

Recursion (רקורסיה)

עודכן לאחרונה

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

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

סיבוכיות זמן וזיכרון

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

גישהזמןזיכרוןהערות
רקורסיה נאיביתO(2^n)O(n)עץ הקריאות מוכפל בכל רמה; הזיכרון הוא המחסנית העמוקה ביותר, לא העץ כולו.
עם memoizationO(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

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)
להריץ את הקוד הזה בעורך ה-Python אונליין

שאלות נפוצות על רקורסיה

מהו מקרה בסיס ברקורסיה?
הקלט שקטן מספיק כדי לענות עליו בלי קריאה רקורסיבית נוספת. עבור fib(n) הוא n <= 1, שמחזיר את n ישירות. בלי מקרה בסיס שאפשר להגיע אליו, הקריאות לא נעצרות, המחסנית ממשיכה לגדול, והתוכנית קורסת עם גלישת מחסנית.
מהי מחסנית הקריאות ולמה היא חשובה?
סביבת הריצה שומרת מסגרת אחת לכל קריאה שהתחילה ועוד לא חזרה, עם הארגומנטים והמשתנים המקומיים שלה. עומק הרקורסיה שווה לגובה המחסנית, ולכן רקורסיה שיורדת n רמות משתמשת ב-O(n) זיכרון גם אם כל קריאה כמעט לא עושה עבודה. שורת התגיות מתחת לאנימציה מראה בדיוק את המחסנית הזו גדלה ומתפרקת.
למה פיבונאצ'י רקורסיבי לוקח זמן אקספוננציאלי?
כי אותן תת בעיות מחושבות שוב ושוב: בדוגמה המפורטת למעלה, fib(2) מחושב פעמיים בתוך fib(4), והכפילות מוכפלת בערך בכל רמה, מה שנותן O(2^n) קריאות. שמירה של כל תוצאה במטמון בפעם הראשונה שהיא מחושבת, מה שנקרא memoization, מכווצת את העץ ל-O(n).
האם רקורסיה עדיפה על איטרציה?
אף אחת מהן לא עדיפה תמיד. כל רקורסיה אפשר לכתוב מחדש כלולאה עם מחסנית מפורשת, וכל לולאה כרקורסיה. רקורסיה מנצחת בקריאות עבור בעיות שדומות לעצמן, כמו מעבר על עץ וחיפוש לעומק; איטרציה מנצחת בזיכרון ובתקורת קריאות עבור מעברים ליניאריים.
מה גורם לגלישת מחסנית בפונקציה רקורסיבית?
או מקרה בסיס חסר או כזה שאי אפשר להגיע אליו, כך שהקריאות לא נעצרות, או רקורסיה נכונה שהעומק שלה פשוט גדול מדי למגבלת המחסנית של סביבת הריצה, כמו קריאה רקורסיבית אחת לכל איבר בקלט של מיליונים. הפתרונות הם להבטיח את מקרה הבסיס, להגביל את העומק או להמיר לאיטרציה.
אילו אלגוריתמים רקורסיביים באופן טבעי?
מיונים מסוג הפרד ומשול כמו מיון מיזוג ו-quicksort, מעברים על עץ בינארי ועל גרפים, חיפוש בינארי, חידות backtracking כמו N-queens, וכל דבר שמוגדר על מבנה מקונן, כמו JSON או מערכת קבצים.
איור של שפות התכנות ב-Coddy

לשלוט באלגוריתמים עם Coddy

להתחיל