Presentation is loading. Please wait.

Presentation is loading. Please wait.

שיעור רביעי: פונקציות, מבוא לרקורסיה

Similar presentations


Presentation on theme: "שיעור רביעי: פונקציות, מבוא לרקורסיה"— Presentation transcript:

1 שיעור רביעי: פונקציות, מבוא לרקורסיה
קורס תכנות שיעור רביעי: פונקציות, מבוא לרקורסיה 1

2 לולאות - תזכורת לולאה: קטע קוד המתבצע שוב ושוב ברצף כל עוד תנאי מסוים מתקיים לולאות for: כאשר רוצים לבצע את הלולאה בצורה סדרתית: תבצע את הלולאה 10 פעמים ... תבצע את הלולאה n פעמים ... לולאות while: כאשר רוצים לבצע את הלולאה מספר שרירותי של פעמים: כל עוד i זוגי... כל עוד לא הייתה טעות בחישוב ... 2

3 לולאות - תזכורת break: מסיים (שובר) את ריצת הלולאה continue:
מסיים רק את הסיבוב (iteration) הנוכחי של הלולאה 3

4 לולאות do-while ההבדל בין do-while ל-while:
הפקודות מבוצעות לפחות פעם אחת (אפילו אם התנאי לא מתקיים לעולם). התנאי נבדק לאחר ביצוע הפקודות. initialize ... do { do something ... increment ... } while ( condition ); 4

5 כיצד מחשבים? כיצד מחשבים את: 220 + 315 + 517 חישוב 220 חישוב 315
#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; for(result = 1, i = 1; i <= 15; ++i) result = result * 3; for(result = 1, i = 1; i <= 17; ++i) result = result * 5; printf("2^20 + 3^15 + 5^17= %g", sum); return 0; } כיצד מחשבים את: חישוב 220 חישוב 315 חישוב 517 5

6 מה היינו רוצים? בעיות: שכפול של קטע קוד כמעט זהה 3 פעמים
אם היו יותר מחוברים היה צריך לשכפל פעמים נוספות התכנית מתארכת ונהיית מסורבלת מקור לבאגים וטעויות copy / paste קשה להבין "למה התכוון המשורר" מה היינו רוצים? לכתוב פעם אחת בלבד את הקוד להשתמש באותו קטע קוד מספר פעמים אבל עם פרמטרים שונים בכל פעם 6

7 פתרון - פונקציות הגדרת פונקציה בשם power המחשבת את baseexponent
#include <stdio.h> double power(double base, int exponent) { int i = 0; double result = 1; 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; הגדרת פונקציה בשם power המחשבת את baseexponent התוכנית מתחילה לרוץ מכאן קריאה לפונקציה 7

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

9 ושוב ... למה להשתמש בפונקציות?
חלוקת התכנית לחלקים לוגים שונים כל חלק מבצע פעולה בסיסית אחרת קריאת נתונים, עיבוד נתונים, הדפסה ... פירוק בעיה מסובכת לתת בעיות פשוטות יותר - אבסטרקציה שימוש חוזר באותו קטע קוד מספר פעמים באותה התוכנית שימוש חוזר באותו קטע קוד בתכניות שונות ניתן להשתמש בפונקציות שכתבנו גם בתוכניות אחרות לדוגמא printf שמופיעה בספרייה stdio.h. 9

10 משתנים בפונקציה - דוגמא
#include <stdio.h> double power(double base, int exponent) { int i = 0; double result = 1; 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; המשתנים: i, result, base, exponent מוגדרים אך ורק בפונקציה power המשתנה sum מוגדר אך ורק ב- main 10

11 משתנים בפונקציה - דוגמא
#include <stdio.h> double power(double base, int exponent) { int i = 0; double result = 1; 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; אפשר להגדיר בפונקציות שונות משתנים שונים באותו השם (למשל result) אין כל קשר בין שני המשתנים! 11

12 שינוי ערך המשתנה בפונקציה לא משפיע על המשתנה המקורי
פונקציות – העברת ערכים #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; אל פונקציה מועברים ערכים (השמה) ולא המיקום בזיכרון, למעשה יש כאן העתקה של הערך במשתנה שינוי ערך המשתנה בפונקציה לא משפיע על המשתנה המקורי הפלט : The square of 16 is 256 12

