Transcript lesson4
קורס תכנות
שיעור רביעי :פונקציות ,מבוא
לרקורסיה
1
לולאות -תזכורת
• לולאה:
• קטע קוד המתבצע שוב ושוב ברצף כל עוד תנאי מסוים מתקיים
• לולאות :for
• כאשר רוצים לבצע את הלולאה בצורה סדרתית:
• תבצע את הלולאה 10פעמים ...
• תבצע את הלולאה nפעמים ...
• לולאות :while
• כאשר רוצים לבצע את הלולאה מספר שרירותי של פעמים:
• כל עוד iזוגי...
• כל עוד לא הייתה טעות בחישוב ...
2
לולאות -תזכורת
• :break
• מסיים (שובר) את ריצת הלולאה
• :continue
• מסיים רק את הסיבוב ( )iterationהנוכחי של הלולאה
3
לולאות do-while
ההבדל בין do-whileל:while-
• הפקודות מבוצעות לפחות פעם אחת
(אפילו אם התנאי לא מתקיים לעולם).
• התנאי נבדק לאחר ביצוע הפקודות.
4
initialize ...
do
{
do something ...
increment ...
}
;) while ( condition
?כיצד מחשבים
#include <stdio.h>
int main()
{
int i = 0;
double sum = 0, result;
for (result = 1, i = 1; i <= 20; ++i)
result = result * 2;
sum += result;
:כיצד מחשבים את
220 + 315 + 517
220 חישוב
for(result = 1, i = 1; i <= 15; ++i)
result = result * 3;
sum += result;
315 חישוב
for(result = 1, i = 1; i <= 17; ++i)
result = result * 5;
sum += result;
517 חישוב
printf("2^20 + 3^15 + 5^17= %g", sum);
return 0;
}
5
מה היינו רוצים?
• בעיות:
• שכפול של קטע קוד כמעט זהה 3פעמים
• אם היו יותר מחוברים היה צריך לשכפל פעמים נוספות
• התכנית מתארכת ונהיית מסורבלת
• מקור לבאגים וטעויות copy / paste
• קשה להבין "למה התכוון המשורר"
• מה היינו רוצים?
• לכתוב פעם אחת בלבד את הקוד
• להשתמש באותו קטע קוד מספר פעמים אבל עם פרמטרים שונים
בכל פעם
6
פונקציות- פתרון
#include <stdio.h>
double power(double base, int exponent)
{
int i = 0;
power הגדרת פונקציה בשם
double result = 1;
exponent
base
המחשבת את
for (i = 1; i <= exponent; i++)
result = result * base;
return result;
}
התוכנית מתחילה לרוץ מכאן
int main()
{
double sum = power(2,20) + power(3,15) + power(5,17);
printf("2^20 + 3^15 + 5^17= %g", sum);
return 0;
}
קריאה
לפונקציה
7
פונקציות
• פונקציה:
• קטע קוד בעל שם
• יכול לקבל ערכי קלט לתוך משתנים
• יכול להחזיר ערך פלט יחיד
• שימושים:
• אפשר לקרוא לפונקציה הרבה פעמים במהלך התוכנית ,קריאה עם
קלט שונה יכולה להניב תוצאה שונה
• מיחזור קוד:
• כותבים פעם אחת ,בודקים פעם אחת ...משתמשים הרבה פעמים
• לדוגמאprintf, scanf :
• בהירות קוד :פיתוח וקריאה (אבסטרקציה)
• עקרון ההכמסה ()encapsulation
8
ושוב ...למה להשתמש בפונקציות?
• חלוקת התכנית לחלקים לוגים שונים
• כל חלק מבצע פעולה בסיסית אחרת
• קריאת נתונים ,עיבוד נתונים ,הדפסה ...
• פירוק בעיה מסובכת לתת בעיות פשוטות יותר -אבסטרקציה
• שימוש חוזר באותו קטע קוד מספר פעמים באותה התוכנית
• שימוש חוזר באותו קטע קוד בתכניות שונות
• ניתן להשתמש בפונקציות שכתבנו גם בתוכניות אחרות
• לדוגמא printfשמופיעה בספרייה .stdio.h
9
דוגמא- משתנים בפונקציה
#include <stdio.h>
double power(double base, int exponent)
{
int i = 0;
:המשתנים
double result = 1;
i, result, base, exponent
for (i = 1; i <= exponent; ++i)
result = result * base;
power מוגדרים אך ורק בפונקציה
return result;
}
sum המשתנה
main -מוגדר אך ורק ב
int main()
{
double sum = power(2,20) + power(3,15) + power(5,17);
printf(“2^20 + 3^15 + 5^17= %g”, sum);
return 0;
}
10
דוגמא- משתנים בפונקציה
#include <stdio.h>
double power(double base, int exponent)
{
int i = 0;
double result = 1;
אפשר להגדיר בפונקציות שונות משתנים
שונים באותו השם
)result (למשל
for (i = 1; i <= exponent; ++i)
result = result * base;
return result;
}
!אין כל קשר בין שני המשתנים
int main()
{
double result = power(2,20) + power(3,15) + power(5,17);
printf(“2^20 + 3^15 + 5^17= %g”, result);
return 0;
}
11
פונקציות – העברת ערכים
>#include <stdio.h
אל פונקציה מועברים ערכים (השמה)
ולא המיקום בזיכרון ,למעשה יש כאן
העתקה של הערך במשתנה
שינוי ערך המשתנה בפונקציה לא משפיע על
המשתנה המקורי
)int square(int num
{
;num = num * num
;return num
}
)(int main
{
;int num = 16
;))printf(“The square of %d is %d”, num, square(num
;return 0
}
הפלט :
12
The square of 16 is 256
בזמן ריצה
source code
double power(double base, int exponent)
{
int i = 0;
double result = 1;
call stack
main
for (i = 1; i <= exponent; i++)
result = result * base;
base
return result;
}
power
int main()
{
double result = power(2,20) +
power(3,15) +
power(5,17);
result
exponent
=5
3
2
= 17
15
20
i
result
printf("2^20 + 3^15 + 5^17= %g",
result);
return 0;
}
13
הכרזה על פונקציות
• על מנת להשתמש בפונקציה ,יש להגדירה
• לכן הפונקציה מומשה לפני הmain-
• אופציה נוספת היא:
• להכריז על הפונקציה בתחילת הקובץ (ללא מימוש)
• Function Declaration
• לממש את הפונקציה בהמשך הקובץ ,או בקובץ נפרד
• Implementation
• ההכרזה על הפונקציה ( )prototypeוהמימוש חייבים להיות זהים!
14
דוגמה להכרזה על פונקציות
double power(double base, int exponent);
...
...
implement something
...
...
implement something else
...
...
double power(double base, int exponent)
{
int i;
double result = 1;
for (i = 1; i <= exponent; i++)
result = result * base;
return result;
}
הכרזה על הפונקציה
)function declaration(
מימוש הפונקציה
)function implementation(
15
בשביל מה צריך הכרזה על פונקציות?
• לפעמים נוח לראות בתחילת הקובץ אילו פונקציות ממומשות בקובץ
• אם התכנית מורכבת ממספר קבצים,
• ההגדרה של הפונקציות צריכה להופיע רק באחד מהקבצים
• בכל שאר הקבצים תופיע רק ההכרזה
• למשל ,שימוש בספריות של פונקציות ,כמו stdio.h
16
הכרזה על פונקציות – ספריות
• >#include <filename
• הקומפיילר מצרף לקובץ שלנו את הקובץ filename
• כאשר filenameהוא אחד מקבצי ההכרזות של C
• הקובץ מכיל הכרזות של פונקציות שימושיות
• לדוגמא stdio.hמכיל פונקציות קלט /פלט (כמו )printf,scanf
• כך אנחנו יכולים להשתמש בפונקציות מבלי להכיר את המימוש שלהן
• בזמן הקומפילציה ה linker -מצרף את המימוש שלהן (מתוך הספריות
הקיימות של )Cלתוכנית שלנו
17
ספריות נוספות
• ב C -קיימות ספריות נוספות לשימושינו
• ב math.h -נוכל למצוא פונקציות כמו:
• sin
• cos
• abs
• log
• sqrt
• ...
• ב ctype.h -נוכל למצוא פונקציות שעוסקות בתווים כמו:
• toupper
• Islower
• ...
18
סיכום ביניים
•
•
•
•
19
מהן פונקציות ולמה הן שימושיות
קריאה לפונקציה והערך שמוחזר ממנה
משתנים בפונקציות והעברת ערכים אליהן
הכרזה על פונקציות וספריות של פונקציות
פונקציה יכולה לקרוא גם לעצמה
?• מה תעשה התוכנית הבאה
void printNum(int num)
{
printf("%d ", num);
printNum(num-1);
{
int main()
{
printNum(3);
return 0;
{
...התוכנית "תעוף" כשיגמר הזכרון
main
printNum
num=3
printNum
num=2
printNum
num=1
20
תנאי עצירה
?• מה תעשה התוכנית הבאה
void printNum(int num)
{
if (num == 0)
return;
printf("%d ", num);
printNum(num-1);
{
int main()
{
printNum(3);
return 0;
{
main
printNum
num=3
printNum
num=2
printNum
num=1
printNum
num=0
21
חשוב לקדם משתנה מקריאה לקריאה
?• מה תעשה התוכנית הבאה
void printNum(int num)
{
if (num == 0)
return;
printf("%d ", num);
printNum(num);
{
main
printNum
num=3
printNum
num=3
int main()
printNum
{
printNum(3);
num=3
return 0;
{
...התוכנית "תעוף" כשיגמר הזכרון
22
דוגמא נוספת
?• מה תעשה התוכנית הבאה
void printNum(int num)
{
if (num == 0)
return;
printf("%d ", num);
printNum(num-1);
printf("%d ", num);
{
int main(){
printNum(3);
return 0;
{
main
printNum
num=3
printNum
num=2
printNum
num=1
printNum
num=0
23
רקורסיה
• פונקציה שקוראת לעצמה נקראת פונקציה רקורסיבית.
24
מגדלי הנוי
• משימה:
• העבירו את כל הדיסקיות מיתד )source( Sליתד )target( T
• השתמשו ביתד עזר )auxiliary( A
25
T
A
S
החוקים
• מותר להעביר רק את הדיסקית העליונה
• אסור להניח דיסקית גדולה על דיסקית קטנה
26
T
A
S
עבור דסקית אחת – קל מאוד!
•
27
דסקית תכלת מ S-לT-
T
A
S
עבור 2דסקיות – קל מאוד!
•
•
•
28
דסקית כחולה מ S-לA-
דסקית תכלת מ S-לT-
דסקית כחולה מ A-לT-
T
A
S
עבור 3דסקיות – קל
•
•
•
•
•
•
•
29
דסקית ירוקה מ S-לT-
דסקית כחולה מ S-לA-
דסקית ירוקה מ T-לA-
דסקית תכלת מ S-לT-
דסקית ירוקה מ A-לS-
דסקית כחולה מ A-לT-
דסקית ירוקה מ S-לT-
T
A
S
עבור 4דסקיות?
• ראינו שיש פתרון עבור 3דסקיות
• אפשר להשתמש בו על מנת לפתור עבור !4
30
T
A
S
פתרון עבור 4דסקיות
31
T
A
S
T
A
S
T
A
S
T
A
S
עבור nדסקיות?
32
T
A
S
נניח שיש לי פתרון עבור n-1דסקיות
33
T
A
S
העברת nדיסקיות מ S-לT-
.1העברת n-1הדיסקיות הקטנות מ S-ל ,A-בעזרת יתד
עזר T
34
T
A
S
העברת nדיסקיות מ S-ל( T-המשך)
.2העברת הדיסקית שנותרה מ S-לT-
35
T
A
S
העברת nדיסקיות מ S-ל( T-המשך)
.3העברת n-1הדיסקיות העליונות מ A-ל ,T-בעזרת יתד
עזר S
36
T
A
S
ומה עם פתרון עבור n-1דסקיות?
!Easy
37
אלגוריתם – עבור nדסקיות
T
.1
.2
.3
.4
38
A
S
אם צריך להעביר דסקית אחת – קל!
העבר n-1דסקיות מ S-ל( A-יתד עזר )T
העבר דסקית מ S-לT-
העבר n-1דסקיות מ A-ל( T-יתד עזר )S
נתרגם זאת לשפת ...C
לפתירת מגדלי הנויC תוכנית
void hanoi(char diskNum, char s, char t, char a);
int main(){
hanoi(3,'S','T','A');
return 0;
{
!אם צריך להעביר דסקית אחת – קל.1
)T (יתד עזרA- לS- דסקיות מn-1 העבר.2
T- לS-העבר דסקית מ.3
)S (יתד עזרT- לA- דסקיות מn-1 העבר.4
void hanoi(char diskNum, char s, char t, char a){
if (diskNum == 1){
printf("Move disk from %c to %c\n", s, t);
return;
{
hanoi(diskNum-1,s,a,t);
printf("Move disk from %c to %c\n", s, t);
hanoi(diskNum-1,a,t,s);
{
?זוכרים את מחסנית הקריאות
main
hanoi(3,'S','T','A')
hanoi(2,'S',‘A',‘T')
hanoi(1,'S',‘T',‘A')
40
?זוכרים את מחסנית הקריאות
main
hanoi(3,'S','T','A')
hanoi(2,'S',‘A',‘T')
hanoi(1,‘T',‘A',‘S')
...וכן הלאה
41
רקורסיה
• פתרנו את בעיית מגדלי הנוי בעזרת רקורסיה
• כלומר בעזרת פונקציה שקוראת לעצמה.
• רקורסיה מאפשרת לנו לפתור בעיה "גדולה" בעזרת
פתרון של בעיות "קטנות" המרכיבות אותה.
• בכל קריאה רקורסיבית אנחנו "מקטינים" את הבעיה
ולבסוף מגיעים למקרה קצה שאותו קל לפתור באופן
ישיר.
דוגמא :הגדרת נוסחאות
• דרך מקובלת להגדיר נוסחה או פעולה מתמטית היא
לרשום את שלבי החישוב שלה:
n! = 1*2*3*….*n
an = a*a*…..*a
nפעמים
• אפשר לחשב את ערך הנוסחה שלב-אחר-שלב
(איטרטיבית) למשל:
4! = 1*2*3*4 = 2*3*4 = 6*4 = 24
הגדרת נוסחאות
• דרך נוספת להגדיר נוסחאות – הגדרה רקורסיבית
מגדירים את הערך של השלב האחרון בעזרת תוצאות השלבים שלפניו.
• במקום להגדיר עצרת על-ידי:
n!=1*2*3*….*n
• אפשר להגדיר על-ידי:
!)n!=n*(n-1
0!=1
ואז החישוב יהיה:
= ))!4! = 4*3! = 4*(3*2!) = 4*(3*(2*1
= )4*(3*(2*(1*0!))) = 4*(3*(2*1)) = 4*(3*2
4*6 = 24
הגדרה רקורסיבית
•
•
מורכבת משני חלקים:
.1פירוט של שלב אחד בנוסחה
.2ציון תוצאה עבור ערך התחלתי כלשהו ("בסיס הרקורסיה")
(אחרת חישוב הנוסחה לא מסתיים)
דוגמא נוספת:
הגדרה איטרטיבית:
an = a*a*…..*a
הגדרה רקורסיבית:
an = a*an-1
a0 = 1
ואז:
= ))43 = 4*42 = 4*(4*41) = 4*(4*(4*40
= 4*(4*(4*1)) = 4*(4*4) = 4*16 = 64
יתרונות
• קצר
• בהרבה מקרים ,ההגדרה הרקורסיבית קצרה בהרבה
מהאיטרטיבית
• נוח
• במקרים מסוימים ,ההגדרה הרקורסיבית היא ההגדרה הטבעית
והנוחה ביותר של מה שרוצים לחשב
דוגמא -סדרת פיבונאצ'י
• איברי הסדרה
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89 ...
• שני האיברים הראשונים הם 0ו1-
• שאר האיברים מוגדרים כסכום שני האיברים שלפניהם
Fib(1) = 0
Fib(2) = 1
)Fin(n) = Fin(n-1) + Fib(n-2
• פחות נוח לרשום במקרה הזה הגדרה איטרטיבית (נוסחה מפורשת).
• החישוב עפ"י הנוסחא הרקורסיבית הוא:
))Fib(4) = Fib(3)+Fib(2) = (Fib(2)+Fib(1))+Fib(2) = (1 + Fib(1
+ Fib(2) = … = 2
C חישוב נוסחאות רקורסיביות בשפת
n! = n*(n-1)!
0! = 1
int factorial (int n) /* n >= 0 */
{
if (n == 0)
return 1;
return (n * factorial(n-1));
}
• עצרת
דוגמת הרצה
int main()
{
printf("%d\n", factorial(3));
return 0;
{
main
3
3*2
factorial
2
2*1
factorial
1
1*1
factorial
0
1
factorial
דוגמת הרצה
int factorial(int n) /* n >= 0 */
{
if (n==0)
return 1;
return (n * factorial(n-1));
}
int main()
{
printf("%d\n", factorial(3));
return 0;
{
main
דוגמת הרצה
int factorial(int n) /* n >= 0 */
{
if (n==0)
return 1;
return (n * factorial(n-1));
}
int main()
{
printf("%d\n", factorial(3));
return 0;
{
fac
main
n
3
דוגמת הרצה
int factorial(int n) /* n >= 0 */
{
if (n==0)
return 1;
return (n * factorial(n-1));
}
int main()
{
printf("%d\n", factorial(3));
return 0;
{
fac
n
2
fac
n
3
main
דוגמת הרצה
int factorial(int n) /* n >= 0 */
{
if (n==0)
return 1;
return (n * factorial(n-1));
}
int main()
{
printf("%d\n", factorial(3));
return 0;
{
fac
n
1
fac
n
2
fac
n
3
main
בסיס הרקורסיה
int factorial(int n) /* n >= 0 */
{
if (n==0)
return 1;
return (n * factorial(n-1));
}
int main()
{
printf("%d\n", factorial(3));
return 0;
{
1
facc
n
0
fac
n
1
fac
n
2
fac
n
3
main
int factorial(int n) /* n >= 0 */
{
if (n==0)
return 1;
return (n * factorial(n-1));
}
int main()
{
printf("%d\n", factorial(3));
return 0;
{
1
fac
n
1
fac
n
2
fac
n
3
main
int factorial(int n) /* n >= 0 */
{
if (n==0)
return 1;
return (n * factorial(n-1));
}
int main()
{
printf("%d\n", factorial(3));
return 0;
{
2
fac
n
2
fac
n
3
main
int factorial(int n) /* n >= 0 */
{
if (n==0)
return 1;
return (n * factorial(n-1));
}
int main()
{
printf("%d\n", factorial(3));
return 0;
{
6
fac
main
n
3
int factorial(int n) /* n >= 0 */
{
if (n==0)
return 1;
return (n * factorial(n-1));
}
int main()
{
printf("%d\n", factorial(3));
return 0;
{
main
int factorial(int n) /* n >= 0 */
{
if (n==0)
return 1;
return (n * factorial(n-1));
}
int main()
{
printf("%d\n", factorial(3));
return 0;
{
printf
main
6
נקודות לתשומת-לב
int factorial (int n) /* n >= 0 */
{
ללא תנאי העצירה (בסיס הרקורסיה),
)if (n==0
החישוב לא יסתיים לעולם
;return 1
;))return (n * factorial(n-1
}
בכל פונקציה רקורסיבית חייב להיות תנאי עצירה:
מקרה בו הפונקציה תחזיר ערך מבלי לקרוא לעצמה שוב
נקודות לתשומת-לב
int factorial (int n) /* n >= 0 */
{
)if (n==0
;return 1
;))return (n * factorial(n-1
}
התקדמות לעבר תנאי העצירה
כדי שהתוכנית תסתיים ,צריך לדאוג לכך שבאיזשהו
שלב תנאי העצירה יתקיים (בדיוק כמו בלולאות)
נקודות לתשומת-לב :זכרון
• כשקוראים לפונקציה מתוך עצמה ,משתנים שהוגדרו בפונקציה
הקוראת נשארים בזיכרון (בסביבה במחסנית).
משום שהמשתנים נשארים בזיכרון עד שהפונקציה מסתיימת.
• הרבה מאוד קריאות רקורסיביות עלולות למלא את הזיכרון של
המחשב (דוגמא בהמשך)
• גם אם נחסוך במשתנים ,נדרש מקום בכל קריאה לפונקציה:
עבור הנקודה שאליה היא צריכה לחזור
עבור הערך שהיא צריכה להחזיר
איך נחשב חזקה באופן רקורסיבי?
• ההגדרה הרקורסיבית (למעריך שלם ואי-שלילי):
an = a.an-1, a0=1
• והפונקציה ב:C -
)double power(double base, int exp
{
;if (exp==0) return 1
;)return base * power(base, exp-1
}
אפשר לטפל גם במעריך שלילי
איך נחשב חזקה באופן רקורסיבי
: ההגדרה היא,• אם יתכן מעריך שלילי
an = 1/a-n
:n>0 עבור
an = a . an-1,
a0=1
:n≥0 עבור
:C -• והפונקציה ב
double power(double base, int exp)
{
if (exp < 0) return 1/power(base, -exp);
if (exp==0) return 1;
return base * power(base, exp-1);
}
סדרת פיבונאצ'י
• נוסחה שמוגדרת באופן רקורסיבי:
)fib(n)=fib(n-1)+fib(n-2
fib(1)=0, fib(2)=1
• נוכל לכתוב את זה כך ב:C -
)int fib (int n
{
))if ((n==1) || (n==2
;return n-1
;)return fib(n-1)+fib(n-2
}
מה ההבדל בין הדוגמא הזאת
לשתי הדוגמאות הקודמות שראינו?
חישוב סדרת פיבונאצ'י
• כל קריאה לפונקציה עם ,n>2יוצרת שתי קריאות נוספות לפונקציה
• אם גם בהן ,n>2כל אחת מהן יוצרת שתי קריאות נוספות
• וכן הלאה.
• כבר עבור nקטן יחסית (למשל :)50
נקבל מספר עצום של קריאות לפונקציה
)int fib (int n
{
))if ((n==1) || (n==2
;return n-1
;)return fib(n-1)+fib(n-2
}
קריאות לפונקציה fib
מספר קריאות
ערך
n
1
1
1
1
1
2
3
2
3
92735
28657
23
150049
46368
24
fib קריאות לפונקציה
Fib(6)
Fib(4)
Fib(5)
Fib(4)
Fib(3)
Fib(3)
Fib(2)
Fib(2)
Fib(1)
Fib(2)
Fib(3)
Fib(1)
Fib(2)
Fib(2)
Fib(1)
סדרת פיבונאצ'י – בעיית יעילות
• לחישוב האיבר ה 24-בסדרה צריך לחשב את 23הערכים שלפניו
אבל בחישוב הרקורסיבי יתבצעו עשרות אלפי קריאות לפונקציה
כלומר יחושבו עשרות אלפי ערכים.
• הסיבה היא ששני הערכים שמחושבים בקריאה הרקורסיבית לא נשמרים
לכן ברקורסיה יבוצעו הרבה פעמים את הקריאות ) ,fib(1), fib(2וכו'.
• מסקנה:
אם פונקציה רקורסיבית צריכה לקרוא לעצמה יותר מפעם אחת ,אז
חישובה הוא בעייתי ,ויתאפשר רק למספרים קטנים.
עבור מספרים גדולים נצטרך לחשב ולהגדיר איטרטיבית.
איטרטיבי- סדרת פיבונאצ'י
int fib(int n)
{
int i, next, fib1 = 1, fib2 = 1;
if (n == 1 ||
return n-1;
n == 2)
for (i = 3; i <= n; i++)
{
next = fib1 + fib2;
fib1 = fib2;
fib2 = next;
}
return fib2;
}
שימוש ברקורסיה
• מצד אחד:
• לפעמים לשימוש ברקורסיה יש יתרון -קל יותר לכתוב
באמצעותו את החישוב
• מצד שני:
• לא תמיד קל למצוא הגדרה רקורסיבית לפונקציה
• לא תמיד קל להבין תוכניות שנכתבו באופן רקורסיבי
• לכן נבחר להשתמש ברקורסיה במקרים שבאמת נוחים לכך.
שאלות?