תרגול - 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 2025