פרק קודם:
פייתון 7 - פונקציותמה נלמד
- מהי רקורסיה
- תנאי עצירה
- import
- Call stack
רקורסיה
הגדרת רקורסיה היא -
פרוצדורה או הגדרה שחוזרת על עצמה
פרוצדורה בעניין שלנו זו פונקציה, כפי שלמדנו בפרק הקודם.
זו בעצם פונקציה שחוזרת על עצמה!
איטרציה מול רקורסיה
האם זו רקורסיה?
1 | def func(): |
לפי ההגדרה בלבד הדוגמא הזו יכולה להיות רקורסיה כי זו פונקציה שחוזרת על עצמה ולפי תנאי הלולאה - רצה לעד.
פונקציה רקורסיבית
בתכנות - רקורסיה זה קיצור לפונקציה רקורסיבית
.
פונקציה רקורסיבית היא פונקציה שקוראת לעצמה מתוך גוף הפונקציה.
רקורסיה פשוטה תיראה כך:
1 | def func(n): |
מה שיודפס מהפונקציה הזו:
1 | 5 |
מה שקורה פה מתואר בדיאגרמה הבאה:
אנחנו מתחילים מלמעלה ויורדים למטה וחוזרים בסוף.
כל שלב אנחנו בודקים את תנאי העצירה
.
אם המספר קטן יותר מ-1
הפונקציה מחזירה 0
.
אם לא - ממשיכים לפונקציה הרקורסיבית עם מספר נמוך יותר ב-1.
עד שלבסוף אנחנו מגיעים לתנאי עצירה שלנו, וחוזרים למעלה.
כל השלבים נקראים מחסנית קריאות
או call-stack
.
למה אני מקבל גם 0 בפלט?
0 מתקבל מהפלט של הפונקציה הסופית, כפי שקראנו לה.
אז 1-5 מודפס מתוך הפונקציה ובסוף הפונקציה עושה return 0 והתוכנה של פייתון מדפיסה את הפלט הזה למסך.
מחסנית קריאות
מחסנית קריאות זהו מבנה נתונים בתוכנה ששומר את סדר הקריאות של פונקציות.
יש לזה כמה שמות נוספים:
- מחסנית ריצה
- מחסנית תוכנה
- מחסנית הרצה
- מחסנית מכונה
לעיתים מכונה גם “המחסנית”.
בקריאה לפונקציה הריצה שלה נשמרת.
1 | func(5) |
ניתן לראות במחסנית כתיעוד לריצה של פונקציה על מנת שכשתוכנה מסיימת את הפונקציה היא תוכל לחזור לנקודה האחרונה שהיא רצה ממנה.
המחשה באנימציה הבאה:
בתוכנה Visual Studio Code
ניתן לראות את סדר הקריאות ב-Debug:
במקרה שלנו הרצת הקוד הרקורסיבי תשמור במחסנית כל ריצת func()
עם הפרמטר n
.
ואז כשהריצה חוזרת אחורה - כל פונקציה “יוצאת מהמחסנית” בסדר ההפוך שהן נכנסו - האחרון שנכנס הוא הראשון יוצא.
או במילים אחרות LIFO - Last in First out
.
תנאי עצירה
פונקציה רקורסיבית מורכבת מ-2 חלקים:
- הלוגיקה של הפונקציה - חישוב, או ביצוע פעולה כלשהי.
- תנאי עצירה המגדיר את גבולות הפונקציה.
בפסודו קוד זה נראה כך:
1 | פונקציה: |
מה קורה במידה ואין לנו תנאי עצירה או שטעינו בו?
הריצו את קטע הקוד הבא:
1 | def func(n): |
קיבלנו שגיאה מפייתון על כך שהגענו ל”עומק” המקסימלי של הפונקציה.
זה מלמד אותנו על כך שאין דבר כזה רקורסיה אינסופית.
למה אין דבר כזה רקורסיה אינסופית?
תחשבו לפני שאתם קוראים את הקטע הבא.
זוכרים את הcall-stack
?
נניח שהגבלנו את הstacktrace
ל5
.
אם הרקורסיה שלנו גדולה מ-4
אנחנו נמלא את כל המחסנית ולא יהיה מקום לעוד קריאות של פונקציות.
ובמידה והרקורסיה שלנו גדולה מ-5, אנחנו נקבל שגיאה כזו!.
לקריאה נוספת:
Wikipedia - callstack
תנאי עצירה הוא החלק הכי חשוב ברקורסיה כי תנאי עצירה שגוי ייפגע בפונקציונליות ויכול ליצור שגיאות שקשה למצוא!
כל קריאה נוספת לפונקציה נכנסת “עמוק” יותר לסדר הקריאות.
וככל שזה עמוק יותר כך גם זה “יקר” יותר.
לעיתים הקריאות של הקוד חשובה ולכן נוכל “לפצל” רקורסיות ולבנות אותן עם לולאות.
אני מוצא קריאות רקורסיביות פחות קריאות מכיוון שמדובר באלגוריתמים יחסית מורכבים, אבל זה לא הופך אותך לפחות יעילים.
דוגמא לשימוש ברקורסיה
רקורסיה היא לא הדבר המועדף על מתכנתים, הרבה מהן מעדיפים לממש בצורה איטרטיבית.
“Iterative” או “Iteration” - חזרה על פעולה.
כאשר אומרים “בצורה איטרטיבית” בד”כ מתכוונים ללולאה.
כעת נראה דוגמא למתי רקורסיה יכולה לעזור לנו.
עצרת
עצרת זוהי מכפלה של סדרה יורדת ב1.
5! = 5 * 4 * 3 * 2 * 1
1 | def Factorial(n): |
כל מה שאנחנו עושים זה מכפילים עד שמגיעים לתנאי עצירה שלנו.
- האם אתם יכולים לזהות את תנאי העצירה?
- האם אתם מזהים בעיה בפונקציה הזו?
התנאי הוא:
if n == 1
מה ייקרה למספרים שליליים או אם נעביר לפונקציה הזו 0?
היא תרוץ לעד!
כדי לתקן את זה נבדוק תנאי עצירה אחר:
1 | def Factorial(n): |
פיבונאצ’י
זוכרים את סדרת פיבונאצ’י מפרק 6?
כעת נבחן את המימוש של אותו אלגוריתם בצורה רקורסיבית.
1 | def RecursiveFibonacci(times): |
בדוגמא הזו אנחנו קוראים לפונקציה פעמיים למה?
כי פיבונאצ’י מורכב מחיבור של שני המספרים הקודמים לו לכן בכל איטרציה אנחנו מחשבים את המספרים הקודמים.
הפונקציה ממומשת בצורה מאוד פשוטה אך היא לא הכי יעילה כי עבור כל מספר אנחנו צריכים לחשב את המספרים הקודמים.
תנסו לשחק עם הפונקציה ולתת לה מספרים שונים: 10, 20, 50, 500.
מהי ההגבלה של הרקורסיה?
בעזרת הקוד הפשוט הזה אפשר לבדוק מהו הגבול הרקורסיבי של פייתון במחשב:
1 | import sys |
כשאני הרצתי את הקוד יצא:
1 | Python 3.9.0 (tags/v3.9.0:9cf6752, Oct 5 2020, 15:34:40) [MSC v.1927 64 bit (AMD64)] on win32 |
תנסו בעצמכם ליצור רקורסיה יותר גדולה מהמספר שייצא לכם ותראו מה קורה!
היכרות עם import
כדי להקל עלינו ועל השימוש בפונקציות שכבר מומשו פייתון מציעה כלי שנקרא מודולים - Modules
.
מודול הוא פונקציות המאוגדות ביחד - האיגוד הזה נקרא חבילה.
בעזרת החבילות ניתן לייצר פונקציות או לייבא.
ה-import
היא מילת מפתח המייבאת מודולים.
כפי שראינו בפרק הראשון ,הפקודה pip
שמנהלת חבילות, ונותנת את האופציה להתקין חבילות אחרות.
בפרק 14 נכיר את המודולים יותר לעומק.
אך לעת עתה הזכרתי את ה-import
כי השימוש של זה יילך וייגדל.
תרגילים
כתבו רקורסיה המדפיסה את כל האיברים השלמים עד מספר מסוים שהמשתמש בחר.
למשל עבור 5 זה ידפיס: 1, 2, 3, 4, 5.כתבו פונקציה רקורסיבית שמקבלת מערך מספרים כקלט ומחזירה את ההמספר הנמוך ביותר שכל המספרים במערך מתחלקים בו.
למשל עבור[2, 3, 4]
הפונקציה תחזיר12
.כתבו פונקציה רקורסיבית המקבלת רשימה והופכת אותה.
למשל רשימה1,2,3
תודפס הפוך:3,2,1
.ממשו חיפוש בינארי
Binary search
בעזרת פונקציה רקורסיבית.
על הפונקציה לקבל רשימה מסודרת ולחפש אלמנט שנותנים לו.
1 | def BinarySearch(list, numToFind): |
מהו חיפוש בינארי?
חיפוש בינארי הוא אלגוריתם לחיפוש אשר “מפצל” את הרשימה לשניים עד שהוא מוצא או לא מוצא את המספר.
במידה ולא מצאנו אנחנו ממשיכים לפצל עד שנמצא!
בתמונה כשפיצלנו מס’ אי זוגי הלכנו לאיבר הגבוה יותר, ז”א במקום 5 לקחנו את 6 והמשכנו לבדוק.
לעיתים האלגוריתם יכול ללכת למספר התחתון במקום העליון.
- עליכם לכתוב פונקציה המוצאת את המספר הקטן ביותר בעץ בינארי.
עץ בינארי הוא עץ שיש בו לכל היותר 2 ענפים.
את העץ אני אגדיר בעזרת רשימות, סדר האיברים:
- מספר
- רשימה
- מספר 2
- רשימה 2
למשל עץ עם 2 ענפים 1,2 ו3:
1 | tree = [1,[2,[],3,[]]] |
זאת אומרת ענף עם בן אחד יהיה בעל רשימה של 2 אלמנטים.
ו-2 ענפים יהיו 4 אלמנטים.
להלן קוד שניתן להיעזר בו:
1 | def Minimum(tree): |
1 | def RecursivePrint(number, limitNumber): |
תנאי העצירה שלנו התהפך בפונקציה הזו, במקום לומר “בתנאי ש… תצא” אני אומר “בתנאי ש…תמשיך”.
כל תנאי ניתן להפיכה והסוד פה הוא לבחור בוריאציה שנראת קריאה יותר, במידה והייתי הופך את הוריאציה הזו היה יוצא:
1 | def RecursivePrint(number, limitNumber): |
הפונקציה הזו עובדת כפונקציה סופרת מהמספר number
עד ל -limitNumber
.
ניתן לפשט את התנאי ולהפוך אותו:if number < limitNumber:
.
כל עוד המספר קטן מ-limitNumber
אז תמשיך.
זהו “תנאי המשך” - כל עוד תנאי מתקיים תמשיך עם הפעולה.
בחרתי לכתוב את זה כ-“תנאי עצירה”.
אם המספר גדול או שווה לתנאי עצירה אזי תעצור.
זה בשביל שהקוד יהיה מובן יותר וקריא יותר.
1 | def FindLeastCommonNumberMultiplier(numberList, allMultilperNumber=0): |
הפונקציה עולה מ-0 ומעלה עד שהיא מוצאת את המספר שהוא המכנה המשותף של כל המספרים ברשימה.
ניתן לכתוב את קטע הקוד הזה:
1 |
|
בצורה יותר פשוטה:
1 | if all(counter % num == 0 for num in arr): |
הפונקציה all
מקבלת רשימה של תנאים שכולם צריכים להיות True
כדי שייתקבל True
לתנאי עצירה.
התחביר הזה הוא תחביר מקוצר שיוצר רשימה.
הרשימה הזו מכילה True
או False
בהתאם לתנאי counter % num == 0
.
המודולו (%
) הוא אופרטור המחלק את המספר במספר אחר ומחזיר את השארית.
והבדיקה הזו בעצם בודקת אם המספר מתחלק ללא שארית.
1 | def Reverse(list: list, currIndex=0): |
האלגוריתם עובד בצורה פשוטה - לוקחת את האלמנט הראשון ומכניסה אותו.
ואז מוציאה בעזרת pop
את האלמנט האחרון שהכנסו כבר באינדקס currIndex
.
ולאחר מכן אנחנו פשוט עוברים מההתחלה של הרשימה ועד לסופה.
תנאי העצירה שלנו בודק אם הגענו לסוף if currIndex == len(list)
.
1 | def BinarySearch(list, toFind): |
בדוגמא הזו יצרנו פונקציית עזר שהיא הפונקציה הרקורסיבית שלנו - __BinarySearch
.
לאחר מכן אנחנו מחשבים את האינדקסים - במידה והגבוה נמוך יותר מהאינדקס הנמוך אז נצא.
כי הוא לא נמצא בעצם…
אחר כך מחשבים את האמצע: mid = (high + low) // 2
האופרטור //
מחזיר לנו מספר שלם.
ואז מתבצעת בדיקה:
אם מצאנו אותו אנחנו נחזיר True
.
במידה והמספר שאנחנו מחזיקים באמצע גדול יותר מהמספר שאנחנו מחפשים,
האלגוריתם צריך ללכת ימינה ברשימה.
אחרת - שיילך לחלק הנמוך יותר שמאלה.
שימו לב
האלגוריתם עובד על רשימה מסודרת.
1 | def Minimum(tree): |
נסביר את הפונקציה הרקורסיבית:
קודם כל צריך להבין כמה ענפים יש לנו בעץ ע”י לקיחת גודל הרשימה וחלוקה ב-2.
למה? כי יש לנו 2 אלמנטים פר ענף - הערך שלו והרשימה שלו שהיא הילדים שלו.
1 | numOfBranches = len(currentNode) // 2 |
מכיוון שיש 2 אלמנטים עבור כל ענף אנחנו צריכים להכפיל ב-2 כדי להגיע לאינדקס הנכון ברשימה:
1 | index = i * 2 |
וכך יש לנו את הערך valBranch
והילדים שלו children
החלק הרקורסיבי הוא די פשוט פה, כל מה שעלינו לעשות זה להעביר לפונקציה הרקורסיבית את הילדים של הענף:
1 | if len(children) > 0: |
בעצם בכך אנחנו יורדים היררכיה בתוך העץ, וכאשר יוצאים ממנה מקבלים את המינימום.
לבסוף מעדכנים את המינימום גם בהתאם לערך וגם בהתאם לערך הילדים:
1 | if valBranch < currMin: |
1 | if min < currMin: |
פרק הבא:
פייתון 9 - מילונים, טופלים וסטים