13 בזמן ריצה source code call stack main power = 3 = 5 = 2 = 17 = 15 = 20
double power(double base, int exponent) { int i = 0; double result = 1; 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; result main base exponent result i power = 3 = 5 = 2 = 17 = 15 = 20 13

14 הכרזה על פונקציות על מנת להשתמש בפונקציה, יש להגדירה
לכן הפונקציה מומשה לפני ה-main אופציה נוספת היא: להכריז על הפונקציה בתחילת הקובץ (ללא מימוש) Function Declaration לממש את הפונקציה בהמשך הקובץ, או בקובץ נפרד Implementation ההכרזה על הפונקציה (prototype) והמימוש חייבים להיות זהים! 14

15 דוגמה להכרזה על פונקציות
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

16 בשביל מה צריך הכרזה על פונקציות?
לפעמים נוח לראות בתחילת הקובץ אילו פונקציות ממומשות בקובץ אם התכנית מורכבת ממספר קבצים, ההגדרה של הפונקציות צריכה להופיע רק באחד מהקבצים בכל שאר הקבצים תופיע רק ההכרזה למשל, שימוש בספריות של פונקציות, כמו stdio.h 16

17 הכרזה על פונקציות – ספריות
#include <filename> הקומפיילר מצרף לקובץ שלנו את הקובץ filename כאשר filename הוא אחד מקבצי ההכרזות של C הקובץ מכיל הכרזות של פונקציות שימושיות לדוגמא stdio.h מכיל פונקציות קלט / פלט (כמו printf,scanf) כך אנחנו יכולים להשתמש בפונקציות מבלי להכיר את המימוש שלהן בזמן הקומפילציה ה- linker מצרף את המימוש שלהן (מתוך הספריות הקיימות של C) לתוכנית שלנו 17

18 ספריות נוספות ב- C קיימות ספריות נוספות לשימושינו
ב- math.h נוכל למצוא פונקציות כמו: sin cos abs log sqrt ... ב- ctype.h נוכל למצוא פונקציות שעוסקות בתווים כמו: toupper Islower 18

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

20 פונקציה יכולה לקרוא גם לעצמה
מה תעשה התוכנית הבאה? 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 התוכנית "תעוף" כשיגמר הזכרון...

21 תנאי עצירה מה תעשה התוכנית הבאה? main printNum num=3 printNum num=2
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

22 חשוב לקדם משתנה מקריאה לקריאה
מה תעשה התוכנית הבאה? void printNum(int num) { if (num == 0) return; printf("%d ", num); printNum(num); int main() printNum(3); return 0; main printNum num=3 printNum num=3 printNum num=3 התוכנית "תעוף" כשיגמר הזכרון...

23 דוגמא נוספת מה תעשה התוכנית הבאה? main printNum num=3 printNum num=2
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

24 רקורסיה פונקציה שקוראת לעצמה נקראת פונקציה רקורסיבית. 24

25 מגדלי הנוי משימה: העבירו את כל הדיסקיות מיתד S (source) ליתד T (target) השתמשו ביתד עזר A (auxiliary) S A T 25

26 החוקים מותר להעביר רק את הדיסקית העליונה
אסור להניח דיסקית גדולה על דיסקית קטנה S A T 26

27 עבור דסקית אחת – קל מאוד! דסקית תכלת מ-S ל-T S A T 27

28 עבור 2 דסקיות – קל מאוד! דסקית כחולה מ-S ל-A דסקית תכלת מ-S ל-T
דסקית כחולה מ-A ל-T S A T 28

29 עבור 3 דסקיות – קל דסקית ירוקה מ-S ל-T דסקית כחולה מ-S ל-A
דסקית ירוקה מ-T ל-A דסקית תכלת מ-S ל-T דסקית ירוקה מ-A ל-S דסקית כחולה מ-A ל-T S A T 29

30 עבור 4 דסקיות? ראינו שיש פתרון עבור 3 דסקיות
אפשר להשתמש בו על מנת לפתור עבור 4! S A T 30

31 פתרון עבור 4 דסקיות S T A S T A S T A S T A 31

32 עבור n דסקיות? S A T 32

33 נניח שיש לי פתרון עבור n-1 דסקיות
S A T 33

34 העברת n דיסקיות מ-S ל-T העברת n-1 הדיסקיות הקטנות מ-S ל-A, בעזרת יתד עזר T S A T 34

35 העברת n דיסקיות מ-S ל-T (המשך)
A T 35

36 העברת n דיסקיות מ-S ל-T (המשך)
העברת n-1 הדיסקיות העליונות מ-A ל-T, בעזרת יתד עזר S S A T 36

37 ומה עם פתרון עבור n-1 דסקיות?
Easy! 37

38 אלגוריתם – עבור n דסקיות
S T A אם צריך להעביר דסקית אחת – קל! העבר n-1 דסקיות מ-S ל-A (יתד עזר T) העבר דסקית מ-S ל-T העבר n-1 דסקיות מ-A ל-T (יתד עזר S) נתרגם זאת לשפת C... 38

39 תוכנית C לפתירת מגדלי הנוי
void hanoi(char diskNum, char s, char t, char a); int main(){ hanoi(3,'S','T','A'); return 0; { אם צריך להעביר דסקית אחת – קל! העבר n-1 דסקיות מ-S ל-A (יתד עזר T) העבר דסקית מ-S ל-T העבר n-1 דסקיות מ-A ל-T (יתד עזר S) 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); hanoi(diskNum-1,a,t,s);

40 זוכרים את מחסנית הקריאות?
main hanoi(3,'S','T','A') hanoi(2,'S',‘A',‘T') hanoi(1,'S',‘T',‘A')

41 זוכרים את מחסנית הקריאות?
main hanoi(3,'S','T','A') hanoi(2,'S',‘A',‘T') hanoi(1,‘T',‘A',‘S') וכן הלאה...

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

43 דוגמא: הגדרת נוסחאות דרך מקובלת להגדיר נוסחה או פעולה מתמטית היא לרשום את שלבי החישוב שלה: n! = 1*2*3*….*n an = a*a*…..*a אפשר לחשב את ערך הנוסחה שלב-אחר-שלב (איטרטיבית) למשל: 4! = 1*2*3*4 = 2*3*4 = 6*4 = 24 n פעמים

44 הגדרת נוסחאות דרך נוספת להגדיר נוסחאות – הגדרה רקורסיבית מגדירים את הערך של השלב האחרון בעזרת תוצאות השלבים שלפניו. במקום להגדיר עצרת על-ידי: 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

45 הגדרה רקורסיבית מורכבת משני חלקים: פירוט של שלב אחד בנוסחה
ציון תוצאה עבור ערך התחלתי כלשהו ("בסיס הרקורסיה") (אחרת חישוב הנוסחה לא מסתיים) דוגמא נוספת: הגדרה איטרטיבית: 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

46 יתרונות קצר נוח בהרבה מקרים, ההגדרה הרקורסיבית קצרה בהרבה מהאיטרטיבית
במקרים מסוימים, ההגדרה הרקורסיבית היא ההגדרה הטבעית והנוחה ביותר של מה שרוצים לחשב

47 דוגמא - סדרת פיבונאצ'י איברי הסדרה שני האיברים הראשונים הם 0 ו-1
שאר האיברים מוגדרים כסכום שני האיברים שלפניהם פחות נוח לרשום במקרה הזה הגדרה איטרטיבית (נוסחה מפורשת). החישוב עפ"י הנוסחא הרקורסיבית הוא: Fib(4) = Fib(3)+Fib(2) = (Fib(2)+Fib(1))+Fib(2) = (1 + Fib(1)) + Fib(2) = … = 2 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, Fib(1) = 0 Fib(2) = 1 Fin(n) = Fin(n-1) + Fib(n-2)

48 חישוב נוסחאות רקורסיביות בשפת C
עצרת n! = n*(n-1)! 0! = 1 int factorial (int n) /* n >= 0 */ { if (n == 0) return 1; return (n * factorial(n-1)); }

49 דוגמת הרצה int main() { printf("%d\n", factorial(3)); return 0; 3 2 1
main factorial factorial factorial factorial 3*2 2*1 1*1 1

50 דוגמת הרצה 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; main

51 דוגמת הרצה 3 n fac 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; 3 n fac main

52 דוגמת הרצה 2 n fac 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 n fac 3 main

53 דוגמת הרצה 1 n fac 2 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 n fac 2 3 main

54 בסיס הרקורסיה n facc 1 fac 2 3 main 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; n facc 1 fac 2 3 main 1

55 1 n fac 2 3 main 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; 1 n fac 2 3 main 1

56 2 n fac 3 main 2 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 n fac 3 main 2

57 3 n fac main 6 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; 3 n fac main 6

58 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; main

59 6 printf 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 printf main

60 ללא תנאי העצירה (בסיס הרקורסיה), החישוב לא יסתיים לעולם
נקודות לתשומת-לב int factorial (int n) /* n >= 0 */ { if (n==0) return 1; return (n * factorial(n-1)); } ללא תנאי העצירה (בסיס הרקורסיה), החישוב לא יסתיים לעולם בכל פונקציה רקורסיבית חייב להיות תנאי עצירה: מקרה בו הפונקציה תחזיר ערך מבלי לקרוא לעצמה שוב

61 התקדמות לעבר תנאי העצירה
נקודות לתשומת-לב int factorial (int n) /* n >= 0 */ { if (n==0) return 1; return (n * factorial(n-1)); } התקדמות לעבר תנאי העצירה כדי שהתוכנית תסתיים, צריך לדאוג לכך שבאיזשהו שלב תנאי העצירה יתקיים (בדיוק כמו בלולאות)

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

63 איך נחשב חזקה באופן רקורסיבי?
ההגדרה הרקורסיבית (למעריך שלם ואי-שלילי): an = a.an-1, a0=1 והפונקציה ב- C: double power(double base, int exp) { if (exp==0) return 1; return base * power(base, exp-1); } אפשר לטפל גם במעריך שלילי

64 איך נחשב חזקה באופן רקורסיבי
אם יתכן מעריך שלילי, ההגדרה היא: עבור 0>n: an = 1/a-n עבור n≥0: an = a . an-1, a0=1 והפונקציה ב- 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); }

65 סדרת פיבונאצ'י נוסחה שמוגדרת באופן רקורסיבי: 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); } מה ההבדל בין הדוגמא הזאת לשתי הדוגמאות הקודמות שראינו?

66 חישוב סדרת פיבונאצ'י כל קריאה לפונקציה עם 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); }

67 fibקריאות לפונקציה n ערך מספר קריאות 1 2 3 23 28657 92735 24 46368
150049

68 fibקריאות לפונקציה Fib(6) Fib(4) Fib(5) Fib(4) Fib(3) Fib(2) Fib(1)

69 סדרת פיבונאצ'י – בעיית יעילות
לחישוב האיבר ה-24 בסדרה צריך לחשב את 23 הערכים שלפניו אבל בחישוב הרקורסיבי יתבצעו עשרות אלפי קריאות לפונקציה כלומר יחושבו עשרות אלפי ערכים. הסיבה היא ששני הערכים שמחושבים בקריאה הרקורסיבית לא נשמרים לכן ברקורסיה יבוצעו הרבה פעמים את הקריאות fib(1), fib(2), וכו'. מסקנה: אם פונקציה רקורסיבית צריכה לקרוא לעצמה יותר מפעם אחת, אז חישובה הוא בעייתי, ויתאפשר רק למספרים קטנים. עבור מספרים גדולים נצטרך לחשב ולהגדיר איטרטיבית.

70 סדרת פיבונאצ'י - איטרטיבי
int fib(int n) { int i, next, fib1 = 1, fib2 = 1; if (n == 1 || n == 2) return n-1; for (i = 3; i <= n; i++) next = fib1 + fib2; fib1 = fib2; fib2 = next; } return fib2;

71 שימוש ברקורסיה מצד אחד:
לפעמים לשימוש ברקורסיה יש יתרון - קל יותר לכתוב באמצעותו את החישוב מצד שני: לא תמיד קל למצוא הגדרה רקורסיבית לפונקציה לא תמיד קל להבין תוכניות שנכתבו באופן רקורסיבי לכן נבחר להשתמש ברקורסיה במקרים שבאמת נוחים לכך.

72 שאלות?


Download ppt "שיעור רביעי: פונקציות, מבוא לרקורסיה"

Similar presentations


Ads by Google