בניגוד לפרקים אחרים בהם אנו מכירים מרכיבים שונים של השפה, פרק זה אינו מלמד היבט חדש של השפה, אלא אוֹפנוּת כתיבת תוכניות. רקורסיה הינה דרך לפתרון בעיות. במילים אחרות זוהי תכונה אפשרית של אלגוריתם: אלגוריתם עשוי להיות רקורסיבי, ועשוי להיות לא רקורסיבי. ישנן בעיות שניתן לפתור בנוחות באמצעות אלגוריתמים רקורסיביים.
רקורסיה הינה אחד הנושאים היותר קשים להבנה והטמעה ע"י סטודנטים. על כן, אם אתם נתקלים בקשיים מרובים בהבנת הנושא התייחסו לכך כאל המצב הצפוי, הטבעי, חרקו שיניים, וקוו לטוב...
בסעיף זה נציג מספר פתרונות רקורסיביים לבעיות פשוטות. הבעיות שנציג ניתנות לפתרון פשוט וקל גם באמצעות אלגוריתמים לא רקורסיביים, ועל-כן ראוי לפתרן ללא שימוש ברקורסיה. הסיבה להצגת הבעיות והפתרונות המופיעים בסעיף זה היא לסייע לכם לרכוש את צורת החשיבה הרקורסיבית. כפי שתווכחו כל לולאה ניתן להמיר ברקורסיה, זה לא אומר שכל לולאה גם ראוי להמיר ברקורסיה. מניסיוני אני יודע שתלמידים שרכשו בדִי עמל את צורת החשיבה הרקורסיבית ממהרים לעיתים יתר על המידה להשתמש בה. אל תלקו בהרגל לא רצוי זה.
כתבו תכנית הכוללת את הפונ': unsigned int my_div( unsigned int divided, unsigned int divider) על הפונ' לחשב את המנה הטבעית: divided/divider תוך שימוש בפעולות חיבור וחיסור בלבד (בלי להשתמש בפעולות אריתמטיות אחרות). הפונ' גם לא תעשה שימוש בשום פקודות לולאה שהיא, כלומר עליה להיות רקורסיבית.. לדוגמה: עבור 8/10 תוחזר המנה 0, ועבור 38/10 תוחזר המנה 3.
כתבו תכנית הכוללת פונק' המציגה את כל מחלקיו של מספר טבעי num. הנחיה: הפונ' תקבל (מעבר ל: num) פרמטר נוסף div . בזימון הראשון לפונ' יועבר ל: div הארגומנט 1. הרקורסיה תעצור עת div גדול יותר מחצי num. כל קריאה רקורסיבית תבדוק האם ערכו הנוכחי של div מחלק את num ואם כן תדפיסו, וכן תזמן קריאה רקורסיבית עם ... (השלימו בכחות עצמכם).
כתבו את הפונ' ההפוכה לזו שהוצגה מעל: הפונ' תקבל מספר בבסיס 2, ותחזיר אותו בבסיס 10. למשל: אם יועבר לה 110 היא תחזיר 6. כמובן שהפונ' לא תשתמש בכל פקודת לולאה שהיא.
דוגמה זאת מוצגת רק באמצעות טקסט כתוב (ללא סרטון)
כתבו תכנית המגדירה מערך בן חמישה תאים, ומזמנת פונ' read_data הקוראת נתונים למערך. הפונ' לא תכלול כל פקודת לולאה שהיא, כלומר היא תהיה רקורסיבית.
הנחיה: הפונ' תקבל פרט למערך את מספר התא לתוכו עליה לקרוא נתון. הפונ' תקרא נתון לתוך התא הרצוי, ואם תא זה אינו התא האחרון במערך, אזי היא תזמן את עצמה, כדי לקרוא ערך גם לתא הבא במערך. בזימון הראשון לפונ' מהתכנית הראשית יועבר הערך אפס בתור מספר התא לתוכו יש לקרוא (את הנתון הראשון).
עתה התכנית הראשית תקרא מספר נוסף, ותזמן פונ' המחזירה האם הערך כן\לא מצוי במערך (ללא שימוש בלולאה). התכנית הראשית תשלח פלט על סמך ערך ההחזרה של הפונ'.
הנחיה: הפונ' תקבל את המערך, את הערך המבוקש, ואת התא בו הקריאה הנוכחית בודקת האם הערך המבוקש מצוי (בקריאה הראשונה לפונ' מהתכנית הראשית יועבר התא מספר אפס). אם הערך המבוקש מצוי בתא 'הנוכחי' אזי הקריאה הרקורסיבית הנוכחית תחזיר את הערך true. אם התא הנוכחי הוא האחרון במערך (והערך לא נמצא) אזי הקריאה הרקורסיבית הנוכחית תחזיר את הערך false. בכל מקרה אחר: הקריאה הרקורסיבית הנוכחית תזמן קריאה רקורסיבית עם מספרו של התא הבא במערך; והקריאה הנוכחית תחזיר את הערך שהחזירה לה הקריאה המזומנת.
ייחודה של דוגמה זאת בכך שכך קריאה רקורסיבית בה מזמנת שתי קריאות רקוסיביות. על כן גם המעקב אחרי הקוד בעת סימלוצו נעשה מורכב יותר
בניגוד לתכניות שראינו בסעיף הקודם, ואשר ביצעו משימה שניתן ולכן גם ראוי לפתור ברמצעות לולאה. מיון מהיר הוא אלגוריתם שפתרונו הטבעי והנכון הוא באמצעות רקורסיה.
נתחיל בהצגת רעיון האלג'.
נציג עתה את קוד הפונ' שמממשת את המיון
נדון בזמן הריצה של מיון מהיר במקרה הטוב ביותר, ובמקרה הגרוע ביותר
מגדלי האנוי היא בעיה נוספת שנותנת את עצמה לפתרון רקורסיבי קצר ולאגנטי. תכונתה המעניינת הנוספת היא זמן הריצה שלה, שהינו מעריכי, וגם בו נגון במסגרת הדיון שלנו.
מגדלי האנוי היא בעיה נוספת שנותנת את עצמה לפתרון רקורסיבי קצר ולאגנטי. תכונתה המעניינת הנוספת היא זמן הריצה שלה, שהינו מעריכי, וגם בו נגון במסגרת הדיון שלנו.
חשבו איזה טבעת תועבר בצעד המאה, ומאיזה עמוד לאיזה עמוד, עת יש להעביר מגדל בן 7 שעות מעמוד a לעמוד c (תוך שימוש בעמוד b כעמוד העזר)
אם לא די צרות היו לנו עד כה, אזי בסעיף זה נראה דוגמה לבעיות הקרויות בעיות backtracking בהן אנו מנסים לבנות פתרון, ואם מגלים שלא ניתן להשלים את הפתרון החלקי שבנינו עד כה לכדי פתרון מלא, אזי אנו חוזרים בנו, ומנסים כיוון פתרון אחר (קצת כמו העכבר שמתקדם במבוך). לשם כך, בעיות של backtracking מזמנות קריאות רקורסיביות בלולאה, מה שהופך את הבנתן לקשה עוד יותר (ביחס לרקורסיה 'סתם').
אם לא די צרות היו לנו עד כה, אזי בסעיף זה נראה דוגמה לבעיות הקרויות בעיות backtracking בהן אנו מנסים לבנות פתרון, ואם מגלים שלא ניתן להשלים את הפתרון החלקי שבנינו עד כה לכדי פתרון מלא, אזי אנו חוזרים בנו, ומנסים כיוון פתרון אחר (קצת כמו העכבר שמתקדם במבוך). לשם כך, בעיות של backtracking מזמנות קריאות רקורסיביות בלולאה, מה שהופך את הבנתן לקשה עוד יותר (ביחס לרקורסיה 'סתם').
באופן שעשוי להיראות לסטודנטים פרדוקסלי, או מוזר, דווקא תכנית שמציגה פתרון יחיד של בעיית שמונה המלכות הינה מורכבת יותר מאשר תכנית שמציגה את כל הפתרונות. בסעיף זה ארצה להציג תכנית כזאת
עד כאן הצגנו סימולציית הרצה של בעיית שמונה המלכות אשר הציגה את כל הפתרונות האפשריים. באופן פרדוקסלי לכאורה, פתירת בעיית שמונה המלכות עת נדרש פתרון יחיד היא משימה יותר מורכבת. לתלמידים רבים נראה בַתחילה שכל שיש לעשות הוא להוסיף לפונקציה שכתבנו, מייד אחרי הקריאה ל- print, פקודת return. זה פתרון שגוי; ומייד נסביר למה. נבחן למשל את דוגמת ההרצה שהצגנו; ונניח כי הכנסנו את השיפור המוצע. לכן, העותק מסעיף י', מייד אחרי הצגת מצב הלוח, מסיים. הביצוע חוזר לעותק מסעיף ט'. כפי שציינו, בעותק מסעיף ט' ערכו של i הוא אפס. עתה העותק מסעיף ט' צריך לשאול את עצמו: מדוע ובאילו נסיבות הסתיים העותק מסעיף י'--- האם העותק מסעיף י' הסתיים אחרי שהוא הציג פתרון, ולכן אני (העותק מסעיף ט') צריך לסיים כבר עכשיו? או שמא העותק מסעיף י' הסתיים אחרי שהוא נכשל בהצבת מלכה נוספת, ולכן אני (העותק מסעיף ט') צריך לנסות להזיז את המלכה שלי מעט, ולשוב לקרוא רקורסיבית? לעותק מסעיף ט' אין כל דרך לענות על השאלה. הליקוי שגילינו גם רומז על הדרך הנכונה לבצע את המשימה:
א. עת עותק כלשהו של הפונקציה מסיים עליו להחזיר ערך המעיד האם הוא סיים בהצלחה, כלומר האם הוא או הקריאות הרקורסיביות שנולדו ממנו ישירות ובעקיפין הצליחו למלא את הלוח כנדרש, או לא.
ב. עת עותק כלשהו של הפונקציה מחזיר ערך (לעותק שקרא לו), על העותק הקורא לבדוק האם הוחזר איתות על הצלחה בהשלמת המשימה, או הוחזר איתות על חוסר הצלחה. אם הוחזר איתות על הצלחה אזי העותק הנוכחי (אליו חזר הערך) מסיים מיידית, תוך שגם הוא מחזיר איתות על הצלחה למי שקרא לו. מנגד, אם הוחזר איתות על כשלון ימשיך העותק הנוכחי בהרצת הלולאה שלו.
ג. אם העותק הנוכחי השלים את הרצת הלולאה שלו בלי שהוא חזר קודם לכן עם איתות על הצלחה, אזי על העותק הנוכחי להחזיר איתות על כשלון.
בסעיף זה אציג שאלות מבחינות בנושא רקורסיה. בכל שאלה יש לכתוב פונ' אשר מקבלת נתונים כלשהם, ומבצעת עליהם משימה (רקורסיבית).
אני ממליץ לכם לכתוב את הפונ' על דף, ולסמלצן. על דרך השלילה, אני לא ממליץ לכתוב תכנית שלמה אשר כוללת את הפונ', שכן הטרחה בכתיבה של תכנית שלמה, ובהבאתה למצב של ריצה אינו מצדיק, לעניות דעתי, את התועלת הכרוכה בכך (היכולת לבדוק באופן חד-משמעי את הקוד שלכם).