Smart-World Surf

יחידה 7: רקורסיה

פתרון בעיות באמצעות רקורסיה, כולל דוגמאות מתקדמות ו-Backtracking.

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

יסודות הרקורסיה

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

רקורסיה (Recursion): טכניקת תכנות שבה פונקציה קוראת לעצמה כדי לפתור בעיה על ידי פירוקה לתת-בעיות קטנות ודומות.

מרכיבי פתרון רקורסיבי

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

כיצד פועלת רקורסיה? מחסנית הקריאות

מאחורי הקלעים, מערכת ההפעלה (או ה-JVM במקרה של Java) משתמשת במחסנית קריאות (Call Stack) כדי לנהל קריאות לפונקציות. בכל פעם שפונקציה נקראת, "מסגרת מחסנית" (Stack Frame) חדשה נוצרת ונשמרת במחסנית. מסגרת זו מכילה את המשתנים המקומיים של הפונקציה, פרמטרים וכתובת החזרה. כאשר פונקציה רקורסיבית קוראת לעצמה, נוצרת מסגרת חדשה עבור הקריאה הפנימית, והיא נדחפת לראש המחסנית. רק כאשר הקריאה העליונה במחסנית מסתיימת, היא נשלפת, והביצוע חוזר לקריאה הקודמת.

רקורסיה

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

איטרציה (לולאות)

פתרון באמצעות לולאות (for, while). לרוב יעיל יותר מבחינת זיכרון וזמן ריצה. לעיתים קוד ארוך ומסורבל יותר עבור בעיות מורכבות מסוימות.

ניתוח ופתרון בעיות רקורסיביות

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

סיבוכיות זמן ומקום

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

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

דוגמאות מתקדמות ו-Backtracking

מעבר לדוגמאות קלאסיות כמו חישוב עצרת (Factorial) או סדרת פיבונאצ'י (Fibonacci), רקורסיה משמשת לפתרון בעיות מורכבות יותר, במיוחד כאלה הכרוכות בחיפוש במרחב מצבים.

Backtracking (נסיגה)

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

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

דוגמאות ל-Backtracking

  • בעיית N-מלכות (N-Queens Problem): הצבת N מלכות על לוח שחמט בגודל N×N כך שאף מלכה לא מאיימת על מלכה אחרת. אלגוריתם Backtracking ינסה להציב מלכה בכל עמודה, ואם מיקום מסוים מוביל למצב לא חוקי, הוא יחזור אחורה וינסה מיקום אחר.
  • פתרון סודוקו: מילוי לוח סודוקו על ידי ניסיון להציב מספרים בתאים הריקים. אם מספר מסוים מפר את חוקי הסודוקו, חוזרים אחורה ומנסים מספר אחר.
  • מציאת כל הפרמוטציות/קומבינציות: יצירת כל הסידורים האפשריים או כל תתי-הקבוצות של קבוצת איברים.

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

שאלות לדיון

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

נקודות לתשובת מודל

  • רקורסיה מול איטרציה: רקורסיה עדיפה לבעיות בעלות מבנה טבעי רקורסיבי (כמו עץ, גרף, או בעיות "הפרד ומשול"), כאשר הקוד הופך אלגנטי וקריא יותר. איטרציה עדיפה כאשר יש צורך בביצועים אופטימליים (פחות תקורה של מחסנית) או כאשר הבעיה אינה מתאימה באופן טבעי לרקורסיה.
  • הבטחת סיום רקורסיה: יש להגדיר מקרה בסיס ברור ונגיש, ולוודא שבכל צעד רקורסיבי הקלט מתקדם באופן עקבי לעבר מקרה הבסיס (לדוגמה, קלט קטן יותר, או מצב שונה).
  • תפקיד מחסנית הקריאות: המחסנית שומרת את מצב הביצוע של כל קריאה לפונקציה (משתנים מקומיים, פרמטרים, כתובת חזרה). ברקורסיה, היא מאפשרת לפונקציה "לזכור" היכן להמשיך לאחר שקריאה פנימית יותר מסתיימת, ומבטיחה סדר ביצוע נכון.
  • בעיית Backtracking: לדוגמה, בעיית מציאת כל הדרכים לצאת ממבוך. ניישם על ידי ניסיון ללכת לכל כיוון אפשרי (למעלה, למטה, ימינה, שמאלה) מכל נקודה. אם נתקלים בקיר או בדרך ללא מוצא, נסמן את הדרך כ"נסתרת" ונחזור אחורה (backtrack) לנקודה הקודמת כדי לנסות כיוון אחר.
מצאתם טעות או שחסר משהו?
→ הקודמת
פולימורפיזם וממשקים
הבאה ←
יעילות וסיבוכיות