תרגול - 6 רקורסיות מבוא למדעי המחשב ,בן גוריון 1 רקורסיה פונקציה רקורסיבית היא פונקציה שקוראת לעצמה. פונקציה רקורסיבית מחושבת כמו כל פונקציה אחרת (העברת פרמטרים, משתנים לוקאליים ,תחום הכרה של המשתנים וכו'). מוטיבציה :ישנן בעיות רבות שעבורן פתרון רקורסיבי פשוט יותר מפתרון איטרטיבי. מבוא למדעי המחשב ,בן גוריון 2 רקורסיה דוגמה :1סכום מספרים טבעיים עד n אפשר בלולאה (פתרון איטרטיבי): { )public static int sum(int n ;int ans = 0 { )for (int i = 1; i <= n; i = i + 1 ;ans = ans + i } ;return ans } מבוא למדעי המחשב ,בן גוריון 3 רקורסיה אפשר גם בדרך אחרת :נניח שיש לנו פונקציה אחרת בשם magicשמחזירה את הסכום ).1 + 2+ ... + (n-1 אז sumיכולה להראות כך: { )public static int sum(int n ;int ans = magic(n)+ n ;return ans } אבל ) magic(nמחזירה בדיוק מה ש sum(n-1)-הייתה מחזירה. מבוא למדעי המחשב ,בן גוריון 4 רקורסיה ניתן להגדיר נוסחת נסיגה עבור החישובsum(n) = sum(n-1)+n : … ;int ans = sum(n-1)+ n … מבוא למדעי המחשב ,בן גוריון 5 רקורסיה התוצאה היא פונקציה אחת שקוראת לעצמה: { )sum(int n // stop condition ;1 ;sum(n - 1) + n public static int ;int ans )if (n == 1 = ans else = ans ;return ans } *** מעקב על דוגמת הרצה וציור טבלאות מעקב משתנים. מבוא למדעי המחשב ,בן גוריון 6 רקורסיה שלושת הכללים לבניית פונקציה רקורסיבית .1תנאי עצירה שניתן לענות עליו ללא קריאה רקורסיבית. אם לא נשים תנאי עצירה התוכנית עלולה להיכנס ללולאה אינסופית. .2קריאה רקורסיבית עם קלט הקרוב יותר לתנאי העצירה ("הקטנת הבעיה"). אם לא מקטינים את הבעיה לא נגיע לתנאי העצירה ,כלומר שוב תהיה לולאה אינסופית. .3שימוש בתוצאת הקריאה הרקורסיבית לחישוב התוצאה המוחזרת (הנחת האינדוקציה). מבוא למדעי המחשב ,בן גוריון 7 דוגמה – 2משולש פסקל תזכורת :משולש פסקל הוא סידור של מספרים בצורת משולש ,הנבנה באופן הבא: הקודקוד העליון של משולש זה מכיל את המספר ,1וכל מספר במשולש מהווה את סכום שני המספרים שנמצאים מעליו (המספרים שנמצאים על שוקי המשולש הם כולם .)1 n 1 0 1 1 1 1 2 1 2 ,נותן את nבשורה הm -המספר ה- 1 3 3 1 3 התשובה לשאלה "בכמה דרכים מתוך 1 4 6 4עצמים4 1 mשונות אפשר לבחור 1 5 ( "10 10 5 1 5 מקדם בינומי)n . עצמים? 0 1 2 3 4 5 m מבוא למדעי המחשב ,בן גוריון 8 דוגמה – 2משולש פסקל נכתוב פונקציה רקורסיבית ) pascal(int n, int mשתחשב את המספר המופיע בשורה nובעמודה mבמשולש פסקל. תאור האלגוריתם • תנאי עצירה :אם mהוא 0נחזיר ערך .1אם n=mנחזיר ערך .1 • חוקיות הקלט n :ו m -הם שלמים אי-שליליים .אם m>nנציין שיש שגיאה בקלט. • קריאות רקורסיביות על קלט קטן יותר: קריאה אחת עם n-1ו( m -המספר מעליו) קריאה שנייה עם n-1ו ( m-1המספר מעל ומשמאל) • שילוב התוצאות לקבלת תשובה :החזרת סכום של הערכים שהתקבלו משתי הקריאות הרקורסיביות. מבוא למדעי המחשב ,בן גוריון 9 – משולש פסקל2 דוגמה // Pascal number in row n and column m. public static int pascal(int n, int m){ int ans; if (m<0 || n<0 || m>n) { ans = -1; } else if (m==0 || n == m) { ans = 1; } else { ans = pascal(n-1,m) + pascal(n-1,m-1); } return ans; 10 בן גוריון,מבוא למדעי המחשב } דוגמה -3זוגיים ואי זוגיים המטרה :רוצים לבדוק האם מספר טבעי nהוא זוגי או אי-זוגי באמצעות הפונקציות evenו- ( oddללא פעולות חלוקה ושארית). { )public static boolean even(int n ;boolean ans )if (n == 0 ;ans = true else ;)ans = odd(n - 1 ;return ans } { )public static boolean odd(int n ;boolean ans )if (n == 1 מה קורה כאשר ;ans = true evenמפעילים את else על מספר אי-זוגי ;)ans = even(n - 1 גדול מ ?0 ;return ans מבוא למדעי המחשב ,בן גוריון } 11 זוגיים ואי זוגיים-3 דוגמה :נסיון שני public static boolean even(int n) { boolean ans; if (n == 0) ans = true; else ans = odd(n - 1); return ans; } public static boolean odd(int n) { boolean ans; if (n == 0) ans = false; else ans = even(n - 1); return ans; } 12 בן גוריון,מבוא למדעי המחשב :רקורסיה הדדית even קוראת לעצמה דרךodd - ו,odd קוראת לעצמה דרך even . דוגמא 1 נכתוב פונקציה רקורסיבית שמציירת משולש הפוך בגובה של nשורות. לדוגמא ,עבור n=7נקבל את המשולש: דרך פעולה: נדפיס שורת כוכביות באורך ,nואז נדפיס משולש בגובה .n-1 תנאי העצירה יהיה כאשר נגיע להדפיס משולש בגובה .0 ******* ****** ***** **** *** ** * 13 :והפתרון public static void drawTriangle(int n) { int i; if (n > 0) { for (i=0; i<n; i=i+1) System.out.print('*'); System.out.println(); drawTriangle(n-1); } } 14 *** ** * :n=3 עבור המקרה בו קוראים לפונקציה עם !כן ?רקורסית זנב ??מה היה קורה אם היינו רוצים להדפיס משולש ישר ,n-1 עםdrawTriangle -היינו צריכים קודם לקרוא ל :n ואז להדפיס שורה של כוכביות באורך ******* ****** ***** **** *** ** * public static void drawTriangle(int n) { int i; if (n > 0) { drawTriangle(n-1); for (i=0; i<n; i=i+1) System.out.print('*'); System.out.println(); } } 15 :אם נשלב את שני החלקים public static void drawHourglass (int n) { int i; if (n > 0) { for (i=0; i<n; i=i+1) System.out.print('*'); System.out.println(); drawHourGlass(n-1); for (i=0; i<n; i=i+1) System.out.print('*'); System.out.println(); } } 16 :נקבל ***** **** *** ** * * ** *** **** ***** דוגמא 2 כתבו פונקציה רקורסיבית שמקבלת מחרוזת ומחזירה trueאם היא פָּ ִלינְ ְדרֹום ו false -אחרת. תזכורת :פָּ ִלינְ ְדרֹום היא מחרוזת שניתן לקרוא משני הכיוונים ,משמאל לימין ומימין לשמאל, ולקבל אותה תוצאה. לדוגמא :המחרוזות "ארון קיר היה ריק נורא"" ,ילד כותב בתוך דלי" ו madam -הן פָּ ִלינְ ְדרֹום ,לעומת זאת המחרוזת helloאינה פָּ ִלינְ ְדרֹום. הנחת יסוד :מחרוזת ריקה (ללא תווים) ומחרוזת בעלת תו אחד הן פָ ִלינְ ְדרֹום. 17 דוגמא – 2דרך פעולה דרך פעולה: מקרה הבסיס: אם מדובר במחרוזת ריקה (ללא תווים) או במחרוזת בעלת תו אחד ניתן להחזיר .true אחרת ,נבדוק עבור מחרוזת קטנה יותר (הקטנת הבעיה) ע"י צמצום המחרוזת בתו אחד מכל צד. אם הפעלת הפונקציה הרקורסיבית על המחרוזת המוקטנת תחזיר trueוגם שני התווים הקיצוניים שהורדנו מהמחרוזת המקורית שווים נחזיר ,trueאחרת נחזיר .false 18 :והפתרון public static boolean isPalindrome(String pal) { boolean isPal = false; int length = pal.length(); if (length == 0 || length == 1) // can be “if (length <= 1)” instead isPal = true; else { isPal = (pal.charAt(0)==pal.charAt(length-1) && isPalindrome(pal.substring(1,length-1))); } return isPal; } 19 בעיית )SUSU( Subset Sum • בהנתן מערך של משקולות ומשקל נוסף ,נרצה לבדוק האם ניתן להרכיב מהמשקולות משקל השווה למשקל הנתון. • דוגמא לקלט: }weights={1,7,9,3 Sum = 12 • במקרה זה הפונקציה תחזיר trueכי ניתן לחבר את המשקולות 9ו 3ולקבל את הסכום .12 • מה יוחזר עבור ?sum=15 • תשובהfalse : 20 אסטרטגיית הפתרון נעבור על כל האיברים במערך ונבחן אפשרויות בהן איבר נבחר או לא נבחר. נתבונן באיבר הראשון במערך .ייתכן שהוא ייבחר לקבוצת המשקולות שתרכיב את sumויתכן שלא. • אם הוא לא ייבחר – אזי נותר לפתור בעיה קטנה יותר והיא :האם ניתן להרכיב את הסכום sumמבין המשקולות שבתאים ]weights[1,…,n-1 • אם הוא ייבחר – אזי נותר לפתור בעיה קטנה יותר והיא :האם ניתן להרכיב את הסכום ] sum-weight[0מבין המשקולות שבתאים ].weights[1,…, n-1 כנ"ל לגבי יתר האיברים בצורה רקורסיבית. 21 אסטרטגיית הפתרון – המשך נעבור על כל האיברים במערך ונבחן אפשרויות בהן איבר נבחר או לא נבחר. קיימים שני מקרי בסיס: .1הגענו לסכום הדרוש או במילים אחרות הסכום הנותר הינו אפס. .2הגענו לסוף המערך – עברנו על כל האיברים ולא מצאנו צירוף של איברים שסכומם שווה לסכום הנדרש. 22 פתרון פתרון זה קל להציג כפונקציה רקורסיבית: )calcWeights(int[] weights, int i, int sum אשר מקבלת בנוסף על sumו weightsפרמטר נוסף iשמייצג את המשקולת שבודקים כעת ומחזירה תשובה לשאלה :האם ניתן להרכיב את הסכום מבין קבוצת המשקולות שבתת המערך ].weights[i…weights.length public static boolean { )calcWeights(int[] weights, int sum ;)return calcWeights(weights, 0, sum } 23 )פתרון (המשך public static boolean calcWeights(int[] weights, int i, int sum) { boolean res = false; if (sum == 0) res = true; else if (i >= weights.length) res = false; else res =( calcWeights(weights,i+1,sum-weights[i]) || calcWeights(weights, i + 1, sum) ); return res; } 24 דוגמא 4 מטריצה תקרא משופעת אם: .1כל איברי האלכסון הראשי שווים לאפס. .2כל האיברים שנמצאים מתחת לאלכסון הראשי הם שליליים. .3כל האיברים שנמצאים מעל לאלכסון הראשי הם חיוביים. (מניחים כי המימד הראשי מגדיר את השורות והמשני מגדיר את העמודות). מטריצה משופעת: 2 7 4 5 0 1 -2 0 -8 -1 0 3 -3 -9 -6 0 מטריצה לא משופעת: 2 4 1 0 7 5 0 -2 3 6 -8 -1 0 -6 -3 9 25 דוגמא – 4הרעיון נשים לב שאם נחסיר ממטריצה משופעת את השורה הראשונה ואת העמודה הראשונה ,נקבל מטריצה משופעת. 0 4 1 5 0 -2 0 -8 -1 1 4 5 0 0 0 -1 0 -2 -8 נשאר לבדוק האם השורה והעמודה שהחסרנו מקיימות את התנאים. 26 דוגמא – 4המשך נניח שקיימת פונקציה checkהמקבלת מטריצה ריבועית ואינדקס של שורה/עמודה ומחזירה האם השורה והעמודה מקיימות את התנאים. כתבו פונקציה רקורסיבית המקבלת מטריצה מלאה במספרים, ומחזירה trueאם היא משופעת ו false-אחרת: ][][public static boolean slope(int ;)data 27 דוגמא – 4המשך דרך פעולה: בכל שלב של הרקורסיה נתייחס למטריצה הקטנה יותר (נתקדם לאורך האלכסון) ונבדוק אם תת המטריצה משופעת וגם שאר התנאים עבור המטריצה הנוכחית מתקיימים . 4 5 0 1 -2 0 0 -8 -1 5 0 0 -1 0 מהו מקרה הבסיס? ומה נבצע עבורו? תת המערך בגודל 1x1ולכן נבדוק אם מכיל אפס. 28 :והפתרון public static boolean slope(int[][] data) { return slope(data, 0); } public static boolean slope(int[][] data, int index) { boolean isSlope = false; //end of array – last cell, if it’s 0 then it’s OK if (index == data.length - 1) isSlope = (data[index][index] == 0); else isSlope = (check(data,index) && slope(data, index+1)); return isSlope; } 29 –המשך4 דוגמא ?check איך נראת הפונקציה /* Check if row index, in data array, contains positive numbers and column index contains negative numbers (starting from index to the right & down) */ public static boolean check(int[][] data, int index) { boolean flag = (data[index][index] == 0); for (int i=1;(i<data.length-index) && flag; i=i+1) { if (data[index][index+i]<=0 || data[index+i][index]>=0) ניתן לבצע זאת גם flag = false; .בצורה רקורסיבית } ?כיצד return flag; } 30 דוגמה - 5המבוך 31 המבוך • נתון מערך דו-מימדי n x mשל שלמים ,בשם ,grid המתאר מבוך: • נקודת ההתחלה היא התא במיקום ) (0,0במערך, נקודת הסיום היא התא במיקום ),(n-1,m-1 במצב ההתחלתי לכל תא ערך 0או ,1 כאשר 1מסמל "דרך פנויה" ו 0-מסמל "דרך חסומה" (קיר). • הפיתרון הרצוי הוא מסלול רציף מנקודת ההתחלה לנקודת הסיום (מותר ללכת למטה ,ימינה ,למעלה ושמאלה) .את המסלול נסמן בעזרת החלפת ה-1 -ים שעל המסלול ,ב.7 - 32 המבוך -דוגמא • מבוכים: 1110 1011 0001 1101 1110 1010 1110 0001 מבוך ללא פתרון מה יכול להתרחש אם נטייל על המסלול ,ולא נבדוק האם ביקרנו כבר בתא אליו אנו הולכים? • בפיתרון :נסמן ב -ערך 3את התאים שבהם כבר ביקרנו כדי שלא נחזור אליהם שנית. נסמן ב 7-את הדרך מההתחלה אל הסיום. 33 המבוך • בפיתרון :נסמן ב 3-את התאים שבהם כבר ביקרנו כדי שלא נחזור אליהם שנית. נסמן ב 7-את הדרך מההתחלה אל הסיום. • מבוך: 1110110001111 1011101111001 0000101010100 1110111010111 1010000111001 1011111101111 1000000000000 1111111111111 34 המבוך פתרון רצוי: 7770110001111 3077707771001 0000707070300 7770777070333 7070000773003 7077777703333 7000000000000 7777777777777 35 איך חושבים על זה בצורה רקורסיבית? • ננסה להסתכל מהתא הנוכחי על התאים השכנים ולראות אם יש דרך מאחד מהן. • אם כן – הדרך מהתא הנוכחי היא הדרך המתקבלת ע"י הוספת התא הנוכחי לתחילת הדרך שבה ניתן להגיע מהתא השכן. • המשימה שלנו היא למצוא מעבר מנקודת ההתחלה לנקודת הסיום ,אך האלגוריתם שנתאר אינו מתחיל דווקא בנקודת ההתחלה ,אלא ימצא את הדרך ליציאה מכל נקודה במבוך. )solve(int[][] grid, int row, int col 36 תיאור האלגוריתם • הקריאה הראשונה לאלגוריתם תעביר כקלט את הכניסה בתור שורה 0וטור .0 )solve(grid, 0, 0 • האלגוריתם נעזר בפונקציה )valid(grid, row, col המקבלת כקלט מבוך ,שורה וטור, ומחזירה ערך בוליאני – trueאםם התא במבוך המוגדר ע"י השורה והטור הוא תא חוקי. • תא חוקי = לא חורג מגבולות המבוך ,פנוי ולא ביקרנו בו כבר (כלומר ,מסומן ב.)1- 37 תיאור )solve(grid, row, col .1הגדר משתנה בוליאני. .2אם ( )) valid(grid, row, col .2.1סימון שביקרנו בתא ואין צורך לבקר בו שוב. .2.2בדיקת תנאי עצירה. .2.3אחרת, .2.3.1נסה למטה. .2.3.2נסה ימינה. .2.3.3נסה למעלה. .2.3.4נסה שמאלה. .2.4אם doneהינו trueסמן שתא זה הוא חלק מהפתרון. .3החזר את .done 38 תיאור )solve(grid, row, col /* done=false */ .1הגדר משתנה בוליאני. .2אם ( )) valid(grid, row, col .2.1סימון שביקרנו בתא ואין צורך לבקר בו שוב. /* grid[row][col]=3 */ .2.2בדיקת תנאי עצירה. */אם rowהיא השורה התחתונה וגם colהוא הטור הימיני ב ,grid -אז /*done=true .2.3אחרת, /*done=solve(grid, row+1, col) */ .2.3.1נסה למטה. /*done=solve(grid, row, col+1) */ .2.3.2נסה ימינה. /*done=solve(grid, row-1, col) */ .2.3.3נסה למעלה. .2.3.4נסה שמאלה/*done=solve(grid, row, col-1) */ . .2.4אם doneהינו trueסמן שתא זה הוא חלק מהפתרון. /*grid[row][col]=7 */ 39 .3החזר את .done public static boolean solve(int[][] grid, int row, int col) { boolean done = false; if (valid(grid, row, col)) { grid[row][col] = 3; // mark visted if ((row == grid.length-1) && (col == grid[0].length-1)) done = true; // maze is solved else { done=( solve(grid, row + 1, col) || // try down solve(grid, row, col+1) || // try right solve(grid, row-1, col) || // try up solve(grid, row, col-1) ); // try left } if (done) grid[row][col] = 7; // mark as part of the path } return done; } 40 דוגמת הרצה • נק' התחלה נסה למטה נסה למטה נסה ימינה נסה למעלה נסה שמאלה חזור צעד אחד אחורה נסה ימינה ... 33 1110110001111 3 1011101111001 0000101010100 1110111010111 1010000111001 1011111101111 1000000000000 1111111111111 41 • בשלב כלשהו בריצה ,אנו נמצאים בקריאה אשר סימנה ב 3-את התא המודגש)row=3, col=6( : 3330110001111 3033301111001 0000301010100 1110333010111 1010000111001 1011111101111 1000000000000 1111111111111 42 • בשלב כלשהו בריצה ,אנו נמצאים בקריאה אשר סימנה ב 3-את התא המודגש)row=3, col=6( : 3330110001111 3033301111001 0000301010100 1110333010111 1010000111001 1011111101111 1000000000000 1111111111111 • בשלב זה תהיה קריאה רקורסיבית על המשבצת מתחתיה ,משבצת המסומנת ב ,0-כלומר קיר .בדיקת ה- validתחזיר falseואותה קריאה תסתיים. 43 • תתבצע קריאה נוספת מהמשבצת המסומנת ,הפעם ימינה ,ובדיקת ה valid-תיכשל שוב. 3330110001111 3033301111001 0000301010100 1110333010111 1010000111001 1011111101111 1000000000000 1111111111111 44 • בקריאה הבאה מהמשבצת המסומנת ,הפעם למעלה, בדיקת ה valid-תחזיר true 3330110001111 3033301111001 0000301010100 1110333010111 1010000111001 1011111101111 1000000000000 1111111111111 45 • הקריאה הרקורסיבית במשבצת העליונה תסמנה ב3- והריצה תמשיך. 3330110001111 3033301111001 0000303010100 1110333010111 1010000111001 1011111101111 1000000000000 1111111111111 46 • זו התמונה אחרי כמה קריאות .בשלב זה תתבצע קריאה מהמשבצת המסומנת והיא נכשלת .הקריאות חוזרות. נכשלנו :נחזור חזרה בכל קריאה ונחזיר false 3330110001111 3033303331001 0000303030300 1110333030333 3 1010000133003 1011111103333 1000000000000 1111111111111 • הקריאה הבאה מתבצעת למשבצת משמאל עם הערך .1 47 • בתום הקריאה הרקורסיבית במשבצת המסומנת במרובע ה – gridיראה כך .הקריאה הרקורסיבית במשבצת זו תחזיר ערך .true 3330110001111 7 3033303331001 0000303070300 7770333070333 7070000773003 7077777703333 7000000000000 7777777777777 • במסגרת הקוראת (הקריאה במשבצת המסומנת באדום) תסמן גם 7ותחזיר .trueהמשבצת הימנית לא תיבדק. 48 • בסיום הריצה ,כשהגענו אל היציאה ,הדרך תסומן ב7- והמבוך יראה כך: דיון: האם תמיד יש פתרון אחד? אם לא ,איזה פתרון נמצא? מה היה יכול לקרות אם לא היינו מסמנים כל תא שבדקנו (כאן סימון זה היה המספר ?)3 7770110001111 3077707771001 0000707070300 7770777070333 7070000773003 7077777703333 7000000000000 7777777777777 • ונחזיר true 49 דוגמא 6 כתבו פונקציה רקורסיבית שמקבלת מחרוזת ומחזירה את המחרוזת הפוכה. לדוגמא :עבור הקלט ” “helloהפונקציה תחזיר את הפלט ”.“olleh דרך פעולה: מקרה הבסיס :אם מדובר במחרוזת ריקה (ללא תווים) או במחרוזת בעלת תו אחד ניתן להחזיר את התו עצמו (ובמקרה של ריקה להחזיר ""( אחרת ,נמצא את המחרוזת ההפוכה של מחרוזת קטנה יותר (הקטנת הבעיה) ע"י צמצום המחרוזת בתו אחד השמאלי ,ונוסיף את התו השמאלי מימין למחרוזת ההפוכה שנקבל מהקריאה הרקורסיבית. 50 :והפתרון public static String reverse(String s){ String res = ""; if (s.length()==0) res = s; else res = reverse(s.substring(1)) + s.charAt(0); return res; } .i ממקוםs מחזירה את המחרוזתs.substring(i) הפונקציה:שימו לב 51 )לא! (בכיתה ראינו דוגמא למימוש עם רקורסיית זנב ?רקורסית זנב
© Copyright 2024