ch10-ch17

Transcription

ch10-ch17
‫שיא הסי‪-‬ים‬
‫)‪(C/C++‬‬
‫)חלק ב'(‬
‫מאת‪ :‬יורם ביברמן‬
‫© כל הזכויות שמורות למחבר‪.‬‬
‫אין לעשות כל שימוש מסחרי בספר זה או בקטעים ממנו‪ .‬ניתנת הרשות להשתמש‬
‫בו לצורכי לימוד של המשתמש בלבד‪.‬‬
‫‪1‬‬
‫‪ .11‬מצביעים ומערכים ‪7 .....................................................................................‬‬
‫עקרונות בסיסיים ‪7 ..........................................................................‬‬
‫‪11.1‬‬
‫הדמיון והשוני בין מצביע לבין מערך ‪9 ................................................‬‬
‫‪11.2‬‬
‫נוטציה מערכית לעומת נוטציה מצביעית ‪11 .............................‬‬
‫‪11.2.1‬‬
‫פונקציות המקבלות מצביעים או מערכים ‪15 ......................................‬‬
‫‪11.3‬‬
‫מצביע כפרמטר הפניה ‪19 ..................................................................‬‬
‫‪11.4‬‬
‫פונקציה המחזירה מצביע ‪22 .............................................................‬‬
‫‪11.5‬‬
‫שינוי גודלו של מערך ‪24 ....................................................................‬‬
‫‪11.6‬‬
‫מערך של מצביעים כפרמטר ערך ‪25 ...................................................‬‬
‫‪11.7‬‬
‫מערך של מצביעים כפרמטר הפניה ‪28 ................................................‬‬
‫‪11.8‬‬
‫מצביעים במקום פרמטרי הפניה ‪31 ...................................................‬‬
‫‪11.9‬‬
‫‪11.10‬‬
‫מצביע מדרגה שנייה )** ‪ (int‬כפרמט קבוע )‪33 ......................... (const‬‬
‫פרמטרים המועברים ל‪ argc :main -‬ו‪33 ............................ argv -‬‬
‫‪11.11‬‬
‫תרגילים ‪35 ..................................................................................‬‬
‫‪11.12‬‬
‫תרגיל מספר אחד‪ :‬איתור תת‪-‬מערך במערך‪35 .........................‬‬
‫‪11.12.1‬‬
‫תרגיל מספר שתיים‪ :‬מסד נתוני משפחות ‪36 ............................‬‬
‫‪11.12.2‬‬
‫תרגיל מספר שלוש‪ :‬טיפול בסיסי במערכים ומצביעים ‪36 ..........‬‬
‫‪11.12.3‬‬
‫‪38 .......................................................................................... struct‬‬
‫‪12‬‬
‫מוטיבציה ‪38 ...................................................................................‬‬
‫‪12.1‬‬
‫‪12.2‬‬
‫העברת ‪ struct‬כפרמטר לפונקציה ‪39 ..............................................‬‬
‫מערך של ‪41 ....................................................................... struct‬‬
‫‪12.3‬‬
‫מצביע ל‪ struct -‬כפרמטר ערך ‪45 ...................................................‬‬
‫‪12.4‬‬
‫מצביע ל‪ struct -‬כפרמטר הפניה ‪46 ................................................‬‬
‫‪12.5‬‬
‫תרגילים ‪48 .....................................................................................‬‬
‫‪12.6‬‬
‫תרגיל מספר אחד‪ :‬פולינומים ‪48 .............................................‬‬
‫‪12.6.1‬‬
‫תרגיל מספר שתיים‪ :‬סידור כרטיסים בתבנית רצויה ‪49 ............‬‬
‫‪12.6.2‬‬
‫תרגיל מספר שלוש‪ :‬בעיית מסעי הפרש ‪49 ................................‬‬
‫‪12.6.3‬‬
‫תרגיל מספר ארבע‪ :‬נתוני משפחות‪50 ......................................‬‬
‫‪12.6.4‬‬
‫תרגיל מספר חמש‪ :‬סימולציה של רשימה משורשרת ‪52 .............‬‬
‫‪12.6.5‬‬
‫‪ 13‬רשימות מקושרות )משורשרות( ‪54 .................................................................‬‬
‫‪ 13.1‬בניית רשימה מקושרת ‪54 ........................................................................‬‬
‫הצגת הנתונים השמורים ברשימה מקושרת ‪56 ....................................‬‬
‫‪13.2‬‬
‫בניית רשימה מקושרת ממוינת ‪58 .....................................................‬‬
‫‪13.3‬‬
‫מחיקת איבר מרשימה ‪67 .................................................................‬‬
‫‪13.4‬‬
‫שחרור תאי רשימה ‪68 ......................................................................‬‬
‫‪13.5‬‬
‫היפוך רשימה ‪69 ..............................................................................‬‬
‫‪13.6‬‬
‫בניית רשימה ממוינת יחידאית מרשימה לא ממוינת ‪74 ........................‬‬
‫‪13.7‬‬
‫מיון מיזוג של רשימות משורשרות ‪77 .................................................‬‬
‫‪13.8‬‬
‫‪ 13.8.1‬הצגת אלגוריתם המיון‪ :‬מיון מיזוג ‪77 ................................................‬‬
‫‪ 13.8.2‬מיון מיזוג של רשימות משורשרות ‪81 .................................................‬‬
‫‪ 13.8.3‬זמן הריצה של מיון מיזוג ‪84 ..............................................................‬‬
‫בניית רשימה מקושרת ממוינת בשיטת המצביע למצביע ‪86 .................‬‬
‫‪13.9‬‬
‫‪ 13.10‬הערה בנוגע ל‪93 ..................................................................... const-‬‬
‫תרגילים ‪95 ..................................................................................‬‬
‫‪13.11‬‬
‫תרגיל מספר אחד‪ :‬רשימות משורשרות של משפחות‪95 .............‬‬
‫‪13.11.1‬‬
‫תרגיל מספר שתיים‪ :‬איתור פרמידה ברשימה ‪97 ......................‬‬
‫‪13.11.2‬‬
‫תרגיל מספר שלוש‪ :‬רשימה מקושרת דו‪-‬כיוונית ‪98 .................‬‬
‫‪13.11.3‬‬
‫תרגיל מספר ארבע‪ :‬רשימה מקושרת עם מיון כפול‪98 ..............‬‬
‫‪13.11.4‬‬
‫‪2‬‬
‫תרגיל מספר חמש‪ :‬רשימה מקושרת עם מיון כפול ‪99 ..............‬‬
‫‪13.11.5‬‬
‫תרגיל מספר שש‪ :‬מאגר ציוני תלמידים ‪100 .............................‬‬
‫‪13.11.6‬‬
‫תרגיל מספר שבע‪ :‬מיון הכנסה של רשימה מקושרת ‪101 ..........‬‬
‫‪13.11.7‬‬
‫תרגיל מספר שמונה‪ :‬פולינומים‪102 ........................................‬‬
‫‪13.11.8‬‬
‫תרגיל מספר תשע‪ :‬הפרדת רשימות משורשרות שהתמזגו‪102 ....‬‬
‫‪13.11.9‬‬
‫תרגיל מספר עשר‪ :‬בדיקת איזון סוגריים ‪103 .........................‬‬
‫‪13.11.10‬‬
‫תרגיל מספר אחת‪-‬עשרה‪ :‬המרת תא ברשימה ברשימה ‪104 .....‬‬
‫‪13.11.11‬‬
‫‪ 13.11.12‬תרגיל מספר שתים‪-‬עשרה‪ :‬מחיקת איבר מינימלי מסדרות‬
‫המרכיבות רשימה מקושרת ‪104 .................................................................‬‬
‫‪ 14‬עצים בינאריים ‪106 ......................................................................................‬‬
‫‪ 14.1‬הגדרת עץ בינארי ‪106 .............................................................................‬‬
‫מימוש עץ בינארי בתכנית מחשב ‪108 .................................................‬‬
‫‪14.2‬‬
‫בניית עץ חיפוש בינארי ‪109 ...............................................................‬‬
‫‪14.3‬‬
‫‪ 14.4‬ביקור בעץ חיפוש בינארי ‪122 ...................................................................‬‬
‫‪ 14.5‬חיפוש ערך מבוקש בעץ בינארי כללי ובעץ חיפוש בינארי ‪124 .......................‬‬
‫‪ 14.6‬ספירת מספר הצמתים בעץ בינארי ‪126 .....................................................‬‬
‫‪ 14.7‬מציאת עומקו של עץ בינארי ‪127 ..............................................................‬‬
‫‪ 14.8‬האם עץ בינארי מקיים שלכל צומת פנימי יש שני בנים ‪128 .........................‬‬
‫‪ 14.9‬האם עץ בינארי מקיים שעבור כל צומת סכום הערכים בבת‪-‬העץ השמאלי‬
‫קטן או שווה מסכום הערכים בתת‪-‬העץ הימני ‪129 ............................................‬‬
‫‪ 14.11‬מחיקת צומת מעץ בינארי ‪134 ...............................................................‬‬
‫‪ 14.11.1‬תיאור האלגוריתם ‪134 ...................................................................‬‬
‫‪ 14.11.2‬המימוש ‪135 ..................................................................................‬‬
‫‪ 14.12‬כמה זה עולה? ‪143 ................................................................................‬‬
‫‪ 14.12.1‬הוספת נתון ‪143 .............................................................................‬‬
‫‪ 14.12.2‬מחיקת נתון ‪144 .............................................................................‬‬
‫‪ 14.12.3‬חיפוש נתון ‪144 ..............................................................................‬‬
‫‪ 14.13‬האם עץ בינארי מלא ‪144 .......................................................................‬‬
‫‪ 14.14‬האם בכל הצמתים בעץ השוכנים באותה רמה מצוי אותו ערך ‪150 .............‬‬
‫‪ 14.14.1‬הפונקציה‪151 ...................................... equal_vals_at_level :‬‬
‫‪ 14.14.2‬הפונקציה‪151 ........................................ get_val_from_level :‬‬
‫‪ 14.14.3‬הפונקציה‪156 .......................................................check_level :‬‬
‫‪ 14.14.4‬זמן הריצה של ‪157 ................................. equal_vals_at_level‬‬
‫‪ 14.14.5‬פתרון חלופי לבעיה‪158 ...................................................................‬‬
‫‪ 14.15‬האם כל הערכים בעץ שונים זה מזה ‪158 .................................................‬‬
‫‪ 14.16‬תרגילים ‪160 ........................................................................................‬‬
‫‪ 13.14.1‬תרגיל ראשון‪ :‬עץ ‪160 ............................................................. radix‬‬
‫‪ 13.14.2‬תרגיל שני‪ :‬מציאת קוטר של עץ ‪161 .................................................‬‬
‫‪ 13.14.3‬תרגיל שלישי‪ :‬מה עושה התכנית? ‪162 ..............................................‬‬
‫‪ 13.14.4‬תרגיל רביעי‪ :‬בדיקה האם עץ א' משוכן בעץ ב' ‪162 ............................‬‬
‫‪ 13.14.5‬תרגיל חמישי‪ :‬תת‪-‬עץ מרבי בו השורש גדול מצאצאיו ‪164 ..................‬‬
‫‪ 13.14.6‬תרגיל שישי‪ :‬בדיקה האם תת‪-‬עץ שמאלי מחלק כל שורש‪ ,‬וכל שורש‬
‫מחלק תת‪-‬עץ ימני ‪165 ................................................................................‬‬
‫‪ 13.14.7‬תרגיל שביעי‪ :‬בדיקה האם עץ סימטרי ‪165 .......................................‬‬
‫‪ 13.14.8‬תרגיל שמיני‪ :‬איתור הערך המזערי בעץ ‪166 ......................................‬‬
‫‪ 13.14.9‬תרגיל תשיעי‪ :‬עץ ביטוי )עבור ביטוי אריתמטי( ‪166 ............................‬‬
‫‪ 13.14.10‬תרגיל עשירי‪ :‬חישוב מספר עצי החיפוש בני ‪ n‬צמתים ‪167 ................‬‬
‫‪ 13.14.11‬תרגיל מספר אחד‪-‬עשר‪ :‬איתור הצומת העמוק ביותר בעץ ‪167 ..........‬‬
‫‪ 13.14.12‬תרגיל מספר שניים‪-‬עשר‪ :‬בדיקה האם עץ כמעט מלא‪168 .................‬‬
‫‪3‬‬
‫‪ 13.14.13‬תרגיל מספר שלושה‪-‬העשר‪ :‬האם כל תת‪-‬עץ מכיל מספר זוגי של‬
‫צמתים ‪168 ...............................................................................................‬‬
‫‪ 13.14.14‬תרגיל מספר ארבעה‪-‬עשר‪ :‬שמירת עומקם של עלי העץ במערך ‪168 ....‬‬
‫‪ 15‬מצביעים לפונקציות ‪170 ...............................................................................‬‬
‫‪ 15.1‬דוגמה פשוטה ראשונה למצביע לפונקציה ‪170 ...........................................‬‬
‫‪ 15.2‬דוגמה שניה למצביע לפונקציה ‪172 ..........................................................‬‬
‫אפשרות א' לכתיבת פונקצית המיון‪ :‬שימוש בדגל בלבד ‪172 ......‬‬
‫‪15.2.1‬‬
‫אפשרות ב' לכתיבת פונקצית המיון‪ :‬שימוש בפונקציות השוואה‬
‫‪15.2.2‬‬
‫‪174‬‬
‫ובדגל‬
‫אפשרות ג' לכתיבת פונקצית המיון‪ :‬שימוש במצביע לפונקציה‬
‫‪15.2.3‬‬
‫‪176‬‬
‫תרגילים ‪180 ...................................................................................‬‬
‫‪15.5‬‬
‫‪ 15.3.1‬תרגיל מספר אחד‪ :‬האם פונקציה אחת גדולה יותר משניה בתחום‬
‫כלשהו ‪180 ................................................................................................‬‬
‫‪ 15.3.2‬תרגיל מספר שתיים‪ :‬מציאת שורש של פונקציה ‪181 ............................‬‬
‫‪ .16‬מצביעים גנריים )* ‪182 ........................................................................ (void‬‬
‫‪ 16.1‬דוגמה ראשונה‪ :‬פונ' המשתמשת במצביע גנרי ‪182 .....................................‬‬
‫‪ 16.2‬דוגמה שניה‪ ,‬משופרת‪ :‬פונ' המשתמשת במצביע גנרי‪183 ............................‬‬
‫‪ 16.3‬דוגמה שלישית‪ :‬פונ' מיון גנרית ‪186 .........................................................‬‬
‫‪ 16.4‬דוגמה שלישית‪ :‬בניית רשימה מקושרת ממוינת גנרית ‪189 .........................‬‬
‫‪ .17‬חלוקת תכנית לקבצים ‪195 ...........................................................................‬‬
‫‪ 17.1‬הנחיית הקדם מהדר ‪195 ............................................................. #define‬‬
‫‪ 17.2‬הנחיית הקדם מהדר ‪198 ............................................................... #ifdef‬‬
‫‪ 17.3‬הנחיית הקדם מהדר ‪199 ............................................................ #include‬‬
‫‪ 17.4‬חלוקת תכנית לקבצים‪ ,‬דוגמה א'‪202 .......................................................‬‬
‫‪ 17.4.1‬הקבצים שנכללים בתכנית ‪202 ..........................................................‬‬
‫‪ 17.4.2‬הידור התכנית ‪205 ...........................................................................‬‬
‫‪ 17.4.3‬שימוש בקובצי כותר )‪208 ...................................................... (header‬‬
‫‪ 17.5‬חלוקת תכנית לקבצים‪ ,‬דוגמה ב'‪ :‬תרומתה של הנחיית המהדר ‪210 ... ifndef‬‬
‫‪ 17.5.1‬הקובץ ‪211 ............................................................................Point.h‬‬
‫‪ 17.5.2‬הקובץ ‪212 .......................................................................... Point.cc‬‬
‫‪ 17.5.3‬הקובץ ‪213 ..................................................................... Rectangle.h‬‬
‫‪ 17.5.4‬הקובץ ‪214 .................................................................... Rectangle.cc‬‬
‫‪ 17.5.5‬הקובץ ‪214 ..................................................................... my_prog.cc‬‬
‫‪ 17.5.6‬בעיות ההידור המתעוררות והתיקונים הדרושים בעזרת ‪215 ...... #ifndef‬‬
‫‪ 17.5.7‬קובץ ה‪ makefile -‬לפרויקט ‪217 ..........................................................‬‬
‫‪ 17.6‬קבועים גלובליים ‪217 .............................................................................‬‬
‫‪ 17.7‬מבוא לתכנות מונחה עצמים‪ :‬עקרון הכימוס ‪219 .......................................‬‬
‫‪ 17.8‬יצירת ספריה סטאטית ‪220 .....................................................................‬‬
‫‪ 17.9‬יצירת ספריה דינאמית )משותפת( ‪221 ......................................................‬‬
‫‪4‬‬
‫ליעננ‬
‫שבכל אחד מאיתנו‪,‬‬
‫לנ‪.‬‬
‫של כולנו‬
‫ולמאבק איתם ובהם‬
‫ָב ֶע ֶרב‬
‫ֲר ִתי‬
‫אָמ ָרה ִלי ַנע ָ‬
‫ְכ ֶש ְ‬
‫ֵל ְך‬
‫ָר ְד ִתי ָל ְרחוֹב ְל ִה ְת ַה ֵל ְך‬
‫יַ‬
‫וּמ ְס ַת ֵב ְך‬
‫הוֹל ְך ִ‬
‫יתי ֵ‬
‫וְ ָהיִ ִ‬
‫הוֹל ְך‬
‫ִמ ְס ַת ֵב ְך וְ ֵ‬
‫וּמ ְס ַת ֵב ְך‬
‫הוֹל ְך ִ‬
‫הוֹל ְך וְ ֵ‬
‫וְ ֵ‬
‫)נתן זך(‬
‫‪5‬‬
6
‫עודכן ‪1/2011‬‬
‫‪ .11‬מצביעים ומערכים‬
‫בקצרה ובפשטות ניתן לומר כי מצביע הוא משתנה המכיל כתובת של משתנה אחר‪,‬‬
‫במילים אחרות מצביע הוא משתנה אשר ַמפנה אותנו למשתנה אחר‪.‬‬
‫‪ 11.1‬עקרונות בסיסיים‬
‫נניח כי בתכנית כלשהי הגדרנו את המשתנים הבאים‪:‬‬
‫; ‪int num1, num2, *intp1, *intp2‬‬
‫מה קיבלנו? קיבלנו ארבעה משתנים‪ ,‬טיפוסם של שני הראשונים ביניהם מוכר לנו‬
‫היטב מימים ימימה‪ :‬אלה הם משתנים אשר מסוגלים לשמור מספר שלם; טיפוסם‬
‫של השניים האחרונים‪ ,‬אלה שלפני שמם כתבנו כוכבית )*(‪ ,‬הוא * ‪ ,int‬ומהותם‬
‫חדשה לנו‪ :‬משתנים אלה הם מצביעים לתאי זיכרון )כלומר למשתנים( המכילים‬
‫מספרים שלמים‪ .‬ראשית‪ ,‬נצייר כיצד נראית המחסנית בעקבות הגדרת המשתנים‬
‫הללו‪ ,‬ואחר נמשיך בהסבר‪ .‬שימו לב כי מצביעים אנו מציירים עם חץ שראשו הוא‬
‫בצורת ‪ ,v‬וזאת בניגוד לפרמטרי הפניה אותם אנו מציירים עם חץ שראשו הוא‬
‫משולש שחור‪:‬‬
‫‪num1‬‬
‫‪num2‬‬
‫‪intp1‬‬
‫‪intp2‬‬
‫המשתנים ‪ num1, num2‬מסוגלים לשמור מספר שלם‪ ,‬על‪-‬כן נוכל לכתוב‪:‬‬
‫;‪ num1 = 17; num2 = 3879‬ובתאי הזיכרון המתאימים ישמרו הערכים‬
‫הרצויים‪ .‬מצב המחסנית אחרי ביצוע שתי השמות אלה יהיה‪:‬‬
‫‪num1= 17‬‬
‫‪num2 = 3879‬‬
‫‪intp1‬‬
‫‪intp2‬‬
‫‪7‬‬
‫לעומת זאת‪ intp1, intp2 ,‬הם מצביעים‪ .‬עליהם אנו רשאים לבצע פעולה כגון‪:‬‬
‫;‪ . intp1 = &num1‬נסביר את משמעות הפעולה‪ :‬אופרטור ה‪ & -‬מחזיר את‬
‫כתובתו של המשתנה )של האופרנד( עליו הוא מופעל‪ ,‬במקרה שלנו הוא מחזיר את‬
‫כתובתו של ‪ ,num1‬כתובת זאת אנו משימים למשתנה ‪ ,intp1‬ועל כן עתה ‪intp1‬‬
‫מצביע על ‪ ,num1‬מבחינה ציורית נציג זאת‪:‬‬
‫‪num1= 17‬‬
‫‪num2 = 3879‬‬
‫‪intp1‬‬
‫‪intp2‬‬
‫עתה אנו יכולים לפנות לתא הזיכרון הקרוי ‪ num1‬גם באופן הבא‪ . *intp1 :‬למשל‬
‫אנו רשאים לכתוב‪:‬‬
‫;‪ *intp1 = 0‬והדבר יהיה שקול לכך שנכתוב‪:‬‬
‫;‪ . num1 = 0‬אנו גם רשאים לכתוב‪ , (*intp1)++; :‬והדבר יהיה שקול‬
‫לפקודה‪ . num1++; :‬כלומר ‪ *intp1‬הוא עתה שם נרדף ל‪ .num1 -‬נדגיש כי‬
‫‪ intp1‬הוא מצביע ל‪ ,int -‬לעומת זאת‪ *intp1 :‬הוא תא הזיכרון עליו ‪intp1‬‬
‫מורה‬
‫מצביע‪ .‬הפעולה בה אנו פונים באמצעות מצביע לשטח הזיכרון עליו המצביע ֵ‬
‫נקראת ‪.dereferencing‬‬
‫עוד נדגיש כי בשפת סי לכוכבית יש משמעות שונה לגמרי בהגדרת המשתנה )עת אנו‬
‫כותבים ;‪ ,(int *p‬לעומת ַבשימוש בו בהמשך‪ :‬בעוד בהגדרת המשתנה הכוכבית‬
‫מורה שמדובר במצביע ל‪) int -‬ולא ב‪ ;(int -‬בעת השימוש ַבמשתנה ַבהמשך‬
‫הכוכבית מורה שיש לפנות לתא הזיכרון עליו המצביע מורה‪ .‬על כן ההשמה‪:‬‬
‫;‪ p = 0‬מכניסה את הערך אפס למצביע )ובכך‪ ,‬למעשה‪ ,‬מסמנת שהמצביע אינו‬
‫מורה לשום מקום(‪ ,‬בניגוד לה‪ ,‬ההשמה‪ *p =0; :‬מכניסה את הערך אפס לתא‬
‫הזיכרון עליו ‪ p‬מורה‪ .‬היא חוקית רק אם קודם לכן שלחנו את ‪ p‬להצביע על תא‬
‫זיכרון כלשהו )למשל‪ ,‬עת ביצענו‪.(p = &num1; :‬‬
‫באופן דומה אנו רשאים לבצע גם את ההשמה‪ intp2 = &num2; :‬והאפקט שלה‬
‫יהיה דומה‪ .‬עתה נוכל לבצע את ההשמה‪ , *intp1 = *intp2; :‬מה עושה‬
‫השמה זאת? היא מכניסה לתוך תא הזיכרון עליו מצביע ‪ ,intp1‬כלומר למשתנה‬
‫‪ num1‬את הערך המצוי בתא הזיכרון עליו מצביע ‪ ,intp2‬כלומר את הערך המצוי‬
‫ב‪ .num2 -‬במילים אחרות ההשמה‪ *intp1 = *intp2; :‬שקולה להשמה‪:‬‬
‫; ‪. num1=num2‬‬
‫לעומת ההשמה האחרונה שראינו‪ ,‬ההשמה‪ intp1 = intp2; :‬מכניסה למצביע‬
‫‪ intp1‬את הערך המצוי במצביע ‪ .intp2‬במילים אחרות ב‪ intp1 -‬תהיה אותה‬
‫כתובת המצויה ב‪ ;intp2 -‬ובמילים פשוטות‪ ,‬עתה שני המצביעים יצביעו על תא‬
‫הזיכרון ‪ .num2‬מבחינה ציורית נציג זאת כך‪:‬‬
‫‪num1= 17‬‬
‫‪num2 = 3879‬‬
‫‪intp1‬‬
‫‪intp2‬‬
‫‪8‬‬
‫במצב הנוכחי ‪ *intp1, *intp2, num2‬הם שלושה שמות שונים לאותו תא‬
‫זיכרון‪ .‬הדבר עלול לגרום לתוצאות לכאורה מפתיעות‪ .‬לדוגמה נביט בקטע התכנית‬
‫הבא )אשר מניח כי מצב המצביעים הוא כפי שמתואר בציור האחרון(‪:‬‬
‫; ‪num2 = 3‬‬
‫; ‪*intp1 = 5‬‬
‫; ‪cout << num2‬‬
‫הפלט שיוצג יהיה חמש‪ ,‬וזאת למרות שהפקודה האחרונה אשר הכניסה ערך‬
‫למשתנה ‪ num2‬שמה בו את הערך שלוש‪ .‬הסיבה לכך שהפלט יהיה חמש היא שגם‬
‫הפקודה ;‪ *intp1 = 5‬השימה ערך לאותו שטח זיכרון‪ ,‬ועשתה זאת אחרי‬
‫ההשמה‪ַ . num2 = 3; :‬למצב בו לאותו שטח בזיכרון ניתן לפנות באמצעות מספר‬
‫שמות אנו קוראים ‪ .alias‬זהו מצב לא רצוי‪ ,‬שכן הוא עלול לגרום לתופעות לכאורה‬
‫משונות‪ ,‬כפי שראינו‪ .‬למרות שהוא מצב לא רצוי‪ ,‬בהמשך נעשה בו שימוש רב‪.‬‬
‫שימו לב כי הפקודה‪ intp1 = 3; :‬היא פקודה שגויה‪ ,‬שכן אנו מנסים להכניס‬
‫למצביע )שטיפוסו‪ (int * :‬ערך שלם )כלומר ערך מטיפוס ‪ .(int‬זה לא יהיה בלתי‬
‫סביר לחשוב שכתובות במחשב מיוצגות באמצעות מספרים טבעיים‪ ,‬ולמרות זאת‬
‫השמה כגון הנ"ל היא שגויה‪.‬‬
‫‪ 11.2‬הדמיון והשוני בין מצביע לבין מערך‬
‫כל השימוש שעשינו עד כאן במצביעים היה כדי להפנותם לתאי זיכרון קיימים‪ .‬יבוא‬
‫הקנטרן וישאל‪ :‬אם זה כל השימוש שניתן לעשות במצביעים‪ ,‬אז מה תועלתם?‬
‫התשובה היא שבמצביעים ניתן לעשות גם שימושים אחרים‪ .‬מצביע הוא למעשה‬
‫פוטנציאל למערך‪ .‬נסביר‪ :‬נניח שהגדרנו‪ . double a[5], *floatp; :‬נציג את‬
‫המחסנית‪:‬‬
‫=‪a‬‬
‫‪4‬‬
‫‪3‬‬
‫‪1‬‬
‫‪2‬‬
‫‪0‬‬
‫=‪floatp‬‬
‫מה קיבלנו על‪-‬גבי המחסנית? קיבלנו מערך בשם ‪ a‬בן חמישה תאים‪ ,‬ומצביע בשם‬
‫‪) .floatp‬בהמשך יובהר מדוע גם לצד ‪ a‬ציירנו חץ(‪ .‬עתה אנו רשאים לבצע את‬
‫פקודה‪ְ . floatp = new (std::nothrow) double[7]; :‬למה תגרום‬
‫פקודה זאת? פקודה זאת מממשת את הפוטנציאל הגלום במצביע‪ ,‬והופכת אותו‬
‫למערך ככל מערך אחר‪ .‬מייד ניתן הסבר מפורט; אך נקדים לתשובה רקע הכרחי‪:‬‬
‫כל המשתנים שראינו עד כה הוקצו על‪-‬גבי המחסנית‪ .‬המחסנית היא שטח בזיכרון‬
‫הראשי אשר מערכת ההפעלה מקצה לתכניתכם‪ .‬בשלב זה של חיינו אנו גם מבינים‬
‫מדוע שטח הזיכרון הזה מכונה מחסנית‪ :‬בשל שיטת ה‪ last in first out -‬על‪-‬פיה הוא‬
‫מתנהל‪ ,‬ואשר קובעת באיזה אופן חלקים נוספים מזיכרון זה מוקצים לתכנית‪,‬‬
‫ואחר משוחררים‪ .‬פרט למחסנית‪ ,‬מקצה מערכת ההפעלה לתכניתכם גם שטח נוסף‬
‫בזיכרון הראשי‪ .‬שטח זה נקרא ערמה )‪ .(heap‬הוא נקרא כך שכן הוא מנוהל בבלגן‬
‫יחסי‪ ,‬באופן דומה לדרך בה אנו זורקים דברים נוספים על ערמה‪ ,‬או מושכים‬
‫‪floatp = new‬‬
‫דברים מערמה‪ .‬עת תכנית מבצעת פקודה כדוגמת‪:‬‬
‫;]‪ (std::nothrow) double[7‬קורה הדבר הבא‪ :‬על‪-‬גבי הערמה מוקצה‬
‫מערך בן ‪ 7‬תאים ממשיים‪ ,‬והמצביע ‪ floatp‬עובר להצביע לתא מספר אפס במערך‬
‫זה‪.‬‬
‫‪9‬‬
‫מבחינה ציורית נציג זאת כך‪:‬‬
‫=‪a‬‬
‫‪6‬‬
‫‪5‬‬
‫‪4‬‬
‫‪3‬‬
‫‪2‬‬
‫‪1‬‬
‫‪4‬‬
‫‪0‬‬
‫‪3‬‬
‫‪2‬‬
‫‪1‬‬
‫‪0‬‬
‫=‪floatp‬‬
‫המלבן השמאלי מייצג את המחסנית‪ ,‬והימני את הערמה‪.‬‬
‫המערך שהתקבל יקרא ‪ .floatp‬אל תאיו נפנה כפי שאנו פונים לכל מערך אחר‪.‬‬
‫לדוגמה אנו רשאים לכתוב‪ floatp[0]=17; :‬או‪ .cin >> float[6]; :‬אנו‬
‫רואים‪ ,‬אם כן‪ ,‬כי אחרי שהקצנו למצביע מערך )באמצעות הפקודה‪floatp = :‬‬
‫…‪ ,(new‬כלומר אחרי שמימשנו את הפוטנציאל הגלום במצביע‪ ,‬אנו מתייחסים ל‪-‬‬
‫‪ floatp‬כאל כל מערך אחר‪.‬‬
‫אנו אומרים כי המערך ‪ floatp‬הוקצה דינאמית‪ ,‬בעת ריצת התכנית‪ ,‬וזאת בניגוד‬
‫למערך ‪ a‬שהוקצה סטאטית‪ ,‬וגודלו נקבע בעת שהמתכנת כתב את התכנית‪.‬‬
‫מספר הערות על פקודת ה‪:new -‬‬
‫א‪ .‬כמובן ששם הטיפוס שיופיע אחרי המילה השמורה ‪ new‬יהיה תואם לטיפוס‬
‫המצביע‪ .‬כלומר לא יתכן שנגדיר מצביע‪ , int *p; :‬ובהמשך נכתוב‪:‬‬
‫;]‪.p = new (std::nothrow) float[7‬‬
‫ב‪ .‬הקפידו שגודל המערך אותו ברצונכם להקצות יופיע בתוך סוגריים מרובעים‪,‬‬
‫)ולא בתוך סוגריים מעוגלים!(‪.‬‬
‫ג‪ .‬פקודת ה‪ new -‬עלולה להיכשל‪ ,‬כלומר היא עלולה שלא להצליח להקצות מערך‬
‫כנדרש‪ .‬במקרה זה ערכו של המצביע יהיה הקבוע ‪ .NULL‬הערך ‪ NULL‬מורה‬
‫שהמצביע אינו מצביע לשום כתובת שהיא‪ .‬נדון במשמעותו ביתר הרחבה‬
‫בהמשך‪ .‬כלל הזהב למתכנת המתחיל הוא כי אחרי ביצוע ‪ new‬יש לבדוק האם‬
‫ערכו של המצביע המתאים שונה מ‪ .NULL -‬במידה וערכו של המצביע הוא‬
‫‪ ,NULL‬סביר שתרצו לעצור את ביצוע התכנית‪ ,‬תוך שאתם משגרים הודעה‬
‫מתאימה‪ .‬נציג דוגמה‪:‬‬
‫; ]‪floatp = new (std::nothrow) double[7‬‬
‫{ )‪if (floatp == NULL‬‬
‫; "‪cout << "Can not allocate memory\n‬‬
‫; )‪exit(EXIT_FAILURE‬‬
‫}‬
‫הסבר‪ :‬אם בעקבות ביצוע פקודת ה‪ new -‬ערכו של ‪ floatp‬הוא ‪ NULL‬אזי‬
‫אנו משגרים הודעת שגיאה‪ ,‬ועוצרים את ביצוע התכנית באמצעות הפקודה‬
‫)‪ . exit(EXIT_FAILURE‬פקודה זאת תחזיר את הקוד המופיע בסוגריים‬
‫למערכת ההפעלה‪ .‬על‪-‬מנת שהקומפיילר יכיר את פקודת ה‪ , exit -‬ואת הקבוע‬
‫‪ NULL‬יש לכלול בתכניתכם את ההוראה‪ . #include <stdlib.h> :‬בדרך‬
‫כלל נוהגים לכלול את זוג הפקודות המבוצעות במקרה וההקצאה נכשלה‬
‫בפונקציה קטנה נפרדת שמקבלת מחרוזת‪ ,‬וערך שלם; הפונקציה מציגה את‬
‫המחרוזת‪ ,‬וקוטעת את ביצוע התכנית תוך החזרת ערך השלם‪.‬‬
‫ד‪ .‬הפסוקית )‪ (std::nothrow‬היא זו שדואגת שעת הקצאת הזיכרון נכשלת‬
‫התכנית לא תעוף‪ ,‬אלא למצביע יוכנס הערך ‪ .NULL‬לו לא היינו כותבים פסוקית‬
‫זאת‪ ,‬עת הקצאת הזיכרון נכשלת 'נזרקת חריגה'‪ ,‬מונח שאינו מוכר לנו עדיין‪,‬‬
‫אך שתוצאתו הבסיסית היא העפת התכנית‪ .‬על מנת שהמהדר יכיר את‬
‫הפסוקית הנ"ל יש לכלול בתכנית את הנחיית המהדר‪#include <new> :‬‬
‫‪10‬‬
‫האפשרות להקצות מערך דינמית גורמת לכך שעתה יש ביכולתנו לכתוב קטע קוד‬
‫כדוגמת הבא )נניח את ההגדרות‪:(int num_of_stud, *bible; :‬‬
‫; ‪cin >> num_of_stud‬‬
‫; ]‪bible = new (std::nothrow) int[num_of_stud‬‬
‫)‪if (bible == NULL‬‬
‫; )‪terminate(“can not allocate memory\n”, 1‬‬
‫)‪for (int i = 0; i < num_of_stud; i++‬‬
‫; ]‪cin >> bible[i‬‬
‫אחר אנו‬
‫הסבר‪ :‬ראשית אנו קוראים מהמשתמש כמה תלמידים יש בכיתתו‪ַ ,‬‬
‫מקצים מערך בגודל הדרוש‪ ,‬ולבסוף )בהנחה שההקצאה הצליחה(‪ ,‬אנו קוראים‬
‫נתונים לתוך המערך‪ .‬במידה וההקצאה נכשלה‪ ,‬אנו מזמנים את הפונקציה‬
‫‪ terminate‬כפי שתואר בפסקה הקודמת‪.‬‬
‫‪11.2.1‬‬
‫נוטציה מערכית לעומת נוטציה מצביעית‬
‫אמרנו כי מצביע הוא פוטנציאל למערך‪ .‬הוספנו שאנו מממשים את הפוטנציאל‬
‫באמצעות פקודת ה‪ ,new -‬ואחרי שמימשנו את הפוטנציאל המצביע והמערך היינו‬
‫הך הם )כמעט(‪ .‬עוד אמרנו כי המצביע‪ ,‬אחרי שהוקצה לו מערך מצביע למעשה על‬
‫תא מספר אפס במערך‪ .‬נעקוב אחרי קביעות אלה שוב‪ :‬נניח כי הגדרנו‪:‬‬
‫; ‪int a[5], *ip‬‬
‫ואחר הקצנו ל‪ ip -‬מערך‪:‬‬
‫;]‪ip = new (std::nothrow) int[5‬‬
‫)נניח כי ההקצאה הצליחה(‪.‬‬
‫עתה יש לנו בתכנית שני מערכים בגודל של חמישה תאים‪ .‬על כן אנו יכולים לכתוב‬
‫קוד כדוגמת הבא‪:‬‬
‫)‪for (int i= 0; i< 5; i++‬‬
‫; ‪a[i] = ip[i] = 0‬‬
‫האם אנו יכולים לכתוב גם‪ ? *ip = 3879; :‬ואם כן מה משמעות ההשמה הנ"ל?‬
‫התשובה היא שאנו יכולים לכתוב השמה כנ"ל‪ ,‬וההשמה תכניס לתא מספר אפס‬
‫במערך ‪ ip‬את הערך ‪ ;3879‬שכן כפי שאמרנו ‪ ip‬מצביע על התא מספר אפס ַבמערך‬
‫שהוקצה דינמית‪.‬‬
‫;‪*a = 3879‬‬
‫מה שאולי יפתיע אתכם יותר הוא שאנו רשאים לכתוב גם‪:‬‬
‫ופקודה זאת תכניס את הערך ‪ 3879‬לתא מספר אפס במערך ‪ .a‬כלומר גם ‪ a‬הוא‬
‫למעשה מצביע‪ :‬מצביע אשר מורה על התא מספר אפס ַבמערך שהוקצה סטאטית‬
‫על‪-‬גבי המחסנית‪ .‬עתה אתם גם מבינים מדוע גם לצד ‪ a‬ציירנו חץ שהצביע על התא‬
‫מספר אפס במערך המתאים‪.‬‬
‫מערכית'‪ ,‬אנו רשאים‬
‫את לולאת איפוס המערכים שכתבנו קודם בצורת כתיבה ' ַ‬
‫לכתוב גם בצורת כתיבה 'מצביעית'‪:‬‬
‫)‪for (i=0; i<5; i++‬‬
‫; ‪*(a + i) = *(ip + i) = 0‬‬
‫נסביר‪ :‬אנו מבצעים כאן אריתמטיקה של מצביעים‪ .‬ערכו של הביטוי‪(a + i) :‬‬
‫הוא המצביע אשר מורה על התא שבהסטה ‪ i‬מהתא עליו מורה ‪ .a‬לכן אם ‪ a‬מורה‬
‫על התא מספר אפס במערך‪ ,‬וערכו של ‪ i‬הוא שלוש‪ ,‬אזי הביטוי )‪ (a + i‬הוא‬
‫המצביע לתא מספר שלוש במערך‪ .‬לכן אם אנו כותבים‪ ,*(a + i) :‬כלומר אנו‬
‫הולכים בעקבות המצביע‪ ,‬אזי אנו פונים לתא מספר שלוש במערך‪ ,‬ובלולאה שלנו‬
‫‪11‬‬
‫אנו מאפסים אותו‪ .‬במילים פשוטות‪ :‬הכתיבה‪ *(a + i) :‬שקולה לכתיבה‪:‬‬
‫]‪ .a[i‬אנו רואים אם כן שוב את הדמיון בין מערך לבין מצביע שהוקצה לו מערך‪.‬‬
‫האם קיימת זהות מלאה בין מערך שהוקצה סטטית )על‪-‬גבי המחסנית(‪ ,‬לבין כזה‬
‫שהוקצה דינמית )על‪-‬גבי הערמה(? לא לגמרי‪ .‬עתה נציין את ההבדלים‪ .‬נבחן את‬
‫קטע הקוד הבא‪ ,‬תוך שאנו מניחים את ההגדרות‪:‬‬
‫; ‪int a[3], *p1, *p2, *p3‬‬
‫; ]‪p1 = new (std::nothrow) int[4‬‬
‫; ]‪p2 = new (std::nothrow) int[2‬‬
‫; ‪p3 = p1‬‬
‫; ‪p1 = p2‬‬
‫; ‪a = p2‬‬
‫מקטע הקוד השמטנו את הבדיקה האם ההקצאה הדינמית‬
‫נסביר‪ :‬ראשית ֵ‬
‫הצליחה‪ .‬אנו עושים זאת לצורך הקיצור‪ ,‬וההתמקדות ַבעיקר‪ .‬בתכניות שלכם‬
‫הקפידו תמיד לערוך את הבדיקה המתאימה‪ .‬מעבר לכך‪ :‬אנו מקצים ל‪ p1 -‬ול‪p2 -‬‬
‫שני מערכים‪ .‬עתה אנו מפנים את ‪ p3‬להצביע על המערך ‪ ,p1‬ולכן אם נכתוב ]‪p3[0‬‬
‫או נכתוב ]‪ p1[0‬נִ פנה לאותו שטח זיכרון‪ .‬נתאר זאת על‪-‬גבי המחסנית‪:‬‬
‫=‪a‬‬
‫=‪p1‬‬
‫=‪p2‬‬
‫=‪p3‬‬
‫מפנה את ‪ p1‬להצביע על אותו מערך כמו ‪) p2‬ולכן עתה‬
‫הפקודה הבאה‪ְ p1 = p2; :‬‬
‫רק ‪ p3‬מצביע על המערך בן ארבעת התאים שהוקצה דינמית(‪ .‬הפקודה האחרונה‪a :‬‬
‫‪ = p2‬היא שגיאה שתמנע מהתכנית להתקמפל בהצלחה‪ a .‬הוא מצביע אשר‬
‫קשור לנצח נצחים ַלמערך שהוקצה לו על‪-‬גבי המחסנית; שלא כמו ‪ p1‬את ‪ a‬לא‬
‫ניתן להפנות להצביע על מערך אחר‪ .‬מבחינה פורמאלית ‪a‬הוא מצביע קבוע‪ ,‬כזה‬
‫שלא ניתן לשנות את ערכו‪.‬‬
‫מאותה סיבה הפעולה ;‪ p1++‬אשר מקדמת את ‪ p1‬להצביע על התא הבא במערך‪,‬‬
‫היא תקינה‪ :‬את ‪ p1‬ניתן להזיז באופן חופשי‪ .‬לעומתה הפקודה ;‪ a++‬היא שגיאה‪.‬‬
‫שימו לב כי הפקודה ;‪ p1++‬שונה לחלוטין מהפקודה ;‪ (*p1)++‬אשר שקולה‬
‫לפקודה‪ . p1[0]++; :‬שתי הפקודות האחרונות מגדילות באחד את ערכו של תא‬
‫הזיכרון עליו מצביע ‪ ,p1‬במילים אחרות הן מגדילות באחד את הערך המצוי ַבתא‬
‫מספר אפס במערך ‪ ,p1‬כלומר הן מבצעות אריתמטיקה במספרים שלמים‪ .‬בעוד‬
‫הפקודה ‪ p1++‬היא אריתמטיקה במצביעים‪ :‬היא מסיטה את ‪ p1‬להצביע מקום‬
‫אחד ימינה יותר במערך‪.‬‬
‫נִ ראה שימוש לפקודת ההגדלה העצמית במצביעים‪ .‬נניח כי הגדרנו‪,char *cp; :‬‬
‫אחר הקצנו ל‪ cp -‬מערך )באמצעות הפקודה‪cp = new (std::nothrow) :‬‬
‫;]‪ .(char[3879‬עתה אנו יכולים לאפס את המערך גם באופן הבא‪:‬‬
‫)‪for (int i=0; i<3879; i++‬‬
‫} ; ‪{ *cp = 0; cp++‬‬
‫הסבר‪ :‬בכל סיבוב בלולאה אנו מאפסים את התא במערך עליו מצביע ‪) ,cp‬וכזכור‬
‫בתחילת התהליך ‪ cp‬מצביע על התא מספר אפס במערך(‪ ,‬ואחר מקדמים את ‪ cp‬כך‬
‫שהוא יצביע על התא הבא במערך‪.‬‬
‫‪12‬‬
‫להיכן מצביע ‪ cp‬אחרי ביצוע הלולאה? אל מחוץ לגבולות המערך‪ ,‬ליתר דיוק ַלתו‬
‫הראשון שאחרי המערך‪ .‬כדי להשיב את ‪ cp‬להצביע על התא מספר אפס במערך אנו‬
‫יכולים לבצע את הפקודה‪ cp -= 3879; :‬פקודה זאת מחזירה את ‪ cp‬להצביע‬
‫‪ 3879‬תאים אחורנית‪ ,‬כלומר לראש המערך‪ .‬כמובן שלוּ היינו כותבים במקום את‬
‫הפקודה‪ cp -= 3879; :‬את הפקודה‪ cp -= 4000; :‬אזי היינו שולחים את‬
‫‪ cp‬להצביע אי שם למקום לא ידוע ַבזיכרון‪ ,‬וזאת פעולה מסוכנת‪ ,‬שאת אחריתה מי‬
‫יישור‪.‬‬
‫הדוגמה האחרונה רמזה לנו על כמה מהסכנות הטמונות בשימוש לא די זהיר וקפדני‬
‫במצביעים‪ .‬נראה דוגמה נוספת‪ :‬נניח שהגדרנו‪ bool *bp; :‬ואחר‪ ,‬בלא שהפננו‬
‫את ‪ bp‬להצביע על תא זיכרון מוגדר כלשהו‪ ,‬אנו מבצעים‪ . *bp = true; :‬מה‬
‫אנו עושים בכך? ב‪ ,bp -‬כמו בכל משתנה שלא נקבע לו עדיין ערך‪ ,‬יש ערך 'זבל'‬
‫מקרי כלשהו‪ ,‬כלומר הוא מצביע לתא מקרי כלשהו בזיכרון‪ .‬עתה אנו נגשים לאותו‬
‫תא מקרי ומכניסים לתוכו את הערך ‪ .true‬התא המקרי שעליו 'דרכנו' עשוי להכיל‬
‫משתנים אחרים של התכנית‪ .‬התוצאה עלולה להיות שתכניתנו תתנהג בצורה‬
‫ביזארית‪ ,‬שלא לומר אכזרית‪ .‬לצערי‪ ,‬כבר ראיתי מתכנתים מתחילים שבאמצעות‬
‫שימוש לא די זהיר במצביעים יצרו באגים שאת האפקט שלהם ניתן לתאר בלשון‬
‫המעטה כ‪'-‬מפתיע‪ ,‬יצירתי‪ ,‬עולה על כל דמיון'‪ .‬הצד המרגיע של הנושא הוא שכל‬
‫התקלות שתחוללו באמצעות שימוש קלוקל במצביעים תוגבלנה לזיכרון הראשי‪,‬‬
‫ולכן תעלמנה עת תכבו את המחשב; במילים אחרות‪ ,‬לא תגרמו לנזק בלתי הפיך‬
‫למחשב‪ ,‬או לתוכנו של הדיסק‪.‬‬
‫אחד ממנגנוני הבטיחות שעוזר לנו להישמר מפני תקלות כדוגמת זו שראינו הוא‬
‫השימוש ב‪ .NULL -‬כאשר ערכו של מצביע ‪ p‬הוא ‪ NULL‬משמעות הדבר היא כי ‪p‬‬
‫אינו מצביע לשום תא בזיכרון‪ ,‬ועל‪-‬כן לא ניתן לפנות באמצעותו ַלזיכרון‪ ,‬כלומר לא‬
‫ניתן לבצע‪ . *p = …; :‬כיצד נוכל להיעזר ב‪ NULL -‬בתכניות שאנו כותבים?‬
‫א‪ .‬בעת הגדרת המצביע נאתחל אותו לערך ‪. char *bp = NULL; :NULL‬‬
‫ב‪ַ .‬בהמשך אולי כן ואולי לא נפנה את ‪ bp‬להצביע על תא כלשהו ַבזיכרון )למשל‪,‬‬
‫באמצעות פקודה כגון … ‪.(bp = new‬‬
‫ג‪ .‬לבסוף‪ ,‬לפני שאנו פונים לתא הזיכרון עליו מצביע ‪) bp‬כלומר לפני שאנו‬
‫נישאל האם ערכו של ‪ bp‬שונה מ‪ .NULL -‬אם ערכו של ‪bp‬‬
‫מבצעים‪ְ (*bp = … :‬‬
‫שונה מ‪ NULL -‬אנו מובטחים כי ‪ bp‬הופנה לתא כלשהו ַבזיכרון‪ ,‬ועל כן הפנִ יה‬
‫… = ‪ *bp‬היא בטוחה‪ .‬אם‪ ,‬לעומת זאת‪ bp ,‬לא הופנה להצביע על תא כלשהו‬
‫ַבזיכרון‪ ,‬אזי ערכו של ‪ bp‬יישאר כפי שהוא היה בעקבות האיתחול‪ ,‬כלומר ערכו‬
‫יהיה ‪ ,NULL‬ואז לא ננסה לפנות לתא הזיכרון עליו ‪ bp‬מצביע‪.‬‬
‫נראה דוגמה‪:‬‬
‫הגדרת המצביע‪. char *bp = NULL; :‬‬
‫אחר‪ -‬כך‪ ,‬אי שם בהמשך התכנית נכתוב‪:‬‬
‫{ )…( ‪if‬‬
‫; ]‪bp = new (std::nothrow) char[17‬‬
‫)‪if (bp == NULL‬‬
‫; )‪terminate("Can not allocate memory\n”, 1‬‬
‫}‬
‫כלומר אם תנאי כלשהו )שלא פרטנו( מתקיים אנו מקצים מערך‪ ,‬ומפנים את ‪bp‬‬
‫להצביע עליו‪.‬‬
‫ולבסוף‪ ,‬הלאה יותר בתכנית אנו כותבים‪:‬‬
‫)‪if (bp != NULL‬‬
‫; ‪bp[1] = bp[3] = 3879‬‬
‫‪13‬‬
‫בזכות האיתחול של ‪ bp‬ל‪ NULL -‬בעת הגדרתו‪ ,‬מתקיים כי הבדיקה‪:‬‬
‫)‪ if(bp != NULL‬מעידה האם ל‪ bp -‬הוקצה מערך‪ ,‬ועל כן אנו יכולים לפנות‬
‫בבטחה לתאים מספר אחד‪ ,‬ומספר שלוש במערך‪ ,‬או שמא ל‪ bp -‬לא הוקצה מערך‪,‬‬
‫ועל‪-‬כן ערכו נותר כפי שהוא היה בעת הגדרתו‪ ,‬כלומר ‪.NULL‬‬
‫כלל הזהב האחרון אותו עלינו ללמוד ַב ֵהקשר של מצביעים ומערכים הוא‪ :‬כל מערך‬
‫שהוקצה באמצעות פקודת ‪ ,new‬ישוחרר בשלב זה או אחר‪) ,‬אולי רק לקראת סיום‬
‫ריצת התכנית(‪ ,‬באמצעות פקודת ‪ .delete‬לדוגמה‪ ,‬כדי לשחרר את המערך עליו‬
‫מצביע ‪ bp‬נכתוב‪ . delete [] bp; :‬משמעות הפקודה היא שאנו מורים‬
‫למערכת ההפעלה כי איננו זקוקים יותר למערך‪ ,‬ולכן ניתן למחזר את שטח הזיכרון‬
‫שלו‪ .‬פניה לזיכרון שמוחזר מהווה שגיאה; על כן היזהרו מקוד כדוגמת הבא‪:‬‬
‫; ]‪intp1 = new (std::nothrow) int[17‬‬
‫; ‪inp2 = intp1‬‬
‫…‬
‫; ‪delete [] intp2‬‬
‫; ‪intp1[0] = 12‬‬
‫בקוד זה ‪ intp2‬מצביע לאותו שטח זיכרון כמו ‪ .intp1‬על‪-‬כן עת שחררנו את שטח‬
‫הזיכרון‪ ,‬באמצעות הפקודה‪' , delete [] intp2; :‬שמטנו את השטיח גם‬
‫מתחת לרגלי ‪ ,'intp1‬כלומר שטח הזיכרון עליו מצביע )עדיין( ‪ intp1‬כבר אינו‬
‫עומד לרשותנו‪ ,‬וזו שגיאה לפנות אליו ) ַבפקודה‪.(intp1[0] = 12; :‬‬
‫פקודת ה‪ delete -‬אינה מכניסה למצביע ערך ‪ .NULL‬היא מותירה במצביע ערך‬
‫'זבל'‪.‬‬
‫טעות שכיחה אצל מתכנתים מתחילים היא לכתוב קטע קוד כדוגמת הבא‪:‬‬
‫; ] … [‪intp2 = new int‬‬
‫…‬
‫; ] … [‪intp1 = new (std::nothrow) int‬‬
‫; ‪intp1 = intp2‬‬
‫מקְצה מערך ל‪ .intp2 -‬בהמשך‬
‫נסביר‪ :‬הפקודה הראשונה בין השלוש המתוארות ְ‬
‫התכנית‪ַ ) ,‬בקטע המתואר בשלוש הנקודות(‪ ,‬אנו עושים שימושים שונים במערך‪.‬‬
‫עתה אנו רוצים להפנות את ‪ intp1‬להצביע לאותו מערך כמו ‪ ,intp2‬וזה לגיטימי;‬
‫אולם להקדים ַלפקודה‪ intp1 = intp2; :‬את הפקודה‪intp1 = new … :‬‬
‫; ] … [‪ int‬זו שטות‪ .‬נסביר מדוע‪ :‬נתאר את מצב הזיכרון אחרי ביצוע שתי‬
‫פקודות ההקצאה‪:‬‬
‫‪intp1‬‬
‫‪intp2‬‬
‫עתה‪ ,‬הפקודה האחרונה‪ intp1 = intp2; :‬יוצרת את המצב הבא‪:‬‬
‫‪intp1‬‬
‫‪intp2‬‬
‫‪14‬‬
‫התוצאה היא שהמערך עליו הצביע ‪ intp1‬עד עתה הפך להיות מין ‪ ,zombie‬שמחד‬
‫גיסא מצוי בזיכרון‪ ,‬ומאידך גיסא אין כל דרך לפנות אליו‪ .‬לכן אם ברצונכם להפנות‬
‫מצביע ‪ p‬להצביע על מערך עליו מורה מצביע ‪ ,q‬לעולם אל תקדימו לפקודת ההשמה‬
‫;‪ p = q‬פקודה‪. p = new … :‬‬
‫עד כאן הצגנו את אופן השימוש במצביעים לשם יצירת מערכים‪ .‬הדוגמות שבהמשך‬
‫הפרק תסייענה לכם‪ ,‬ראשית‪ ,‬להטמיע את שהוסבר עד כה‪ ,‬ושנית ללמוד כיצד לשלב‬
‫את השימוש במצביעים ובפונקציות‪.‬‬
‫‪ 11.3‬פונקציות המקבלות מצביעים או מערכים‬
‫בעבר הכרנו את הפונקציה ‪ .strlen‬להזכירכם‪ ,‬פונקציה זאת מקבלת מערך של‬
‫תווים‪ ,‬ומחזירה את אורכו של הסטרינג השמור במערך )לא כולל ה‪ .(‘\0’ -‬אנו‬
‫יכולים לכתוב את הפונקציה באופן הבא‪:‬‬
‫{ )][‪int strlen( const char s‬‬
‫; ‪int i‬‬
‫)‪for (i=0; s[i] != ‘\0’; i++‬‬
‫;‬
‫; )‪return(i‬‬
‫}‬
‫הסבר‪ :‬לולאת ה‪ for -‬מתקדמת על המערך כל עוד התו המצוי בתא הנוכחי במערך‬
‫אינו ’‪ .‘\0‬בגוף הלולאה מתבצעת הפקודה הריקה‪ .‬אחרי הלולאה אנו מחזירים‬
‫את ערכו של ‪.i‬‬
‫עתה‪ ,‬שלמדנו את השקילות בין מערכים ומצביעים‪ ,‬אנו יכולים לכתוב את‬
‫הפונקציה בצורה דומה אך שונה‪:‬‬
‫{ )‪int strlen( const char *s‬‬
‫; ‪int i‬‬
‫)‪for (i=0; s[i] != ‘\0’; i++‬‬
‫;‬
‫; )‪return(i‬‬
‫}‬
‫הסבר‪ :‬את הפרמטר תיארנו הפעם בנוטציה מצביעית‪ ,‬במקום בנוטציה מערכית‪.‬‬
‫בשל השקילות בין מערך למצביע הדבר לגיטימי‪ .‬את גוף הפונקציה לא שינינו‪ .‬נדגיש‬
‫כי נוסח זה‪ ,‬כמו הנוסח שראינו לפניו‪ ,‬וכמו אלה שנראה בהמשך‪ ,‬אינו מחייב‬
‫קריאה עם מערך שהוקצה דינמית או כזה שהוקצה סטטית; ניתן לקרוא לכל אחת‬
‫מהפונקציות שאנו כותבים בסעיף זה הן עם מערך שהוקצה סטטית על המחסנית‪,‬‬
‫והן עם כזה שהוקצה דינמית על הערמה‪.‬‬
‫אנו יכולים לכתוב את אותה פונקציה גם בצורה שלישית‪:‬‬
‫{ )‪int strlen( const char *s‬‬
‫; ‪int i‬‬
‫)‪for (i=0; *(s +i) != ‘\0’; i++‬‬
‫;‬
‫‪15‬‬
‫; )‪return(i‬‬
‫}‬
‫הסבר‪ :‬הפעם גם את תנאי הסיום בלולאה המרנו מנוטציה מערכית לנוטציה‬
‫מצביעית‪ .‬כפי שאנו יודעים הביטוי )‪ *(s+i‬פונה לתא מספר ‪ i‬במערך עליו מצביע‬
‫‪ .s‬במילים אחרות )‪ (s+i‬הוא מצביע לתא המצוי בהסטה ‪ i‬מהתא עליו מצביע ‪,s‬‬
‫ולכן )‪ *(s+i‬פונה לתא המצוי בהסטה ‪ i‬מהתא עליו מצביע ‪.s‬‬
‫ולבסוף נציג את אותה פונקציה בצורת כתיבה רביעית‪:‬‬
‫{ )‪int strlen( const char *s‬‬
‫; ‪int i‬‬
‫)‪for (i=0; *s != ‘\0’; i++, s++‬‬
‫;‬
‫; )‪return(i‬‬
‫}‬
‫הסבר‪ :‬הפעם אנו משתמשים באריתמטיקה על מצביעים‪ .‬אנו מקדמים את‬
‫המצביע ‪ s‬כל עוד התא עליו הוא מצביע כולל ערך שונה מ‪ .‘\0’ -‬במקביל אנו‬
‫סופרים כמה פעמים קידמנו את ‪ ,s‬וזהו אורכו של הסטרינג‪.‬‬
‫‪16‬‬
‫הגרסה האחרונה שכתבנו מדגימה נקודה אותה חשוב להבין‪ :‬המצביע המועבר‬
‫לפונקציה מועבר כפרמטר ערך‪ ,‬ועל כן שינויים שהפונקציה עושה לפרמטר שלה ‪,s‬‬
‫לא ישפיעו‪ ,‬אחרי תום ביצוע הפונקציה‪ ,‬על הארגומנט עמו הפונקציה נקראה‪.‬‬
‫ארגומנט זה ימשיך להצביע על התא הראשון במערך‪ .‬עת מצביע מועבר כפרמטר‬
‫ערך לפונקציה‪ ,‬המחשב פועל באותו אופן בו הוא פועל עת משתנה פרימיטיבי כלשהו‬
‫מועבר כפרמטר ערך‪ :‬המחשב מעתיק את ערכו של הארגומנט המתאים על הפרמטר‬
‫המתאים‪ .‬במילים אחרות הכתובת המוחזקת בארגומנט מועתקת לפרמטר‪ .‬מבחינה‬
‫ציורית לפרמטר יוקצה חץ משלו‪ ,‬חץ זה יאותחל להצביע על אותו מקום כמו‬
‫הארגומנט‪ .‬נדגים זאת בציור‪ .‬נניח כי בתכנית הוגדר מערך ]‪ ,name[4‬ועתה אנו‬
‫קוראים לפונקציה ‪) strlen‬בכל אחד מארבעת הנוסחים שכתבנו(‪ .‬נציג את מצב‬
‫המחסנית טרם הקריאה‪:‬‬
‫= ‪name‬‬
‫’‪‘\0‬‬
‫‪c‬‬
‫‪a‬‬
‫‪b‬‬
‫אנו זוכרים כי מערך )כדוגמת ‪ (name‬הוא למעשה מצביע לתא מספר אפס במערך‪.‬‬
‫עתה נציג כיצד תראה המחסנית אחרי בניית רשומת ההפעלה של ‪) strlen‬נשמיט‬
‫מרשומת ההפעלה את כתובת החזרה‪ ,‬ואת המשתנה הלוקלי ‪ ,i‬שאינם מעניינים‬
‫אותנו עתה(‪.‬‬
‫= ‪name‬‬
‫’‪‘\0‬‬
‫‪c‬‬
‫‪b‬‬
‫‪a‬‬
‫= ‪s‬‬
‫הסבר‪ :‬גם ‪ s‬הוא מצביע )אפילו אם הוא מוגדר בכותרת הפונקציה כ‪,(char s[] -‬‬
‫ולכן ציירנו אותו עם חץ בצורת ‪) v‬כפי שאנו מציירים מצביעים; בניגוד למשולש‬
‫שחור המשמש לציור פרמטרי הפניה(‪ .‬לאן מצביע ‪ ?s‬לאותו מקום כמו הארגומנט‬
‫המתאים לו‪ ,‬כלומר ‪ .name‬במילים אחרות ערכו של ‪ name‬הועתק על ‪ .s‬אם עתה‬
‫נסיט את ‪ s‬למקום אחר איננו משנים בכך את המקום אליו מצביע ‪) name‬כפי‬
‫שמתאים שיקרה עם פרמטר ערך(‪.‬‬
‫הסבר זה מסייע לנו להבין תופעה שמוכרת לנו כבר מזמן‪ :‬עת מערך מועבר כפרמטר‬
‫ְלפונקציה‪ ,‬הפונקציה יכולה לשנות את תוכנם של תאי המערך‪ .‬עתה נניח כי‬
‫בפונקציה שלנו אנו מבצעים‪ *s = 'x’; :‬או במילים אחרות‪ s[0]=’x’; :‬מה‬
‫יקרה? אנו פונים לתא מספר אפס במערך עליו מצביע ‪ s‬ולשם מכניסים את הערך‬
‫’‪ .‘x‬ערך זה יוותר כמובן במערך גם אחרי תום ביצוע הפונקציה‪.‬‬
‫כדי לחדד את התופעה נציג את הפונקציה הבאה‪:‬‬
‫{ )‪void f( int *pp, int nn‬‬
‫;‪np = 3879; *pp = 17; pp = NULL‬‬
‫}‬
‫לפונקציה יש שני פרמטרי ערך‪ :‬הראשון בהם הוא מצביע )במילים אחרות מערך(‪,‬‬
‫והשני הוא מספר שלם‪.‬‬
‫‪17‬‬
‫נניח כי בתכנית הראשית הוגדרו‪, int n = 0, *p = new int[5]; :‬‬
‫ולחמשת תאי המערך ‪ ,p‬הוכנס הערך חמש‪.‬‬
‫הזיכרון הכולל את משתני התכנית הראשית נראה‪:‬‬
‫‪n = 0‬‬
‫‪5‬‬
‫‪5‬‬
‫‪5‬‬
‫‪5‬‬
‫‪5‬‬
‫= ‪p‬‬
‫עתה אנו קוראים לפונקציה‪f(p, n); :‬‬
‫בעקבות הקריאה לפונקציה נוספת על‪-‬גבי המחסנית רשומת ההפעלה של ‪ .f‬נציג‬
‫את מצב הזיכרון‪:‬‬
‫‪n = 0‬‬
‫‪5‬‬
‫‪5‬‬
‫‪5‬‬
‫‪5‬‬
‫‪5‬‬
‫= ‪p‬‬
‫‪np = 0‬‬
‫= ‪pp‬‬
‫נסביר‪ :‬כפי של‪ np -‬הועתק ערכו של ‪ ,n‬כך ל‪ pp -‬הועתק ערכו של ‪ ,p‬ולכן ‪ pp‬מצביע‬
‫לאותו מערך כמן ‪.p‬‬
‫עתה הפונקציה ‪ f‬מתחילה להתבצע‪ .‬ההשמה‪ np = 3879; :‬מכניסה ערך ל‪,np -‬‬
‫ואינה משנה את ערכו של ‪ .n‬ההשמה‪) *pp = 17; :‬השקולה להשמה‪:‬‬
‫;‪ (pp[0] = 17‬מכניסה את הערך ‪ 17‬לתא עליו מצביע ‪ .pp‬ההשמה‪:‬‬
‫;‪ pp = NULL‬מכניסה ל‪ pp -‬את הערך ‪ ,NULL‬ואינה משנה את ערכו של ‪ .p‬מצב‬
‫הזיכרון אחרי ביצוע שלוש הפעולות הללו יהיה‪:‬‬
‫‪n = 0‬‬
‫‪5‬‬
‫‪5‬‬
‫‪5‬‬
‫‪5‬‬
‫‪17‬‬
‫= ‪p‬‬
‫‪np = 3879‬‬
‫= ‪pp‬‬
‫סימון 'ההארקה' לצד ‪ pp‬הוא הדרך בה אנו מציירים מצביע שערכו הוא ‪.NULL‬‬
‫עתה הפונקציה ‪ f‬מסתיימת‪ ,‬ורשומת ההפעלה שלה מוסרת מעל המחסנית‪ .‬כפי‬
‫שאנו יודעים השינויים שהפונקציה הכניסה לתוך ‪ np‬אינם נותרים ב‪ .n -‬באופן‬
‫דומה‪ ,‬השינויים שהפונקציה הכניסה ל‪ pp -‬אינם נותרים ב‪ .p -‬ולבסוף‪ ,‬בהתאמה‬
‫למה שאנו כבר מכירים היטב‪ ,‬שינויים שהפונקציה הכניסה לתוך המערך עליו הורה‬
‫‪ pp‬נותרים במערך גם אחרי תום ביצוע הפונקציה‪.‬‬
‫‪18‬‬
‫בעבר ראינו כי אם אנו רוצים למנוע מפונקציה המקבלת מערך לשנות את ערכם של‬
‫תאי המערך אנו יכולים להגדיר את הפרמטר של הפונקציה באופן הבא‪const :‬‬
‫][‪ .int a‬אם מתכנת כלשהו ינסה בגוף הפונקציה לכתוב פקודה כגון‪a[0]=17; :‬‬
‫אזי התכנית לא תעבור קומפילציה )ולכן העברת מערך כפרמטר שהינו קבוע אינה‬
‫שקולה להעברת משתנה פרימיטיבי כפרמטר ערך(‪ .‬עתה למדנו כי אם פונקציה‬
‫מקבלת מערך כפרמטר אנו יכולים לתאר את הפרמטר של הפונקציה גם באופן‬
‫הבא‪ . int *a :‬גם במקרה זה כדי למנוע מהפונקציה לשנות את ערכם של תאי‬
‫המערך אנו רשאים לכתוב‪ .const int *a :‬אולם כתיבה זאת לא תמנע‬
‫מהפונקציה לשנות את ערכו של המצביע‪ .‬בגוף הפונקציה נוכל לכתוב פקודה כגון‪:‬‬
‫;‪ a = NULL‬ולא יהיה בכך כל פסול‪ .‬מה שלא נוכל לכתוב הוא‪*a = 3879; :‬‬
‫כלומר אין ביכולתנו לשנות את ערכם של תאי המערך עליו מצביע ‪ ,a‬אך יש‬
‫ביכולתנו לשנות את ערכו של ‪ .a‬כדי למנוע מפונקציה לשנות את ערכו של המצביע‬
‫המועבר לה נכתוב‪ . int * const a :‬עתה פקודה כגון‪ a= NULL; :‬תגרום‬
‫לשגיאת קומפילציה‪ .‬לעומתה פקודה כגון‪ *a = 1; :‬היא עתה לגיטימית‪ ,‬שכן‬
‫הפעם לא אסרנו לשנות את ערכם של תאי המערך עליו מצביע ‪ ,a‬רק אסרנו לשנות‬
‫את המקום עליו ‪ a‬מצביע‪ .‬כמובן שניתן לשלב את שני האיסורים גם יחד‪const :‬‬
‫‪ . int * const a‬לבסוף נזכור כי אם מצביע הועבר כפרמטר ערך אזי בדרך כלל‬
‫לא כל‪-‬כך יפריע לנו שהפונקציה תוכל לשנות את ערכו של הפרמטר שלה‪ .‬שהרי‬
‫השינוי לא יוותר בארגומנט המתאים לאותו פרמטר‪.‬‬
‫‪ 11.4‬מצביע כפרמטר הפניה‬
‫עתה נניח כי ברצוננו לכתוב תכנית אשר מגדירה מצביע );‪,(int *p = NULL‬‬
‫מקצה את המערך‬
‫קוראת מהמשתמש גודל מערך רצוי )לתוך המשתנה ‪ְ ,(size‬‬
‫)באמצעות פקודת ‪ ,(new‬ולבסוף‪ ,‬קוראת נתונים לתוך המערך‪ .‬עוד נניח כי כדי‬
‫לסמן את סוף המערך )שגודלו אינו ידוע לתכנית הראשית(‪ ,‬מציבה הפונקציה בתא‬
‫האחרון במערך את הערך הקבוע ‪ .THE_END‬נציג את קטע התכנית הבא‪ ,‬אשר‬
‫מבצע את המשימה באמצעות פונקציה?‬
‫{ )‪void read_data(int *arr‬‬
‫; ‪int size‬‬
‫; ‪cin >> size‬‬
‫; ]‪arr = new (std::nothrow) int[size‬‬
‫… )‪if (arr == NULL‬‬
‫)‪for (int i=0; i< size -1; i++‬‬
‫; ]‪cin >> arr[i‬‬
‫; ‪a[size –1] = THE_END‬‬
‫}‬
‫נניח כי אחרי שהגדרנו את‪ , int *bible = NULL; :‬אנו מזמנים את‬
‫הפונקציה‪ . read_data(bible); :‬האם קטע תכנית זה עונה על צרכינו?‬
‫התשובה היא‪ :‬לא ולא‪ .‬נסביר‪ :‬הפונקציה ‪ read_data‬מקבלת את המצביע‬
‫כפרמטר ערך‪ .‬על כן שינויים שהפונקציה מכניסה ל‪ ,arr -‬אינם נותרים בתום ביצוע‬
‫הפונקציה ב‪ ,bible -‬וערכו נותר ‪ NULL‬כפי שהוא היה לפני הקריאה‪ .‬נציג את‬
‫התהליך בעזרת המחסנית‪ :‬נתחיל בהצגת מצב המחסנית עליה מוקצה המשתנה‬
‫‪ bible‬של התכנית הראשית‪:‬‬
‫= ‪bible‬‬
‫‪19‬‬
‫ערכו של ‪ bible‬אותחל בהגדרה להיות ‪ ,NULL‬ועל כן ציירנו אותו כמתואר‪ .‬עתה‬
‫נערכת קריאה לפונקציה‪ .‬מצב המחסנית בעקבות הקריאה יהיה הבא )מהציור‬
‫השמטנו פרטים שאינם מהותיים לצורך דיוננו הנוכחי(‪:‬‬
‫= ‪bible‬‬
‫= ‪arr‬‬
‫מציור המחסנית השמטנו את המשתנים הלוקליים של ‪ ,read_data‬ואת כתובת‬
‫החזרה‪ .‬ערכו של ‪ arr‬הוא ‪ NULL‬שכן הארגומנט המתאים ל‪ arr -‬הוא ‪,bible‬‬
‫ערכו של ‪ bible‬הוא ‪ ,NULL‬וערך זה מועתק ל‪.arr -‬‬
‫עתה הפונקציה מתחילה להתבצע‪ .‬הפקודה‪ arr = new …; :‬יוצרת את המצב‬
‫הבא‪:‬‬
‫= ‪bible‬‬
‫= ‪arr‬‬
‫אחר הפונקציה‬
‫כלומר על המערך שהוקצה מצביע ‪) arr‬אך לא ‪ַ .(bible‬‬
‫‪ read_data‬קוראת נתונים לתוך המערך‪ ,‬ועת היא מסתיימת‪ ,‬ורשומת הפעלה‬
‫שלה מוסרת מעל‪-‬גבי המחסנית‪ ,‬מצב הזיכרון הינו‪:‬‬
‫= ‪bible‬‬
‫כלומר המערך הפך ל‪ zombie -‬שאין דרך לפנות אליו‪ ,‬והמצביע ‪ bible‬נותר‬
‫בתומתו‪ :‬משמע ערכו עדיין ‪.NULL‬‬
‫אנו רואים אם כן ש‪ read_data -‬לא עשתה את המצופה‪ .‬הסיבה היא שהיא קבלה‬
‫את המערך כפרמטר ערך‪ .‬כיצד נשנה את הפונקציה על‪-‬מנת שהיא כן תשיג את‬
‫המבוקש? כלומר שבתום פעולתה המצביע ‪ bible‬יורה על המערך שהפונקציה‬
‫הקצתה? התשובה היא שעלינו להעביר את המערך כפרמטר הפניה; ואז שינויים‬
‫‪20‬‬
‫שהפונקציה תבצע על הפרמטר ‪ ,arr‬יוותרו בתום ביצוע הפונקציה בארגומנט‬
‫המתאים ‪.bible‬‬
‫עתה‪ ,‬אם כך‪ ,‬נשאל כיצד מעבירים מצביע כפרמטר הפניה? התשובה היא שבהגדרת‬
‫הפרמטר‪ ,‬אחרי שם הטיפוס יש להוסיף את התו &‪ .‬טיפוסו של מצביע ל‪ int -‬הוא‬
‫* ‪ ,int‬ועל‪-‬כן כדי שהפרמטר מטיפוס * ‪ int‬יהיה פרמטר הפניה עלינו להגדירו‬
‫כ‪.int *& -‬‬
‫נראה עתה את הפונקציה ‪ read_data‬בגרסתה המתוקנת‪:‬‬
‫{ )‪void read_data(int *&arr‬‬
‫; ‪int size‬‬
‫; ‪cin >> size‬‬
‫; ]‪arr = new (std::nothrow) int[size‬‬
‫… )‪if (arr == NULL‬‬
‫)‪for (int i=0; i< size -1; i++‬‬
‫; ]‪cin >> arr[i‬‬
‫; ‪arr[size –1] = THE_END‬‬
‫}‬
‫השינוי היחיד שהכנסנו הוא תוספת ה‪ & -‬אחרי שם הפרמטר‪ .‬נתאר את התנהלות‬
‫העניינים מבחינת מצב הזיכרון‪ :‬ראשית‪ ,‬כמו קודם‪ ,‬מוגדר המשתנה ‪ bible‬של‬
‫התכנית הראשית‪ .‬מצב הזיכרון הוא‪:‬‬
‫= ‪bible‬‬
‫עתה נערכת קריאה לפונקציה ‪ .read_data‬אולם בגרסתה הנוכחית ל‪-‬‬
‫‪ read_data‬יש פרמטר הפניה‪ .‬כדרכם של פרמטרי הפניה‪ ,‬אנו שולחים חץ )עם‬
‫ראש משולש שחור(‪ ,‬מהפרמטר ַלארגומנט המתאים‪ .‬נצייר את מצב המחסנית )תוך‬
‫שאנו מתרכזים רק בפרמטר ‪ ,arr‬ומשמיטים מהציור את יתר הפרטים(‪:‬‬
‫= ‪bible‬‬
‫=‪arr‬‬
‫אנו רואים כי החץ נשלח מ‪ ,arr -‬אל ‪ ,bible‬וכל שינוי שיערך ב‪ ,arr -‬יתבצע‬
‫למעשה על ‪.bible‬‬
‫עתה הפונקציה מתחילה לרוץ‪ .‬פקודת הקצאת הזיכרון תיצור‪ ,‬בגרסה הנוכחית של‬
‫הפונקציה‪ ,‬את המצב הבא‪:‬‬
‫= ‪bible‬‬
‫=‪arr‬‬
‫‪21‬‬
‫כלומר המצביע שערכו השתנה הוא ‪ ,bible‬שכן עת הפונקציה בצעה‬
‫… ‪ , arr = new‬ומכיוון ש‪ arr -‬הוא פרמטר הפניה‪ַ ,‬הלך המחשב בעקבות החץ‬
‫)עם הראש השחור(‪ ,‬וביצע את השינוי על הארגומנט המתאים‪.‬‬
‫ַבהמשך קוראת הפונקציה נתונים לתוך המערך‪ .‬עת הפונקציה מסיימת‪ ,‬ורשומת‬
‫ההפעלה שלה מוסרת מעל‪-‬גבי המחסנית‪ ,‬המערך לא נותר ‪ ,zombie‬ו‪ bible -‬אינו‬
‫‪ :NULL‬כי אם ‪ bible‬מצביע על המערך כמבוקש‪.‬‬
‫‪ 11.5‬פונקציה המחזירה מצביע‬
‫הפונקציה ‪ read_data‬שכתבנו שינתה את הפרמטר היחיד שלה‪ .‬כפי שמיצינו‬
‫בעבר‪ ,‬פונקציה אשר מחזירה רק נתון יחיד ראוי לכתוב לא עם פרמטר הפניה‪ ,‬אלא‬
‫כפונקציה המחזירה את הנתון המתאים באמצעות פקודת ‪ .return‬נראה עתה‬
‫גרסה של ‪ read_data‬הפועלת על‪-‬פי עקרון זה‪ .‬נקרא לה ‪ .read_data2‬הקריאה‬
‫לפונקציה מהתכנית הראשית תהיה‪:‬‬
‫; )(‪bible = read_data2‬‬
‫מהקריאה אנו למדים‪ ,‬באופן לא מפתיע‪ ,‬שהפונקציה מחזירה מצביע ל‪) int -‬שכן‬
‫הערך המוחזר על‪-‬ידה מוכנס לתוך משתנה שזה טיפוסו(‪ .‬כיצד מוגדרת הפונקציה‪:‬‬
‫{)(‪int *read_data2‬‬
‫; ‪int *arr, size‬‬
‫; ‪cin >> size‬‬
‫; ]‪arr = new (std::nothrow) int[size‬‬
‫… )‪if (arr == NULL‬‬
‫)‪for (int i=0; i< size; i++‬‬
‫; ]‪cin >> arr[i‬‬
‫; ‪arr[size –1] = THE_END‬‬
‫; ) ‪return( arr‬‬
‫}‬
‫נסביר‪ :‬הפונקציה אינה מקבלת כל פרמטרים‪ .‬יש לה משתנה לוקלי מטיפוס * ‪int‬‬
‫ששמו הוא ‪ .arr‬הפונקציה מקצה מערך‪ ,‬מפנה את המצביע להצביע על המערך‪,‬‬
‫וקוראת נתונים לתוך המערך‪ .‬בסוף פעולתה הפונקציה מחזירה את ערכו של‬
‫המצביע )כלומר את הכתובת עליה המצביע מורה(‪ .‬מכיוון שכתובת זאת מוכנסת‬
‫למשתנה ‪ bible‬של התכנית הראשית‪ ,‬אזי מעתה ‪ bible‬יצביע על המערך‬
‫שהוקצה‪.‬‬
‫‪22‬‬
‫כדרכנו נציג את התנהלות העניינים בזיכרון‪ .‬נתחיל עם המחסנית עליה הוגדר‬
‫המשתנה ‪ bible‬של התכנית הראשית‪:‬‬
‫= ‪bible‬‬
‫עתה נערכת קריאה לפונקציה ‪ .read_data2‬נציג את מצב המחסנית )תוך‬
‫התרכזות ַבעיקר בלבד(‪:‬‬
‫= ‪bible‬‬
‫= ‪arr‬‬
‫הסבר‪ַ :‬בפונקציה מוגדר משתנה לוקלי בשם ‪ .arr‬המשתנה אינו מאותחל בהגדרה‪,‬‬
‫ועל כן הוא מצביע למקום מקרי כלשהו‪.‬‬
‫עתה הפונקציה מתחילה לרוץ‪ ,‬בפרט היא מקצה מערך‪ ,‬ומפנה את ‪ arr‬להצביע‬
‫עליו‪ .‬מצב הזיכרון הוא עתה‪:‬‬
‫= ‪bible‬‬
‫= ‪arr‬‬
‫אַחר הפונקציה קוראת ערכים לתוך המערך‪ ,‬ולבסוף היא מסיימת‪ ,‬תוך שהיא‬
‫מחזירה את ‪ .arr‬אנו זוכרים כי עת פונקציה מחזירה ערך‪ ,‬הערך שהיא מחזירה‬
‫'מושאר בצד' )ואחר מוכנס ַלמשתנה המתאים(‪ַ .‬במקרה של מצביע הערך ש‪'-‬מושם‬
‫בצד' היא הכתובת אליה המצביע מורה‪ ,‬ומבחינה ציורית המקום עליו החץ מורה‪.‬‬
‫נציג זאת כך‪:‬‬
‫= ‪bible‬‬
‫‪2‬‬
‫‪5‬‬
‫‪2‬‬
‫‪7‬‬
‫המלבן הימני התחתון מציג את הערך ש‪' -‬הושאר בצד'‪ ,‬והחץ שבו מורה לאותו‬
‫מקום עליו הורה ‪.arr‬‬
‫עתה‪ ,‬מכיוון שהקריאה לפונקציה הייתה‪ , bible = read_data2(); :‬אזי‬
‫הערך ש‪'-‬הושאר בצד' מוכנס למשתנה ‪ ,bible‬כלומר ‪ bible‬עובר להצביע על‬
‫המערך שהוקצה על‪-‬ידי הפונקציה‪ .‬והציור המתאים‪:‬‬
‫= ‪bible‬‬
‫‪5‬‬
‫‪2‬‬
‫‪7‬‬
‫‪2‬‬
‫‪23‬‬
‫ושוב‪ ,‬השגנו את האפקט הרצוי‪ :‬בתום ביצוע הפונקציה מצביע ‪ bible‬על מערך‬
‫שהוקצה על‪-‬ידי הפונקציה‪ ,‬ומכיל נתונים כנדרש‪.‬‬
‫‪ 11.6‬שינוי גודלו של מערך‬
‫עתה ברצוננו לכתוב פונקציה אשר תוכל לקרוא מחרוזת באורך לא ידוע‪ .‬הפונקציה‬
‫תקרא את תווי המחרוזת בזה אחר זה עד קליטת התו מעבר‪-‬שורה‪ ,‬אשר יציין את‬
‫סופה של המחרוזת‪ .‬הקריאה לפונקציה תהיה‪ , a_s = read_s(); :‬עבור‬
‫משתנה ;‪ . char *a_s‬כלומר הפונקציה תחזיר מצביע ַלסטרינג שהיא קראה לתוך‬
‫מערך שהוקצה דינאמית על‪-‬גבי הערמה‪.‬‬
‫הפונקציה שונה מזו שראינו בסעיף הקודם בכך שהיא מקצה מערך בגודל ‪ N‬תאים‪,‬‬
‫וקוראת לתוכו תווים‪ .‬עת המערך מתמלא הפונקציה מגדילה אותו בשיעור ‪N‬‬
‫וממשיכה בתהליך הקריאה; וכך שוב ושוב כל עוד יש בכך צורך‪.‬‬
‫נציג את הפונקציה‪ ,‬ואחר נסבירה ביתר פירוט‪:‬‬
‫‪// pointer to the array the func alloc‬‬
‫][‪// the size of the_s‬‬
‫‪// how many chars were read, so far‬‬
‫{ )(‪char *read_s‬‬
‫‪char *the_s,‬‬
‫‪*temp,‬‬
‫; ‪c‬‬
‫‪int arr_size,‬‬
‫; ‪s_size‬‬
‫‪the_s = new (nothrow) char[N] ; // alloc initial array‬‬
‫… )‪if (the_s == NULL‬‬
‫; ‪arr_size = N‬‬
‫; )(‪c= cin.get‬‬
‫)'‪while (c != '\n‬‬
‫{‬
‫'‪// read the string until u read '\n‬‬
‫‪if (s_size == arr_size -1) { // if the_s[] is full‬‬
‫; ]‪temp = new (nothrow) char[arr_size + N‬‬
‫‪// alloc bigger 1‬‬
‫‪if (temp == NULL) ...‬‬
‫)‪for (int i = 0; i < s_size; i++‬‬
‫; ]‪temp[i] = the_s[i‬‬
‫‪// copy old onto new‬‬
‫; ‪delete [] the_s‬‬
‫‪// free old‬‬
‫; ‪the_s = temp‬‬
‫‪// the_s point to new‬‬
‫; ‪arr_size += N‬‬
‫‪// update size‬‬
‫}‬
‫; ‪the_s[ s_size++ ] = c‬‬
‫; )(‪c = cin.get‬‬
‫}‬
‫; ’‪the_s[s_size -1] = ‘\0‬‬
‫; ‪return the_s‬‬
‫}‬
‫יתר כדי להקל על הבנתה(‪ :‬אנו‬
‫נסביר את הפונקציה )אף שתיעדנו אותה תיעוד ֵ‬
‫מתחילים בכך שאנו מקצים למצביע ‪ the_s‬מערך )וכמובן בודקים שההקצאה‬
‫‪24‬‬
‫הצליחה!(‪ .‬המשתנה ‪ arr_size‬שומר את גודלו של המערך שהוקצה‪ .‬עתה אנו‬
‫נכנסים ללולאת ‪ while‬אשר קוראת תו‪ ,‬תו עד קליטת ’‪ .‘\n‬בכל סיבוב בלולאה‬
‫אנו בודקים האם כבר אין מקום במערך להוספת תו נוסף )כלומר האם ‪s_size‬‬
‫‪ .(== arr_size-1‬אם אכן זה המצב אנו‪) :‬א( מקצים מערך חדש בגודל‬
‫ומפנים את‬
‫‪ ,arr_size +N‬כלומר מערך הגדול ב‪ N -‬תאים מהמערך הנוכחי‪ְ ,‬‬
‫המשתנה ‪ temp‬להצביע על המערך החדש‪) .‬ב( אנו מעתיקים ַלמערך החדש את‬
‫תוכנו של המערך הישן‪) .‬ג( אנו משחררים את המערך הישן )באמצעות פקודת ה‪-‬‬
‫מפנים את המצביע ‪) the_s‬שכבר אינו מצביע על מערך כלשהו(‬
‫‪) .(delete‬ד( אנו ְ‬
‫להצביע על המערך עליו מורה ‪) .temp‬ה( אנו מעדכנים את ‪ arr_size‬על‪-‬ידי‬
‫הגדלתו בשיעור ‪ .N‬אחרי שהגדלנו )או לא הגדלנו( את המערך‪ ,‬אנו פונים לקריאת‬
‫התו הבא מהקלט‪.‬‬
‫‪ 11.7‬מערך של מצביעים כפרמטר ערך‬
‫בסעיפים הקודמים ראינו כי במקום להגדיר מערך סטטי‪ char s[N]; :‬אנו‬
‫יכולים להגדיר מצביע‪ char *sp; :‬אשר בהמשך יהפוך למערך )שמוקצה‬
‫דינאמית(‪ .‬אנו גם זוכרים כי בשפת ‪ C‬מערך דו‪-‬ממדי הוא למעשה סדרה של מערכים‬
‫חד‪-‬ממדיים‪ ,‬או במילים אחרות מערך שכל תא בו הוא מערך חד‪-‬ממדי‪ .‬אם נצרף‬
‫שבמקום להגדיר מערך דו‪-‬ממדי‪:‬‬
‫ְ‬
‫את שתי המסקנות הללו יחד נוכל להסיק‬
‫; ]‪ ,char ss[ROWS][COLS‬אנו יכולים להגדיר מערך של מצביעים‪char :‬‬
‫;]‪ . *ssp[ROWS‬כל תא במערך זה הוא מצביע )כלומר יֵצור מטיפוס * ‪(char‬‬
‫שיכול בהמשך להפוך למערך חד‪-‬ממדי‪ ,‬אשר יוכל לשמור מחרוזת‪.‬‬
‫עתה נניח כי הגדרנו בתכנית הראשית את המשתנה‪char *strings[ROWS]; :‬‬
‫)כלומר הגדרנו מערך של מצביעים‪ ,‬שהינו מערך של פוטנציאלים למערכים חד‪-‬‬
‫ממדיים‪ ,‬כלומר פוטנציאל למערך דו‪-‬ממדי(; אחר אתחלנו את תאיו )שכל אחד‬
‫מהם הוא מצביע( להכיל את הערך ‪:NULL‬‬
‫)‪for (int i=0; i < ROWS; i++‬‬
‫; ‪strings[i] = NULL‬‬
‫עתה אנו קוראים לפונקציה‪.read_strings(strings); :‬‬
‫הגדרת הפונקציה‪:‬‬
‫{ ) ]‪void read_strings( char *strings[N‬‬
‫; ]‪char temp[COLS‬‬
‫; ‪int i = 0‬‬
‫{ )‪while (i < ROWS‬‬
‫; ‪cin >> setw(COLS) >> temp‬‬
‫)‪if (strcmp(temp, ".”) == 0‬‬
‫; ‪break‬‬
‫; ] ‪strings[i]= new (nothrow) char[ strlen(temp) +1‬‬
‫‪if (strings[i] == NULL) ...‬‬
‫; )‪strcpy(strings[i++], temp‬‬
‫}‬
‫}‬
‫נסביר‪ :‬הפונקציה מתנהלת כלולאה‪ .‬בכל סיבוב בלולאה אנו קוראים מחרוזת לתוך‬
‫משתנה העזר ‪) temp‬שהינו מערך סטאטי(‪ .‬במידה והסטרינג הוא ”‪ “.‬אנו עוצרים‬
‫את תהליך הקריאה )ולכן גם את ביצוע הפונקציה באופן כללי(‪ .‬במידה והסטרינג‬
‫אינו ”‪ “.‬אנו מקצים מערך ַבגודל הדרוש )כלומר כאורכו של הסטרינג השמור ב‪-‬‬
‫מפנים את ]‪ strings[i‬להצביע על‬
‫‪ temp‬ועוד תא אחד‪ ,‬בו יאוחסן ה‪ְ ,(‘\0’ -‬‬
‫אותו מערך‪ ,‬ומעתיקים את הסטרינג המצוי ב‪ temp -‬על ]‪ .strings[i‬שימו לב‬
‫‪25‬‬
‫שהתוצאה המתקבלת היא לא בדיוק מערך דו‪-‬ממדי‪ ,‬אלא מערך בו כל שורה היא‬
‫באורך שונה‪.‬‬
‫עתה ברצוננו לשאול שאלה כללית יותר‪ :‬האם הפונקציה תבצע את משימתה‬
‫כהלכה? האם טיפוס הפרמטר של הפונקציה הוא כנדרש? האם שינויים‬
‫שהפונקציה תכניס לתוך הפרמטר ‪ strings‬יישארו בארגומנט המתאים בתום‬
‫ביצוע הפונקציה? התשובה היא‪ :‬כן! וכן! נסביר מדוע‪ :‬אנו יודעים היטב כי שינויים‬
‫שפונקציה מכניסה לתוך תאי מערך שהינו פרמטר של הפונקציה נשארים בתום‬
‫ביצוע הפונקציה בארגומנט המתאים; וְ כלל זה נכון גם עבור מערך שכל תא בו הוא‬
‫מצביע )אשר הופנה ַבפונקציה להצביע על סטרינג שהוקצה דינמית על הערמה(‪.‬‬
‫נתאר את השתלשלות העניינים בזיכרון‪.‬‬
‫‪26‬‬
‫ראשית‪ ,‬בתכנית הראשית‪ ,‬על‪-‬גבי המחסנית‪ ,‬מוגדר מערך של מצביעים‪ ,‬ותאי‬
‫מאותחלים לערך ‪ .NULL‬נציג את המחסנית‪:‬‬
‫=‪strings‬‬
‫עתה מתבצעת קריאה ַלפונקציה‪ .‬המערך מועבר כפרמטר ערך‪ ,‬כלומר ערכו של‬
‫הארגומנט ‪ ,strings‬מועתק על הפרמטר ‪ .strings‬מבחינה ציורית משמעות‬
‫הדבר היא שהפרמטר יצביע לאותו מקום כמו הארגומנט‪ .‬נציג את מצב המחסנית‪:‬‬
‫=‪strings‬‬
‫=‪strings‬‬
‫עת הפונקציה מבצעת פקודה כגון‪ strings[0] = new char[4]; :‬מתקבל‬
‫המצב הבא בזיכרון‪:‬‬
‫=‪strings‬‬
‫=‪strings‬‬
‫כלומר המצביע שבתא מספר אפס במערך המצביעים המקורי )והיחיד הקיים( הוא‬
‫שעובר להצביע על הסטרינג המוקצה‪ .‬אם אחרי קריאת הסטרינג השני יוקצה מערך‬
‫בגודל שלושה תאים אזי מצב הזיכרון יהיה‪:‬‬
‫=‪strings‬‬
‫=‪strings‬‬
‫‪27‬‬
‫ַבפונקציה ‪ read_strings‬שכתבנו הגדרנו את הפרמטר באופן הבא‪:‬‬
‫]‪ . *strings[N‬כפי שאנו יודעים במקום לתאר פרמטר כמערך )ואפילו הוא מערך‬
‫של מצביעים( אנו יכולים לתארו כמצביע‪ .‬לכן במקום להגדיר את הפרמטר כ‪:‬‬
‫]‪ char *strings[N‬אנו יכולים להגדירו כ‪ ,char **strings :‬במילים‪:‬‬
‫כמצביע למצביע לתו )מבחינה תחבירית אנו ממירים ]‪ [N‬ב‪ .(* -‬גוף הפונקציה לא‬
‫יצטרך לעבור שינוי גם אם נבחר לתאר את הפרמטר באופן זה‪.‬‬
‫‪char‬‬
‫‪ 11.8‬מערך של מצביעים כפרמטר הפניה‬
‫מערך המצביעים ‪ ,char **strings‬שראינו בסעיף שעבר‪ ,‬הועבר כפרמטר ערך‪,‬‬
‫ולכן ביכולתה של הפונקציה היה לשנות רק את ערכם של תאי המערך‪ ,‬אך לא את‬
‫גודלו של המערך‪ .‬נניח כי ברצוננו לכתוב פונקציה אשר קוראת סדרת מחרוזות‪.‬‬
‫מספר המחרוזות אינו ידוע למתכנת‪ ,‬ולכן בתכנית הראשית לא ניתן להגדיר‬
‫משתנה‪ ,char *strings[ROWS] :‬שכן משתנה שכזה מגדיר את מספר‬
‫המחרוזות להיות לכל היותר ‪ .ROWS‬מה יהיה הפתרון? אנלוגי לזה שראינו עם מערך‬
‫חד‪-‬ממדי שגודלו היה לא ידוע למתכנת‪ :‬במקום להגדיר מערך ]‪,char s[N‬‬
‫הגדרנו מצביע ‪) char *s‬כלומר פוטנציאל למערך‪ .‬מבחינה תחבירית המרנו את‬
‫]‪ [N‬ב‪ .(* -‬את המצביע העברנו כפרמטר הפניה ְלפונקציה אשר יכלה לממש אותו‬
‫לכדי מערך ַבגודל הרצוי לה )ואף לשנות את גודלו של המערך עת זה מה שנדרש(‪ .‬גם‬
‫במקום להגדיר מערך סטטי של‬
‫עם מערך של מצביעים ננקוט באותו פתרון‪ְ :‬‬
‫מצביעים ]‪ char *strings[ROWS‬נגדיר מצביע למצביע ‪.char **strings‬‬
‫)שוב המרנו את ה‪ [N] -‬ב‪ .(* -‬את המצביע למצביע נעבר כפרמטר הפניה לפונקציה‬
‫אשר תקצה מערך של מצביעים בגודל הרצוי )ואף תוכל לשנות את גודלו של המערך‬
‫עת זה מה שיידרש(‪ .‬כיצד מתארים פרמטר הפניה שהינו מצביע למצביע? כותבים‬
‫אחרי שם הטיפוס‪ ,‬לדוגמה אחרי ** ‪ ,char‬את התו &‪.‬‬
‫נציג עתה את הפונקציה ‪ alloc_arr_n_read_strings‬אשר תקבל מצביע‬
‫למצביע כפרמטר הפניה‪ ,‬תקרא מהמשתמש כמה מחרוזות ברצונו להזין‪ ,‬תקצה‬
‫ואחר תקרא סדרת מחרוזות תוך שימוש‬
‫ַ‬
‫מערך של מצביעים בגודל הדרוש‪,‬‬
‫בפונקציה ‪ read_strings‬שכתבנו בסעיף שעבר‪ ,‬ואשר מקבלת מערך מוכן של‬
‫מצביעים‪ .‬בפונקציה ‪ read_strings‬עלינו לבצע שינוי קל‪ :‬הפונקציה תקבל‬
‫פרמטר ערך נוסף‪ ,int arr_size ,‬אשר יורה לה מה גודלו של מערך המצביעים‬
‫המועבר לה‪) .‬להזכירכם‪ ,‬בגרסה שכתבנו הנחנו כי גודלו של המערך הוא הקבוע‬
‫תקרא‬
‫מהיותו גלובלי(‪ .‬הפונקציה ‪ְ read_strings‬‬
‫‪ ,ROWS‬אשר מוכר לפונקציה ֵ‬
‫במקום לכל היותר ‪ ROWS‬מחרוזות‪ ,‬כפי שהיא‬
‫לכל יותר ‪ arr_size‬מחרוזות ) ְ‬
‫עשתה במקור(‪.‬‬
‫הפונקציה ‪ alloc_arr_n_read_strings‬שנכתוב תחזיר באמצעות פרמטר‬
‫הפניה את גודלו של מערך המצביעים שהוקצה‪.‬‬
‫בתכנית הראשית נגדיר‪ ,‬אם כן‪:‬‬
‫; ‪char **arr_of_strings‬‬
‫; ‪int arr_size‬‬
‫ונזמן את הפונקציה באופן הבא‪:‬‬
‫; )‪alloc_arr_n_read_strings(arr_of_strings, arr_size‬‬
‫‪28‬‬
‫הגדרת הפונקציה‪:‬‬
‫‪void alloc_arr_n_read_strings(char **&arr_of_strings,‬‬
‫)‪int &arr_size‬‬
‫{‬
‫; ‪cin >> arr_size‬‬
‫; ]‪arr_of_strings = new (nothrow) char *[arr_size‬‬
‫‪if (arr_of_strings == NULL) ...‬‬
‫)‪for (int i = 0; i < arr_size; i++‬‬
‫; ‪arr_of_strings[i] = NULL‬‬
‫; ) ‪read_strings( arr_of_strings, arr_size‬‬
‫}‬
‫הסבר‪ :‬הפונקציה מקבלת את המצביע למצביע )כלומר את הפוטנציאל למערך של‬
‫מצביעים( כפרמטר הפניה‪ .‬לכן שינויים שהפונקציה תערוך ַלפרמטר שלה יוותרו‬
‫בתום ביצוע הפונקציה בארגומנט המתאים‪ .‬פקודת ה‪ new -‬המבוצעת על‪-‬ידי‬
‫ַ‬
‫הפונקציה‬
‫מקצה מערך של מצביעים‪ .‬שימו לב כי עת אנו כותבים ]…[‪new char‬‬
‫אנו מקצים מערך של תווים‪ ,‬ועת אנו כותבים ]…[* ‪ new char‬אנו מקצים מערך‬
‫של מצביעים לתווים‪ .‬כמובן שאחרי ביצוע ההקצאה יש לבדוק שערכו של המצביע‬
‫שונה מ‪.NULL -‬‬
‫אחרי שהקצאנו מערך של מצביעים‪ ,‬ואיתחלנו את תאיו לערך ‪ ,NULL‬אנו יכולים‬
‫לזמן את הפונקציה ‪ read_strings‬אשר מצפה לקבל מערך קיים של מצביעים‪.‬‬
‫נציג את התנהלות התכנית מבחינת ציור הזיכרון‪ .‬כמו תמיד נתרכז רק ַבהיבטים‬
‫המשמעותיים לצורך דיוננו‪ַ .‬בתחילה‪ ,‬אנו מגדירים ַבתכנית הראשית מצביע למצביע‬
‫‪ ,arr_of_strings‬ומשתנה שלם ‪ .arr_size‬נציג את מצב המחסנית‪:‬‬
‫= ‪Arr_of_strings‬‬
‫= ‪arr_size‬‬
‫מכיוון שהמצביע לא אותחל ציירנו אותו כמורה למקום מקרי‪.‬‬
‫עתה אנו מזַמנים את הפונקציה ‪ַ . alloc_arr_n_read_strings‬לפונקציה יש‬
‫שני פרמטרי הפניה‪ ,‬ועל כן אנו שולחים חץ )עם ראש בצורת משולש שחור( מכל אחד‬
‫משני הפרמטרים אל הארגומנט המתאים לאותו פרמטר‪ .‬נציג את מצב המחסנית‪:‬‬
‫= ‪arr_of_strings‬‬
‫= ‪arr_size‬‬
‫= ‪arr_of_strings‬‬
‫= ‪arr_size‬‬
‫‪29‬‬
‫עתה הפונקציה מתחילה להתבצע‪ .‬עת הפונקציה מבצעת את פקודה ה‪ new -‬מוקצה‬
‫על‪-‬גבי הערמה מערך של מצביעים‪ .‬מי יצביע על מערך זה? הפרמטר‬
‫‪ arr_of_strings‬של הפונקציה הוא פרמטר הפניה )חץ עם ראש משולש שחור(‪,‬‬
‫כדרכו של פרמטר הפניה אנו הולכים בעקבות החץ ומטפלים בארגומנט המתאים‪,‬‬
‫כלומר במשתנה ‪ arr_of_strings‬של התכנית הראשית; ומצביע זה )שכדרכו של‬
‫מצביע צויר עם חץ שראשו בצורת ‪ (v‬הוא שמצביע על המערך שהוקצה‪ .‬נתאר זאת‬
‫ציורית‪:‬‬
‫= ‪arr_of_strings‬‬
‫= ‪arr_size‬‬
‫= ‪arr_of_strings‬‬
‫= ‪arr_size‬‬
‫שהנם‪ ,‬כזכור לנו‪ ,‬מצביעים לתווים(‪ .‬ולכן‬
‫אחר‪ ,‬אנו מאתחלים את תאי המערך ) ִ‬
‫ַ‬
‫בשלב הבא אנו קוראים לפונקציה‬
‫בציור תארנו כל תא כמורה על ‪ְ .NULL‬‬
‫‪ read_strings‬ומעבירים לה את מערך המצביעים כפרמטר ערך‪ ,‬כלומר הפרמטר‬
‫של הפונקציה )שהנו חץ עם ראש בצורת ‪ (v‬יצביע גם הוא על מערך המצביעים‪ .‬גם‬
‫גודלו של המערך מועבר כפרמטר ערך‪ ,‬ולכן הערך המתאים מועתק לפרמטר‬
‫‪ .arr_size‬נציג זאת בציור‪:‬‬
‫= ‪arr_of_strings‬‬
‫‪arr_size = 4‬‬
‫= ‪arr_of_strings‬‬
‫= ‪arr_size‬‬
‫= ‪strings‬‬
‫‪arr_size = 4‬‬
‫עת הפונקציה ‪ read_strings‬מתחילה להתבצע אנו חוזרים למצב שתיארנו‬
‫מבצעת‪:‬‬
‫הפונקציה‬
‫עת‬
‫לדוגמה‪,‬‬
‫הקודם‪.‬‬
‫בסעיף‬
‫;]‪ strings[0] = new char[3‬מוקצה מערך‪ ,‬ותא מספר אפס ַבמערך‬
‫‪ strings‬עובר להצביע על מערך זה‪ .‬הציור המתאים יהיה‪:‬‬
‫= ‪arr_of_strings‬‬
‫‪arr_size = 4‬‬
‫= ‪arr_of_strings‬‬
‫= ‪arr_size‬‬
‫= ‪strings‬‬
‫‪arr_size = 4‬‬
‫‪30‬‬
‫‪ 11.9‬מצביעים במקום פרמטרי הפניה‬
‫בעבר‪ ,‬ראינו את הפונקציה ‪ swap‬אשר מחליפה בין ערכם של שני הפרמטרים‬
‫המועברים לה‪ .‬כתבנו אותה באופן הבא‪:‬‬
‫{ )‪void swap( int &var1, int &var2‬‬
‫; ‪int temp = var1‬‬
‫; ‪var1 = var2‬‬
‫; ‪var2 = temp‬‬
‫}‬
‫‪int‬‬
‫כלומר לפונקציה ‪ swap‬יש שני פרמטרי הפניה‪ .‬אם בתכנית מוגדרים‪:‬‬
‫; ‪ a, b‬ולתוך זוג המשתנים הכנסנו ערכים‪ ,‬אזי אנו יכולים לקרוא לפונקציה‬
‫;)‪ swap(a, b‬והפונקציה תחליף בין ערכיהם של ‪ a‬ושל ‪.b‬‬
‫יש המסתייגים מפונקציה כדוגמת ‪ swap‬הנ"ל‪ .‬המסתייגים טוענים כי מהקריאה‬
‫לפונקציה ) ;)‪ (swap(a, b‬לא ניתן לדעת שהפרמטרים של הפונקציה הינם‬
‫לשנות את ערכם של הפרמטרים‪,‬‬
‫פרמטרי הפניה‪ ,‬ולכן‪) :‬א( ַלפונקציה יש את הכוח ַ‬
‫וכן‪) :‬ב( לא ניתן לזמן את הפונקציה באופן הבא‪ . swap(a, 17); :‬המסתייגים‬
‫טוענים כי עת תכניות נעשות גדולות מאוד‪ ,‬ונכתבות לכן על‪-‬ידי צוות של מתכנתים‪,‬‬
‫יש חשיבות עליונה לכך שמאופן הקריאה ַלפונקציה נוכל לדעת את סוג הפרמטרים‬
‫של הפונקציה‪ .‬מצביעים עשויים לבוא לעזרנו גם בסוגיה זאת‪.‬‬
‫התחלנו את דיוננו בנושא מצביעים בכך שראינו שניתן להפנות מצביע להצביע על‬
‫משתנה קיים‪ .‬אם הגדרנו בתכנית‪ , int num1 =17, *ip ; :‬אזי הפקודה‪:‬‬
‫;‪ ip = &num1‬תפנה את המצביע ‪ ip‬להורות על המשתנה ‪ .num1‬מבחינה ציורית‬
‫אנו מתארים זאת כך‪:‬‬
‫‪num1 = 17‬‬
‫= ‪ip‬‬
‫ַבעקרון שהצגנו ִ‬
‫בפסקה האחרונה נעשה שימוש כדי לכתוב את ‪ swap‬בצורה שונה‪,‬‬
‫שתענה על דרישות המסתייגים‪ .‬שוב נניח כי בתכנית הוגדרו המשתנים‪:‬‬
‫;‪ int a, b‬ולצורך דיוננו הנוכחי נניח כי הוגדרו גם‪ . int *pa, *pb; :‬עוד‬
‫נניח כי ל‪ a -‬ול‪ b -‬הוכנסו ערכים‪ ,‬ועתה ברצוננו להחליף בין ערכי המשתנים הללו‪.‬‬
‫;‪pa = &a‬‬
‫נוכל לבצע זאת בדרך הבאה‪ :‬ראשית בתכנית הראשית נכתוב‪:‬‬
‫;‪ . pb = &b‬ועתה נזמן את הפונקציה‪ . new_swap(pa, pb); :‬נציג את‬
‫הגדרת הפונקציה ‪:new_swap‬‬
‫{ )‪void new_swap( int *pvar1, int *pvar2‬‬
‫; ‪int temp = *pvar1‬‬
‫; ‪*pvar1 = *pvar2‬‬
‫; ‪*pvar2 = temp‬‬
‫}‬
‫‪31‬‬
‫עתה נבחן מדוע ואיך הפונקציה שהצגנו עושה את מלאכת ההחלפה בין הערכים‪.‬‬
‫נתחיל בהצגת המחסנית הכוללת את ארבעת משתני התכנית הראשית‪ .‬ונניח כי‬
‫למשתנים ‪ a, b‬הוכנסו ערכים כמתואר בציור‪:‬‬
‫‪a = 17‬‬
‫‪b = 3879‬‬
‫= ‪pa‬‬
‫= ‪pb‬‬
‫הפקודות‪pb = &b; :‬‬
‫;‪ִ pa = &a‬תצורנה את המצב הבא‪:‬‬
‫‪a = 17‬‬
‫‪b = 3879‬‬
‫= ‪pa‬‬
‫= ‪pb‬‬
‫כלומר כל מצביע מורה על המשתנה המתאים לו‪ .‬עתה אנו מזמנים את הפונקציה‬
‫;)‪ new_swap(pa, pb‬אשר מקבלת את שני המצביעים כפרמטרי ערך‪ ,‬ולכן ערכם‬
‫של הארגומנטים ‪ pa, pb‬מועתק על הפרמטרים ‪ pvar1, pvar2‬בהתאמה;‬
‫מבחינה ציורית‪ pvar1, pvar2 ,‬יצביעו גם הם על אותם מקומות כמו ‪,pa, pb‬‬
‫כלומר על ‪ .a, b‬נציג את מצב המחסנית‪:‬‬
‫‪a = 17‬‬
‫‪b = 3879‬‬
‫= ‪pa‬‬
‫= ‪pb‬‬
‫= ‪pvar1‬‬
‫= ‪pvar2‬‬
‫= ‪temp‬‬
‫עתה הפונקציה מתחילה להתבצע‪ :‬הפקודה‪ temp = *pvar1; :‬מכניסה ל‪temp -‬‬
‫את הערך המצוי בתא עליו מצביע ‪ ,pvar1‬כלומר את הערך ‪ .17‬הפקודה‪:‬‬
‫;‪ *pvar1 = *pvar2‬מכניסה לתא הזיכרון עליו מצביע ‪ pvar1‬את הערך המצוי‬
‫בתא הזיכרון עליו מצביע ‪ ,pvar2‬לכן לתוך תא הזיכרון ‪ a‬מוכנס ערכו של התא‬
‫הזיכרון ‪ ,b‬כלומר ‪ .3879‬לבסוף הפקודה‪ *pvar2 = temp; :‬מכניסה לתוך תא‬
‫הזיכרון עליו מצביע ‪ ,pvar2‬כלומר לתוך תא הזיכרון ‪ ,b‬את הערך ‪ 17‬השמור ב‪-‬‬
‫‪ .temp‬בכך השלימה הפונקציה את פעולתה בהצלחה‪.‬‬
‫עתה נשאל מדוע ‪ new_swap‬עדיפה על‪-‬פני ‪ ?swap‬התשובה היא שעת אנו מעבירים‬
‫מצביע ְלמשתנה הדבר ניכר לעין גם בקריאה ַלפונקציה‪ ,‬ומרמז על כך שהפונקציה‬
‫עתידה לשנות את ערכו של המשתנה‪ ,‬שהרי אחרת היינו מעבירים לה את ערכו של‬
‫המשתנה )כלומר היה לה פרמטר ערך(‪ ,‬ולא מצביע ַלמשתנה‪ .‬מטעמים דידקטיים‬
‫אנו קראנו לפונקציה ‪ new_swap‬בשני שלבים‪ :‬ראשית הפננו את המצביעים‪pa, :‬‬
‫‪ pb‬להצביע על ‪ , a, b‬ושנית קראנו לפונקציה עם ‪ .pa, pb‬למעשה ניתן לקרוא‬
‫;)‪ . new_swap(&a, &b‬בצורת קריאה זאת אנו‬
‫לפונקציה בצעד אחד‪:‬‬
‫מוותרים על משתני העזר ‪ ,pa, pb‬הקריאה לפונקציה‪ֵ ,‬ראשית מחשבת את‬
‫כתובתם של ‪ a‬ושל ‪ ,b‬ושנית משימה בפרמטרים ‪ pvar1, pvar2‬את הכתובות‬
‫הללו‪ ,‬באופן שהפרמטרים מצביעים על הארגומנטים המתאימים כנדרש‪ .‬איחוד שני‬
‫‪32‬‬
‫הצעדים לכדי צעד אחד מדגיש יותר את העובדה כי ‪ new_swap‬מקבלת מצביעים‬
‫ַלמשתנים ‪) a,b‬ולכן סביר להניח שהיא עתידה לשנות את ערכם(‪.‬‬
‫‪11.10‬‬
‫מצביע מדרגה שנייה )** ‪ (int‬כפרמט קבוע )‪(const‬‬
‫פונ' שאמורה לקבל ** ‪ int‬לקריאה בלבד תקבלו‪const int * const * :‬‬
‫הסיבה‪ :‬נניח תסריט‪:‬‬
‫‪// assume this is legel‬‬
‫‪// (actually, fortunately, it is not).‬‬
‫‪// legal as both have the same type:‬‬
‫* ‪// const int‬‬
‫‪// but now p points to one‬‬
‫* ‪// BAD, but legal, p's type is int‬‬
‫‪11.11‬‬
‫; ‪const int one = 1‬‬
‫; ‪int *p = NULL‬‬
‫; ‪const int **p2p = &p‬‬
‫‪*p2p = &one‬‬
‫; ‪*p = 2‬‬
‫פרמטרים המועברים ל‪ argc :main -‬ו‪argv -‬‬
‫בפרק תשע ראינו כי כדי לשמור מערך של ‪ N‬סטרינגים כל אחד באורך של לכל‬
‫היותר ‪ M-1‬תווים אנו מגדירים מערך דו‪-‬ממדי של תווים‪. char s[N][M]; :‬‬
‫במקום להגדיר מערך סטטי כנ"ל אנו יכולים להגדיר מערך‬
‫בפרק הנוכחי ראינו כי ְ‬
‫של מצביעים‪ . char *s[N]; :‬מערך זה יוכל לשמור לכל היותר ‪ N‬סטרינגים‪ ,‬כל‬
‫אחר ראינו כי במקום להגדיר את‬
‫אחד מהם באורך שייקבע במהלך ריצת התכנית‪ַ .‬‬
‫גודלו של המערך להיות בן ‪ N‬מצביעים אנו יכולים להגדיר מצביע למצביע‪char :‬‬
‫;‪ . **s‬באופן זה נוכל תוך כדי ריצת התכנית לקבוע )ולשנות( הן את גודלו של‬
‫שישמר במערך‪.‬‬
‫ֵ‬
‫המערך‪ ,‬והן את אורכו של כל סטרינג וסטרינג‬
‫בעבר העלנו את השאלה האם גם ַלתכנית הראשית‪ ,main ,‬ניתן להעביר‬
‫ַ‬
‫ארגומנטים? ענִ ינו אז על השאלה בחיוב‪ ,‬אך לא יכולנו להסביר כיצד בדיוק הדבר‬
‫מתבצע‪ .‬עתה חכמנו מספיק כדי ללמוד כיצד הדבר מתבצע‪ .‬נניח כי ברצוננו לכתוב‬
‫תכנית אשר מקבלת מספר כלשהו של מספרים שלמים‪ ,‬ומציגה את סכומם‪ .‬נוכל‬
‫לעשות זאת ַבאופן הבא‪:‬‬
‫א‪ .‬נכתוב תכנית‪ ,‬אותה נציג מייד‪ ,‬ונשמור את התכנית בקובץ בשם ‪. sum.cc‬‬
‫)הסיומת ‪ cc‬מעידה כי זהו קובץ המכיל תכנית מקור בשפת ‪ ,C++‬התחילית ‪sum‬‬
‫שנתנו ַלקובץ כדי להעיד על מהות התכנית המצויה בו‪ .‬במערכת הפעלה‬
‫היא שם ַ‬
‫חלונות‪ ,‬ייקרא הקובץ ‪.(sum.cpp‬‬
‫בשפת מכונה שיקרא ‪) .sum.‬בחלונות‬
‫ונייצר קובץ ְ‬
‫ֵ‬
‫ב‪ .‬נקמפל את הקובץ ‪,sum.cc‬‬
‫ייקרא הקובץ ‪ ,sum.exe‬הסיומת ‪ exe‬מעידה כי קובץ זה מכיל תכנית ניתנת‬
‫להרצה‪.(executable code ,‬‬
‫ג‪ .‬אם עבדנו בסביבת עבודה אזי עתה נצא מסביבת העבודה‪ ,‬ונעבור לעבוד אל מול‬
‫מערכת ההפעלה באופנוּת הקרויה ‪ ,command line‬כלומר לאופנות בה אנו‬
‫מקלידים )בשורה‪ ,‬אות אחר אות( את הפקודות שברצוננו לבצע‪ .‬בסביבת‬
‫חלונות‪ ,‬למשל‪ ,‬אנו עוברים לעבוד בצורת עבודה של ‪.MS-DOS prompt‬‬
‫ד‪ .‬נתמקם במחיצה בה מצוי הקובץ ‪. sum.exe‬‬
‫‪33‬‬
‫הבא‪:‬‬
‫באופן‬
‫או‬
‫ה‪ .‬נריץ את התכנית באופן הבא‪sum 3879 17 9 :‬‬
‫‪ , sum –6 4 1 1‬כלומר תוך שאנו כותבים את שם התכנית )‪ (sum‬ואחר את סדרת‬
‫תסכום‪ .‬לשם הפשטות נניח כי התכנית‬
‫המספרים השלמים שברצוננו שהתכנית ְ‬
‫מורצת עם סדרת מספרים תקינה‪.‬‬
‫עתה נציג את התכנית‪ ,‬ואחר נסבירה‪:‬‬
‫>‪#include <iostream.h‬‬
‫>‪#include <stdlib.h‬‬
‫)(‪// needed for atoi‬‬
‫{ )‪int main(int argc, char **argv‬‬
‫; ‪int sum = 0‬‬
‫)‪for (int i = 1; i < argc; i++‬‬
‫; )]‪sum += atoi(argv[i‬‬
‫; ‪cout << sum << endl‬‬
‫; ‪return EXIT_SUCCESS‬‬
‫}‬
‫אנו רואים כי לתכנית הראשית יש שני פרמטרים‪:‬‬
‫א‪ .‬הפרמטר ‪) argc‬קיצור של ‪ (argument counter‬מעיד כמה מחרוזות כוללת‬
‫הפקודה באמצעותה הורצה התכנית‪ .‬מספר המחרוזות יהיה לכל הפחות אחד‪:‬‬
‫שם התכנית‪ .‬במידה והמשתמש הקליד בנוסף לשם התכנית מחרוזות נוספות‬
‫יהיה ערכו של ‪ argc‬כמספר המחרוזות שהוקלדו )כולל שם התכנית(‪ .‬לדוגמה‬
‫אם התכנית מורצת באופן‪ sum 643 –49 –3 1 :‬אזי ערכו של ‪ argc‬הוא חמש‪,‬‬
‫שכן הוזנו חמיש מחרוזות‪) :‬א( ‪) ,sum‬ב( ‪) ,643‬ג( ‪) ,49-‬ד( ‪) ,3-‬ה( ‪.1‬‬
‫ב‪ .‬הפרמטר ‪) argv‬קיצור של ‪ (argument vector‬הוא מערך של מחרוזות‪ .‬התא מספר‬
‫אפס ַבמערך כולל את שם התכנית )בדוגמה שלנו ‪ .(sum‬התאים הבאים כוללים‬
‫את המחרוזות הנוספות‪ ,‬זו אחר זו‪.‬‬
‫בתכנית שלנו אנו מגדירים משתנה בשם ‪ sum‬שיכיל את סכום הארגומנטים‪ .‬אחר‬
‫אנו מבצעים לולאה שעוברת על הארגומנטים‪ ,‬החל בארגומנט מספר אחד )שכן‬
‫]‪ argv[0‬כולל‪ ,‬כזכור‪ ,‬את שם התכנית(‪ ,‬עבוּר כל אחד ואחד מארגומנטים הלולאה‬
‫ממירה את המחרוזת המצויה בתא המתאים במערך ‪ argv‬לכדי ערך שלם‬
‫)באמצעות הפונקציה ‪ ,atoi‬ששמה הוא קיצור של ‪,(ascii to integer‬‬
‫ומוסיפה את הערך השלם ַלמשתנה ‪ .sum‬לבסוף הפונקציה מציגה את ערכו של‬
‫‪.sum‬‬
‫נראה דוגמה נוספת‪ :‬נניח כי ברצוננו לכתוב תכנית בשם ‪ prog‬אשר מקבלת שתי‬
‫ְ‬
‫מחרוזות ומודיעה האם כל אחד ואחד מתווי המחרוזת הראשונה מצוי במחרוזת‬
‫השנייה‪ .‬לדוגמה עבור ההרצה‪ prog abbca cba :‬תציג התכנית את הפלט 'כן'‪,‬‬
‫שכן כל אחד ואחד מתווי המחרוזת ‪ abbca‬מצוי במחרוזת ‪ .cba‬לעומת זאת עבור‬
‫ההרצה‪ prog xyz xy :‬יוצג הפלט 'לא'‪ ,‬שכן התו ’‪ ‘z‬מופיע במחרוזת הראשונה‬
‫אך לא בשנייה‪.‬‬
‫‪34‬‬
‫התכנית תיכתב באופן הבא‪:‬‬
‫{ )‪int main(int argc, char **argv‬‬
‫{ )‪if (argc != 3‬‬
‫]‪cout << "Program should be run:" << arv[0‬‬
‫; ”‪<< “ <string> <string> \n‬‬
‫;) ‪return( EXIT_FAILURE‬‬
‫}‬
‫)‪for (int i1 = 0; i1 < strlen( argv[1] ); i1++‬‬
‫{‬
‫)‪for (int i2 = 0; i2 < strlen( argv[2] ); i2++‬‬
‫)]‪if (arvg[2][i2] == argv[1][i1‬‬
‫; ‪break‬‬
‫) ) ]‪if (i2 == strlen( argv[2‬‬
‫{‬
‫; "‪cout << "no\n‬‬
‫; )‪return(EXIT_SUCCESS‬‬
‫}‬
‫}‬
‫; ”‪cout << "yes\n‬‬
‫; )‪return(EXIT_SUCCESS‬‬
‫}‬
‫נסביר‪ :‬ראשית‪ ,‬אנו יודעים כי בתכנית זאת ערכו של ‪ argc‬צריך להיות בדיוק שלוש‬
‫)שם התכנית פלוס שתי המחרוזות המועברות כארגומנטים לתכנית(‪ ,‬לכן אם ערכו‬
‫של ‪ argc‬שונה משלוש סימן שהמשתמש לא הריץ את התכנית כהלכה; אנו‬
‫משגרים לו הודעת שגיאה‪ ,‬ועוצרים את ביצוע התכנית‪.‬‬
‫ַבהמשך אנו נכנסים ללולאה כפולה‪ .‬הלולאה החיצונית עוברת על כל תווי‬
‫]‪ .argv[1‬עבור כל תו ותו הלולאה בודקת האם הוא מצוי ב‪ .argv[2] -‬עבור כל‬
‫ערך קבוע של ‪ ,i1‬כלומר עבור כל תו של ]‪ argv[1‬הלולאה הפנימית בודקת האם‬
‫תא זה מצוי ב‪ .argv[2] -‬הלולאה הפנימית רצה על כל תאי ]‪ argv[2‬ובמידה‬
‫והיא מוצאת תא ]‪ argv[2][i2‬שערכו שווה ל‪ argv[1][i1] -‬היא מסיקה‬
‫שהתו המתאים ב‪ argv[1] -‬מצוי ב‪ .argv[2] -‬במקרה שכזה אנו שוברים את‬
‫ביצוע הלולאה הפנימית‪ .‬אחרי הרצת הלולאה הפנימית‪ ,‬אם ערכו של ‪ i2‬הוא‬
‫כאורכה של המחרוזת ]‪ argv[2‬סימן שהתו לא נמצא‪ .‬במקרה שכזה אנו מציגים‬
‫הודעה מתאימה למשתמש‪ ,‬ועוצרים את ביצוע התכנית‪ .‬מנֵגד‪ ,‬אם מיצינו את‬
‫הלולאה החיצונית‪ ,‬בלי שהודענו כי תו כלשהו מ‪ argv[1] -‬אינו מצוי ב‪,arv[2] -‬‬
‫סימן שכל תווי ]‪ argv[1‬מצויים ב‪ .argv[2] -‬אנו מודיעים על כך‪ ,‬ועוצרים‪.‬‬
‫‪11.12‬‬
‫תרגילים‬
‫‪11.12.1‬‬
‫תרגיל מספר אחד‪ :‬איתור תת‪-‬מערך במערך‬
‫ממשו את הפונקציה הבאה‪:‬‬
‫‪int array[],‬‬
‫‪int array_size,‬‬
‫‪int lower_bound,‬‬
‫‪int upper_bound,‬‬
‫)‪*subarray_sizep‬‬
‫‪35‬‬
‫‪int *subarray_in_range(const‬‬
‫‪const‬‬
‫‪const‬‬
‫‪const‬‬
‫‪=int‬‬
‫בהינתן המערך ‪ array‬שגודלו ‪ array_size‬עליכם למצוא את תת המערך‬
‫הראשון )קרי הקרוב ביותר לתחילת המערך ‪ (array‬מגודל לא טריביאלי )עם יותר‬
‫מאבר יחיד( שכל אבריו נמצאים בין ‪ lower_bound‬לבין ‪ ,upper_bound‬כולל‬
‫שני הגבולות‪.‬‬
‫אם מצאתם תת מערך כזה ערך החזרה של הפונקציה יהיה כתובתו של האבר‬
‫הראשון בתת המערך‪ ,‬ו‪ *subarray_sizep -‬יעודכן לגודלו של תת מערך זה‪.‬‬
‫אחרת ערך החזרה ו‪ *subarray_sizep -‬יעודכנו שניהם לאפס‪.‬‬
‫שימו לב‪ :‬על הפונקציה שלכם להיכתב תוך שימוש במצביעים בלבד‪.‬‬
‫אל תגדירו משתנים מכל סוג אחר‪ ,‬ואל תשתמשו ב‪subarray_sizep -‬‬
‫לאף מטרה למעט זו שלשמה נועד‪.‬‬
‫‪11.12.2‬‬
‫תרגיל מספר שתיים‪ :‬מסד נתוני משפחות‬
‫תרגיל ‪ 11.6.4‬מתרגל גם מערכים ומצביעים באופן אינטנסיבי‪.‬‬
‫‪11.12.3‬‬
‫תרגיל מספר שלוש‪ :‬טיפול בסיסי במערכים ומצביעים‬
‫בתכנית מוגדרים‪:‬‬
‫{ ‪struct a_nums_list‬‬
‫; ‪int *nums‬‬
‫; ‪unsigned int list_len‬‬
‫; }‬
‫{ ‪struct many_nums_lists‬‬
‫; ‪a_nums_list *lists‬‬
‫; ‪unsigend int num_of_lists‬‬
‫; }‬
‫; ‪many_nums_lists arr‬‬
‫המשתנה ‪ arr‬מסוגל )אם יעשה בו שימוש מתאים( להיהפך למערך שיכיל שורות‬
‫של מספרים שלמים‪ ,‬כל‪-‬אחת באורך שונה‪ .‬לצד כל שורה נחזיק את אורכה‪ ,‬וכן‬
‫נחזיק את מספר השורות שהמשתנה ‪ arr‬מחזיק בכל נקודת זמן‪) .‬ראשית בררו‬
‫לעצמכם מדוע וכיצד מתקיימים כל ההיגדים שנזכרו עד כה(‪ .‬בתכנית שנכתוב נשאף‬
‫שלא להחזיק שורות יותר מהמינימום ההכרחי‪ ,‬ואורכה של כל שורה לא יהיה ארוך‬
‫מהמינימום ההכרחי‪.‬‬
‫כתבו את הפונקציה ‪ַ .add_val‬לפונקציה ארבעה פרמטרים‪ :‬מערך )בשם ‪(arr‬‬
‫מטיפוס ‪ ,many_nums_lists‬ערך שלם )‪ (val‬אותו יש להכניס ַלמערך‪ ,‬מספר‬
‫שורה )‪ (line‬לתוכה יש להכניס את הערך‪ ,‬ומספר תא רצוי )‪ (cell‬באותה שורה‪.‬‬
‫הפונקציה תכניס את הערך ‪ val‬למקום ‪ cell‬בשורה ‪ line‬במערך ‪ arr‬תוך שהיא‬
‫דואגת להיבטים הבאים‪:‬‬
‫‪ .1‬אם ב‪ arr -‬אין די שורות אזי הוא יוגדל כך שתהייה בו שורה מספר ‪.line‬‬
‫‪ .2‬אם בשורה הרצויה אין די מקום אזי היא תוגדל כך שיהיה בה תא מספר‬
‫‪ַ .place‬בתאים הריקים בשורה יושם הערך הקבוע ‪.EMPTY‬‬
‫‪36‬‬
37
‫תוקן ‪10/10‬‬
‫‪struct 12‬‬
‫מבנים )‪ (structures‬הם כלי נוסף אשר יסייע לנו לשפר את סגנונן של התכניות שאנו‬
‫כותבים‪.‬‬
‫‪ 12.1‬מוטיבציה‬
‫נניח כי ברצוננו לכתוב תכנית אשר מחזיקה נתונים אודות המשתמש בה‪ .‬בתכנית‬
‫נרצה לשמור את שמו הפרטי‪ ,‬מינו‪ ,‬ומספר הנעליים של המשתמש‪ .‬לשם כך נוכל‬
‫להגדיר משתנים‪:‬‬
‫; ]‪char name[N‬‬
‫; ‪bool gender‬‬
‫; ‪unsigned int shoe_num‬‬
‫כדי לקרוא את הנתונים הדרושים נכתוב את הפונקציה‪:‬‬
‫‪void read_data(char name[], bool &gender,‬‬
‫;)‪unsigned int &shoe_num‬‬
‫הפונקציה מקבלת את פרמטרי ההפניה הדרושים‪.‬‬
‫צורת הכתיבה שהצגנו היא לגיטימית‪ ,‬אך היא אינה די מדגישה כי שלושת‬
‫המשתנים שהגדרנו מתארים שלושה היבטים של אותו אדם‪ ,‬ועל כן הם אינם בלתי‬
‫קשורים זה לזה כפי ששלושה משתנים כלשהם עשויים להיות‪.‬‬
‫נרצה‪ ,‬על‪-‬כן‪ ,‬כלי באמצעותו נוכל 'לארוז' יחד לכדי 'חבילה אחת' מספר משתנים‬
‫המתארים מספר היבטים שונים של אותו אובייקט‪ .‬הכלי שמעמידה לרשותנו שפת‬
‫‪ C‬לשם השגת המטרה הוא ה‪.struct -‬‬
‫ַבתכנית בה אנו דנים‪ ,‬נוכל לכתוב מתחת להגדרת הקבועים‪:‬‬
‫{ ‪struct User‬‬
‫; ]‪char _name[N‬‬
‫; ‪bool _gender‬‬
‫; ‪unsigned int _shoe_num‬‬
‫; }‬
‫שימו לב לכמה הערות קטנות‪:‬‬
‫א‪ .‬שם של טיפוס\סוג מבנה נהוג שיתחיל באות גדולה‪.‬‬
‫ב‪ .‬שם של חברים במבנה נהוג להתחיל עם קו תחתון‪.‬‬
‫ג‪ .‬אחרי הסוגר הימני מופיעה נקודה‪-‬פסיק )כמו ב‪.(enum -‬‬
‫מה קבלנו? אמרנו בכך למחשב כי אנו מגדירים בזאת 'סוג חבילה' שנקרא ‪User‬‬
‫)המילה השמורה ‪ struct‬היא ֵשמורה שאנו מגדירים כאן 'סוג חבילה'‪ ,‬והמילה‬
‫שמופיעה אחריה מתארת את שם 'החבילה'‪ ,‬במקרה שלנו‪ .(User :‬בתוך הסוגריים‬
‫המסולסלים אנו מתארים אילו מרכיבים יהיו בכל חבילה מסוג ‪.User‬‬
‫אם אנו מעוניינים בכך אנו יכולים להגדיר בתכנית גם סוגים נוספים של חבילות‪.‬‬
‫לדוגמה‪:‬‬
‫{ ‪struct Course‬‬
‫; ]‪char _name[N‬‬
‫; ‪unsigned int _weight‬‬
‫; ‪char _for_year‬‬
‫‪38‬‬
‫;}‬
‫'חבילה' מסוג ‪ Course‬מיועדת לתיאור נתונים אודות קורסים‪ :‬שם הקורס‪ ,‬מספר‬
‫נקודות הזכות שהוא מקנה‪ ,‬לאיזה שנה אקדמית הוא מיועד )…‪.(a,b,c,‬‬
‫נדגיש כי הגדרת 'חבילה' אינה מקצה זיכרון בו מאוחסן משתנה כלשהו; היא רק‬
‫מתארת סוג 'מארז' המכיל היבטים שונים המתארים נושא כלשהו אותו ברצוננו‬
‫לייצג בתכניתנו‪ .‬כלומר‪ ,‬לעת עתה לא ניתן לשמור בתכנית שלנו מידע אודות‬
‫משתמשים )או אודות קורסים(‪.‬‬
‫אחרי שהגדרנו ‪-struct‬ים כפי צרכינו ) ַבמקום שבין הגדרת הקבועים להצהרה על‬
‫הפרוטוטיפים( אנו יכולים בגוף התכנית להגדיר משתנים‪:‬‬
‫; ‪struct User user1, user2‬‬
‫; ‪struct Course c1‬‬
‫בכך הגדרנו זוג משתנים ‪ user1, user2‬שהם חבילות‪ ,‬כלומר כל‪-‬אחד מהם כולל‬
‫‪user1._name, user1._gender,‬‬
‫שלושה מרכיבים‪ .‬המרכיבים הם‬
‫‪ . user1._shoe_num‬משמע כדי לפנות למרכיב כלשהו עלינו לציין את שם‬
‫המשתנה‪ ,‬אחר‪-‬כך את התו נקודה‪ ,‬ואחר‪-‬כך את שם המרכיב‪ .‬בשפת ‪ C‬אנו קוראים‬
‫לכל מרכיב ב‪ struct -‬בשם חבר )‪ .(member‬בשפות אחרות המרכיבים נקראים‬
‫שדות )‪ .(fields‬חבר ְבמבנה הוא משתנה ככל משתנה אחר מהטיפוס המתאים‪ .‬לכן‬
‫אנו רשאים לכתוב פקודות כגון הבאות‪:‬‬
‫; ‪cin >> setw(N) >> user1._name‬‬
‫; )‪cout << strlen(user1._name‬‬
‫; ‪cin >> user1._shoe_num‬‬
‫)‪if (user1._shoe_num < 19 || user1._shoe_num > 55‬‬
‫; ”‪cout << "Are u shure?\n‬‬
‫; ‪cin >> i‬‬
‫)‪if (i == 1‬‬
‫; ‪user1._gender = FEMALE‬‬
‫אנו רואים‪ ,‬אם כן‪ ,‬כי ‪ struct User‬מאפשר לנו לאגד את ההיבטים השונים של‬
‫המשתמש בתכניתנו לכדי משתנה יחיד‪.‬‬
‫אעיר כי בשפת סי חובה להגדיר את המשתנה באופן‪struct User user1 ; :‬‬
‫בעוד בשפת ‪ C++‬ניתן לכתוב גם‪ ,User usr1 :‬כלומר להשמיט את המילה‬
‫‪ .struct‬לטעמי‪ ,‬גם בשפת ‪ C++‬לא כדאי להשמיטה וזאת כדי שלקורא יהיה ברור‬
‫שהטיפוס ‪ User‬הוא טיפוס של מבנים‪.‬‬
‫‪ 12.2‬העברת ‪ struct‬כפרמטר לפונקציה‬
‫נניח כי בתכנית כלשהי הגדרנו את ‪ , struct User‬ואת המשתנים‬
‫‪ .user2‬עתה נרצה לכתוב פונקציה ‪ read_data‬אשר תקרא נתונים לתוך משתנה‬
‫מטיפוס ‪ .struct user‬הפונקציה תיכתב באופן הבא‪:‬‬
‫‪user1,‬‬
‫{ )‪void read_data(struct User &an_user‬‬
‫; ‪int temp‬‬
‫; ‪cin >> setw(N) >> an_user._name‬‬
‫; ‪cin >> an_user._shoe_num‬‬
‫; ‪cin >> temp‬‬
‫; ‪an_user._gender = (temp == 1) ? FEMALE : MALE‬‬
‫‪39‬‬
‫}‬
‫זימון הפונקציה יעשה באופן הבא‪ . read_data(user1); :‬מכיוון שהפרמטר של‬
‫הפונקציה הוא פרמטר הפניה אזי שינויים שהפונקציה תכניס לתוך הפרמטר‬
‫‪ an_user‬יישארו בתום ביצוע הפונקציה בארגומנט המתאים )בדוגמה שלנו‪:‬‬
‫‪.(user1‬‬
‫נבחן עתה פונקציה מעט שונה שתדגים לנו מה קורה עת מבנה מועבר כפרמטר ערך‪.‬‬
‫ראשית נציג את הפונקציה‪ ,‬ואחר נדון בה‪:‬‬
‫{ )‪void f(struct User u‬‬
‫; )”‪strcpy(u._name, “dana‬‬
‫}‬
‫של‪-‬‬
‫ַמנים את הפונקציה ‪ f‬באופן הבא‪ f(user1); :‬וזאת אחרי ֵ‬
‫נניח כי אנו מז ְ‬
‫‪ user1‬הוזנו פרטי המשתמש יוסי‪ .‬האם בתום ביצוע הפונקציה יכיל ‪user1.name‬‬
‫את הערך דנה? בעבר ראינו כי לפונקציה יש את הכוח לשנות את ערכם של תאי‬
‫מערך המועבר לה כפרמטר‪ .‬בפרק ‪ 10‬גם הבנו מדוע הדבר קורה‪ :‬שכן ַלפונקציה‬
‫מועבר מצביע לתא הראשון של המערך‪ ,‬והשינויים שהפונקציה מבצעת קורים על‬
‫המערך המקורי‪ .‬עתה נלמד כי אם המערך הוא חלק ממבנה המועבר לפונקציה‬
‫כפרמטר ערך אזי אין בכוחה של הפונקציה לשנות את ערכם של חברי המבנה‬
‫בארגומנט ִעמו היא נקראה‪ ,‬בפרט לא את ערכו של החבר ‪ ;name‬ועל‪-‬כן בתום‬
‫ביצועה של הפונקציה ‪ f‬ימשיך ‪ user1.name‬להכיל את הערך ”‪ .“yosi‬הסיבה‬
‫לכך היא שעת מבנה מועבר כפרמטר ערך יוצר המחשב עותק חדש של המבנה‬
‫במסגרת המחסנית של הפונקציה )הנבנית על‪-‬גבי המחסנית(‪ .‬השינויים שהפונקציה‬
‫שבפרמטר )ולא על הארגומנט(‪.‬‬
‫עורכת מתבצעים על העותק ַ‬
‫נדגים זאת בעזרת ציור המחסנית‪ :‬נתחיל מכך שבתכנית הראשית הוגדרו שני‬
‫משתנים ‪ user1, user2‬המכילים ערכים כמתואר )בציור הצגנו כל מבנה כמלבן‬
‫נפרד בתוך המלבן המייצג את המחסנית(‪:‬‬
‫= ‪user1‬‬
‫"‪_name = "yosi‬‬
‫‪_shoe_num = 36‬‬
‫‪_gender = MALE‬‬
‫= ‪user2‬‬
‫"‪_name = "yafa‬‬
‫‪_shoe_num = 46‬‬
‫‪_gender = FEMALE‬‬
‫‪40‬‬
‫‪stack frame‬‬
‫‪of main‬‬
‫עתה נניח כי אנו קוראים‪ . f(user1); :‬נציג את מצב המחסנית‪:‬‬
‫= ‪user1‬‬
‫"‪name = "yosi‬‬
‫‪shoe_num = 36‬‬
‫‪gender = MALE‬‬
‫‪stack frame‬‬
‫‪of main‬‬
‫= ‪user2‬‬
‫"‪name = "yafa‬‬
‫‪shoe_num = 46‬‬
‫‪gender = FEMALE‬‬
‫=‪u‬‬
‫"‪name = "yosi‬‬
‫‪shoe_num = 36‬‬
‫‪gender = MALE‬‬
‫‪stack frame‬‬
‫‪of f‬‬
‫עת ‪ f‬מבצעת את הפעולה‪ strcpy(u._name, “dana”); :‬מועתק הסטרינג‬
‫”‪ “dana‬על החבר ‪ _name‬בפרמטר ‪ u‬של ‪ .f‬ערכו של ‪ user1._name‬אינו משתנה‪.‬‬
‫ראינו כי עת מבנה מועבר כפרמטר ערך יוצר המחשב עותק נוסף של המבנה‪ .‬עבור‬
‫מבנה גדול‪ ,‬הכולל חברים רבים‪ ,‬ביניהם מערכים‪ ,‬תחייב עבודת ההעתקה השקעה‬
‫של זמן ושל זיכרון‪ .‬על‪-‬כן נהוג להיזהר מלהעביר מבנים גדולים כפרמטרי ערך‪.‬‬
‫במקום זאת נוהגים להעבירם כפרמטרי הפניה‪ ,‬ואז נשלח חץ )עם ראש משולש‬
‫שחור( מהפרמטר לארגומנט המתאים‪ ,‬ולא מתבצעת ההעתקה של המבנה‪.‬‬
‫לחילופין‪ ,‬ניתן להעביר מצביע למבנה‪ ,‬ובכך שוב לחסוך את עבודת ההעתקה‪.‬‬
‫אם ברצוננו למנוע מהפונ' לשנות את המבנה אזי נעבירו כפרמטר הפניה קבוע‪:‬‬
‫‪ . const User &u‬באופן זה‪ ,‬מצד אחד תודות לכך שזהו פרמטר הפניה לא‬
‫מבוצעת עבודת העתקה )הדורשת זמן וזיכרון(‪ ,‬ומצד שני‪ ,‬תודות לכך שהפרמטר‬
‫הוא קבוע‪ ,‬לא ניתן לשנותו‪ .‬שימו לב שפרמטר שהוגדר כך אינו זהה לפרמטר ערך‪,‬‬
‫שכן פרמטר ערך פונ' רשאית לשנות )רק שהשינוי לא ייוותר בארגומנט(‪ ,‬כאן הפונ'‬
‫אינה רשאית לשנות כלל את הפרמטר‪.‬‬
‫‪ 12.3‬מערך של ‪struct‬‬
‫בסעיף הקודם ראינו כיצד מגדירים מבנה יחיד‪ .‬לעיתים אנו זקוקים למערך של‬
‫מבנים‪ .‬לדומה‪ :‬נניח כי ברצוננו לשמור נתונים אודות כל תלמיד ותלמיד הלומד את‬
‫הקורס 'מבוא לאגיפטולוגיה'‪ .‬עבור כל תלמיד נרצה לשמור את שמו הפרטי‪ ,‬את‬
‫מינו‪ ,‬מספר הנעליים שלו‪ ,‬ומערך שיכיל את ציוניו בתרגילים השונים שניתנו בקורס‪.‬‬
‫נגדיר לכן‪:‬‬
‫{ ‪struct Stud‬‬
‫; ]‪char _name[N‬‬
‫; ‪bool _gender‬‬
‫; ‪unsigned _int shoe_num‬‬
‫; ]‪int _grades[MAX_EX‬‬
‫; }‬
‫‪41‬‬
‫אחר‪ ,‬בתכנית הראשית‪ ,‬נגדיר‪ . struct Stud egypt[MAX_STUD]; :‬מה‬
‫קיבלנו? מערך שכל תא בו הוא מבנה מטיפוס ‪.struct Stud‬‬
‫עתה נוכל לקרוא לפונקציה‪ . read_data(egypt); :‬אנו מעבירים לפונקציה את‬
‫המערך‪ ,‬על‪-‬מנת שהיא תקרא לתוכו נתונים‪ .‬כיצד תראה הגדרת הפונקציה? בפרט‪:‬‬
‫האם את הפרמטר שלה נצטרך להגדיר כפרמטר הפניה? לא‪ ,‬ולא! אנו יודעים כי עת‬
‫מערך מועבר כפרמטר לפונקציה בכוחה של הפונקציה לשנות את ערכם של תאי‬
‫המערך‪ ,‬שכן הפונקציה למעשה מקבלת מצביע לתא מספר אפס במערך‪ ,‬וכל‬
‫השינויים שהפונקציה עורכת מתבצעים על המערך המקורי‪ .‬נציג את הפונקציה‬
‫‪ ,read_data‬נסבירה‪ ,‬ואחר גם נציג את מצב המחסנית בעקבות הקריאה‪:‬‬
‫{ ) ][‪void read_data( struct Stud egypt‬‬
‫; ‪int temp‬‬
‫{ )‪for (int i= 0; i < MAX_STUD; i++‬‬
‫; ‪cin >> setw(N) >> egypt[i]._name‬‬
‫; ‪cin >> egypt[i]._shoe_num‬‬
‫)‪for (int ex = 0; ex < MAX_EX; ex++‬‬
‫; ]‪cin >> egypt[i]._grades[ex‬‬
‫; ‪cin >> temp‬‬
‫; ‪egypt[i]._gender = (temp == 1) ? FEMALE : MALE‬‬
‫}‬
‫}‬
‫הסבר הפונקציה‪ :‬הפונקציה מקבלת מערך של ‪-Stud‬ים‪ ,‬ולכן טיפוס הפרמטר שלה‬
‫הוא ][‪ .struct Stud egypt‬הפונקציה מתנהלת כלולאה אשר קוראת נתונים‬
‫בפקודה‪:‬‬
‫למשל‬
‫נדון‬
‫המערך‪.‬‬
‫תאי‬
‫לתוך‬
‫;‪ . cin >> setw(N) >> egypt[i]._name‬הפרמטר ‪ egypt‬הוא מערך‪,‬‬
‫לכן כדי לפנות לתא רצוי בו עלינו לכתוב ביטוי עם סוגריים מרובעים‪ ,‬כדוגמת‬
‫]‪ . egypt[i‬התא ]‪ egypt[i‬הוא מבנה מטיפוס ‪ ;Stud‬כדי לפנות לחבר רצוי‬
‫במבנה אנו משתמשים בצורת הכתיבה‪ :‬שם‪-‬המבנה‪.‬שם‪-‬החבר‪ ,‬ובמקרה שלנו‪:‬‬
‫‪ . egypt[i]._name‬זהו מערך של תווים‪ ,‬ולכן אנו יכולים לקרוא לתוכו מחרוזת‪.‬‬
‫עתה נבחן את לולאת קריאת הציונים‪ .‬בלולאה זאת המשתנה ‪ ex‬עובר על מספרי‬
‫התרגילים השונים‪ .‬עת ערכו הוא‪ ,‬למשל ‪,‬שלוש אנו קוראים ערך לתוך‬
‫]‪ egypt[i]._grades[3‬נסביר‪ egypt :‬הוא מערך‪ ,‬ולכן כדי לפנות ַלתא של‬
‫התלמיד מספר ‪ i‬עלינו לכתוב‪ . egypt[i] :‬התא ]‪ egypt[i‬הוא תא ְבמערך של‬
‫מבנים‪ ,‬ולכן משתנה מטיפוס ‪ ;Stud‬לכן כדי לפנות לחבר ‪ grades‬במבנה עלינו‬
‫לכתוב‪ . egypt[i]._grades :‬החבר ‪ egypt[i]._grades‬הוא מערך‪ ,‬ולכן‬
‫כדי לפנות לתא מספר שלוש בו עלינו לציין אינדקס רצוי‪:‬‬
‫]‪. egypt[i]._grades[3‬‬
‫עתה נבחן את מצב המחסנית‪ .‬נתחיל במערך שהוגדר בתכנית הראשית‪:‬‬
‫= ‪egypt‬‬
‫=‪name‬‬
‫=‪gender‬‬
‫=‪shoe‬‬
‫=‪grades‬‬
‫=‪name‬‬
‫=‪gender‬‬
‫=‪shoe‬‬
‫=‪grades‬‬
‫‪42‬‬
‫=‪name‬‬
‫=‪gender‬‬
‫=‪shoe‬‬
‫=‪grades‬‬
‫המערך ‪ egypt‬כולל שלושה תאים‪ .‬כל תא הוא מטיפוס ‪ ,struct Stud‬ולכן מכיל‬
‫ארבעה חברים‪ :‬החבר ‪ _name‬הוא למעשה מערך‪ ,‬אולם מכיוון שאנו חושבים עליו‬
‫כעל מחרוזת לא ציירנו אותו כמערך; החברים ‪ _shoe_num‬ו‪ _gender -‬הם‬
‫חברים פרימיטיביים‪ ,‬וציירנו אותם כפי שאנו מציירים כל משתנה פרימיטיבי;‬
‫החבר ‪ _grades‬הוא מערך סטטי בן ארבעה תאים‪ ,‬ולכן ציירנו את תאיו‪ ,‬וכן‬
‫מצביע משם החבר לתא מספר אפס במערך‪ .‬אנו זוכרים כי ‪ egypt‬הוא למעשה‬
‫מצביע לתא מספר אפס במערך המבנים‪ ,‬ולכן לצד שם המשתנה ציירנו מצביע לתא‬
‫הראשון במערך‪.‬‬
‫עת מתבצעת קריאה לפונקציה ‪ ,read_data‬אשר מקבלת את המערך ‪,egypt‬‬
‫כלומר מצביע לתא מספר אפס של המערך‪ ,‬נראית המחסנית באופן הבא‪:‬‬
‫= ‪egypt‬‬
‫=‪name‬‬
‫=‪gender‬‬
‫=‪shoe‬‬
‫=‪grades‬‬
‫=‪name‬‬
‫=‪gender‬‬
‫=‪shoe‬‬
‫=‪grades‬‬
‫=‪name‬‬
‫=‪gender‬‬
‫=‪shoe‬‬
‫=‪grades‬‬
‫= ‪egypt‬‬
‫= ‪i‬‬
‫= ‪ex‬‬
‫אנו רואים כי מהפרמטר ‪ egypt‬של הפונקציה נשלח מצביע לתא מספר אפס במערך‬
‫של התכנית הראשית‪) .‬בדיוק כפי שנשלח מצביע מהמשתנה ‪ egypt‬של התכנית‬
‫הראשית לאותו מערך(‪ .‬לכן עת הפונקציה פונה ל‪ egypt[1]._shoe_num -‬היא‬
‫מעדכנת ישירות את החבר ‪ _shoe_num‬בתא מספר אחד במערך המקורי )והיחיד‬
‫הקיים(‪.‬‬
‫עתה נניח כי ברצוננו לכתוב את הפונקציה ‪ read_data‬בצורה שונה‪:‬‬
‫{ ) ][‪void read_data( struct tud egypt‬‬
‫)‪for (int i= 0; i < MAX_STUD; i++‬‬
‫; ) ]‪read_1_stud( egypt[i‬‬
‫}‬
‫כלומר הפונקציה ‪ read_data‬קוראת בלולאה לפונקציה נוספת ‪.read_1_stud‬‬
‫הפונקציה הנוספת תקרא נתונים של תלמיד יחיד‪ ,‬ולכן הפונקציה ‪read_data‬‬
‫מעבירה לפונקציה ‪ read_1_stud‬כארגומנט תא יחיד במערך ‪ ,egypt‬כלומר‬
‫מבנה יחיד מטיפוס ‪ .stud‬עתה נשאל כיצד יראה הפרוטוטיפ של הפונקציה‬
‫‪ ?read_1_stud‬האם הפרמטר המועבר לה הוא פרמטר ערך או פרמטר הפניה?‬
‫התשובה היא שעל‪-‬מנת שלפונקציה ‪ read_1_stud‬יהיה את הכוח לשנות את ערכו‬
‫של הארגומנט )מטיפוס ‪ (stud‬המועבר לה עליה לקבלו כפרמטר הפניה‪ .‬שהרי‬
‫הפונקציה ‪ read_1_stud‬מקבלת מבנה יחיד‪ ,‬וכבר מיצינו כי על‪-‬מנת שלפונקציה‬
‫‪43‬‬
‫המקבלת מבנה יהיה את הכוח לשנות את הארגומנט המועבר לה‪ ,‬היא חייבת לקבל‬
‫את המבנה כפרמטר הפניה‪ .‬נציג ראשית את הפונקציה‪:‬‬
‫{ )‪void read_1_stud(sruct Stud &a_stud‬‬
‫; ‪int temp‬‬
‫; ‪cin >> a__stud.name‬‬
‫; ‪cin >> a_stud._shoe_num‬‬
‫)‪for (int ex = 0; ex < MAX_EX; ex++‬‬
‫; ]‪cin >> a_stud._grades[ex‬‬
‫; ‪cin >> temp‬‬
‫; ‪a_stud._gender = (temp == 1) ? FEMALE : MALE‬‬
‫}‬
‫בקוד של הפונקציה אין רבותא‪ .‬אך נציג את מצב המחסנית עת אנו קוראים‬
‫לפונקציה באופן הבא‪read_1_stud(egypt[2]); :‬‬
‫= ‪egypt‬‬
‫=‪name‬‬
‫=‪gender‬‬
‫=‪shoe‬‬
‫=‪grades‬‬
‫=‪name‬‬
‫=‪gender‬‬
‫=‪shoe‬‬
‫=‪grades‬‬
‫=‪name‬‬
‫=‪gender‬‬
‫=‪shoe‬‬
‫=‪grades‬‬
‫= ‪egypt‬‬
‫= ‪i‬‬
‫= ‪ex‬‬
‫= ‪a_stud‬‬
‫= ‪temp‬‬
‫= ‪ex‬‬
‫אנו רואים כי מהפרמטר ‪ a_stud‬של הפונקציה נשלח חץ )עם ראש משולש שחור(‬
‫לתא מספר שתיים במערך של התכנית הראשית‪ .‬ועל כן כל שינוי שהפונקציה‬
‫מכניסה לפרמטר שלה מתחולל למעשה על התא המתאים במערך המקורי‪.‬‬
‫מה היה קורה לו הפונקציה ‪ read_1_stud‬הייתה מוגדרת באופן הבא‪:‬‬
‫; )‪void read_1_stud(struct Stud a_stud‬‬
‫כלומר לוּ הפרמטר של הפונקציה היה פרמטר ערך? אזי הקריאה‬
‫)]‪ read_1_stud(egypt[2‬הייתה מעתיקה את תוכנו של התא מספר שתיים‬
‫במערך ‪ egypt‬על מבנה בשם ‪ a_stud‬שהיה מוקצה ברשומת ההפעלה של‬
‫‪ . read_1_stud‬הפרמטר ‪ a_stud‬כבר לא היה מורה על התא מספר שתיים‬
‫במערך המקורי‪ .‬כל שינויי ש‪ read_1_stud -‬הייתה מכניסה לפרמטר שלה היה‬
‫מתחולל על העותק השמור ברשומת ההפעלה שלה במחסנית‪ .‬כמובן שבתום ביצוע‬
‫הפונקציה ערכו של ]‪ egypt[2‬היה נותר כפי שהוא היה טרם הקריאה‪.‬‬
‫‪44‬‬
‫‪ 12.4‬מצביע ל‪ struct -‬כפרמטר ערך‬
‫את הפונקציה ‪ read_1_stud‬אנו יכולים לכתוב גם בדרך שונה‪ .‬במקום‬
‫שהפונקציה תקבל פרמטר הפניה מטיפוס ‪ ,stud‬היא תקבל מצביע למבנה מטיפוס‬
‫‪.stud‬האפקט שיתקבל מבחינה תכנותית יהיה אותו אפקט‪ .‬את הפונקציה נכתוב‬
‫באופן הבא‪:‬‬
‫{ )‪void read_1_stud( stud *stud_p‬‬
‫; ‪int temp‬‬
‫; ‪cin >> setw(N) >> (*stud_p)._name‬‬
‫; ‪cin >> (*stud_p)._shoe_num‬‬
‫)‪for (int ex = 0; ex < MAX_EX; ex++‬‬
‫; ]‪cin >> (*stud_p)._grades[ex‬‬
‫; ‪cin >> temp‬‬
‫; ‪(*stud_p)._gender = (temp == 1) ? FEMALE : MALE‬‬
‫}‬
‫נסביר‪ :‬עתה הפרמטר של הפונקציה מוגדר להיות ‪,sruct Stud *stud_p‬‬
‫כלומר מצביע למבנה מטיפוס ‪ .Stud‬על כן בגוף הפונקציה‪ ,‬כדי לפנות למבנה עליו‬
‫‪ stud_p‬מצביע עלינו להשתמש בכתיבה‪' .*stud_p :‬היצור'‪ *stud_p :‬הוא‬
‫מבנה‪ ,‬ועל כן בו עלינו לפנות לחבר המתאים‪ ,‬למשל‪. (*stup_p).gender :‬‬
‫הקריאה לפונקציה בגרסתה הנוכחית תהיה‪read_1_stud( &(egypt[i])); :‬‬
‫‪ ,‬כלומר אנו מעבירים לפונקציה מצביע לתא מספר ‪ i‬במערך‪.‬‬
‫ציור המחסנית יישאר כפי שהוא היה בגרסה הקודמת פרט לשינוי יחיד‪ :‬החץ )עם‬
‫הראש השחור( שנשלח בגרסה הקודמת מהפרמטר ‪ a_stud‬לתא המתאים במערך‪,‬‬
‫מומר עתה במצביע )עם ראש בצורת ‪ (v‬אשר נשלח מאותו מקום ולאותו מקום‪.‬‬
‫הסיבה לכך היא שעתה הפונקציה מקבלת מצביע )ולא פרמטר הפניה(‪ .‬ובקריאה‬
‫לפונקציה מעבירים בצורה מפורשת מצביע לתא המתאים )באמצעות הכתיבה …&(‪,‬‬
‫ולא את התא המתאים כפרמטר הפניה )כפי שהיה בגרסה הראשונית(‪.‬‬
‫מכיוון שבשפת ‪ C‬מקובל מאוד להשתמש במצביע למבנה‪ ,‬מאפשרת לנו השפה‬
‫לכתוב במקום ביטוי כגון‪ (*stup_p)._gender :‬ביטוי כדוגמת‪:‬‬
‫במקום ראשית לפנות למבנה עליו מצביע ‪stud_p‬‬
‫‪ . stup_p -> _gender‬כלומר ְ‬
‫‪) ,‬באמצעות ‪ (*stud_p‬ושנית ַלחבר המתאים במבנה זה )באמצעות‬
‫‪ ,( (*stud_p)._gender‬אנו כותבים ישירות‪ְ :‬פנֵה לחבר ַבמבנה עליו מצביע‬
‫‪ .stud_p‬לכן את הפונקציה שכתבנו לאחרונה נוכל לכתוב גם בצורת הכתיבה‬
‫הבאה‪ ,‬תוך שימוש בנוטציה שהצגנו זה עתה‪:‬‬
‫{ )‪void read_1_stud( struct Stud *stud_p‬‬
‫; ‪int temp‬‬
‫; ‪cin >> setw(N) >> stud_p-> _name‬‬
‫; ‪cin >> stud_p-> _shoe_num‬‬
‫)‪for (int ex = 0; ex < MAX_EX; ex++‬‬
‫; ]‪cin >> stud_p-> _grades[ex‬‬
‫; ‪cin >> temp‬‬
‫; ‪stud_p-> _gender = (temp == 1) ? FEMALE : MALE‬‬
‫}‬
‫‪45‬‬
‫‪ 12.5‬מצביע ל‪ struct -‬כפרמטר הפניה‬
‫במקום להגדיר מערך סטאטי‪ , int a[N]; :‬אנו יכולים להגדיר‬
‫בעבר ראינו כי ְ‬
‫פוטנציאל למערך בדמות מצביע‪ ,‬ובהמשך לממש את הפוטנציאל על‪-‬פי צרכי‬
‫התכנית בעת ריצתה‪.‬‬
‫מצב דומה חל עם מבנים‪ ,‬ואין כל הבדל עקרוני בין מבנים למשתנים פרימיטיביים‪.‬‬
‫נציג את הנושא עבור מבנים‪ ,‬ולו בשם החזרה‪.‬‬
‫נניח כי בתכנית הראשית הגדרנו‪ struct Stud *egypt_p; :‬ועתה ברצוננו‬
‫לקרוא לפונקציה ‪ alloc_arr_n_read_data‬אשר תקצה מערך בגודל המתאים‪,‬‬
‫ותקרא לתוכו נתונים‪ .‬ניתן לכתוב את הפונקציה בדרכים שונות )למשל הפונקציה‬
‫עשויה לקבל פרמטר הפניה מטיפוס מצביע ל‪ ,(stud -‬אנו נציג את הגרסה הבאה‪,‬‬
‫)בה הפונקציה מחזירה באמצעות פקודת ‪ return‬מצביע למערך שהוקצה על‪-‬ידה(‪:‬‬
‫{ )(‪sruct Stud *alloc_arr_n_read_data‬‬
‫; ‪sruct Stud *arr_p‬‬
‫; ‪int size‬‬
‫; ‪cin >> size‬‬
‫; ]‪arr_p = new stud[size +1‬‬
‫)‪if (arr_p == NULL‬‬
‫; )‪terminate(“Memory allocation error\n“, 1‬‬
‫‪// end of data sign‬‬
‫; )”“ ‪strcpy(arr_p[size]._name,‬‬
‫; )‪read_data(arr_p, size‬‬
‫) ‪return( arr_p‬‬
‫}‬
‫הקריאה לפונקציה תהיה‪. egypt_p = alloc_arr_n_read_data(); :‬‬
‫הסבר הפונקציה‪ :‬לפונקציה יש משתנה לוקלי‪ . stud *arr_p :‬הפונקציה קוראת‬
‫מהמשתמש את גודלו של המערך הדרוש לו ואחר מקצה מערך הכולל תא אחד יותר‬
‫מהדרוש‪) .‬במידה וההקצאה נכשלת אנו עוצרים את ביצוע התכנית‪ (.‬לתוך המרכיב‬
‫‪ _name‬בתא האחרון שבמערך משימה הפונקציה את הסטרינג הריק‪ ,‬וכך תדענה‬
‫פונקציות אחרות לזהות את סוף המערך‪ .‬אחר הפונקציה קוראת לפונקציה‬
‫‪ read_data‬אשר תקרא נתונים לתאי המערך‪ .‬הפונקציה ‪ read_data‬דומה‬
‫לפונקציה שכתבנו בעבר‪ ,‬פרט לכך שאנו מעבירים לה גם את מספר התאים לתוכם‬
‫‪ read_data‬אמורה לקרוא נתונים )‪ read_data‬כפי שנכתבה בעבר קראה ערכים‬
‫לתוך ‪ MAX_STUD‬תאים במערך(‪.‬‬
‫לרצות‬
‫הדוגמה האחרונה תשרת אותנו כדי להכיר מצב נוסף בו אנו עשויים ְ‬
‫מצביע‪:‬‬
‫הוגדר‬
‫לאחרונה‬
‫שראינו‬
‫בתכנית‬
‫במבנה‪:‬‬
‫להשתמש‬
‫‪ , struct Stud *egypt_p‬ואחר למצביע הוקצה מערך‪ .‬את התא האחרון‬
‫במקום‬
‫במערך סימנו באמצעות שדה ‪ name‬שכלל את הסטרינג הריק )”“(‪ְ .‬‬
‫להשתמש בשיטת הסטרינג הריק לסימון סוף המערך נוכל לחשוב על אפשרות‬
‫אחרת‪ ,‬נקייה יותר‪ .‬ראשית‪ ,‬בנוסף למבנה ‪ ,stud‬נגדיר מבנה‪:‬‬
‫{ ‪struct Stud_arr‬‬
‫; ‪struct Stud *_studs‬‬
‫; ‪int _size‬‬
‫; }‬
‫‪46‬‬
‫כפי שאנו רואים‪ ,‬מבנה מסוג ‪ Stud_arr‬מכיל שני מרכיבים‪ :‬המרכיב ‪_studs‬‬
‫הוא פוטנציאל למערך של נתוני סטודנטים‪ ,‬המרכיב ‪ _size‬מציין את גודלו של‬
‫המערך עליו מצביע ‪._studs‬‬
‫עתה נגדיר בתכנית הראשית משתנה‪ .stud_arr egypt; :‬שימו לב כי איננו‬
‫מגדירים מצביע כי אם מבנה )הכולל בתוכו מצביע(‪ .‬נתאר את מצב המחסנית‪:‬‬
‫= ‪egypt‬‬
‫= ‪_studs‬‬
‫= ‪_size‬‬
‫עתה נקרא לפונקציה‪ alloc_arr_n_read_data2(egypt); :‬הפונקציה‬
‫מקבלת את הארגומנט ‪ egypt‬כפרמטר הפניה‪ .‬נבחן את הגדרת הפונקציה‪:‬‬
‫{ )‪void alloc_arr_n_read_data2(struct Stud_arr &egypt‬‬
‫; ‪cin >> egypt._size‬‬
‫; ] ‪egypt._studs = new stud[ egypt._size‬‬
‫)‪if (egypt._size == NULL‬‬
‫; )‪terminate(“Memory allocation error\n“, 1‬‬
‫; ) ‪read_data2( egypt‬‬
‫}‬
‫הסבר‪ :‬הפונקציה ראשית קוראת מהמשתמש את גודלו של המערך הדרוש לו לתוך‬
‫החבר ‪ _size‬בפרמטר המשתנה ‪ .egypt‬אחר הפונקציה מקצה מערך ל‪-‬‬
‫‪ egypt._studs‬שהוא מרכיב מטיפוס * ‪) struct Stud‬כפי שאנו רואים‬
‫בהגדרת הטיפוס ‪ ,Stud_arr‬שהוא טיפוסו של הפרמטר ‪ . (egypt‬לבסוף‬
‫הפונקציה מזמנת את‪ . read_data2( egypt ); :‬נבחן את מצב הזיכרון עם‬
‫הכניסה לפונקציה ‪) alloc_arr_n_read_data2‬וטרם שהיא הקצתה את‬
‫המערך(‪:‬‬
‫= ‪egypt‬‬
‫= ‪studs‬‬
‫= ‪size‬‬
‫= ‪egypt‬‬
‫‪47‬‬
‫הסבר‪ :‬הפונקציה מקבלת פרמטר הפניה בשם ‪ .egypt‬מכיוון שזה פרמטר הפניה‬
‫אנו שולחים חץ מהפרמטר לארגומנט המתאים‪ .‬לכן עת הפונקציה פונה ל‪-‬‬
‫‪ egypt._size‬אנו הולכים בעקבות החץ‪ ,‬ומעדכנים את המרכיב ‪_size‬‬
‫בארגומנט שהועבר לפונקציה בקריאה לה‪ .‬באפן דומה גם הקצאת זיכרון ַתפנה את‬
‫המצביע ‪ studs‬בארגומנט של הפונקציה להצביע על המערך שהוקצה‪ .‬נציג זאת‬
‫בציור‪:‬‬
‫= ‪egypt‬‬
‫=‪name‬‬
‫=‪genger‬‬
‫=‪shoe‬‬
‫=‪grades‬‬
‫=‪name‬‬
‫=‪genger‬‬
‫=‪shoe‬‬
‫=‪grades‬‬
‫=‪name‬‬
‫=‪genger‬‬
‫=‪shoe‬‬
‫=‪grades‬‬
‫= ‪_studs‬‬
‫‪_size = 3‬‬
‫= ‪egypt‬‬
‫מטרתה של הדוגמה האחרונה הייתה להראות כי לעיתים מבנה עשוי להכיל‪) :‬א(‬
‫מערך כלשהו‪ ,‬או מבנה אחר‪ ,‬אשר שומרים את הנתונים בהם מעוניין המשתמש‪) ,‬ב(‬
‫נתונים אודות המערך )או המבנה( כגון גודלו‪ ,‬מתי הוא עודכן לאחרונה‪ ,‬לאילו מתאי‬
‫המערך יש להתייחס כריקים וכו'‪.‬‬
‫‪ 12.6‬תרגילים‬
‫‪12.6.1‬‬
‫תרגיל מספר אחד‪ :‬פולינומים‬
‫נחזור לתרגיל הפולינומים שהוצג בפרק שש‪ .‬הפעם נייצג מונום באופן הבא‪:‬‬
‫{ ‪struct monom‬‬
‫; ‪float coef, degree‬‬
‫; }‬
‫כלומר עבור כל מונום נשמור את המקדם והחזקה‪.‬‬
‫פולינום ייוצג באופן הבא‪:‬‬
‫{ ‪polynom‬‬
‫; ‪monom *the_poly‬‬
‫; ‪unsiged int num_of_monoms‬‬
‫; }‬
‫כלומר נחזיק מערך של מונומים‪ ,‬ושדה המציין כמה מונומים מרכיבים את‬
‫הפולינום‪.‬‬
‫סדרה של פולינומים תיוצג‪ ,‬לפיכך‪ ,‬כמערך של מבנים מטיפוס ‪ .polynom‬את‬
‫המערך נוכל להגדיר סטטית ַבאופן‪:‬‬
‫; ]‪polynom polynoms[MAX_POLY‬‬
‫או שנקצה את המערך דינמית‪ ,‬על הערמה‪ ,‬ואז נוכל גם לשנות את גודלו במידת‬
‫הצורך; אם נבחר באפשרות זאת נגדיר את המצביע‪:‬‬
‫; ‪polynom *polynoms‬‬
‫עתה כתבו שנית את תכנית הפולינומים כפי שתוארה בפרק שש‪ ,‬תוך שאתם‬
‫משתמשים במבנה הנתונים שתיארנו בפרק זה‪.‬‬
‫‪48‬‬
‫‪12.6.2‬‬
‫תרגיל מספר שתיים‪ :‬סידור כרטיסים בתבנית רצויה‬
‫המשחק לסדר תשעה קלפים‬
‫ֵ‬
‫במשחק בו על‬
‫ְ‬
‫בפרק שמונה הצגנו תרגיל אשר דן‬
‫בתבנית רצויה‪ .‬ראשית חיזרו לתיאור המשחק כפי שמופיע שם‪ .‬עתה נרצה לכתוב‬
‫שוב את התכנית הדרושה תוך שאנו עושים שימוש במבני נתונים חכמים יותר‪ ,‬אשר‬
‫יגבירו את קריאותה של התכנית‪.‬‬
‫מבנה הנתונים הראשון שנגדיר ישמש לתיאור כל אחד מארבעת צדדיו של כל‬
‫כרטיס‪:‬‬
‫; } ‪enum color { RED, YELLOW, GREEN, BLUE‬‬
‫{ ‪stuct side‬‬
‫; ‪bool head_tail‬‬
‫; ‪color a_color‬‬
‫; }‬
‫כרטיס שלם מורכב מארבעה צדדים‪:‬‬
‫; } ‪enum urdl { UP=0, RIGHT=1, DOWN=2, LEFT=3‬‬
‫{ ‪struct card‬‬
‫; ]‪side sides[4‬‬
‫; ‪urdl direction‬‬
‫; }‬
‫הטיפוס ‪ urdl‬ישמש אותנו הן כאינדקס למערך ‪ sides‬כדי להגביר את הקריאות‬
‫)]‪ sides[UP‬הוא ביטוי קריא יותר מ‪ ,(sides[0] -‬והן כדי לציין האם הכרטיס‬
‫מוצב כאשר הצד המתואר כ‪ UP -‬אכן למעלה‪ ,‬או שמא הוא מסובב ימינה‪ ,‬הפוך‬
‫)כשצדו התחתון הוא שפונה כלפי מעלה(‪ ,‬או שמאלה‪.‬‬
‫מאגר הכרטיסים אותם יש לסדר במהלך המשחק יתואר באופן הבא‪:‬‬
‫; ]‪card cards[9‬‬
‫ולתוכו יהיה על התכנית לקרוא את הנתונים מהמשתמש‪.‬‬
‫לבסוף‪ ,‬על התכנית לסדר את תשעת הכרטיסים בתבנית הרצויה )תוך שימוש‬
‫באלגוריתם שתואר בפרק שמונה(‪ ,‬ואת הסידור הנ"ל יתאר המערך‪:‬‬
‫; ]‪unsigned int arrange[3][3‬‬
‫כל תא במערך יכיל אינדקס של כרטיס במערך ‪.cards‬‬
‫‪12.6.3‬‬
‫תרגיל מספר שלוש‪ :‬בעיית מסעי הפרש‬
‫נחזור לבעיית מסעי הפרש‪ ,‬כפי שהוצגה בפרק שמונה )תרגיל ‪ .(8.7.10‬להזכירכם‪,‬‬
‫המשימה אותה היה עלינו לממש היא מלוי לוח שחמט בגודל ‪ NxN‬בצעדי פרש‪ ,‬תוך‬
‫שהפרש מבקר פעם אחת בדיוק בכל משבצת בלוח‪.‬‬
‫הפעם נרצה להשלים את המשימה תוך שימוש במבנים‪ .‬נגדיר על‪-‬כן את המבנה‪:‬‬
‫ונגדיר את המשתנה‪:‬‬
‫{ ‪stuct square‬‬
‫; ‪bool visited‬‬
‫; ‪unsigned int next_x, next_y‬‬
‫; }‬
‫;]‪ .square board[N][N‬משתנה זה ייצג את הלוח‪.‬‬
‫התכנית הראשית תאתחל את המשתנה כך שערכי השדה ‪ visited‬בכל תאי‬
‫המערך יכילו את הערך ‪.false‬‬
‫‪49‬‬
‫אחר‪ ,‬התכנית הראשית תקרא מהמשתמש את מקומה של המשבצת ממנה על הפרש‬
‫ַ‬
‫להתחיל את מסעו‪.‬‬
‫הפונקציה הרקורסיבית שתהווה את לב ליבה של התכנית תמלא את הלוח בצעדי‬
‫הפרש תוך שהיא מסמנת בכל תא בו הפרש מבקר‪ ,‬בשדה ‪ ,visited‬כי הפרש ביקר‬
‫בתא זה‪ .‬השדות ‪ next_x, next_y‬יתארו את המשבצת אליה התקדם הפרש‬
‫מהמשבצת הנוכחית‪.‬‬
‫בתום תהליך החיפוש הציגו פלט שיכלול שני מרכיבים‪:‬‬
‫א‪ .‬רישום של סדרת המשבצות בהן ביקר הפרש‪ ,‬בסדר בו בוקרו המשבצות השונות‪.‬‬
‫ב‪ .‬תיאור של הלוח )כטבלה דו‪-‬ממדית(‪ ,‬כאשר עבור כל משבצת מופיע מספר הצעד‬
‫בו בוקרה משבצת זאת‪.‬‬
‫‪12.6.4‬‬
‫תרגיל מספר ארבע‪ :‬נתוני משפחות‬
‫בתרגיל זה נחזיק מסד נתונים הכולל נתוני משפחות‪.‬‬
‫כל משפחה בתרגיל תהיה משפחה גרעינית הכוללת לכל היותר אם‪ ,‬אב ומספר‬
‫כלשהו של ילדים‪ .‬נתוני משפחה יחידה ישמרו במבנה מהטיפוס‪:‬‬
‫‪struct family‬‬
‫{‬
‫; ‪char *family_name‬‬
‫‪char *mother_name, *father_name ; // parents' names‬‬
‫; ‪child *children_list‬‬
‫‪// list of the children in this family‬‬
‫; ‪unsigned child_list_size‬‬
‫‪// size of child_list array‬‬
‫;}‬
‫נתוני כל ילד ישמרו ב‪:‬‬
‫‪struct child‬‬
‫{‬
‫; ‪char *child_name‬‬
‫; ‪bool sex‬‬
‫;}‬
‫מבנה הנתונים המרכזי של התכנית יהיה‪family *families :‬‬
‫התכנית תאפשר למשתמש לבצע את הפעולות הבאות‪:‬‬
‫‪ .1‬הזנת נתוני משפחה נוספת‪ .‬לשם כך יזין המשתמש את שם המשפחה )וזהו‬
‫מרכיב חובה(‪ ,‬ואת שמות בני המשפחה‪ .‬במידה והמשפחה אינה כוללת אם יזין‬
‫המשתמש במקום שם האם את התו מקף )‪) (-‬המרכיב ‪ mother_name‬במבנה‬
‫המתאים יקבע אז‪ ,‬כמובן‪ ,‬להיות ‪ ,(NULL‬באופן דומה יצוין שאין אב במשפחה‪.‬‬
‫לפני הזנת נתוני הילדים יוזן מספר הילדים במשפחה‪ .‬התכנית תקצה מערך‬
‫הכולל שני תאים נוספים‪ ,‬מעבר למספר הילדים הנוכחי במשפחה‪ ,‬וזאת לשימוש‬
‫עתידי‪ .‬שמות הילדים בתאים הריקים ייקבעו להיות ‪ NULL‬וכך נסמן כי התאים‬
‫ריקים‪ .‬תיתכן משפחה ללא ילדים‪ .‬לא תיתכן משפחה בה אין לא אם ולא אב‬
‫)יש לתת הודעת שגיאה במידה וזה הקלט מוזן‪ ,‬ולחזור על תהליך קריאת שמות‬
‫ההורים(‪ .‬אתם רשאים להגדיר משתנה סטטי יחיד מסוג מערך של תווים לתוכו‬
‫תקראו כל סטרינג מהקלט‪ .‬את רשימת המשפחות יש להחזיק ממוינת על‪-‬פי שם‬
‫משפחה‪ .‬את רשימת ילדי המשפחה החזיקו בסדר בה הילדים מוזנים‪ .‬הניחו כי‬
‫שם המשפחה הוא מפתח קבוצת המשפחות‪ ,‬כלומר לא תתכנה שתי משפחות‬
‫‪50‬‬
‫להן אותו שם משפחה )משמע ניסיון להוסיף משפחה בשם כהן‪ ,‬עת במאגר כבר‬
‫קיימת משפחת כהן מהווה שגיאה(‪ .‬אין צורך לבדוק כי לא מוזנים באותה‬
‫משפחה שני ילדים בעלי אותו שם‪ .‬במידה ומערך המשפחות התמלא יש‬
‫להגדילו‪.‬‬
‫דוגמה לאופן קריאת הנתונים‪:‬‬
‫‪Enter family name: Cohen‬‬
‫‪Enter father name: Yosi‬‬
‫ ‪Enter mother name:‬‬‫‪Enter children's names: Dani Dana -‬‬
‫‪.2‬‬
‫‪.3‬‬
‫‪.4‬‬
‫‪.5‬‬
‫‪.6‬‬
‫‪.7‬‬
‫הוספת ילד למשפחה‪ .‬יוזן שם המשפחה ושמו של הילד‪ .‬הילד יוסף בסוף רשימת‬
‫הילדים‪ .‬במידה ומשפחה בשם הנ"ל אינה קיימת יש להודיע על‪-‬כך למשתמש‬
‫)ולחזור לתפריט הראשי(‪ .‬במידה ומערך הילדים מלא יש להגדילו בשיעור‬
‫שלושה תאים נוספים‪.‬‬
‫פטירת בן משפחה‪ .‬יש להזין‪) :‬א( את שם המשפחה‪) ,‬ב( האם מדובר באם )ולשם‬
‫כך יוזן הסטרינג ‪ ,(mother‬באב )יוזן ‪ ,(father‬או באחד הילדים )יוזן ‪) (child‬ג(‬
‫במידה ומדובר בילד יוזן גם שם הילד שנפטר‪ .‬במידה ואין במאגר משפחה‬
‫כמצוין‪ ,‬או שבמשפחה אין בן‪-‬משפחה כמתואר )למשל אין אב והתבקשתם‬
‫לעדכן פטירת אב‪ ,‬או אין ילד בשם שהוזן( יש לדווח על שגיאה )ולחזור לתפריט‬
‫הראשי(‪ .‬כמו כן לא ניתן לדווח על פטירת הורה במשפחה חד‪-‬הורית )ראשית יש‬
‫להשיא את המשפחה למשפחה חד‪-‬הורית אחרת‪ ,‬כפי שמתואר בהמשך‪ ,‬ורק אז‬
‫יוכל ההורה המתאים לעבור לעולם שכולו טוב(‪.‬‬
‫נשואי שתי משפחות קיימות‪ .‬יש לציין את שמות המשפחות הנישאות‪ ,‬ואת שם‬
‫המשפחה החדש שתישא המשפחה המאוחדת‪ .‬פעולה זאת לגיטימית רק אם‬
‫אחת משתי המשפחות אינה כוללת אם בעוד השניה אינה כוללת אב‪ .‬בעקבות‬
‫ביצוע הפעולה תאוחדנה רשימות הילדים והתא שהחזיק את נתוני אחת משתי‬
‫המשפחות ייחשב כריק‪ ,‬ולכן תוזזנה הרשומות בהמשך המערך‪ ,‬כדי לצמצם‬
‫הפער‪.‬‬
‫הצגת נתוני המשפחות בסדר עולה של שמות משפחה‪) .‬זהו‪ ,‬כזכור‪ ,‬הסדר בו‬
‫מוחזקת רשימת המשפחות(‪ .‬עבור כל משפחה ומשפחה יש להציג את שם‬
‫המשפחה‪ ,‬ואת שמות בני המשפחה‪.‬‬
‫הצגת נתוני משפחה בודדת רצויה‪ .‬יש להזין את שם המשפחה ויוצגו פרטי בני‬
‫המשפחה‪) .‬במידה והמאגר אינו כולל משפחה כמבוקש תוצג הודעת שגיאה(‪.‬‬
‫הצגת נתוני המשפחות בסדר יורד של מספר בני המשפחה‪ .‬יש להציג את שם‬
‫המשפחה ואת מספר בני המשפחה‪ .‬לשם סעיף זה החזיקו מערך נוסף‪ .‬כל תא‬
‫במערך יהיה מטיפוס‪:‬‬
‫‪struct family_size‬‬
‫‪{ unsigned the_family ; // index of a family in the families array‬‬
‫; ‪int size‬‬
‫; ‪// number of children in this family‬‬
‫;}‬
‫המצביע למערך זה יהיה ‪ .family_size *families_sizes‬המערך ימוין בסדר יורד‬
‫של המרכיב ‪ .size‬על‪-‬ידי סריקה של מערך זה ופניה ממנו בכל פעם למשפחה‬
‫המתאימה )תוך שימוש באינדקס ‪ (the_family‬תוכלו להציג את המשפחות בסדר‬
‫יורד של גודל המשפחות‪ .‬שימו לב כי יש גם לתחזק רשימה זאת‪ .‬בין כל‬
‫המשפחות להן אותו גודל אין חשיבות לסדר ההצגה‬
‫‪ .8‬הצגת נתוני שכיחות שמות ילדים‪ .‬יש להציג עבור כל שם ילד המופיע במאגר‪,‬‬
‫כמה פעמים הוא מופיע במאגר‪ .‬אין צורך להציג את הרשימה ממוינת‪.‬‬
‫)רמז\הצעה‪ :‬סרקו את מאגר המשפחות משפחה אחר משפחה‪ ,‬עבור כל משפחה‬
‫עיברו ילד אחר ילד‪ ,‬במידה ושמו של הילד הנוכחי עדיין לא מופיע ברשימת‬
‫‪51‬‬
‫הילדים ששמם הודפס סיפרו כמה פעמים מופיע שם הילד במאגר הנתונים‪,‬‬
‫ואחר הוסיפו את שמו של הילד לרשימת שמות הילדים שהוצגו(‪.‬‬
‫‪ .9‬סיום‪ .‬בשלב זב עליכם לשחרר את כל הזיכרון שהוקצה על‪-‬ידכם דינמית‪.‬‬
‫הערות כלליות‪:‬‬
‫א‪ .‬התכנית תנהל כלולאה בה בכל שלב המשתמש בוחר בפעולה הרצויה לו‪ ,‬הפעולה‬
‫מתבצעת )או שמוצגת הודעת שגיאה(‪ ,‬והתכנית חוזרת לתפריט הראשי‪.‬‬
‫ב‪ .‬הקפידו שלא לשכפל קוד‪ ,‬למשל כתבו פונקציה יחידה אשר מציגה נתוני‬
‫משפחה‪ ,‬והשתמשו בה במקומות השונים בהם הדבר נדרש‪ .‬וכו'‪.‬‬
‫‪ Needless to say‬שיש להקפיד על כללי התכנות המקובלים‪ ,‬בפרט ובמיוחד‬
‫מודולריות‪ ,‬פרמטרים מסוגים מתאימים )פרמטרי ערך ‪ vs‬פרמטרי הפניה(‪ ,‬ערכי‬
‫החזרה של פונקציות ושאר ירקות‪ .‬תכנית רצה אינה בהכרח גם תכנית טובה! )קל‬
‫וחומר לגבי תכנית מקרטעת(…‬
‫תרגיל מספר חמש‪ :‬סימולציה של רשימה משורשרת‬
‫‪12.6.5‬‬
‫נתונות ההגדרות הבאות‪:‬‬
‫; … = ‪const int N‬‬
‫{ ‪struct node‬‬
‫; ‪char ch‬‬
‫; ‪unsigned int next‬‬
‫; }‬
‫{ ‪struct linked_list‬‬
‫; ]‪node list[N‬‬
‫; ‪unsigned int head‬‬
‫; }‬
‫משתנה מטיפוס ‪ linked_list‬מכיל שני מרכיבים‪:‬‬
‫הפניה לתא נוסף במערך‪ .‬דוגמה אפשרית‬
‫א‪ .‬מערך ‪ list‬המכיל בכל תא תו‪ ,‬וכן ְ‬
‫למרכיב ‪ list‬ברשומה כלשהי‪:‬‬
‫‪d‬‬
‫‪i‬‬
‫‪a‬‬
‫‪n‬‬
‫‪a‬‬
‫‪o‬‬
‫‪x‬‬
‫‪y‬‬
‫‪x‬‬
‫‪s‬‬
‫‪7‬‬
‫‪#9‬‬
‫‪17‬‬
‫‪#8‬‬
‫‪6‬‬
‫‪#7‬‬
‫‪5‬‬
‫‪#6‬‬
‫‪1‬‬‫‪#5‬‬
‫‪0‬‬
‫‪#4‬‬
‫‪1‬‬
‫‪#3‬‬
‫‪4‬‬
‫‪#2‬‬
‫‪3‬‬
‫‪#1‬‬
‫‪8‬‬
‫‪#0‬‬
‫ב‪ .‬מספר של תא כלשהו במערך‪ ,‬השמור בשדה ‪.head‬‬
‫מספר מחרוזות‪ ,‬את כתובת ההתחלה של‬
‫נוכל להתייחס למרכיב המערך כמכיל ְ‬
‫אחת מהן מחזיק המרכיב ‪ .head‬לדוגמה‪ :‬אם ערכו של ‪ head‬הוא ‪ ,2‬אזי המחרוזת‬
‫בה מתמקדים מורכבת מהתווים הבאים‪ :‬התו בתא מספר שתיים במערך )‪ ,(y‬מתו‬
‫זה אנו מתקדמים על פי השדה ‪ next‬לתא מספר ארבע במערך‪ ,‬בו שמור התו ‪,o‬‬
‫משם לתא מספר אפס המכיל את ‪ ,s‬ומשם לתא מספר שמונה המכיל את ‪ .i‬מכיוון‬
‫שבתא מספר שמונה מצוי ערך שאינו בתחום ‪ 0..N-1‬אנו מסיקים כי בכך תם‬
‫הסטרינג‪ ,‬ולפיכך הסטרינג בו עסקינן הוא ‪ .yosi‬באופן דומה בתא מספר תשע‬
‫במערך מתחיל הסטרינג ‪.dana‬‬
‫לעומת זאת בתא מספר שלוש במערך אנו מוצאים ַה ְפניה לתא מספר חמש‪ ,‬בתא‬
‫מספר חמש קיימת הפניה לתא מספר שלוש‪ ,‬וחוזר חלילה‪ .‬אנו אומרים כי סטרינג‬
‫זה אינו נגמר‪ ,‬ולכן הוא בגדר שגיאה‪.‬‬
‫‪52‬‬
‫כתבו פונקציה המקבלת מבנה מסוג ‪ linked_list‬ומציגה את הסטרינג שעל התו‬
‫הראשון שלו מורה המרכיב ‪ head‬אלא אם הסטרינג הינו שגוי‪ ,‬ואז תוצג הודעת‬
‫שגיאה‪ ,‬ולא יוצג כל תו מהסטרינג‪.‬‬
‫‪53‬‬
‫‪ 1/2011‬עודכן‬
‫‪ 13‬רשימות מקושרות )משורשרות(‬
‫בפרקים הקודמים ראינו כיצד יש ביכולתנו לשמור סדרה של נתונים במערך‬
‫שמוקצה באופן סטטי או בכזה שמוקצה באופן דינמי‪ .‬בפרק זה‪ ,‬ובבא אחריו‪ ,‬נכיר‬
‫דרך אחרת לשמירת סדרת נתונים‪.‬‬
‫‪ 13.1‬בניית רשימה מקושרת‬
‫נניח כי בתכנית כשלהי עלינו לשמור כמות שאינה ידועה מראש של מספרים שלמים‪.‬‬
‫כמו כן לעיתים נרצה להוסיף נתונים לרשימה‪ ,‬ולעיתים נרצה להסיר מהרשימה‬
‫נתונים שאין לנו בהם עוד צורך‪ .‬נוכל נקוט לשם כך בפתרון הבא‪ :‬נגדיר בתכנית את‬
‫המבנה‪:‬‬
‫{ ‪struct Node‬‬
‫; ‪int _data‬‬
‫; ‪struct Node *_next‬‬
‫; }‬
‫מהביצה‪,‬‬
‫ִ‬
‫מה קיבלנו? כמו הברון מנכאוזן שמשך את עצמו בשערות ראשו וכך נחלץ‬
‫כך אנו שקענו בביצה שעה שהגדרנו מבנה בשם ‪ Node‬אשר אחד ממרכיביו הוא‬
‫מצביע ַלמבנה ‪ .Node‬מייד נראה לאן תובילנו הגדרה מפתיעה‪ ,‬אך לגיטימית זאת‪.‬‬
‫עתה נניח כי בתכנית הגדרנו‪ , struct Node *head = NULL, *temp; :‬וכן‬
‫משתנה שלם ‪ .num‬נציג את מצב המחסנית‪:‬‬
‫= ‪head‬‬
‫= ‪temp‬‬
‫= ‪num‬‬
‫בגוף התכנית נכלול את הלולאה הבאה‪:‬‬
‫; ‪cin >> num‬‬
‫{ )‪while (num != 0‬‬
‫; ‪temp = new (std::nothrow) struct Node‬‬
‫)‪if (temp == NULL‬‬
‫; )‪terminate(“cannot alloc mem”,1‬‬
‫; ‪temp -> _data = num‬‬
‫; ‪temp -> _next = head‬‬
‫; ‪head = temp‬‬
‫; ‪cin >> num‬‬
‫}‬
‫נניח כי המשתמש מזין את הקלט‪ 17) 0 3 3879 17 :‬מוזן ראשון‪ 0 ,‬מוזן אחרון(‪.‬‬
‫נעקוב אחר התנהלות התכנית‪ :‬אחרי קריאת הערך ‪ 17‬ל‪ num -‬מוקצית על‪-‬גבי‬
‫הערמה קופסה יחידה מסוג ‪ ,Node‬ו‪ temp -‬עובר להצביע עליה‪) .‬כאשר אנו כותבים‬
‫מבין‬
‫‪ ,new (std::nothrow) Node‬ואיננו מציינים כמה תאים יש להקצות‪ִ ,‬‬
‫המחשב כי ברצוננו להקצות תא יחיד(‪ .‬לתוך המרכיב ‪ _data‬בקופסה עליה מצביע‬
‫‪ temp‬מוכנס הערך ‪ ,17‬ולתוך המרכיב ‪) _next‬שהינו מצביע לקופסות מסוג ‪(Node‬‬
‫מוכנס הערך המצוי ב‪ ,head -‬כלומר הערך ‪ . NULL‬נציג את מצב המחסנית אחרי‬
‫‪54‬‬
‫ביצוע הפעולות הללו )ולפני ביצוע ההשמה‪ .(head = temp; :‬מעתה נניח כי כל‬
‫מה שלא מופיע בתוך המסגרת המציינת את המחסנית‪ ,‬מוקצה על הערמה‪.‬‬
‫= ‪head‬‬
‫= ‪temp‬‬
‫= ‪num‬‬
‫=‪data‬‬
‫‪17‬‬
‫=‪next‬‬
‫מה תעשה‪ ,‬עתה‪ ,‬ההשמה‪ ? head = temp; :‬היא תשלח את ‪ head‬להצביע על‬
‫אותה קופסה כמו ‪ .temp‬נציג זאת על הזיכרון‪:‬‬
‫= ‪head‬‬
‫= ‪temp‬‬
‫= ‪num‬‬
‫=‪data‬‬
‫‪17‬‬
‫=‪next‬‬
‫בכך מסתיים הסיבוב הראשון של הלולאה‪ .‬אני מסב את תשומת לבכם לנקודה‬
‫חשובה‪ :‬המצביע בקופסה של ‪ ,17‬קיבל את ערכו של ‪ head‬עת ערכו של האחרון היה‬
‫‪ ,NULL‬ולכן ערכו הוא ‪ .NULL‬הלולאה ממשיכה להתנהל‪ .‬נפנה עתה למעקב אחר‬
‫ביצוע הסיבוב השני בה‪ .‬בסיבוב זה ‪ num‬מקבל את הערך ‪ .3879‬הפקודה = ‪temp‬‬
‫;‪ new (std::nothrow) Node‬יוצרת קופסה חדשה על הערמה‪ ,‬ומפנה את‬
‫‪ temp‬להצביע על קופסה זאת ) ִב ְמקום על הקופסה עליה הוא הצביע בעבר‪ ,‬ושעתה‬
‫רק ‪ head‬מצביע עליה(‪ .‬למרכיב ה‪ _data -‬בקופסה עליה מצביע ‪ temp‬אנו‬
‫ולמרכיב ‪ _next‬אנו משימים את הערך השמור ב‪,head -‬‬
‫משימים את הערך ‪ַ ,3879‬‬
‫על מה מצביע ‪ ?head‬על הקופסה של ‪ ,17‬לכן גם המצביע _‪ next‬בקופסה עליה‬
‫מצביע ‪ ,temp‬יצביע על הקופסה של ‪ .17‬נציג את מצב המחסנית‪:‬‬
‫= ‪head‬‬
‫= ‪temp‬‬
‫= ‪num‬‬
‫=‪data‬‬
‫‪17‬‬
‫=‪next‬‬
‫=‪data‬‬
‫‪3879‬‬
‫=‪next‬‬
‫הפקודה האחרונה בסיבוב זה של הלולאה הינה‪ head = temp; :‬והיא מפנה את‬
‫‪ head‬להצביע על אותה קופסה כמו ‪ ,temp‬כלומר על הקופסה של ‪ .3879‬מצב‬
‫המחסנית הינו‪:‬‬
‫= ‪head‬‬
‫= ‪temp‬‬
‫= ‪num‬‬
‫=‪data‬‬
‫‪17‬‬
‫=‪next‬‬
‫=‪data‬‬
‫‪3879‬‬
‫=‪next‬‬
‫עתה אנו פונים לסיבוב השלישי בלולאה‪ .‬בסיבוב זה אנו קוראים את הערך ‪ 3‬לתוך‬
‫המשתנה ‪ .num‬שוב אנו מקצים קופסה חדשה‪ ,‬ושולחים את ‪ temp‬להצביע עליה‪.‬‬
‫שוב אנו מכניסים ל‪ ,temp-> _data -‬כלומר למרכיב ה‪ data_ -‬בקופסה עליה‬
‫מצביע ‪ ,temp‬את הערך שב‪ .num -‬ושוב אנו שולחים את ‪ ,temp-> _next‬כלומר‬
‫את המצביע )שטיפוסו * ‪ (struct Node‬בקופסה עליה מצביע ‪ temp‬להצביע כמו‬
‫‪) head‬שגם טיפוסו הוא * ‪ ;(struct Node‬הפעם הדבר גורם ל‪temp-> -‬‬
‫‪ _next‬להצביע על הקופסה של ‪ .3879‬נציג זאת בציור‪:‬‬
‫= ‪head‬‬
‫= ‪temp‬‬
‫= ‪num‬‬
‫=‪data‬‬
‫‪17‬‬
‫=‪next‬‬
‫=‪data‬‬
‫‪3879‬‬
‫‪data= 3‬‬
‫=‪next‬‬
‫=‪next‬‬
‫‪55‬‬
‫הפקודה האחרונה בגוף הלולאה היא‪ head = temp; :‬והיא מפנה את ‪head‬‬
‫להצביע על אותה קופסה כמו ‪ ,temp‬כלומר על הקופסה של ‪ .3‬נציג זאת‪:‬‬
‫= ‪head‬‬
‫= ‪temp‬‬
‫= ‪num‬‬
‫=‪data‬‬
‫‪17‬‬
‫=‪next‬‬
‫=‪data‬‬
‫‪3879‬‬
‫‪data= 3‬‬
‫=‪next‬‬
‫=‪next‬‬
‫בסיבוב הבא בלולאה אנו קוראים את הערך אפס‪ ,‬ובזאת מסיימים את ביצוע‬
‫הלולאה‪.‬‬
‫מה קיבלנו? המצביע ‪ head‬מצביע על הקופסה של הנתון שהוזן אחרון )שלוש(‪,‬‬
‫המצביע בקופסה זאת מצביע על הקופסה הכוללת את הנתון שהוזן לפני אחרון‬
‫)‪ ;(3879‬והמצביע בקופסה של ‪ 3879‬מורה על הקופסה של הנתון שהוזן ראשון )‪.(17‬‬
‫ולבסוף‪ ,‬ערכו של המצביע בקופסה האחרונה הוא ‪.NULL‬‬
‫קטע הקוד שבנה את הרשימה המקושרת כולל קריאה לפונקציה ‪.terminate‬‬
‫פונקציה זו אינה חלק מהשפה‪ ,‬אלא עלינו לממשה‪ .‬הייתי ממליץ לכם לכלול‬
‫פונקציה זאת בכל תכנית שמקצה זיכרון‪:‬‬
‫{ )‪void terminate(const char *msg, int err_code‬‬
‫; ‪cout << msg << endl‬‬
‫; ) ‪exit( err_code‬‬
‫}‬
‫הסבר הפונקציה‪ :‬הפונקציה מקבלת סטרינג‪ ,‬וערך שלם‪ .‬היא מציגה את הסטרינג‬
‫על המסך‪ ,‬ואחר קוטעת את ביצוע התכנית‪ ,‬תוך החזרת הערך השלם המועבר לה‪.‬‬
‫אני מזכיר לכם כי אם ברצונכם לכלול בתכניתכם את הפקודה ‪ ,exit‬אזי עליכם‬
‫לכלול בתכנית הוראת ‪ include‬לקובץ ‪. stdlib.h‬‬
‫מתכנתים מתחילים מוטרדים לעיתים מכך שהנתונים מופיעים ברשימה בסדר הפוך‬
‫לסדר הזנתם‪ .‬מספר מענים יש לנו על‪-‬כך‪:‬‬
‫א‪ .‬בקטע תכנית זה אנו מניחים כי מבחינת המשתמש אין חשיבות לסדר בו‬
‫מוחזקים הנתונים שבין כה וכה אינם ממוינים‪.‬‬
‫ב‪ .‬אני מזמין אתכם לבנות את הרשימה כך שהנתונים ישמרו בה בסדר בו הם‬
‫הוזנו‪ .‬כדי לעשות זאת החזיקו מצביע נוסף‪ struct Node *last; :‬אשר‬
‫נק ֵצה קופסה‬
‫יצביע כל פעם לתא האחרון ברשימה‪ .‬כל פעם שנקרא נתון נוסף ְ‬
‫חדשה עליה יצביע בתחילה ‪ .temp‬אחר נַפנה את ‪ last->_next‬להצביע כמו‬
‫‪ .temp‬ולבסוף נקדם את ‪ last‬להצביע על הקופסה שנוספה אך זה עתה לקצה‬
‫הרשימה‪ .‬כיצד יאותחל ‪ ?last‬וכיצד יטופל ‪ ?head‬זאת אני משאיר לכם‪.‬‬
‫ג‪ .‬בהמשך נראה כיצד ניתן לבנות את הרשימה המקושרת כך שהנתונים יוחזקו בה‬
‫ממוינים‪.‬‬
‫‪ 13.2‬הצגת הנתונים השמורים ברשימה מקושרת‬
‫לשם הפשטות לא הצגנו את בניית הרשימה המקושרת בפונקציה נפרדת‪ .‬אולם‬
‫מעתה נבצע כל פעולה על הרשימה בפונקציה נפרדת )כפי שראוי היה לעשות גם עם‬
‫תהליך בניית הרשימה(‪ .‬עתה נכתוב פונקציה אשר מציגה את הנתונים השמורים‬
‫‪56‬‬
‫ברשימה‪ .‬הפונקציה תזומן באופן הבא‪ . display_list(head); :‬נציג את‬
‫הפונקציה‪:‬‬
‫{ )‪void display_list(const struct Node *run‬‬
‫{ )‪while (run != NULL‬‬
‫; " " << ‪cout << run-> _data‬‬
‫; ‪run = run-> _next‬‬
‫}‬
‫}‬
‫הסבר‪ :‬הפונקציה מתנהלת כלולאה אשר מתבצעת כל עוד )‪ . (run != NULL‬אנו‬
‫רשאים לנסח תנאי זה שכן עת בנינו את הרשימה דאגנו לכך שערכו של המצביע‬
‫‪ _next‬בקופסה האחרונה יהיה ‪ ,NULL‬ולכן הלולאה תעצור בדיוק במקום‬
‫המתאים‪ .‬בכל סיבוב בלולאה אנו מציגים את ערכו של המרכיב ‪ _data‬בקופסה‬
‫עליה מצביע ‪ ,run‬ואחר מקדמים את ‪ run‬לאיבר הבא ברשימה באמצעות הפקודה‪:‬‬
‫;‪ . run = run -> _next‬נציג סימולציית הרצה שתסייע לנו להשתכנע בנכונות‬
‫הפונקציה‪ ,‬ולהטמיע את השימוש ברשימה מקושרת‪ :‬מצב הזיכרון עם הקריאה‬
‫לפונקציה הוא כדלהלן‪:‬‬
‫= ‪head‬‬
‫= ‪temp‬‬
‫= ‪num‬‬
‫=‪data‬‬
‫‪17‬‬
‫=‪next‬‬
‫=‪data‬‬
‫‪3879‬‬
‫‪data= 3‬‬
‫=‪next‬‬
‫=‪next‬‬
‫= ‪run‬‬
‫הסבר‪ :‬הפרמטר ‪ run‬הוא פרמטר ערך‪ ,‬על‪-‬כן ערכו של ‪ head‬מועתק על ‪,run‬‬
‫ומבחינה ציורית המצביע ‪ run‬מצביע לאותו מקום כמו ‪ .head‬מכיוון ש‪ run -‬הוא‬
‫פרמטר ערך אזי שינויים שנערוך עליו במהלך ביצוע הפונקציה לא ישנו את ערכו של‬
‫‪ head‬של התכנית הראשית; ‪ head‬ימשיך להצביע לראש הרשימה שבנינו‪.‬‬
‫עתה אנו מתחילים לבצע את הפונקציה‪ .‬התנאי בלולאה מתקיים )‪ run‬מצביע על‬
‫קופסה נחמדה ויפה‪ ,‬וערכו שונה מ‪ ,(NULL -‬ולכן אנו נכנסים לסיבוב ראשון‬
‫בלולאה‪ .‬אנו מציגים את ‪ ,run-> _data‬כלומר את ערכו של שדה ה‪data_ -‬‬
‫בקופסה עליה מצביע ‪ ,run‬משמע את הערך ‪ .3‬אחר אנו מבצעים את הפקודה‪:‬‬
‫‪ . run = run-> _next‬נסביר פקודה זאת‪ :‬ראשית נבחן את אגף ימין של‬
‫ההשמה‪ run .‬הוא מצביע‪ run-> ,‬היא הקופסה עליה ‪ run‬מצביע‪ ,‬כלומר הקופסה‬
‫של ‪ run-> _next ,3‬הוא השדה ‪ next‬בקופסה עליה ‪ run‬מצביע‪ ,‬כלומר המצביע‬
‫אשר מורה על הקופסה של ‪ .3879‬את ערכו של מצביע זה אנו מכניסים ל‪ ,run -‬ולכן‬
‫עתה גם ‪ run‬מצביע לקופסה של ‪ .3879‬בזאת הסתיים הסיבוב הראשון בלולאה‪.‬‬
‫נציג את מצב הזיכרון‪:‬‬
‫= ‪head‬‬
‫= ‪temp‬‬
‫= ‪num‬‬
‫=‪data‬‬
‫‪17‬‬
‫=‪next‬‬
‫=‪data‬‬
‫‪3879‬‬
‫‪data= 3‬‬
‫=‪next‬‬
‫=‪next‬‬
‫= ‪run‬‬
‫לפני כניסה לסיבוב השני בלולאה אנו בודקים את תנאי הלולאה‪ .‬התנאי עדיין‬
‫מתקיים‪ run ,‬מצביע‪ ,‬כאמור‪ ,‬לקופסה של ‪ ,3879‬וערכו אינו ‪ NULL‬כלל וכלל‪ .‬אנו‬
‫‪57‬‬
‫נכנסים לסיבוב שני בלולאה‪ .‬בסיבוב זה אנו שוב מציגים את ‪,run-> _data‬‬
‫כלומר את מרכיב ה‪ _data -‬בקופסה עליה מצביע ‪ ,run‬משמע את הערך ‪.3879‬‬
‫אחר אנו מבצעים שוב את הפקודה‪ . run = run-> _next; :‬נבחן את האפקט‬
‫של פקודה זאת‪ :‬כמו קודם נתחיל בבחינת אגף ימין של ההשמה‪ run :‬הוא מצביע‪,‬‬
‫>‪ run-‬היא הקופסה עליה ‪ run‬מצביע‪ run-> _next ,‬הוא המרכיב ‪_next‬‬
‫בקופסה עליה ‪ run‬מצביע‪ .‬מרכיב זה הוא מצביע‪ ,‬והוא מורה על הקופסה של ‪.17‬‬
‫לכן אם אנו מפנים את ‪ run‬להצביע כמו ‪ run-> _next‬אנו שולחים גם את ‪run‬‬
‫להצביע על הקופסה של ‪ .17‬בזאת סיימנו את הסיבוב השני בלולאה‪ .‬לקראת כניסה‬
‫לסיבוב השלישי אנו שוב בודקים את התנאי שבראש הלולאה; והוא עדיין מתקיים‪.‬‬
‫אנו נכנסים לסיבוב שלישי בלולאה‪ .‬שוב אנו מציגים את ‪ ,run-> _data‬כלומר‬
‫את הערך ‪ ,17‬ושוב אנו מקדמים את‬
‫‪ run‬באמצעות הפקודה‪:‬‬
‫;‪ . run = run-> _next‬נבחן פקודה זאת )בפעם האחרונה‪ ,‬אני מבטיח(‪run :‬‬
‫הוא מצביע‪ run-> ,‬היא הקופסה עליה ‪ run‬מצביע‪ ,‬כלומר הקופסה של ‪.17‬‬
‫‪ run-> _next‬הוא מרכיב ה‪ _next -‬בקופסה עליה ‪ run‬מצביע; וערכו של‬
‫מרכיב זה הוא ‪ .NULL‬לכן עת אנו משימים ל‪ run -‬את ערכו של ‪ run-> _next‬אנו‬
‫מכניסים ל‪ run -‬את הערך ‪ .NULL‬לקראת סיבוב נוסף בלולאה אנו בודקים שוב את‬
‫התנאי )‪ .(run != NULL‬הפעם תנאי זה לא מתקיים‪ ,‬וטוב שכך‪ .‬איננו נכנסים‬
‫לסיבוב נוסף בלולאה‪ ,‬ומכיוון שהפונקציה אינה כוללת דבר פרט ללולאה‪ ,‬אנו‬
‫מסיימים בזאת את ביצוע הפונקציה‪.‬‬
‫‪ 13.3‬בניית רשימה מקושרת ממוינת‬
‫בעבר ראינו כיצד בונים רשימה מקושרת בה כל איבר חדש נוסף בראש הרשימה‪.‬‬
‫עתה נרצה לבנות רשימה מקושרת ממוינת )מקטן לגדול(‪ ,‬בה כל איבר חדש נוסף‬
‫לרשימה ַבמקום המתאים‪ ,‬כך שבעקבות כל הוספה הרשימה נשארת ממוינת‪.‬‬
‫נניח כי בתכנית הוגדר המבנה ‪ ,struct Node‬ובתכנית הראשית הוגדר‪:‬‬
‫;‪struct Node *head = NULL‬‬
‫עתה ברצוננו לקרוא לפונקציה‪ . build_list(head); :‬פונקציה זאת תבנה את‬
‫הרשימה ה מקושרת הממוינת‪ .‬הפונקציה תקבל כארגומנט את ‪.head‬‬
‫נתחיל בכך שנשאל את עצמנו האם הפונקציה צריכה לקבל את ‪ head‬כפרמטר ערך‬
‫או כפרמטר הפניה? התשובה היא שעל הפונקציה לבנות רשימה מקושרת שעל‬
‫ראשה יצביע הארגומנט המועבר לה‪ ,‬ובמקרה הנוכחי המשתנה ‪ head‬של ‪.main‬‬
‫ערכו של ‪ head‬הוא עתה ‪ ,NULL‬כלומר על הפונקציה לשנות את ערכו של ‪,head‬‬
‫ולכן הפרמטר של פונקציה צריך להיות פרמטר הפניה‪.‬‬
‫נציג את הגדרת הפונקציה‪:‬‬
‫{ )‪void build_list(struct Node *&head‬‬
‫; ‪int num‬‬
‫; ‪cin >> num‬‬
‫{ )‪while (num != 0‬‬
‫; )‪insert(num, head‬‬
‫; ‪cin >> num‬‬
‫}‬
‫}‬
‫‪58‬‬
‫הפונקציה שכתבנו מתנהלת כלולאה‪ ,‬בה בכל פעם אנו קוראים נתון חדש )עד‬
‫קליטת אפס(‪ ,‬ואז קוראים לפונקציה ‪ insert‬אשר מכניסה את הנתון החדש‬
‫לרשימה‪ .‬נעיר כי בתכניות המטפלות במבני נתונים שונים )כדוגמת רשימות‬
‫משורשרות כאלה או אחרות( מקובל לכתוב פונקציה ייעודית להוספת נתון נוסף‬
‫)שהתקבל ממקור כזה או אחר( למבנה הנתונים‪.‬‬
‫לפני שאנו פונים לפונ' ‪ insert‬נעיר שאת הפונ' ‪ build_lust‬אפשר‪ ,‬וגם עדיף‬
‫לכתוב כך שהיא תחזיר מצביע לראש הרשימה המקושרת הנבנית על‪-‬ידה )ולא‬
‫תשתנה את ערכו של פרמטר ההפניה המועבר לה(‪ .‬שהרי ‪ build_list‬מחזירה לנו‬
‫ערך יחיד‪ ,‬וכבר מיצינו שעת פונ' מחזירה ערך יחיד‪ ,‬ראוי דהוא יוחזר באמצעות ערך‬
‫החזרה‪ .‬על כן ראוי לכתוב את הפונ' באופן הבא‪:‬‬
‫{ )(‪struct Node *build_list‬‬
‫; ‪struct Node *head = NULL‬‬
‫; ‪int num‬‬
‫; ‪cin >> num‬‬
‫{ )‪while (num != 0‬‬
‫; )‪insert(num, head‬‬
‫; ‪cin >> num‬‬
‫}‬
‫; ‪return head‬‬
‫}‬
‫זימון הפונ'‪ ,‬בגירסתה נוכחית‪ ,‬מהתכנית הראשית יהיה‪:‬‬
‫; )(‪head = build_list‬‬
‫‪59‬‬
‫נפנה עתה לדיון בפונ' ‪ .insert‬לפונקציה ‪ insert‬שני פרמטרים‪ :‬הראשון הוא‬
‫הנתון שיש להוסיף לרשימה המקושרת הממוינת‪ ,‬השני הוא מצביע לראש הרשימה‪.‬‬
‫נציג את הגדרת הפונקציה ואחר נדון בה בהרחבה‪:‬‬
‫{ )‪void insert(int num, struct Node *&head‬‬
‫; ‪Node *front, *rear, *temp‬‬
‫{)‪if (head == NULL‬‬
‫‪// insert first item‬‬
‫; ‪head = new (std::nothrow) struct Node‬‬
‫)‪if (head == NULL‬‬
‫;)‪terminate("can not alloc mem", 1‬‬
‫; ‪head -> _data = num‬‬
‫; ‪head -> _next = NULL‬‬
‫‪// end of list sign‬‬
‫}‬
‫{ )‪else if (num < head -> _data‬‬
‫‪// insert before‬‬
‫‪temp = new (std::nothrow) Node ; // current first‬‬
‫)‪if (head == NULL‬‬
‫;)‪terminate("can not alloc mem", 1‬‬
‫; ‪temp -> _data = num‬‬
‫; ‪temp -> _next = head‬‬
‫; ‪head = temp‬‬
‫}‬
‫{ ‪else‬‬
‫; ‪rear = head‬‬
‫; ‪front = rear -> _next‬‬
‫{ )‪while (front != NULL && front-> _data < num‬‬
‫; ‪rear = front‬‬
‫; ‪front = front -> _next‬‬
‫}‬
‫; ‪temp = new (std::nothrow) struct Node‬‬
‫)‪if (temp == NULL‬‬
‫;)‪terminate("can not alloc mem", 1‬‬
‫; ‪temp -> _data = num‬‬
‫; ‪temp -> _next = front‬‬
‫; ‪rear -> _next = temp‬‬
‫}‬
‫}‬
‫עת אנו כותבים פונקציה המטפלת ברשימה אנו מבחינים בין מספר מקרים שונים‪,‬‬
‫במילים אחרות בין מספר מצבים שונים‪ .‬חובתנו לדאוג לכך שהפונקציה שנכתוב‬
‫תטפל בכל המצבים הללו‪:‬‬
‫א‪ .‬הרשימה המועברת ריקה‪ַ .‬בפונקציה ‪ insert‬משמעותו של מקרה זה היא כי‬
‫יש להוסיף לרשימה איבר ראשון‪.‬‬
‫ב‪ .‬יש לשנות את ראש הרשימה‪ .‬בפונקציה ‪ insert‬מצב זה חל עת יש להוסיף‬
‫נתון בראש הרשימה‪.‬‬
‫ג‪ .‬יש לערוך שינוי אי שם באמצע רשימה קיימת‪ .‬בפונקציה ‪ insert‬מצב זה חל‬
‫עת יש להוסיף איבר נוסף באמצע הרשימה‪.‬‬
‫ד‪ .‬יש לערוך שינוי באיבר האחרון ברשימה‪ .‬בפונקציה ‪ insert‬מקרה זה מכוסה‬
‫על‪-‬ידי הטיפול המתבצע עבור המקרה מסעיף ג'‪ .‬בפונקציות אחרות יש להכניס‬
‫טיפול מיוחד גם למקרה שכזה‪.‬‬
‫‪60‬‬
‫כדי להשתכנע שהפונקציה שלנו פועלת כהלכה נעקוב אחר ריצת התכנית עבור‬
‫הקלט‪ 17) 0 3879 15 3 13 17 :‬מוזן ראשון ‪ 0‬מוזן אחרון(‪ .‬נסמלץ את הריצה של‬
‫‪ build_list‬בגירסתה הראשונה‪ ,‬הפחות מוצלחת‪ ,‬זו בה היא מקבלת פרמטר‬
‫הפניה‪.‬‬
‫בתחילה‪ ,‬מוגדר בתכנית הראשית המשתנה‪. struct Node *head = NULL; :‬‬
‫נציג את מצב המחסנית‪:‬‬
‫= ‪head‬‬
‫עתה נערכת קריאה לפונקציה ‪ ,build_list‬אשר מקבלת את ‪ head‬כפרמטר‬
‫הפניה‪ .‬מצב המחסנית הוא‪:‬‬
‫= ‪head‬‬
‫= ‪head‬‬
‫= ‪num‬‬
‫אני מזכיר לכם כי פרמטר הפניה אנו מציירים באמצעות חץ עם ראש משולש שחור‬
‫הנשלח לכיוון הארגומנט המתאים; בניגוד לכך מצביע מצוייר כחץ עם ראש בצורת‬
‫‪ ,v‬הנשלח לכיוון האובייקט עליו המצביע מורה )במילים אחרות‪ ,‬האובייקט שאת‬
‫כתובתו המצביע מכיל(‪.‬‬
‫הפונקציה ‪ build_list‬מתחילה להתבצע‪ .‬היא קוראת את הערך ‪ 17‬לתוך‬
‫המשתנה הלוקלי שלה ‪ ,num‬ומזמנת את ‪ . insert‬ל‪ insert -‬יש שני פרמטרים‪:‬‬
‫פרמטר ערך שלם‪ ,‬לתוכו מועתק ערכו של הארגומנט ‪ , num‬ופרמטר הפניה שהינו‬
‫מצביע‪ ,‬ועל‪-‬כן ממנו נשלח חץ לארגומנט ‪ head‬של ‪) build_list‬ומשם למצביע‬
‫‪ head‬של ‪ .(main‬נציג את מצב המחסנית‪:‬‬
‫= ‪head‬‬
‫= ‪head‬‬
‫‪num = 17‬‬
‫= ‪head‬‬
‫‪num = 17‬‬
‫= ‪temp‬‬
‫= ‪rear‬‬
‫= ‪front‬‬
‫‪ insert‬מתחילה להתבצע‪ .‬ראשית אנו בודקים‪ . if (head == NULL) :‬בכך‬
‫אנו מטפלים במקרה הראשון מבין ארבעת המקרים בהם ציינו כי עלינו לטפל‪:‬‬
‫המקרה שבו הרשימה ריקה‪ .‬במצב הנוכחי הרשימה אכן ריקה‪ .‬על כן אנו מבצעים‬
‫את הקוד הבא‪:‬‬
‫; ‪head = new new (std::nothrow) struct Node‬‬
‫; ‪head -> _data = num‬‬
‫; ‪head -> _next = NULL‬‬
‫אנו מקצים קופסה חדשה‪ ,‬ושולחים את המצביע ‪ head‬של התכנית הראשית‬
‫להצביע על קופסה זאת‪) .‬אנו מגיעים למשתנה ‪ head‬של התכנית הראשית תוך‬
‫שימוש בחצים עם הראש השחור המובילים מהפרמטרים המשתנים ‪ head‬של שתי‬
‫‪61‬‬
‫הפונקציות הנוספות אל המשתנה ‪ head‬של התכנית הראשית(‪ .‬למרכיב ה‪_data -‬‬
‫בקופסה שנוצרה אנו מכניסים את הערך ‪ ,17‬ולמרכיב ה‪ _next -‬את הערך ‪.NULL‬‬
‫מצב הזיכרון הוא עתה‪:‬‬
‫= ‪head‬‬
‫= ‪head‬‬
‫‪num = 17‬‬
‫‪data=17‬‬
‫=‪next‬‬
‫= ‪head‬‬
‫‪num = 17‬‬
‫= ‪temp‬‬
‫= ‪rear‬‬
‫= ‪front‬‬
‫עתה ‪ insert‬מסתיימת‪ ,‬ורשומת ההפעלה שלה מוסרת מעל‪-‬גבי המחסנית‪ .‬אנו‬
‫שבים ל‪ ,build_list -‬קוראים את הערך ‪ 13‬לתוך המשתנה ‪ ,num‬ומזמנים שוב‬
‫נראה בשרטוט האחרון פרט לכך‬
‫את ‪ .insert‬מצב הזיכרון יראה כפי שהוא ֵ‬
‫שבמשתנה ‪ num‬של ‪ ,build_list‬ובפרמטר ‪ num‬של ‪ insert‬מצוי עתה הערך ‪,13‬‬
‫)במקום ‪ 17‬כפי שמופיע בציור האחרון(‪.‬‬
‫‪ insert‬מתחילה להתבצע‪ .‬התנאי )‪ (head == NULL‬אינו מתקיים; לעומת זאת‬
‫התנאי )‪ (num < head-> _data‬כן מתקיים‪ .‬שימו לב כי אנו רשאים לבדוק‬
‫תנאי זה רק משום שאנו מובטחים שערכו של ‪ head‬שונה מ‪ ,NULL -‬כלומר ש‪-‬‬
‫‪ head‬מצביע על קופסה כלשהי )הכוללת בין היתר את המרכיב _‪ .(data‬אנו‬
‫מובטחים שערכו של ‪ head‬שונה מ‪ NULL -‬שכן לו ערכו היה ‪ NULL‬התנאי הקודם‬
‫היה מתקיים‪ ,‬ולא היינו מגיעים עד הלום‪) .‬אני מדגיש נקודה זאת שכן היא עקרונית‬
‫וחשובה‪ :‬אם ‪ p‬הוא מצביע אזי מותר להתעניין בערכו של …>‪ p-‬רק אחרי שהבטחנו‬
‫ש‪ p -‬מצביע על קופסה קיימת כלשהי‪ (.‬מכיוון שהתנאי >‪(num < head-‬‬
‫)‪ _data‬מתקיים אנו מבצעים את הגוש‪:‬‬
‫; ‪= new (std::nothrow) struct Node‬‬
‫; ‪-> _data = num‬‬
‫; ‪-> _next = head‬‬
‫; ‪= temp‬‬
‫‪temp‬‬
‫‪temp‬‬
‫‪temp‬‬
‫‪head‬‬
‫שתי הפקודות הראשונות מקצות קופסה חדשה‪ ,‬ומכניסות למרכיב ה‪_data -‬‬
‫בקופסה את הערך ‪ .13‬מצב הזיכרון הוא‪:‬‬
‫= ‪head‬‬
‫= ‪head‬‬
‫‪num = 17‬‬
‫‪data=17‬‬
‫=‪next‬‬
‫‪data=13‬‬
‫=‪next‬‬
‫‪62‬‬
‫= ‪head‬‬
‫‪num = 17‬‬
‫= ‪temp‬‬
‫= ‪rear‬‬
‫= ‪front‬‬
‫הפקודה השלישית מבין ארבע הפקודות שאנו מבצעים ְ‬
‫מפנה את המצביע ‪_next‬‬
‫בקופסה עליה מצביע ‪ temp‬להצביע לאותו מקום כמו ‪ ,head‬כלומר לקופסה של‬
‫‪ .17‬מצב הזיכרון הוא‪:‬‬
‫= ‪head‬‬
‫= ‪head‬‬
‫‪num = 17‬‬
‫‪data=17‬‬
‫=‪next‬‬
‫‪data=13‬‬
‫=‪next‬‬
‫= ‪head‬‬
‫‪num = 17‬‬
‫= ‪temp‬‬
‫= ‪rear‬‬
‫= ‪front‬‬
‫הפקודה הרביעית מפנה את ‪ head‬להצביע לאותו מקום כמו ‪ ,temp‬כלומר לקופסה‬
‫של ‪ .13‬מצב הזיכרון הוא‪:‬‬
‫= ‪head‬‬
‫= ‪head‬‬
‫‪num = 17‬‬
‫‪data=17‬‬
‫=‪next‬‬
‫‪data=13‬‬
‫=‪next‬‬
‫= ‪head‬‬
‫‪num = 17‬‬
‫= ‪temp‬‬
‫= ‪rear‬‬
‫= ‪front‬‬
‫בכך מסתיימת ‪ .insert‬מה היה לנו? ‪ head‬מצביע על הקופסה של ‪ ,13‬המצביע‬
‫בקופסה זאת מצביע על הקופסה של ‪ .17‬וערכו של המצביע בקופסה של ‪ 17‬הוא‬
‫‪ .NULL‬הרשימה שלנו מוחזקת ממוינת כנדרש‪ .‬שימו לב כי הפעם טיפלנו במקרה בו‬
‫יש להוסיף איבר בראש הרשימה‪.‬‬
‫עם סיומה של ‪ insert‬חוזר הביצוע ל‪ .build_list -‬הערך שנקרא עתה הוא ‪.3‬‬
‫לא נעקוב אחר תהליך הוספתו לרשימה שכן מקרה זה דומה לקודמו‪ :‬גם בו אנו‬
‫מוסיפים איבר בראש הרשימה‪ .‬מצב הזיכרון בתום ריצתה של ‪ ,insert‬אחרי‬
‫שהיא הוסיפה את הערך ‪ 3‬הוא‪:‬‬
‫= ‪head‬‬
‫= ‪head‬‬
‫‪num = 3‬‬
‫‪data=17‬‬
‫=‪next‬‬
‫‪data=13‬‬
‫=‪next‬‬
‫‪data=3‬‬
‫=‪next‬‬
‫‪63‬‬
‫= ‪head‬‬
‫‪num = 3‬‬
‫= ‪temp‬‬
‫= ‪rear‬‬
‫= ‪front‬‬
‫מסגרת המחסנית של ‪ insert‬מוסרת מהמחסנית‪ .‬אנו שבים ל‪build_list -‬‬
‫אשר קוראת את הערך ‪ ,15‬ומזמנת שוב את ‪ .insert‬עתה יהיה על ‪ insert‬לטפל‬
‫במקרה השלישי מבין המקרים שמנינו‪ :‬הוספת איבר באמצע רשימה‪ .‬נראה כיצד‬
‫הדבר קורה‪ :‬התנאי‪ (head ==NULL) :‬אינו מתקיים‪ .‬כך גם לגבי התנאי‪:‬‬
‫)‪ ,(num < head-> _data‬ולכן אנו מגיעים ַל‪ else -‬הכולל את הגוש‪:‬‬
‫; ‪rear = head‬‬
‫; ‪front = rear -> next‬‬
‫{ )‪while (front != NULL && front-> _data < num‬‬
‫; ‪rear = front‬‬
‫; ‪front = front -> _next‬‬
‫}‬
‫; ‪temp = new (std::nothrow) struct Node‬‬
‫; ‪temp -> _data = num‬‬
‫; ‪temp -> _next = front‬‬
‫; ‪rear -> _next = temp‬‬
‫שתי הפקודות הראשונות מפנות את ‪ rear‬להצביע כמו ‪) head‬כלומר על הקופסה‬
‫של ‪ ,(3‬ואת ‪ front‬להצביע כמו ‪ ,head-> _next‬כלומר כמו המצביע ‪next‬‬
‫בקופסה עליה מצביע ‪ ,head‬משמע על הקופסה של ‪ .13‬שימו לב כי גם הפעם הפניה‬
‫ל‪ head-> _next -‬הינה בטוחה רק משום שאנו מובטחים שאם הגענו עד הלום‬
‫אזי ערכו של ‪ head‬שונה מ‪.NULL-‬‬
‫נציג את מצב הזיכרון‪:‬‬
‫= ‪head‬‬
‫= ‪head‬‬
‫‪num = 15‬‬
‫‪data=17‬‬
‫=‪next‬‬
‫‪data=13‬‬
‫=‪next‬‬
‫‪data=3‬‬
‫=‪next‬‬
‫= ‪head‬‬
‫‪num = 15‬‬
‫= ‪temp‬‬
‫= ‪rear‬‬
‫= ‪front‬‬
‫עתה אנו נכנסים לביצוע הלולאה‪:‬‬
‫{ )‪while (front != NULL && front-> _data < num‬‬
‫; ‪rear = front‬‬
‫; ‪front = front -> _next‬‬
‫}‬
‫שני התנאים שבכותרת לולאה מתקיימים‪ ,‬ועל כן אנו נכנסים לגוף הלולאה‪.‬‬
‫בלולאה אנו מקדמים את זוג המצביעים ‪ rear, front‬תא אחד קדימה‪.‬‬
‫שימו לב לדרך בה אנו מנסחים את התנאי שבכותרת הלולאה‪ :‬המרכיב השמאלי‬
‫בודק האם ערכו של ‪ front‬שונה מ‪ .NULL -‬במידה ותנאי זה אינו מתקיים כבר‬
‫ברור שלא ניכנס לסיבוב )נוסף( בגוף הלולאה‪ ,‬ועל כן המחשב אינו בודק את התנאי‬
‫הימני )‪ (front-> _data < num‬וטוב שכך‪ ,‬שכן לו הוא היה מנסה לבדוק תנאי‬
‫זה התכנית הייתה עפה )שכן אנו מנסים לפנות לשדה ה‪ _data -‬בקופסה עליה‬
‫‪64‬‬
‫מצביע ‪ front‬שעה שערכו של ‪ front‬הוא ‪ .(NULL‬מנגד‪ ,‬אם התנאי השמאלי‬
‫מתקיים‪ ,‬אזי המחשב יכול לגשת בבטחה לבדיקת התנאי הימני‪ :‬אם התנאי‬
‫השמאלי מתקיים אזי ‪ front‬מצביע על קופסה כשלהי‪ ,‬ולכן ניתן לבדוק את ערכו‬
‫של ‪ .front-> _data‬כמובן שלו היינו כותבים את התנאים בסדר הפוך תכניתנו‬
‫הייתה שגויה‪.‬‬
‫נציג את מצב הזיכרון אחרי ביצוע הסיבוב הראשון בלולאה‪:‬‬
‫= ‪head‬‬
‫= ‪head‬‬
‫‪num = 15‬‬
‫‪data=17‬‬
‫=‪next‬‬
‫‪data=13‬‬
‫=‪next‬‬
‫‪data=3‬‬
‫=‪next‬‬
‫= ‪head‬‬
‫‪num = 15‬‬
‫= ‪temp‬‬
‫= ‪rear‬‬
‫= ‪front‬‬
‫לקראת כניסה לסיבוב השני בלולאה נבדק שוב התנאי‪ ,‬אולם עתה הוא כבר אינו‬
‫מתקיים )לא נכון ש‪ .(front-> _data < num :‬שימו לב כי הלולאה הביאה‬
‫אותנו למצב בו ‪ front‬מצביע לאיבר הראשון ברשימה הכולל נתון שערכו גדול או‬
‫שווה ל‪ ,num -‬בעוד ‪ rear‬מצביע על האיבר האחרון שכולל ערך שעדיין קטן מ‪-‬‬
‫‪ .num‬עתה עלינו לצור קופסה חדשה‪ ,‬ולשרשרה בין הקופסה עליה מצביע ‪rear‬‬
‫לקופסה עליה מצביע ‪ .front‬קטע הקוד שאחראי על כך הוא‪:‬‬
‫; ‪= new (std::nothrow) struct Node‬‬
‫; ‪-> _data = num‬‬
‫; ‪-> _next = front‬‬
‫; ‪-> _next = temp‬‬
‫‪temp‬‬
‫‪temp‬‬
‫‪temp‬‬
‫‪rear‬‬
‫שתי הפקודות הראשונות יוצרות קופסה חדשה‪ ,‬ומפנות את ‪ temp‬להצביע על אותה‬
‫קופסה‪ .‬מצב הזיכרון הוא‪:‬‬
‫‪data=3‬‬
‫= ‪head‬‬
‫=‪next‬‬
‫= ‪head‬‬
‫‪num = 15‬‬
‫‪data=17‬‬
‫=‪next‬‬
‫‪data=13‬‬
‫=‪next‬‬
‫‪data=3‬‬
‫=‪next‬‬
‫‪65‬‬
‫= ‪head‬‬
‫‪num = 15‬‬
‫= ‪temp‬‬
‫= ‪rear‬‬
‫= ‪front‬‬
‫עתה מתבצעת הפקודה‪ . temp-> _next = front; :‬פקודה זאת מפנה את‬
‫המצביע בקופסה עליה מצביע ‪ temp‬להצביע כמו ‪ ,front‬כלומר על הקופסה של‬
‫‪ .17‬ולבסוף מתבצעת הפקודה‪ . rear-> _next = temp; :‬פקודה זאת מסיטה‬
‫את המצביע בקופסה עליה מצביע ‪ ,rear‬כלומר המצביע בקופסה של ‪ ,13‬להצביע‬
‫על אותה קופסה עליה מצביע ‪ ,temp‬כלומר על הקופסה של ‪) 15‬במקום להצביע על‬
‫הקופסה של ‪ ,17‬עליה הוא הצביע עד עתה(‪ .‬מצב הזיכרון הוא‪:‬‬
‫‪data=3‬‬
‫= ‪head‬‬
‫=‪next‬‬
‫= ‪head‬‬
‫‪num = 15‬‬
‫‪data=17‬‬
‫=‪next‬‬
‫‪data=13‬‬
‫=‪next‬‬
‫‪data=3‬‬
‫=‪next‬‬
‫= ‪head‬‬
‫‪num = 15‬‬
‫= ‪temp‬‬
‫= ‪rear‬‬
‫= ‪front‬‬
‫כפי שנקל לראות מהציור‪ ,‬הרשימה שלנו נותרה ממוינת‪ .‬בכך מסתיימת ריצתה‬
‫הנוכחית של ‪ ,insert‬ואנו שבים ל‪ build_list -‬אשר קוראת את ‪) 3879‬הבלתי‬
‫נמנע(‪ .‬הפעם יהיה על ‪ insert‬להוסיף איבר אחרון לרשימה‪ .‬אני מזמין אתכם‬
‫לבצע את סימולציית ההרצה‪ ,‬ולהשתכנע שתכניתנו מטפלת כהלכה גם במקרה‬
‫)רביעי ואחרון מבין המקרים שמנינו שבהם יש לטפל( זה‪.‬‬
‫‪66‬‬
‫‪ 13.4‬מחיקת איבר מרשימה‬
‫מחיקת איבר מרשימה )ממוינת או שאינה( מתבצעת באופן דומה ביותר להוספת‬
‫איבר לרשימה ממוינת‪ .‬אנו מציגים גם פעולה זאת שכן היא אחת הפעולות‬
‫הסטנדרטיות על רשימה )כמו על כל מבנה נתונים אחר(‪ .‬יחד עם זאת‪ ,‬בשל הדמיון‪,‬‬
‫לא נסביר את הפונקציה שנכתוב בהרחבה‪ .‬כדי לשנות מעט מהפונקציה שהוסיפה‬
‫איבר לרשימה ממוינת נקבע כי הפונקציה שנכתוב תחזיר ערך בולאני שיעיד האם‬
‫הנתון הרצוי נמצא ברשימה‪ ,‬והוסר ממנה‪ ,‬או שמא מלכתחילה הוא לא היה מצוי‬
‫ברשימה‪ .‬במקרה בו הנתון נמצא ברשימה אנו מסירים מופע יחיד שלו מהרשימה‪.‬‬
‫נציג את הפונקציה המתאימה‪:‬‬
‫{ )‪bool del(int num, struct Node *&head‬‬
‫‪struct Node *front, *rear,‬‬
‫; ‪*temp‬‬
‫)‪if (head == NULL‬‬
‫; ) ‪return( false‬‬
‫{ )‪if (head-> _data == num‬‬
‫; ‪temp = head‬‬
‫; ‪head = head -> _next‬‬
‫; ‪delete temp‬‬
‫; ) ‪retun( true‬‬
‫}‬
‫; ‪rear = head‬‬
‫; ‪front = rear -> _next‬‬
‫{ )‪while (front != NULL && front -> _data < num‬‬
‫; ‪rear = front‬‬
‫; ‪front = front -> _next‬‬
‫}‬
‫)‪if (front == NULL || front -> _data != num‬‬
‫; ) ‪return( false‬‬
‫; ‪rear -> _next = front -> _next‬‬
‫; ‪delete front‬‬
‫; ) ‪return( true‬‬
‫}‬
‫הסבר‪ :‬לפונקציה אותם פרמטרים כמו במקרה הקודם‪ .‬כמו קודם ראשית אנו‬
‫בודקים האם הרשימה ריקה‪ .‬במידה וזה אכן המצב אין מה למחוק ואנו מחזירים‬
‫את הערך ‪.false‬‬
‫אחר אנו בודקים האם יש למחוק את האיבר שבראש הרשימה‪ .‬אם זה המצב אנו‬
‫שולחים את ‪ temp‬להצביע על האיבר שבראש הרשימה‪ ,‬אחר מקדמים את ‪head‬‬
‫להצביע על האיבר הבא ברשימה‪ ,‬ולבסוף מקפידים לשחרר את הזיכרון המכיל את‬
‫הנתון שיש למחוק‪ .‬שימו לב כי אנו משתמשים בפקודה‪ , delete temp; :‬ואיננו‬
‫כותבים ][ שכן במקרה זה איננו משחררים מערך‪ ,‬אלא רק איבר יחיד‪.‬‬
‫לבסוף‪ ,‬אם הרשימה אינה ריקה‪ ,‬והנתון המבוקש אינו בראשה אנו סורקים את‬
‫הרשימה עם זוג מצביעים )‪ (rear, front‬עד אשר אנו מגיעים‪) :‬א( לאיבר שמכיל‬
‫ערך גדול מהערך המבוקש‪ ,‬או‪) :‬ב( לסיומה של הרשימה )ואז אנו מסיקים שהנתון‬
‫המבוקש אינו מצוי ברשימה(‪ ,‬או‪) :‬ג( עד שאנו מגיעים ַלקופסה שמכילה את האיבר‬
‫‪67‬‬
‫הרצוי‪ .‬אם קופסה כזאת נמצאת אזי ‪ front‬מצביע עליה‪ ,‬ו‪ rear -‬מצביע על‬
‫הקופסה שלפניה‪ .‬עתה עלינו לשנות את השרשור כך שהקופסה עליה מצביע ‪front‬‬
‫תנותק מהשרשרת‪ ,‬כלומר המצביע ‪ _next‬בקופסה עליה מצביע ‪ rear‬יעבור‬
‫להצביע על הקופסה שמייד אחרי הקופסה שעליה מצביע ‪ .front‬אני מזמין אתכם‬
‫לבדוק בדקדקנות שזה אכן מה שנעשה‪.‬‬
‫נקודה נוספת עליה כדאי לתת את הדעת היא הביטוי הבולאני‪:‬‬
‫)‪ . (front == NULL || front -> _data != num‬שימו לב כי זוג‬
‫התנאים קשורים בניהם ב‪ .or -‬לכן אם התנאי השמאלי מתקיים ברור שאין כל‬
‫צורך לבדוק את התנאי הימני‪ ,‬שכן התנאי בכללותו יתקיים‪ .‬המחשב ימנע על‪-‬כן‬
‫מלהעריך את הביטוי הבולאני הימני )עת השמאלי מסופק(‪ ,‬וטוב שכך‪.‬‬
‫הפרמטר ‪ head‬של הפונקציה הוא פרמטר הפניה‪ .‬שימו לב כי הצורך בכך נובע רק‬
‫מהמקרה בו יש למחוק את האיבר שבראש הרשימה‪ ,‬שכן רק אז ערכו של ‪head‬‬
‫משתנה )‪ head‬עובר אז להצביע על האיבר שהיה שני ברשימה‪ ,‬או על ‪ NULL‬אם‬
‫הרשימה כללה איבר יחיד(‪ .‬בכל המקרים האחרים השינוי הוא אי‪-‬שם בהמשך‬
‫הרשימה‪ .‬מכך אתם יכולים להסיק מסקנה נוספת‪ :‬בכוחה של פונקציה המקבלת‬
‫מצביע לראש רשימה מקושרת כפרמטר ערך לשנות את מצב הרשימה‪ ,‬עת היא‬
‫משנה מצביעים שנמצאים בתאי הרשימה‪ .‬הפונקציה לא תוכל לשנות את ערכו של‬
‫‪ .head‬אולם שינוי שהפונקציה תכניס למשל ל‪ , head->next -‬יישאר בתום ביצוע‬
‫הפונקציה‪ .‬נחזור לנקודה זאת בהמשך‪ ,‬ואז נסבירה ביתר פירוט‪ ,‬וגם נלמד איך‬
‫להתגונן מכך‪.‬‬
‫שאלה‪ :‬הפונקציה שכתבנו מותאמת למחיקת איבר מרשימה ממוינת‪ .‬היכן באה‬
‫לידי ביטוי בקוד שכתבנו ממוינותה של הרשימה? במילים אחרות‪ :‬איזה שינוי )קל‬
‫ביותר( יש להכניס בפונקציה שכתבנו על‪-‬מנת שהיא תטפל כהלכה גם ברשימה‬
‫שאינה ממוינת?‬
‫‪ 13.5‬שחרור תאי רשימה‬
‫תכנית אשר הקצתה רשימה מקושרת צריכה גם לשחרר את תאי הרשימה לפני‬
‫סיומה )או עת היא אינה זקוקה להם יותר(‪ .‬אם התכנית תבצע רק פקודה‪delete :‬‬
‫‪ head‬אזי כל שהיא תשחרר יהיה את האיבר הראשון ברשימה‪ .‬יתר תאי הרשימה‬
‫יישארו ‪ zombies‬שמחד גיסא לא שוחררו‪ ,‬ומאידך גיסא גם אין דרך לפנות אליהם‪.‬‬
‫על כן הדרך לשחרר רשימה היא בלולאה שתשחרר את התאים בזה אחר זה‪.‬‬
‫נציג את קוד הפונקציה הדרושה‪:‬‬
‫{ )‪void free_list(struct Node *head‬‬
‫; ‪struct Node *temp = head‬‬
‫{ )‪while (head != NULL‬‬
‫; ‪temp = head‬‬
‫; ‪head = head -> _next‬‬
‫;‪delete temp‬‬
‫}‬
‫}‬
‫מפנים את ‪ temp‬להצביע‬
‫הפונקציה מתנהלת כלולאה‪ .‬בכל סיבוב בלולאה אנו ְ‬
‫לאיבר שעליו מצביע ‪ ,head‬מקדמים את ‪ ,head‬ואז משחררים את האיבר עליו‬
‫מצביע ‪ .temp‬שימו לב כי בתום התהליך הרשימה כולה מוחזרה‪ ,‬וערכו של ‪head‬‬
‫‪68‬‬
‫הוא ‪ .NULL‬כאן עלינו לתת את הדעת לנקודה מעט עדינה‪ :‬הפרמטר של הפונקציה‬
‫הוא פרמטר ערך‪ ,‬ועל כן שינוים שהפונקציה הכניסה לפרמטר שלה לא יוותרו בתום‬
‫ביצוע הפונקציה בארגומנט המתאים‪ .‬בפרט ערכו של הארגומנט לא יהיה ‪,NULL‬‬
‫אלא הוא ימשיך להצביע לאותו איבר עליו הוא הצביע לפני הקריאה לפונקציה; רק‬
‫שעתה הזיכרון בו שכן האיבר אינו שייך עוד לתכניתנו‪ ,‬שכן הוא מוחזר על‪-‬ידי‬
‫הפונקציה‪ .‬כמובן שניתן להגדיר את הפרמטר גם כפרמטר הפניה‪ ,‬ואז בתום ביצוע‬
‫הפונקציה יהיה ערכו של הארגומנט המתאים ‪.NULL‬‬
‫‪ 13.6‬היפוך רשימה‬
‫עתה נרצה לכתוב פונקציה המקבלת רשימה מקושרת‪ ,‬והופכת את כיוון ההצבעה‬
‫של המצביעים ברשימה‪ .‬לדוגמה‪ ,‬אם לפונקציה יועבר מצביע לראש הרשימה‪:‬‬
‫‪data=3‬‬
‫=‪next‬‬
‫‪data=13‬‬
‫=‪next‬‬
‫‪data=3879‬‬
‫=‪next‬‬
‫‪data=17‬‬
‫=‪next‬‬
‫אזי הפונקציה תחזיר מצביע לראש הרשימה‪:‬‬
‫‪data=17‬‬
‫=‪next‬‬
‫‪data=3879‬‬
‫=‪next‬‬
‫‪data=13‬‬
‫=‪next‬‬
‫‪data=3‬‬
‫=‪next‬‬
‫הקריאה לפונקציה תהיה‪. rev_head = reverse(head); :‬‬
‫נרצה שהפונקציה שנכתוב לא תקצה תאים חדשים )על‪-‬גבי הערמה(‪ ,‬אלא תשנה את‬
‫כיוון הצבעתם של המצביעים הקיימים באופן שיושג הרצוי‪ .‬לדוגמה‪ :‬את המצביע‬
‫בקופסה של ‪ 3‬היא תפנה להצביע על הקופסה של ‪) 13‬במקום שהמצביע יכיל את‬
‫הערך ‪ ,NULL‬כפי שמצוי בו בעת הקריאה לפונקציה(‪.‬‬
‫הרעיון הבסיסי הוא שנחזיק שלושה מצביעים ‪ front, mid, rear‬שיתקדמו‬
‫על‪-‬פני הרשימה‪ .‬בכל שלב ‪ mid‬יצביע על איבר כלשהו‪ front ,‬יצביע על האיבר‬
‫שלפני האיבר עליו מצביע ‪ ,mid‬ו‪ rear -‬יצביע על האיבר שמאחורי האיבר עליו‬
‫מצביע ‪ .mid‬בכל סיבוב בלולאה נפנה את ‪ mid-> _next‬להצביע על אותו איבר‬
‫כמו ‪ ,rear‬ואחר נסיט את שלושת המצביעים תא אחד ימינה יותר‪.‬‬
‫‪69‬‬
‫הגדרת הפונקציה‪:‬‬
‫{ )‪struct Node *reverse( struct Node *head‬‬
‫; ‪struct Node *rear, *mid, *front‬‬
‫)‪if (head == NULL || head-> _next == NULL‬‬
‫; ) ‪return( head‬‬
‫‪// nothing to do‬‬
‫; ‪rear = head‬‬
‫‪mid = head-> _next ; // safe assignment as head!=NULL.‬‬
‫‪front = mid-> _next ; // safe, as head->next != NULL.‬‬
‫{ )‪while (front != NULL‬‬
‫; ‪mid -> _next = rear‬‬
‫; ‪rear = mid‬‬
‫; ‪mid = front‬‬
‫; ‪front = front-> _next‬‬
‫}‬
‫; ‪mid -> _next = rear‬‬
‫; ‪head -> _next = NULL‬‬
‫; ) ‪return( mid‬‬
‫}‬
‫נציג סימולציית הרצה אשר תסייע לנו להשתכנע בנכונות פעולה של הפונקציה‪.‬‬
‫נתחיל בכך שהמשתנה ‪ head‬של התכנית הראשית מצביע על הרשימה הבאה‪:‬‬
‫=‪head‬‬
‫‪data=3‬‬
‫=‪next‬‬
‫‪data=13‬‬
‫=‪next‬‬
‫‪data=3879‬‬
‫=‪next‬‬
‫‪data=17‬‬
‫=‪next‬‬
‫עתה נערכת קריאה לפונקציה‪ ,‬ו‪ head -‬מועבר כפרמטר ערך‪ .‬מצב הזיכרון הינו‪:‬‬
‫=‪head‬‬
‫)‪(of main‬‬
‫‪data=3‬‬
‫=‪next‬‬
‫‪data=13‬‬
‫=‪next‬‬
‫‪data=3879‬‬
‫=‪next‬‬
‫‪data=17‬‬
‫=‪next‬‬
‫=‪head‬‬
‫=‪rear‬‬
‫=‪mid‬‬
‫=‪front‬‬
‫התנאי‪:‬‬
‫להתבצע‪:‬‬
‫מתחילה‬
‫הפונקציה‬
‫עתה‬
‫)‪ (head==NULL || head-> _next ==NULL‬אינו מתקיים‪) .‬אני מסב‬
‫את תשומת לבכם לסדר הופעתם של שני המרכיבים בתנאי‪ .‬יש חשיבות לסדר(‪.‬‬
‫תנאי זה מתייחס למקרה בו הרשימה ריקה‪ ,‬או כוללת איבר יחיד‪ .‬בשני המקרים‬
‫הללו אין צורך לעשות דבר‪ :‬היפוך הרשימה שווה לרשימה הנתונה; ניתן לסיים‬
‫מיידית‪ ,‬ולהחזיר את ‪.head‬‬
‫‪70‬‬
‫ מצב‬.rear, mid, front ‫שלוש הפקודות הבאות מאתחלות את ערכם של‬
:‫הזיכרון בתום ביצוע שלוש ההשמות הוא‬
head=
(of main)
data=17
next=
data=3879
next=
data=13
next=
data=3
next=
head=
rear=
mid=
front=
‫ בסיבוב הראשון‬.(front !=NULL) ‫עתה אנו נכנסים ללולאה המתבצעת כל עוד‬
‫ כלומר את המצביע בקופסה של‬,mid-> _next ‫ מפנים את‬,‫ ראשית‬,‫בלולאה אנו‬
‫ מצב הזיכרון‬.17 ‫ כלומר על הקופסה של‬,rear ‫ להצביע על אותו איבר כמו‬,3879
:‫הוא‬
head=
(of main)
data=17
next=
data=3879
next=
data=13
next=
data=3
next=
head=
rear=
mid=
front=
:‫עתה אנו מקדמים את שלושת המצביעים‬
rear = mid ;
mid = front ;
front = front-> _next ;
:‫מצב הזיכרון הוא‬
head=
(of main)
data=17
next=
data=3879
next=
head=
rear=
mid=
front=
71
data=13
next=
data=3
next=
‫בזאת הסתיים הסיבוב הראשון בלולאה‪ .‬התנאי שבכותרת הלולאה עדיין מתקיים‪,‬‬
‫ולכן אנו נכנסים לסיבוב נוסף‪ .‬בפקודה הראשונה בגוף הלולאה אנו מפנים את‬
‫‪ ,mid->next‬כלומר את המצביע בקופסה של ‪ ,13‬להצביע על הקופסה עליה מצביע‬
‫‪ ,rear‬כלומר על הקופסה של ‪ .3879‬מצב הזיכרון הוא‪:‬‬
‫=‪head‬‬
‫)‪(of main‬‬
‫‪data=3‬‬
‫=‪next‬‬
‫‪data=13‬‬
‫=‪next‬‬
‫‪data=3879‬‬
‫=‪next‬‬
‫‪data=17‬‬
‫=‪next‬‬
‫=‪head‬‬
‫=‪rear‬‬
‫=‪mid‬‬
‫=‪front‬‬
‫שלוש הפקודות הבאות בגוף הלולאה מקדמות את שלושת המצביעים‪ .‬מצב הזיכרון‬
‫הוא‪:‬‬
‫=‪head‬‬
‫)‪(of main‬‬
‫‪data=3‬‬
‫=‪next‬‬
‫‪data=13‬‬
‫=‪next‬‬
‫‪data=3879‬‬
‫=‪next‬‬
‫‪data=17‬‬
‫=‪next‬‬
‫=‪head‬‬
‫=‪rear‬‬
‫=‪mid‬‬
‫=‪front‬‬
‫שימו לב כי ‪ front‬קיבל את ערכו של ‪ ,front-> _next‬ולכן את הערך ‪.NULL‬‬
‫‪72‬‬
‫איננו נכנסים לסיבוב נוסף בלולאה )התנאי ‪ front!= NULL‬כבר אינו מתקיים(‪.‬‬
‫ואנו פונים לביצוע שלוש הפקודות המסיימות את הפונקציה‪ .‬השתיים הראשונות‬
‫ביניהן הן‪:‬‬
‫; ‪mid -> _next = rear‬‬
‫; ‪head -> _next = NULL‬‬
‫הראשונה שולחת את המצביע בקופסה עליה מצביע ‪ ,mid‬כלומר את המצביע‬
‫בקופסה של ‪ ,3‬להצביע על הקופסה עליה מצביע ‪ ,rear‬כלומר על הקופסה של ‪.13‬‬
‫השניה מכניסה ל‪ ,head->next -‬כלומר למצביע בקופסה של ‪ ,17‬את הערך ‪.NULL‬‬
‫מצב הזיכרון הוא‪:‬‬
‫=‪head‬‬
‫)‪(of main‬‬
‫‪data=3‬‬
‫=‪next‬‬
‫‪data=13‬‬
‫=‪next‬‬
‫‪data=3879‬‬
‫=‪next‬‬
‫‪data=17‬‬
‫=‪next‬‬
‫=‪head‬‬
‫=‪rear‬‬
‫=‪mid‬‬
‫=‪front‬‬
‫אם תבחנו את הציור‪ ,‬המעט מסורבל‪ ,‬תגלו שההצבעה התהפכה כנדרש‪ :‬המצביע‬
‫בקופסה של ‪ 3‬מצביע על הקופסה של ‪ ,13‬המצביע בקופסה של ‪ 13‬מצביע על‬
‫הקופסה של ‪ ,3879‬המצביע בקופסה של ‪ 3879‬מצביע על הקופסה של ‪ ,17‬והמצביע‬
‫בקופסה של ‪ 17‬ערכו ‪ .NULL‬כמו כן ‪ mid‬מצביע על הקופסה של ‪ 3‬שנמצאת בראש‬
‫הרשימה המהופכת‪ .‬על‪-‬כן עת אנו מחזירים את ערכו של ‪ ,mid‬אנו מחזירים מצביע‬
‫לראש הרשימה המהופכת‪.‬‬
‫‪73‬‬
‫‪ 13.7‬בניית רשימה ממוינת יחידאית מרשימה לא ממוינת‬
‫נניח כי בתכניתנו בנינו רשימה מקושרת לא ממוינת‪ ,‬כדוגמת הבאה‪:‬‬
‫‪data=2‬‬
‫=‪next‬‬
‫‪data=15‬‬
‫=‪next‬‬
‫‪data=17‬‬
‫=‪next‬‬
‫‪data=17‬‬
‫=‪next‬‬
‫‪data=15‬‬
‫=‪next‬‬
‫עתה ברצוננו לבנות על‪-‬פי הרשימה הנתונה רשימה מקושרת ממוינת בה כל נתון‬
‫יופיע פעם יחידה‪ ,‬ולצדו יופיע מונה אשר יציין כמה פעמים הנתון הופיע ברשימה‬
‫המקורית )הלא ממוינת(‪.‬‬
‫ראשית‪ ,‬נגדיר את טיפוס המבנה הדרוש‪:‬‬
‫{ ‪struct Data_counter‬‬
‫; ‪int _data, _counter‬‬
‫; ‪Data_counter * _next_dc‬‬
‫; }‬
‫למען שוחרי איכות הסביבה נעיר כי ניתן להגדיר את המבנה בצורה נקיה יותר‪:‬‬
‫תחילה נגדיר מבנה שיכיל את הנתונים הדרושים‪:‬‬
‫{ ‪struct D_c‬‬
‫; ‪int _data, _counter‬‬
‫; }‬
‫ועתה‪ ,‬נגדיר רשימה מקושרת שכל איבר בה מכיל מבנה כנ"ל‪:‬‬
‫{ ‪struct List_of_data_counter‬‬
‫; ‪struct _d_c _info‬‬
‫; ‪struct List_of_data_counter * _next‬‬
‫; }‬
‫הגדרה זאת נקיה יותר שכן היא מפרידה בין הנתונים אותם ברצוננו לשמור‬
‫)ושמיוצגים באמצעות המבנה ‪ ,(D_c‬לבין הרשימה ה מקושרת שאנו יוצרים ואשר‬
‫אין קשר הכרחי בינה לבין הנתונים המיוצגים; היא רק דרך לארגן את הנתונים‬
‫במקום ברשימה מקושרת‬
‫המיוצגים‪ .‬לפחות לכאורה יכולנו לארגן את הנתונים ְ‬
‫במערך‪ . struct D _c arr[N]; :‬עד כאן הערה לחובבי ח"נ‪ .‬אנו נסתפק בצורה‬
‫הפחות נקיה אך היותר קלה לעיקול‪ ,‬ונשמש במבנה ‪.struct Data_counter‬‬
‫ברצוננו לכתוב את הפונקציה‪:‬‬
‫)‪. struct Data_counter *rem_dup_n_sort(struct Node *head‬‬
‫הפונקציה תקבל מצביע לרשימה מקושרת לא ממוינת הכוללת חזרות‪ ,‬כפי‬
‫שתיארנו בתחילת הסעיף‪ ,‬ותחזיר מצביע לרשימה מקושרת ממוינת‪ ,‬שאינה כוללת‬
‫חזרות‪ ,‬אך שמכילה מונה כפי שתיארנו‪ .‬הקריאה לפונקציה תהיה‪:‬‬
‫;)‪head_sort = rem_dup_n_sort(head‬‬
‫עבור המשתנים‪:‬‬
‫; ‪struct Node *head‬‬
‫; ‪struct Data_counter *head_sort = NULL‬‬
‫תחת ההנחה שיצרנו רשימה מקושרת לא ממוינת עליה מצביע ‪.head‬‬
‫הפונקציה שנכתוב תשתמש בשתי פונקציות עזר‪:‬‬
‫‪74‬‬
‫א‪ .‬הפונקציה ‪ search‬מקבלת שני פרמטרים‪ :‬ערך שלם ‪ ,num‬ומצביע‬
‫המורה על ראשה של הרשימה המקושרת הממוינת‬
‫ֵ‬
‫‪sort_n_unique_p‬‬
‫נטולת כפילויות שנבנתה עד כה‪ .‬הפונקציה מחפשת את הערך ‪ num‬ברשימה‬
‫עליה מצביע ‪ .sort_n_unique_p‬הפונקציה מחזירה אחד מהערכים הבאים‪:‬‬
‫‪ .1‬במידה והרשימה ריקה יוחזר הערך ‪.NULL‬‬
‫‪ .2‬במידה והנתון לא מצוי ברשימה‪ ,‬ויש להוסיפו בראש הרשימה יוחזר הערך‬
‫‪.NULL‬‬
‫‪ .3‬במידה והנתון מצוי ברשימה יוחזר מצביע לאיבר הכולל את הנתון‪.‬‬
‫‪ .4‬במידה והרשימה אינה ריקה‪ ,‬הנתון אינו מצוי ברשימה‪ ,‬ואין להוסיפו‬
‫בראש הרשימה‪ ,‬יוחזר מצביע לאיבר שאחריו יש להוסיף את הנתון החדש‪.‬‬
‫ב‪ .‬הפונקציה ‪ insert‬מקבלת שלושה פרמטרים‪ :‬ערך שלם ‪ ,num‬מצביע‬
‫‪ sort_n_unique_p‬המורה על ראשה של הרשימה ה מקושרת הממוינת‬
‫נטולת כפילויות שנבנתה עד כה )או שערכו הוא ‪ ,(NULL‬מצביע ‪ where‬המורה‬
‫היכן יש לשלב תא חדש שיכיל את ‪ num‬ברשימה עליה מצביע‬
‫‪ .sort_n_unique_p‬הפונקציה פועלת באופן הבא‪:‬‬
‫במידה וערכו של המצביע ‪ sort_n_unique_p‬הוא ‪ NULL‬הפונקציה‬
‫‪1‬‬
‫יוצרת איבר חדש‪ .‬מפנה את המצביע להורות על התא‪ ,‬ומשימה באיבר‬
‫את ‪ ,num‬תוך ציון שזה מופעו הראשון ברשימה הממוינת שתבנה‪.‬‬
‫במידה וערכו של ‪ where‬הוא ‪ NULL‬הפונקציה מוסיפה את ‪ num‬בראש‬
‫‪2‬‬
‫הרשימה הממוינת‪.‬‬
‫בכל מקרה אחר‪ ,‬הפונקציה יוצרת איבר חדש‪ ,‬ומשלבת אותו ברשימה‬
‫‪3‬‬
‫במקום שארי האיבר עליו מצביע ‪.where‬‬
‫הגדרת הפונקציה‪:‬‬
‫{ )‪struct Data_counter *rem_dup_n_sort(struct Node *head‬‬
‫; ‪struct Data_counter *new_list = NULL, *where‬‬
‫{ )‪while (head != NULL‬‬
‫; )‪where = search(head-> _data, new_list‬‬
‫)‪if (where != NULL && where-> _data == head-> _data‬‬
‫; ‪(where -> counter)++‬‬
‫; )‪else insert(head-> _data, new_list, where‬‬
‫; ‪head = head-> _next‬‬
‫}‬
‫; ) ‪return( new_list‬‬
‫}‬
‫הסבר הפונקציה‪ :‬הפונקציה כוללת לולאה המתנהלת כל עוד לא סיימנו לעבור על‬
‫כל אברי הרשימה על‪-‬פיה יש לבנות את הרשיתה הממוינת נטולת הכפילויות‪ .‬בכל‬
‫סיבוב בלולאה אנו ראשית מחפשים את הערך המצוי בתא הנוכחי ברשימה הלא‬
‫ממוינת בקרב אברי הרשימה הממוינת‪ .‬אם הערך נמצא אזי אנו מגדילים את‬
‫המונה באחד‪ ,‬ואחרת אנו מוסיפים איבר חדש לרשימה הנבנית‪.‬‬
‫לפני שנפנה להגדרת פונקציות העזר ברצוני להדגיש שלכאורה אם הנתון >‪head-‬‬
‫‪ _data‬מצוי ברשימה עליה מורה ‪ ,new_list‬אזי ‪ search‬כבר יכולה‪ ,‬עת היא‬
‫מאתרת את האיבר בו מצוי הנתון‪ ,‬להגדיל את ערכו של המונה באחד‪ .‬לכאורה‬
‫‪ search‬אכן יכולה לעשות זאת‪ ,‬למעשה מטעמי מבניות לא ראוי שהיא תעשה‬
‫‪75‬‬
‫זאת‪ ,‬שכן ‪ search‬כשמה כן היא צריכה לנהוג‪ :‬לחפש בלבד )ולא לשנות(‪ .‬לכן את‬
‫מלאכת עדכון המונה הטלנו על הפונקציה הראשית‪. rem_dup_n_sort ,‬‬
‫מנגד‪ ,‬שימו לב שכתבנו את ‪ search‬בצורה יחסית חכמה‪ :‬היא לא מחזירה רק‬
‫איתות בולאני האם הנתון הרצוי נמצא או לא ברשימה שהועברה לה‪ .‬היא גם‬
‫מחזירה מצביע שיסייע לנו הן במקרה בו הנתון נמצא )לעדכן את המונה(‪ ,‬והן‬
‫במקרה בו הוא לא נמצא )ויש להוסיפו לרשימה(‪ .‬באופן זה בין אם יש להוסיף את‬
‫הנתון לרשימה הנבנית‪ ,‬ובין אם יש רק לעדכן את המונה‪ ,‬כבר אין צורך לסרוק את‬
‫הרשימה פעם נוספת‪' .‬תבונתה' של ‪ search‬אינה משנה את מורכבות זמן הריצה‬
‫של הפונקציה ‪ ,rem_dup_n_sort‬אך היא הופכת אותה ליעילה יותר‪) .‬אגב‪ ,‬מהי‬
‫מורכבות זמן הריצה של הפונקציה?(‪.‬‬
‫עתה נציג את הפונקציה ‪:search‬‬
‫‪struct Data_counter *search(int num,‬‬
‫* ‪struct Data_counter‬‬
‫{ )‪sort_n_unique_p‬‬
‫; ; ‪struct Data_counter *rear, *front‬‬
‫|| ‪if (sort_n_unique_p == NULL‬‬
‫)‪num < sort_n_unique_p-> _data‬‬
‫; )‪return(NULL‬‬
‫; ‪front = sort_n_unique_p->next_dc‬‬
‫; ‪rear = sort_n_unique_p‬‬
‫{ )‪while (front != NULL && front-> _data < num‬‬
‫; ‪rear = front‬‬
‫; ‪front = front ->next_dc‬‬
‫}‬
‫|| ‪if (front == NULL‬‬
‫‪// num shld be added as last‬‬
‫‪front -> data > num) // should be added in middle‬‬
‫; )‪return(rear‬‬
‫; )‪return(front‬‬
‫‪// num exists in list‬‬
‫}‬
‫הסבר הפונקציה‪ :‬אם ערכו של המצביע לראש הרשימה הנבנית הוא ‪ ,NULL‬או‬
‫שהאיבר שיש להוסיף קטן מהערך המצוי בראש הרשימה אזי ‪ search‬מחזירה את‬
‫הערך ‪ .NULL‬אחרת אנו נכנסים ללולאה אשר מתקדמת עד אשר ‪ front‬מצביע על‬
‫איבר הכולל את הנתון הרצוי‪ ,‬או עד ש‪ front -‬מגיע לאיבר הכולל ערך גדול מהערך‬
‫הרצוי )ואז ניתן להסיק שהערך הרצוי אינו ברשימה(‪ ,‬או עד אשר ערכו של ‪front‬‬
‫הוא ‪) NULL‬ואז ניתן להסיק כי יש להוסיף את האיבר בסוף הרשימה הממוינת(‪.‬‬
‫אחרי היציאה מהלולאה אנו מחזירים את הערך המתאים‪ ,‬כפי שמוסבר בתיעוד‬
‫בגוף הקוד‪.‬‬
‫עתה נותר לנו להציג את ‪ .insert‬שימו לב כי בזכות 'תבונתה' של ‪,search‬‬
‫הפונקציה ‪ insert‬אינה כוללת כל פקודות לולאה‪.‬‬
‫‪void insert(int num,‬‬
‫‪struct Data_counter *&sort_n_unique_p,‬‬
‫{ )‪struct Data_counter *where‬‬
‫; ‪struct Data_counter *temp‬‬
‫‪// list is empty‬‬
‫{ )‪if (sort_n_unique_p == NULL‬‬
‫‪76‬‬
‫; ‪= new struct Data_counter‬‬
‫; ‪-> data = num‬‬
‫; ‪-> counter = 1‬‬
‫; ‪-> _next_dc = NULL‬‬
‫‪sort_n_unique_p‬‬
‫‪sort_n_unique_p‬‬
‫‪sort_n_unique_p‬‬
‫‪sort_n_unique_p‬‬
‫}‬
‫‪// insert as first in non empty list‬‬
‫{ )‪else if (where == NULL‬‬
‫; ‪temp = new struct Data_counter‬‬
‫; ‪temp -> data = num‬‬
‫; ‪temp -> counter = 1‬‬
‫; ‪temp -> _next_dc = sort_n_unique_p‬‬
‫; ‪sort_n_unique_p = temp‬‬
‫}‬
‫{ ‪else‬‬
‫‪// insert in the middle, or as last‬‬
‫; ‪temp = new struct Data_counter‬‬
‫; ‪temp -> data = num‬‬
‫; ‪temp -> counter = 1‬‬
‫; ‪temp -> _next_dc = where -> _next_dc‬‬
‫; ‪where -> _next_dc = temp‬‬
‫}‬
‫}‬
‫אני מזמין אתכם לערוך בעצמכם מעקב אחרי ‪ insert‬ולבדוק את נכונותה‪.‬‬
‫‪ 13.8‬מיון מיזוג של רשימות משורשרות‬
‫בעבר ראינו את אלגוריתם המיון )הלא יעיל( מיון בועות‪ ,‬ואת אלגוריתם מיון היעיל‬
‫מיון מהיר )אשר‪ ,‬כזכור לנו)?( רץ בד"כ בזמן )‪ n*log(n‬עבור ‪ n‬שמציין את כמות‬
‫הנתונים שיש למיין‪ ,‬אך עלול להתדרדר לכדי זמן ריצה של ‪ n2‬במקרים קיצוניים(‪.‬‬
‫עתה נרצה להכיר אלגוריתם מיון יעיל נוסף‪ :‬מיון מיזוג‪.‬‬
‫מיון מיזוג ניתן לבצע על מערך‪ ,‬או על רשימה מקושרת‪ .‬מיון מיזוג על מערך מחייב‬
‫שימוש במערך עזר נוסף שגודלו כגודלו של המערך המקורי‪ ,‬כלומר מיון מיזוג על‬
‫מערכים מחייב הכפלת גודל הזיכרון הנצרך ע"י התכנית‪ .‬זה הופך אותו לאלגוריתם‬
‫לא מעשי עבור מערכים שאינם קטנים‪ .‬בניגוד לכך‪ ,‬מיון מיזוג של רשימה לא מחייב‬
‫תוספת של זיכרון )ובמובן זה הוא דומה למיון מהיר שלא מחייב תוספת של זיכרון(‪,‬‬
‫וזה כבר משמח‪ .‬בנוסף לכך‪ ,‬מיון מיזוג )הן של מערך‪ ,‬והן של רשימה( רץ תמיד בזמן‬
‫של )‪ ,n*log(n‬ובמובן זה יש לו יתרון על‪-‬פני מיון מהיר )אשר‪ ,‬כאמור‪ ,‬במצבים‬
‫קיצוניים‪ ,‬עלול להתדרדר לכדי זמן ריצה ריבועי(‪.‬‬
‫‪ 13.8.1‬הצגת אלגוריתם המיון‪ :‬מיון מיזוג‬
‫מיזוג )‪ (merging‬הוא תהליך בו יוצרים סדרה ממוינת אחת משתיים )או יותר(‬
‫סדרות ממוינות‪ .‬הסדרות עשויות להיות מאוחסנות במערך‪ ,‬ברשימה מקושרת‪ ,‬או‬
‫אחר שמאפשר שמירה של סדרת נתונים‪ .‬לדוגמה אם נתונים‬
‫בכל מבנה נתונים ֵ‬
‫המערכים הממוינים )מקטן לגדול(‪:‬‬
‫‪3879‬‬
‫‪17‬‬
‫‪5‬‬
‫‪13‬‬
‫‪77‬‬
‫‪7‬‬
‫‪5‬‬
‫‪5-‬‬
‫‪5‬‬
‫אזי תהליך מיזוג ייצר מהם מערך בן שמונה ערכים‪:‬‬
‫‪5‬‬
‫‪5‬‬
‫‪7‬‬
‫‪13‬‬
‫‪17‬‬
‫‪3879‬‬
‫‪5‬‬
‫‪5-‬‬
‫תהליך המיזוג פועל האופן הבא‪:‬‬
‫א‪ .‬התחל באיבר הראשון בכל אחת משתי הסדרות שיש למזג‪ .‬קבע אותו כאיבר‬
‫הנוכחי‪.‬‬
‫ב‪ .‬כל עוד הסדרה הראשונה לא הסתיימה וגם הסדרה שניה לא הסתיימה בצע‪:‬‬
‫‪ .1‬אם האיבר הנוכחי בסדרה הראשונה קטן או שווה מהאיבר הנוכחי בסדרה‬
‫השניה‪ ,‬אזי‪) :‬א( העבר את האיבר הנוכחי בסדרה הראשונה לסדרה הנבנית‪,‬‬
‫ו‪) -‬ב( התקדם על הסדרה הראשונה לאיבר הבא‪.‬‬
‫‪ .2‬אחרת )האיבר הנוכחי בסדרה השניה קטן מהאיבר הנוכחי בסדרה‬
‫הראשונה(‪ ,‬בצע‪) :‬א( העבר את האיבר הנוכחי בסדרה השניה לסדרה‬
‫הנבנית‪ ,‬ו‪) -‬ב( התקדם על הסדרה השניה לאיבר הבא‪.‬‬
‫עם היציאה מהלולאה שתוארה בסעיף ב' אחת הסדרות הסתיימה והשניה עדיין לא‪,‬‬
‫לכן יש להעתיק בזה אחר זה את הנתונים מהסדרה שלא הסתימה לסדרה הנבנית‪.‬‬
‫לכן בצע‪:‬‬
‫ג‪ .‬כל עוד לא הסתימה הסדרה הראשונה‪ ,‬העתק בזה אחר זה את אבריה על‬
‫הסדרה הנבנית‪.‬‬
‫ד‪ .‬כל עוד לא הסתימה הסדרה השניה‪ ,‬העתק בזה אחר זה את אבריה על הסדרה‬
‫הנבנית‪.‬‬
‫בפועל רק אחת משתי הלולאות תתבצע )בלולאה שלא תבוצע התנאי לא יתקיים‬
‫מייד עם ההגעה ללולאה(‪.‬‬
‫בדוגמה שלנו‪ :‬נתחיל מהתא הראשון בשני המערכים‪.‬‬
‫א‪ .‬הערך ‪ -5‬במערך השני קטן מהערך ‪ 5‬במערך הראשון‪ ,‬לכן נעתיק את ‪ –5‬למערך‬
‫שאנו יוצרים‪ ,‬ונתקדם לתא השני במערך השני‪.‬‬
‫ב‪ .‬עתה הערך ‪ 5‬במערך הראשון קטן או שווה מהערך ‪ 5‬במערך השני‪ ,‬לכן נעתיק‬
‫את הערך ‪ 5‬מהמערך הראשון למערך הנבנה‪ ,‬ונתקדם לתא השני במערך‬
‫הראשון‪.‬‬
‫ג‪ .‬הערך ‪ 5‬בתא השני של המערך השני קטן מהערך ‪ 13‬בתא השני של המערך‬
‫הראשון‪ ,‬לכן נעתיק את ‪ 5‬למערך שאנו יוצרים‪ ,‬ונתקדם לתא השלישי במערך‬
‫השני‪.‬‬
‫ד‪ .‬הערך ‪ 5‬בתא השלישי של המערך השני קטן מהערך ‪ 13‬בתא השני של המערך‬
‫הראשון‪ ,‬לכן נעתיק את ‪ 5‬למערך שאנו יוצרים‪ ,‬ונתקדם לתא הרביעי במערך‬
‫השני‪.‬‬
‫ה‪ .‬הערך ‪ 7‬בתא הרביעי של המערך השני קטן מהערך ‪ 13‬בתא השני של המערך‬
‫הראשון‪ ,‬לכן נעתיק את ‪ 7‬למערך שאנו יוצרים‪ ,‬ונתקדם לתא החמישי במערך‬
‫השני‪.‬‬
‫בזאת גמרנו להעביר את נתוני המערך השני‪ .‬לכן אנו יוצאים מהלולאה הראשונה‬
‫)המתנהלת כל עוד שני המערכים גם יחד לא מוצו(‪ .‬כל שנותר לנו עתה לעשות הוא‬
‫להעתיק את הנתונים מהמערך הראשון שטרם הועברו למערך הנבנה‪.‬‬
‫ו‪ .‬עתה נעתיק בזה אחר זה את הערכים ‪ 3879 ,17 ,13‬מהמערך הראשון למערך‬
‫הנבנה‪.‬‬
‫‪78‬‬
‫כפי שראינו‪ ,‬תהליך המיזוג סורק כל אחד משני המערכים פעם יחידה‪ .‬ועל כן כדי‬
‫למזג שני מערכים ממוינים בגודל ‪ N‬לכדי מערך ממוין בגודל ‪ 2*N‬עלינו להשקיע‬
‫עבודה בסדר גודל של ‪ N‬פעולות‪.‬‬
‫אלגוריתם המיון מיון מיזוג מקבל סדרה של נתונים שיש למיין‪ ,‬ומחזיר את הסדרה‬
‫ממוינת‪ .‬האלגוריתם פועל באופן הבא‪:‬‬
‫א‪ .‬אם הסדרה שיש למיין ריקה‪ ,‬או הסדרה שיש לציין כוללת נתון יחיד‪ ,‬אזי החזר‬
‫את הסדרה עצמה‪) .‬אין צורך למיינה(‪.‬‬
‫ב‪ .‬אחרת )הסדרה שיש למיין כוללת לפחות שני נתונים(‪:‬‬
‫‪ .1‬פצל את הסדרה לשתי סדרות בעלות אורך שווה ככל שניתן )אם אורכה של‬
‫הסדרה שיש למיין זוגי אזי היא תפוצל לשתי סדרות בעלות אורך שווה‪,‬‬
‫אחרת‪ :‬אחת הסדרות תכיל איבר אחד יותר מהשניה(‪.‬‬
‫‪ .2‬מיין את תת‪-‬הסדרה הראשונה באמצעות מיון מיזוג‪.‬‬
‫‪ .3‬מיין את תת‪-‬הסדרה השניה באמצעות מיון מיזוג‪.‬‬
‫‪ .4‬מזג את שתי תת‪-‬הסדרות הממוינות לכדי סדרה אחת שלמה ממוינת‪.‬‬
‫‪ .5‬החזר את הסדרה שהתקבלה בסעיף הקודם‪.‬‬
‫נדגים ריצה של האלגוריתם על סדרת הנתונים הבאה‪{2 ,5 ,17 ,17 ,3879 ,13 ,7} :‬‬
‫א‪ .‬יש למיין סדרה בת ‪ 6‬ערכים על‪-‬כן‪:‬‬
‫‪ .1‬נפצלה לשתי סדרות }‪ {17 ,3879 ,13 ,7‬ו‪. {2 ,5 ,17} -‬‬
‫‪ .2‬נקרא רקורסיבית עבור הסדרה‪.{17 ,3879 ,13 ,7} :‬‬
‫‪ .3‬נקרא רקורסיבית עבור הסדרה‪. {2 ,5 ,17} :‬‬
‫‪ .4‬הקריאה הרקורסיבית מסעיף ‪ #2‬תחזיר את הסדרה‪.{3879 ,17 ,13 ,7} :‬‬
‫הקריאה הרקורסיבית מסעיף ‪ #3‬תחזיר את הסדרה‪ . {17 ,5 ,2} :‬נמזג את‬
‫שתי הסדרות לכדי הסדרה‪ ,{3879 ,17 ,17 ,13 ,7 ,5 ,2} :‬ואותה נחזיר‪.‬‬
‫ב‪ .‬נדון בקריאה הרקורסיבית מסעיף א‪ .'2‬כתובת החזרה שלה היא סעיף א‪.'2‬‬
‫קריאה זאת מקבלת את הסדרה‪ .{17 ,3879 ,13 ,7} :‬היא מבצעת ארבעה‬
‫צעדים‪:‬‬
‫‪ .1‬פיצול הסדרה לשתי סדרת‪ {13 ,7} :‬ו‪.{17 ,3879} -‬‬
‫‪ .2‬קריאה רקורסיבית עבור הסדרה‪.{13 ,7} :‬‬
‫‪ .3‬קריאה רקורסיבית עבור הסדרה‪.{17 ,3879} :‬‬
‫‪ .4‬מיזוג הסדרה }‪ {13 ,7‬שחזרה מסעיף ‪ ,'2‬עם הסדרה }‪ {3879 ,17‬שחזרה‬
‫מסעיף ‪ ,'3‬לכדי הסדרה }‪ .{3879 ,17 ,13 ,7‬והחזרת סדרה זאת‪.‬‬
‫ג‪ .‬נדון בקריאה הרקורסיבית מסעיף ב‪ .'2‬כתובת החזרה שלה היא סעיף ב‪.'3‬‬
‫קריאה זאת מקבלת את הסדרה‪ .{13 ,7} :‬היא מבצעת ארבעה צעדים‪:‬‬
‫‪ .1‬פיצול הסדרה לשתי סדרת‪ {7} :‬ו‪.{13} -‬‬
‫‪ .2‬קריאה רקורסיבית עבור הסדרה‪.{7} :‬‬
‫‪ .3‬קריאה רקורסיבית עבור הסדרה‪.{13} :‬‬
‫‪ .4‬מיזוג הסדרה }‪ {7‬שחזרה מסעיף ‪ ,'2‬עם הסדרה }‪ {13‬שחזרה מסעיף ‪ ,'3‬לכדי‬
‫הסדרה }‪ .{13 ,7‬והחזרת סדרה זאת‪.‬‬
‫ד‪ .‬נדון בקריאה הרקורסיבית מסעיף ג‪ .'2‬כתובת החזרה שלה היא סעיף ג‪.'3‬‬
‫קריאה זאת מקבלת את הסדרה‪ .{7} :‬ועל כן היא חוזרת מיידית‪ :‬הסדרה‬
‫שהיא קבלה כבר ממוינת‪.‬‬
‫‪79‬‬
‫ה‪ .‬חזרנו לסעיף ג‪ .'3‬הוא מנפיק קריאה רקורסיבית עבור הסדרה‪ .{13} :‬כתובת‬
‫החזרה של קריאה זאת היא סעיף ג‪ .'4‬הקריאה עבור הסדרה }‪ {13‬חוזרת‬
‫מיידית‪.‬‬
‫ו‪ .‬אנו חוזרים לסעיף ג‪ .'4‬ממזגים את שתי הרשימות לכדי הרשימה‪,{13 ,7} :‬‬
‫וחוזרים מסעיף ג'‪ .‬כתובת היא סעיף ב‪.'3‬‬
‫ז‪ .‬סעיף ב‪ '3‬מנפיק קריאה רקורסיבית עם הסדרה‪ .{17 ,3879} :‬כתובת החזרה של‬
‫קריאה זאת היא סעיף ב‪ .'4‬הקריאה מבצעת ארבעה שלבים‪:‬‬
‫‪ .1‬פיצול הסדרה לשתי סדרת‪ {3879} :‬ו‪.{17} -‬‬
‫‪ .2‬קריאה רקורסיבית עבור הסדרה‪.{3879} :‬‬
‫‪ .3‬קריאה רקורסיבית עבור הסדרה‪.{17} :‬‬
‫‪ .4‬מיזוג הסדרה }‪ {3879‬שחזרה מסעיף ‪ ,'2‬עם הסדרה }‪ {17‬שחזרה מסעיף ‪,'3‬‬
‫לכדי הסדרה }‪ .{3879 ,17‬והחזרת סדרה זאת‪.‬‬
‫ח‪ .‬נדלג על הקריאות הרקורסיביות שמנפיקים סעיפים ז‪ '2‬ו‪ -‬ז‪.'3‬‬
‫ט‪ .‬סעיף ז' משלים את פעולת המיזוג ומחזיר את הסדרה‪ .{3879 ,17} :‬כתובת‬
‫החזרה של סעיף ז' היא סעיף ב‪ .'4‬עתה סעיף ב‪ '4‬יכול למזג את שתי הסדרות‬
‫הממוינות שהחזירו סעיפים ג' ו‪-‬ז' לכדי הסדרה‪ .{3879 ,17 ,13 ,7} :‬סדרה זאת‬
‫תוחזר לסעיף א‪.'2‬‬
‫י‪ .‬עתה סעיף א' יכול להתקדם לסעיף א‪ ,'3‬ולזמן קריאה רקורסיבית עם הסדרה‪:‬‬
‫}‪ .{2 ,5 ,17‬כתובת החזרה של קריאה זאת היא סעיף א‪ .'4‬הקריאה‬
‫הרקורסיבית עם הסדרה‪ {2 ,5 ,17} :‬מתפצלת לארבעה שלבים‪:‬‬
‫‪ .1‬פיצול הסדרה לשתי סדרת‪ {5 ,17} :‬ו‪.{2} -‬‬
‫‪ .2‬קריאה רקורסיבית עבור הסדרה‪.{5 ,17} :‬‬
‫‪ .3‬קריאה רקורסיבית עבור הסדרה‪.{2} :‬‬
‫‪ .4‬מיזוג הסדרה }‪ {17 ,5‬שחזרה מסעיף ‪ ,'2‬עם הסדרה }‪ {2‬שחזרה מסעיף ‪,'3‬‬
‫לכדי הסדרה‪ .{17 ,5 ,2} :‬והחזרת סדרה זאת‪.‬‬
‫יא‪ .‬נדלג על הקריאות הרקורסיביות שמנפיק סעיף י‪ '2‬וסעיף י‪ .'3‬סעיף י‪ '4‬ימזג את‬
‫הסדרות החוזרת לכדי הסדרה‪ .{17 ,5 ,2} :‬בכך סעיף י' יסתיים‪ ,‬תוך שהוא‬
‫מחזיר סדרה ממוינת זאת‪.‬‬
‫יב‪ .‬כתובת החזרה של סעיף י' היא סעיף א‪ .'4‬סעיף א‪ '4‬יכול עתה למזג את שתי‬
‫הסדרות הממוינות‪ {3879 ,17 ,13 ,7} :‬ו‪ {17 ,5 ,2} -‬לכדי הסדרה הממוינת‪,2} :‬‬
‫‪ ,{3879 ,17 ,17 ,13 ,7 ,5‬ואותה נחזיר‪.‬‬
‫‪80‬‬
‫‪ 13.8.2‬מיון מיזוג של רשימות משורשרות‬
‫מיון מיזוג של רשימות משורשרות הוא מקרה פרטי של מיון מיזוג‪ ,‬בו מבנה‬
‫הנתונים באמצעותו מוחזקת סדרת הנתונים הוא רשימה מקושרת‪.‬‬
‫נציג את הפונקציה‪:‬‬
‫{ )‪struct Node *merge_sort(struct Node *head‬‬
‫‪struct Node *list1 = NULL, *list2= NULL,‬‬
‫; ‪*list‬‬
‫)‪if (head == NULL || head-> _next == NULL‬‬
‫; ‪return head‬‬
‫; ) ‪split( head, list1, list2‬‬
‫; )‪list1 = merge_sort(list1‬‬
‫; )‪list2 = merge_sort(list2‬‬
‫; )‪list = merge(list1, list2‬‬
‫; ‪return list‬‬
‫}‬
‫הפונקציה ‪ merge_sort‬היא מימוש ישיר של האלגוריתם‪ :‬במידה והרשימה שיש‬
‫למיין ריקה‪ ,‬או כוללת נתון יחיד היא כבר ממוינת‪ ,‬וניתן להחזיר את ‪ .head‬אחרת‪,‬‬
‫אנו מפצלים את הרשימה עליה מצביע ‪ head‬לשתי רשימות‪ ,‬עליהן מצביעים‬
‫‪ .list1, list2‬אחר אנו קוראים רקורסיבית עם ‪ list1‬ועם ‪ ,list2‬ואת הערך‬
‫המוחזר אנו מכניסים חזרה ל‪ .list1, list2 -‬לבסוף אנו ממזגים את שתי‬
‫הרשימות הממוינות עליהם מצביעים ‪ list1, list2‬לכדי רשימה מקושרת‬
‫ממוינת שלמה עליה מצביע ‪ ,list‬ואותה אנו מחזירים‪.‬‬
‫כדרכו של תכנות מעלה‪-‬מטה את עיקר העבודה 'דחפנו מתחת לשטיח' לפונקציות‬
‫‪ ,split‬ו‪ .merge -‬על‪-‬כן עתה נפנה אליהן‪.‬‬
‫גישה נאיבית לכתיבת הפונקציה ‪ split‬תאמר שאת המחצית הראשונה של אברי‬
‫הרשימה נַפנֵה לרשימה ‪ ,list1‬ואת המחצית השניה נפנה לרשימה ‪ .list2‬זוהי‬
‫גישה נאיבית שכן היא מחייבת מעבר מקדים אשר יספור כמה איברים יש ברשימה‪.‬‬
‫אנו נציג חלופה יעילה מעט יותר לכתיבת הפונקציה‪ :‬את האיברים במקומות‬
‫הפרדיים נפנה לרשימה עליה יצביע ‪ ,list1‬ואת אלה שבמקומות הזוגיים נפנה‬
‫לרשימה עליה יצביע ‪ .list2‬באופן כזה פעולת הפיצול תתבצע על‪-‬ידי ביצוע מעבר‬
‫יחיד על הרשימה‪.‬‬
‫הפונקציה שנכתוב מקבלת את ‪ list1, list2‬כפרמטרי הפניה‪ ,‬שכן עליה‬
‫להפנותם להצביע על רשימות שהפונקציה מקצה להם‪ .‬הפונקציה אינה מקצה‬
‫תאים חדשים בזיכרון‪ ,‬וזו תכונה יפה שלה‪ ,‬שכן כך היא אינה מגדילה את צריכת‬
‫הזיכרון של התכנית‪.‬‬
‫‪81‬‬
‫נציג את הפונקציה‪ ,‬ואחר נסבירה ביתר פירוט‪:‬‬
‫‪void split(struct Node *list,‬‬
‫{ )‪struct Node *&list1, struct Node *&list2‬‬
‫; ‪bool to_list1 = true‬‬
‫; ‪struct Node *temp‬‬
‫{ )‪while (list != NULL‬‬
‫; ‪temp = list‬‬
‫; ‪list = list -> _next‬‬
‫{ )‪if (to_list1‬‬
‫; ‪temp-> _next = list1‬‬
‫; ‪list1 = temp‬‬
‫}‬
‫{ ‪else‬‬
‫; ‪temp-> _next = list2‬‬
‫; ‪list2 = temp‬‬
‫}‬
‫; ‪to_list1 = !to_list1‬‬
‫}‬
‫}‬
‫המשתנה הבולאני ‪ to_list1‬מורה להיכן יש להפנות את האיבר הבא ברשימה‬
‫אותה מפצלים‪ :‬לרשימה עליה מורה ‪ list1‬או לזאת עליה מורה ‪ .list2‬לכן בכל‬
‫סיבוב בלולאה אנו מנגדים את ערכו ) האופרטור ! מחזיר את הערך הבולאני ההפוך‬
‫לערכו של האופרנד שלו‪ ,‬לכן הביטוי‪ !to_list1 :‬מחזיר את הערך ההפוך לערך‬
‫המצוי כרגע במשתנה ‪ ,to_list1‬וערך זה אנו מכניסים ל‪.(to_list1 -‬‬
‫בכל סיבוב בלולאה אנו שומרים במשתנה ‪ temp‬מצביע לאיבר עליו מורה עתה‬
‫‪ ,list‬אחר אנו מקדמים את ‪ list‬לאיבר הבא ברשימה אותה יש לפצל‪ ,‬ואז את‬
‫האיבר עליו מורה ‪ temp‬מעבירים לראש הרשימה הרצויה )זו שעליה מורה ‪list1‬‬
‫או זו שעליה מצביע ‪ .(list2‬האפקט המתקבל הוא שאיברים שהיו מאוחרים יותר‬
‫ברשימה ‪ list‬יהיו מוקדמים יותר ברשימות ‪ list1, list2‬אך לכך אין כל‬
‫משמעות שכן כל תפקידה של ‪ split‬הוא לפצל את הרשימה עליה מצביע ‪,list‬‬
‫ולא משנה באיזה סדר יועברו האיברים המתפצלים לרשימות הנוצרות‪ .‬בכל מקרה‬
‫הרשימות הנוצרות תמוינה )על‪-‬ידי קריאה רקורסיבית ל‪.(merge_sort -‬‬
‫נקודה מעט עדינה אליה ראוי לתת את הדעת היא שאנו מסתמכים על כך שערכם‬
‫של ‪ list1, list2‬בעת הקריאה לפונקציה ‪ split‬הוא ‪ .NULL‬ההסתמכות היא‬
‫שכן המצביע ‪ _next‬באיבר הראשון שנוסף לכל רשימה )ושיהיה לכן האיבר‬
‫האחרון ברשימה הנבנית( מקבל את ערכו של ‪) list1‬או ‪ list2‬בהתאמה(‪ .‬כדי‬
‫שערכו של המצביע ‪ _next‬באיבר האחרון יהיה ‪ NULL‬חשוב שערכם של ‪list1,‬‬
‫‪ list2‬בעת הקריאה יהיה ‪.NULL‬‬
‫‪82‬‬
‫עתה נציג את הפונקציה ‪.merge‬‬
‫‪struct Node *merge(struct Node *list1,‬‬
‫{ )‪struct Node *list2‬‬
‫‪struct Node *list = NULL,‬‬
‫‪// head of new list‬‬
‫‪*last,‬‬
‫‪// pointer to last item in list‬‬
‫; ‪*temp‬‬
‫‪// while both lists r not empty‬‬
‫{ )‪while (list1 != NULL && list2 != NULL‬‬
‫{ )‪if (list1-> _data <= list2-> _data‬‬
‫; ‪temp = list1‬‬
‫‪// hold the item that moves‬‬
‫‪list1 = list1 -> _next ; // advance in its list‬‬
‫}‬
‫{ ‪else‬‬
‫; ‪temp = list2‬‬
‫; ‪list2 = list2 -> _next‬‬
‫}‬
‫‪temp -> _next = NULL ; // rem the itm from its list‬‬
‫)‪if (list == NULL‬‬
‫; ‪last = list = temp‬‬
‫{ ‪else‬‬
‫; ‪last -> _next = temp‬‬
‫‪// append as last‬‬
‫; ‪last = temp‬‬
‫‪// and update pointer 2 last‬‬
‫}‬
‫}‬
‫)‪if (list1 == NULL‬‬
‫; ‪last-> _next = list2‬‬
‫‪else‬‬
‫; ‪last-> _next = list1‬‬
‫; )‪return(list‬‬
‫}‬
‫הסבר הפונקציה‪ :‬הפונקציה מחזיקה שני מצביעים לרשימה הנבנית‪ list :‬מצביע‬
‫על ראש הרשימה‪ last ,‬מצביע על האיבר האחרון ברשימה‪.‬‬
‫הלולאה מתנהלת כל עוד שתי הרשימות שיש למזג אינן ריקות‪ .‬בכל סיבוב בלולאה‬
‫אנו ראשית בודקים איזה איבר יש להעביר לרשימה הנבנית )האם את האיבר המצוי‬
‫בראשה של ‪ ,list1‬או את זה שבראשה של ‪ .(list2‬המצביע ‪ temp‬עובר להצביע‬
‫על האיבר המתאים‪ ,‬והמצביע ‪ last1‬או ‪ last2‬מקודם לאיבר הבא ברשימה‬
‫המתאימה‪ .‬הפקודה‪ temp -> _next = NULL ; :‬מנתקת את האיבר שיש‬
‫להעביר לרשימה הנבנית מהרשימה שלו‪ .‬מכיוון שאיבר זה יתווסף בסופה של‬
‫הרשימה הנבנית מתאים שהמצביע ‪ _next‬בו יכיל את הערך ‪ .NULL‬במקרה הקצה‬
‫בו אנו מוסיפים את האיבר הראשון לרשימה הנבנית )‪ (list == NULL‬אנו מפנים‬
‫הן את ‪ list‬והן את ‪ last‬להצביע על האיבר המועבר‪ .‬בכל מקרה אחר אנו מפנים‬
‫את המצביע שבתא האחרון ברשימה הנבנית )‪ (last-> _next‬להצביע על האיבר‬
‫המוסף )עליו מצביע ‪ ,(temp‬ואחר מקדמים את ‪ last‬להצביע על האיבר החדש‬
‫)שהינו עתה האיבר האחרון ברשימה הנבנית(‪.‬‬
‫‪83‬‬
‫נצא מהלולאה עת אחת מהרשימות אותן יש למזג תמוצה‪ .‬אם הלולאה שמוצתה‬
‫היא זו עליה הצביע ‪ lilst1‬אזי ‪ list2‬מצביע על זנב הרשימה השניה‪ ,‬שאת‬
‫אבריה יש לשרשר בהמשכה של הרשימה הנבנית‪ .‬הפקודה‪:‬‬
‫‪ last-> _next = list2‬מבצעת את החיבור המתאים‪ :‬של זנב הרשימה עליה‬
‫מצביע ‪ list2‬להמשכה של הרשימה הנבנית‪ .‬אם הלולאה שמוצתה היא זו עליה‬
‫הצביע ‪ lilst2‬אזי זהו המקרה הסימטרי‪.‬‬
‫אתם מוזמנים לבצע סימולציית הרצה דקדקנית לפונקציה כדי לבדוק את נכונותה…‬
‫‪ 13.8.3‬זמן הריצה של מיון מיזוג‬
‫נפנה עתה לשאלה הבלתי נמנעת מהו זמן הריצה של התכנית? הניתוח יהיה דומה‬
‫מאוד לזה שביצענו עבור מיון מהיר‪ .‬כל ריצה של האלגוריתם מבצעת את הצעדים‬
‫הבאים‪:‬‬
‫א‪ .‬פיצול הרשימה לשתי תת‪-‬רשימות‪ ,‬תוך השקעת עבודה לינארית באורך הרשימה‬
‫שיש לפצל‪.‬‬
‫ב‪ .‬קריאה רקורסיבית עבור מחצית הרשימה האחת‪.‬‬
‫ג‪ .‬קריאה רקורסיבית עבור מחצית הרשימה השניה‪.‬‬
‫ד‪ .‬מיזוג שתי הרשימות שהחזירו הקריאות הרקורסיביות‪ ,‬תוך השקעת עבודה‬
‫לינארית באורך הרשימה המתקבלת‪.‬‬
‫נניח כי האלגוריתם התבקש למיין רשימה בת ‪ N‬נתונים‪ .‬נציג את עץ הקריאות‬
‫הרקורסיביות המתקבל‪ .‬כל צומת בעץ מכיל ארבעה מרכיבים המתאימים לארבעת‬
‫הצעדים המבוצעים בקריאה הרקורסיבית המתאימה‪.‬‬
‫הציור‪:‬‬
‫‪split‬‬
‫)‪(N‬‬
‫הציור‪:‬‬
‫(‪ms‬‬
‫)‪N/2‬‬
‫הציור‪:‬‬
‫(‪merge‬‬
‫‪N/2,‬‬
‫)‪N/2‬‬
‫מייצג פיצול של רשימה בארוך ‪ N‬לכדי שתי רשימות‪.‬‬
‫מייצג קריאה רקורסיבית עם רשימה באורך ‪N/2‬‬
‫מייצג מיזוג של שתי רשימות באורך ‪ N/2‬לכדי רשימה באורך ‪.N‬‬
‫‪84‬‬
‫עץ הקריאות יראה לפיכך באופן הבא‪:‬‬
‫(‪merge‬‬
‫‪N/2,‬‬
‫)‪N/2‬‬
‫(‪merge‬‬
‫‪N/4,‬‬
‫)‪N/4‬‬
‫(‪ms‬‬
‫)‪N/4‬‬
‫(‪ms‬‬
‫)‪N/4‬‬
‫(‪merge‬‬
‫‪N/8,‬‬
‫)‪N/8‬‬
‫(‪merge‬‬
‫‪N/8,‬‬
‫)‪N/8‬‬
‫(‪ms‬‬
‫)‪N/8‬‬
‫(‪ms‬‬
‫)‪N/8‬‬
‫(‪ms‬‬
‫)‪N/2‬‬
‫(‪ms‬‬
‫)‪N/2‬‬
‫‪split‬‬
‫)‪(N/2‬‬
‫(‪ms‬‬
‫)‪N/8‬‬
‫‪split‬‬
‫)‪(N/4‬‬
‫(‪merge‬‬
‫‪N/4,‬‬
‫)‪N/4‬‬
‫‪split‬‬
‫)‪(N/4‬‬
‫(‪ms‬‬
‫)‪N/8‬‬
‫(‪merge‬‬
‫‪N/8,‬‬
‫)‪N/8‬‬
‫‪split‬‬
‫)‪(N‬‬
‫(‪ms‬‬
‫)‪N/4‬‬
‫(‪merge‬‬
‫‪N/8,‬‬
‫)‪N/8‬‬
‫(‪ms‬‬
‫)‪N/8‬‬
‫(‪ms‬‬
‫)‪N/8‬‬
‫(‪ms‬‬
‫)‪N/4‬‬
‫(‪ms‬‬
‫)‪N/8‬‬
‫‪split‬‬
‫)‪(N/2‬‬
‫(‪ms‬‬
‫)‪N/8‬‬
‫‪split‬‬
‫)‪(N/4‬‬
‫עתה נשאל את עצמנו כמה רמות תהינה בעץ? ומה כמות העבודה שתתבצע בכל‬
‫קטן בחצי בכל רמה‪ .‬ברמה‬
‫רמה? אנו רואים שגודלה של הרשימה אותה יש למיין ֵ‬
‫התחתונה יש למיין רשימה בת איבר יחיד‪ .‬על‪-‬כן מספר הרמות יהיה )‪ ,log2(N‬שכן‬
‫הפונקציה )‪ log2(x‬מתארת כמה פעמים ניתן לחצות את ‪ x‬עד שמגיעים לערך אחד‪.‬‬
‫כמות העבודה הנעשית בכל רמה הינה בסדר גודל של ‪ :N‬בשורש אנו מפצלים‬
‫רשימה בגודל ‪) ,N‬תוך השקעת עבודה בשיעור ‪ ,(N‬ואחר ממזגים שתי רשימות‬
‫באורך ‪) N/2‬תוך השקעת עבודה בשיעור ‪ .(N‬ברמה שמתחת לשורש‪ :‬פעמיים אנו‬
‫מפצלים רשימה בגודל ‪ N/2‬לכדי שתי רשימות בגודל ‪) N/4‬ועל כל פיצול כזה אנו‬
‫משלמים עבודה בשיעור ‪ ,(N/2‬ופעמיים אנו ממזגים שתי רשימות בגודל ‪ N/4‬לכדי‬
‫רשימה בגודל ‪) N/2‬ועל כל מיזוג משלמים עבודה בשיעור ‪ .(N/2‬וכך הלאה‪.‬‬
‫מכיוון שבעץ יש )‪ log2(N‬רמות‪ ,‬ובכל רמה מתבצעת עבודה בשיעור ‪ ,N‬אזי סך כל‬
‫העבודה הנעשית היא )‪ . N*log2(N‬כלומר כמות עבודה לזאת הנעשית על‪-‬ידי מיון‬
‫מהיר על קלט מיטבי )ולמעשה‪ ,‬אך אנו לא הוכחנו זאת‪ ,‬גם על קלט ממוצע(‪ .‬שימו‬
‫לב כי במיון מיזוג זאת תהא כמות העבודה שתתבצע עבור כל קלט שהוא )בעוד מיון‬
‫מהיר‪ ,‬על קלט גרוע‪ ,‬יידרש לבצע עבודה בשיעור ‪.(N2‬‬
‫‪85‬‬
‫‪split‬‬
‫)‪(N/4‬‬
‫‪ 13.9‬בניית רשימה מקושרת ממוינת בשיטת המצביע למצביע‬
‫בסעיף ‪ 12.3‬ראינו כיצד ניתן לבנות רשימה מקושרת ממוינת‪ .‬לשם הוספת איבר‬
‫ַלרשימה השתמשנו בשני מצביעי עזר ‪ rear, front‬אשר סרקו את הרשימה‬
‫הקיימת כדי לאתר את המקום בו יש לשלב את האיבר החדש‪ .‬הלולאה שהרצנו‬
‫התקדמה עם המצביעים עד אשר ‪ front‬הגיע לאיבר שהכיל ערך גדול מהערך אותו‬
‫יש להוסיף‪ .‬המצביע ‪ rear‬הצביע אז לאיבר שאחריו יש לשלב את האיבר החדש‪ .‬זו‬
‫היא הדרך הארוכה אך הקצרה לבניית רשימה מקושרת ממוינת‪ :‬היא ארוכה‬
‫באשר היא לא הכי אלגנטית‪ ,‬היא קצרה שכן היא יחסית קלה יותר להבנה‪ .‬בסעיף‬
‫זה נרצה לראות את הדרך הקצרה אך ארוכה לבניית רשימה מקושרת ממוינת‪:‬‬
‫דרך זו תהיה אלגנטית יותר‪ ,‬אך קשה יותר להבנה‪.‬‬
‫הכלי הבסיסי בו נשתמש לבניית הרשימה הוא מצביע למצביע‪.‬‬
‫נניח כי בתכנית הראשית הוגדר‪ . struct Node *head = NULL; :‬אחר‬
‫זומנה הפונקציה‪ . build_list(head); :‬הפונקציה ‪ build_list‬דומה לזו‬
‫שראינו בעבר‪ .‬נציג אותה‪:‬‬
‫{ )‪void build_list(struct Node *&head‬‬
‫; ‪int num‬‬
‫; ‪cin >> num‬‬
‫{ )‪while (num != 0‬‬
‫; )‪insert(num, &head‬‬
‫; ‪cin >> num‬‬
‫}‬
‫}‬
‫הנקודה היחידה אליה יש צורך לתת את הדעת היא זימונה של הפונקציה ‪.insert‬‬
‫שימו לב כי אנו קוראים ל‪ insert -‬עם ‪ ,&head‬כלומר עם מצביע ַלמצביע ‪.head‬‬
‫הוא‪:‬‬
‫‪insert‬‬
‫הפונקציה‬
‫של‬
‫השני‬
‫הפרמטר‬
‫טיפוס‬
‫משמע‬
‫‪) . struct Node **head‬מי שהקריאה הנ"ל מבלבלת אותו יכול לבצעה בשני‬
‫;‪ struct Node **temp‬להכניס את כתובתו של‬
‫צעדים‪) :‬א( ַלמשתנה‪:‬‬
‫‪ head‬כלומר את ‪ ,&head‬וזאת באמצעות ההשמה‪) , temp = &head; :‬ב( לקרוא‬
‫לפונקציה‪.( insert(num, temp); :‬‬
‫‪86‬‬
:‫ נציגה ואחר נסבירה‬.insert ‫נפנה עתה לפונקציה‬
void insert(int num, struct Node **p_2_head) {
struct Node *temp ;
if (*p_2_head == NULL) {
*p_2_head = new (std::nothrow) struct Node ;
if (*p_2_head == NULL)
terminate("cannot allocate memory", 1) ;
(*p_2_head)-> _data = num ;
(*p_2_head)-> _next = NULL ;
return ;
}
while ( (*p_2_head) != NULL &&
(*p_2_head)-> _data < num )
p_2_head = & ( (*p_2_head)-> _next ) ;
temp = new (std::nothrow) struct Node ;
if (temp == NULL)
terminate("cannot allocate memory", 1) ;
temp -> _data = num ;
temp -> _next = (*p_2_head);
(*p_2_head) = temp ;
}
‫ ומי שנתקף בפיק ברכיים עת הוא רואה‬,‫יש להודות שהפונקציה אינה קלה להבנה‬
.(*) ‫ טרם שהוא פונה לראות כוכביות‬,‫ כדאי שיחזק את ברכיו‬,‫( לנגד עיניו‬->) ‫חץ‬
5) .0 ,17 ,3879 ,3 ,5 :‫כדי להבינה נבצע לה סימולציית הרצה מדוקדקת עבור הקלט‬
.(‫ מוזן אחרון‬0 ,‫מוזן ראשון‬
-‫נתחיל בהצגת מצב המחסנית של התכנית הראשית טרם הקריאה ל‬
:build_list
head =
‫ מצב המחסנית‬.‫ כפרמטר הפניה‬head ‫את‬
head =
head =
num=
87
‫ מקבלת‬build_list ‫הפונקציה‬
:‫כן‬-‫בעקבות הקריאה הוא על‬
‫הפונקציה ‪ build_list‬מתחילה להתבצע‪ .‬היא קוראת את הערך ‪ 5‬לתוך ‪,num‬‬
‫וקוראת ל‪ .insert -‬הפונקציה ‪ insert‬מקבלת מצביע למצביע; נסמן מצביע זה‬
‫כפי שאנו מציירים מצביעים בדרך כלל )עם ראש בצורת ‪ ,(v‬ונַפנה את המצביע‬
‫להצביע על המצביע שעליו הוא מורה )‪ head‬של התכנית הראשית(‪ .‬מצב המחסנית‬
‫יהיה‪:‬‬
‫= ‪head‬‬
‫= ‪head‬‬
‫‪num= 5‬‬
‫= ‪p_2_head‬‬
‫‪num= 5‬‬
‫=‪temp‬‬
‫עתה הפונקציה ‪ insert‬מתחילה להתבצע‪ .‬נזכור כי ‪ *p_2_head‬הוא ה‪'-‬יצור'‬
‫עליו ‪) p_2_head‬של ‪ (insert‬מצביע‪ ,‬וזהו המשתנה ‪ head‬של התכנית הראשית‪.‬‬
‫לכן עת הפונקציה שואלת האם ערכו של ‪ *p_2_head‬הוא ‪ NULL‬היא שואלת האם‬
‫‪ head‬של התכנית הראשית ערכו הוא ‪ .NULL‬נוכל לראות זאת גם מהציור‪:‬‬
‫‪ p_2_head‬הוא מצביע‪ *p_2_head ,‬הוא היצור עליו המצביע מצביע‪ ,‬ומהציור אנו‬
‫רואים שהחץ היוצא מ‪ p_2_head -‬מצביע על המשתנה ‪ head‬של התכנית‬
‫הראשית‪.‬‬
‫אם כן‪ ,‬התנאי )‪ (*p_2_head == NULL‬מתקיים‪ .‬לכן מבוצע הגוש הכפוף לו‪:‬‬
‫המצביע ‪ ,*p_2_head‬כלומר המצביע עליו ‪ p_2_head‬מורה )שהוא המצביע ‪head‬‬
‫של התכנית הראשית( עובר להצביע על קופסה חדשה )מטיפוס ‪(struct Node‬‬
‫שמוקצית על הערמה‪ .‬הביטוי‪ (*p_2_head)-> _data :‬מתייחס למרכיב ה‪-‬‬
‫‪ _data‬בקופסה שהוקצתה‪ .‬נסביר מדוע‪ p_2_head :‬הוא המצביע למצביע‬
‫)טיפוסו‪ ,(struct Node ** :‬לכן ‪ *p_2_head‬הוא המצביע עליו ‪p_2_head‬‬
‫מצביע‪ ,‬כלומר המצביע ‪ head‬של התכנית הראשית )שטיפוסו הוא ‪struct Node‬‬
‫*(‪ .‬הביטוי‪ (*p_2_head)-> :‬מפנה אותנו לקופסה שהוקצתה אך זה עתה‪,‬‬
‫מורה‪ .‬ולבסוף‪:‬‬
‫ֵ‬
‫ושעליה )‪ ,(*p_2_head‬כלומר ‪ head‬של התכנית הראשית‪,‬‬
‫‪ (*p_to_head)-> _data‬מתייחס לשדה ה‪ data_ -‬בקופסה שעליה המצביע‬
‫מצביע‪ .‬לשדה ‪ (*p_2_head)-> _data‬אנו משימים את הערך ‪ .5‬באופן דומה‬
‫לשדה המצביע בקופסה עליה מצביע ‪ head‬של התכנית הראשית אנו מכניסים את‬
‫ְ‬
‫הערך ‪ .NULL‬בזאת ‪ insert‬מסתיימת‪.‬‬
‫מצב הזיכרון לקראת סיום ריצתה הראשונה של ‪ insert‬הוא‪:‬‬
‫=‪data‬‬
‫‪5‬‬
‫= ‪head‬‬
‫= ‪head‬‬
‫‪num= 5‬‬
‫= ‪p_2_head‬‬
‫‪num= 5‬‬
‫=‪temp‬‬
‫‪88‬‬
‫עם תום ריצתה של ‪ insert‬רשומת ההפעלה שלה מוסרת מעל‪-‬גבי המחסנית‪ ,‬ואנו‬
‫שבים ל‪ build_list -‬אשר קוראת את הערך ‪ ,3‬ומזמנת שנית את ‪ .insert‬גם‬
‫בקריאה הנוכחית מעבירה ‪ build_list‬ל‪ insert -‬מצביע ל‪ .head -‬מצב‬
‫הזיכרון דומה למתואר בציור האחרון‪ ,‬פרט לכך שערכו של ‪ num‬בשתי רשומות‬
‫ההפעלה הוא עתה ‪) 3‬במקום ‪.(5‬‬
‫אחרי בניית רשומת ההפעלה מתחילה הפונקציה להתבצע‪ .‬התנאי‪:‬‬
‫)‪ (*p_2_head == NULL‬אינו מתקיים )ערכו של המצביע עליו מצביע‬
‫‪ ,p_2_head‬כלומר המצביע ‪ head‬של התכנית הראשית‪ ,‬אינו ‪ .(NULL‬לכן אנו פונים‬
‫ללולאה אשר התנאי בכותרתה הוא‪:‬‬
‫) ‪( (*p_2_head) != NULL && (*p_2_head)-> _data < num‬‬
‫נעקוב אחר הביטוי‪ p_2_head :‬הוא מצביע‪ *p_2_head .‬הוא האובייקט עליו‬
‫‪ p_2_head‬מצביע‪ ,‬כלומר המצביע ‪ head‬של התכנית הראשית‪(*p_2_head)-> .‬‬
‫‪ _data‬הוא מרכיב הנתונים בקופסה עליה ‪ *p_2_head‬מצביע‪ ,‬כלומר מרכיב‬
‫הנתונים בקופסה עליה ‪ head‬מצביע‪ ,‬כלומר הערך ‪ .5‬לכן התנאי‪:‬‬
‫‪(*p_2_head)-> _data < num‬אינו מתקיים )ערכו של ‪ num‬הוא ‪ ,(3‬ולכן איננו‬
‫נכנסים אף לא לסיבוב יחיד בלולאה‪ .‬ערכו של ‪ *p_2_head‬הוא עדיין המצביע‬
‫‪ head‬של התכנית הראשית )כפי שהיה עת הפונקציה נקראה(‪ .‬ברמה העקרונית‬
‫הסיבה לכך היא שאת הנתון הנוכחי עלינו להוסיף בראש הרשימה‪.‬‬
‫‪89‬‬
‫עתה אנו פונים לארבע הפקודות שאחרי לולאת קידומו של ‪ .p_2_head‬הראשונה‬
‫מבין הארבע מקצה קופסה חדשה על הערמה‪ ,‬ומפנה את המצביע )הרגיל‪ ,‬שטיפוסו‬
‫* ‪ temp (struct Node‬להצביע על הקופסה המוקצית )מחמת מיאוס(‪ .‬הפקודה‬
‫השניה מבין הארבע שותלת בשדה ה‪ _data -‬של הקופסה את הערך ‪ .3‬הפקודה‬
‫השלישית מפנה את ‪ temp-> _next‬להצביע כמו ‪ .*p_2_head‬לאן מצביע‬
‫‪ *p_2_head ? *p_2_head‬הוא המצביע ‪ head‬של התכנית הראשית‪ ,‬אשר מצביע‬
‫על הקופסה של ‪ .5‬לכן עתה גם ‪ temp-> _next‬מצביע על הקופסה של ‪ .5‬מצב‬
‫הזיכרון הוא‪:‬‬
‫= ‪head‬‬
‫=‪data‬‬
‫‪5‬‬
‫= ‪head‬‬
‫‪num= 3‬‬
‫=‪data‬‬
‫‪3‬‬
‫= ‪p_2_head‬‬
‫‪num= 3‬‬
‫=‪temp‬‬
‫הפקודה הרביעית‪ ,‬והאחרונה‪ ,‬מפנה את ‪ ,*p_2_head‬כלומר את המצביע‬
‫של התכנית הראשית‪ ,‬להצביע כמו ‪ ,temp‬כלומר על הקופסה של ‪) 3‬זו שנוצרה אך‬
‫זה עתה(‪ .‬מצב הזיכרון הוא‪:‬‬
‫‪head‬‬
‫= ‪head‬‬
‫=‪data‬‬
‫‪5‬‬
‫= ‪head‬‬
‫‪num= 3‬‬
‫=‪data‬‬
‫‪3‬‬
‫= ‪p_2_head‬‬
‫‪num= 3‬‬
‫=‪temp‬‬
‫אנו רואים כי ‪ 3‬הוסף בראש הרשימה‪ .‬כפי שראוי היה‪ .‬בכך מסתיימת ‪.insert‬‬
‫אנו שבים ל‪ build_list -‬אשר קוראת את הערך ‪ ,3879‬ומזמנת שוב את‬
‫‪ .insert‬ציור הזיכרון הוא כמו בציור האחרון‪ ,‬פרט לכך שערכו של ‪ num‬בשתי‬
‫רשומות ההפעלה הוא ‪.3879‬‬
‫‪90‬‬
‫הפונקציה ‪ insert‬מתחילה להתבצע‪ .‬התנאי )‪ (*p_2_head == NULL‬אינו‬
‫מתקיים‪ ,‬שכן ‪ *p_2_head‬הוא המצביע ‪ head‬של התכנית הראשית‪ ,‬וערכו אינו‬
‫‪ .NULL‬אנו מתקדמים ללולאה שהתנאי בראשה הוא‪:‬‬
‫) ‪( (*p_2_head) != NULL && (*p_2_head)-> _data < num‬‬
‫שני המרכיבים של התנאי מסופקים‪ .‬עבור השמאלי הדבר ברור‪ .‬עבור הימני‪:‬‬
‫‪ *p_2_head‬הוא המצביע ‪ ,head‬ולכן ‪ (*p_2_head)-> _data‬הוא הערך ‪,3‬‬
‫וערך זה קטן מערכו של ‪) num‬שהינו ‪ .(3879‬על‪-‬כן אנו נכנסים לגוף הלולאה‪.‬‬
‫הפרמטר ‪ p_2_head‬עובר להצביע על מצביע חדש‪ .‬על איזה מצביע? נבדוק‪:‬‬
‫‪ (*p_2_head)-> _next‬הוא המצביע _‪ next‬בקופסה עליה מצביע‬
‫‪ ,*p_2_head‬כלומר המצביע _‪ next‬בקופסה עליה מצביע ‪ ,head‬כלומר המצביע‬
‫שמצביע על הקופסה של ‪ .5‬המצביע למצביע ‪ p_2_head‬מקבל את כתובתו של‬
‫מצביע זה )בשל ה‪ & -‬שנכתב לפני שמו של המצביע ‪.( (*p_2_head)->next‬‬
‫מבחינה ציורית המצב הוא‪:‬‬
‫= ‪head‬‬
‫=‪data‬‬
‫‪5‬‬
‫= ‪head‬‬
‫‪num=3879‬‬
‫=‪data‬‬
‫‪3‬‬
‫= ‪p_2_head‬‬
‫‪num= 3879‬‬
‫=‪temp‬‬
‫כלומר ‪ p_2_head‬מצביע על‪ :‬המצביע שמורה על הקופסה של ‪ .5‬לקראת כניסה‬
‫לסיבוב נוסף בלולאה אנו שוב בודקים את התנאי‪ ,‬ושוב הוא מסופק‪ ,‬על כן‬
‫‪ p_2_head‬עובר להצביע על המצביע ‪ .(*p_2_head)->next‬נבדוק מיהו מצביע‬
‫זה‪ *p_to_head :‬הוא המצביע המורה על הקופסה של ‪ .5‬על כן‬
‫‪ (*p_2_head)-> _next‬הוא שדה המצביע בקופסה עליה מצביע ‪,*p_2_head‬‬
‫כלומר המצביע בקופסה של ‪) 5‬שערכו הוא ‪ .(NULL‬מבחינה ציורית מצב הזיכרון‬
‫הוא‪:‬‬
‫= ‪head‬‬
‫=‪data‬‬
‫‪5‬‬
‫= ‪head‬‬
‫‪num=3879‬‬
‫=‪data‬‬
‫‪3‬‬
‫‪91‬‬
‫= ‪p_2_head‬‬
‫‪num= 3879‬‬
‫=‪temp‬‬
‫לקראת כניסה לסיבוב נוסף בלולאה אנו שוב בודקים את התנאי‪:‬‬
‫) ‪( (*p_2_head) != NULL && (*p_2_head)-> _data < num‬‬
‫אולם המרכיב השמאלי בו אינו מתקיים‪ :‬ערכו של המצביע ‪ *p_2_head‬אינו שונה‬
‫מ‪ .NULL -‬על כן איננו נכנסים לסיבוב נוסף בלולאה‪ .‬אנו פונים לארבע הפקודות‬
‫שאחרי הלולאה‪ .‬השתיים הראשונות בניהן מביאות אותנו למצב‪:‬‬
‫= ‪head‬‬
‫=‪data‬‬
‫‪5‬‬
‫= ‪head‬‬
‫‪num=3879‬‬
‫=‪data‬‬
‫‪3‬‬
‫=‪data‬‬
‫‪3879‬‬
‫= ‪p_2_head‬‬
‫‪num= 3879‬‬
‫=‪temp‬‬
‫הפקודה השלישית היא‪ . temp->next = (*p_2_head); :‬ערכו של המצביע‬
‫‪) *p_2_head‬כלומר המצביע עליו מצביע ‪ ,(p_2_head‬הוא ‪ .NULL‬על‪-‬כן גם ערכו‬
‫של ‪ temp->next‬יהיה ‪ .NULL‬הפקודה הרביעית היא‪(*p_2_head) = temp; :‬‬
‫‪ .‬המצביע )‪(*p_2_head‬הוא המצביע בקופסה של ‪ ,5‬והוא עובר להצביע לאותו‬
‫מקום כמו ‪ ,temp‬כלומר לקופסה של ‪ .3879‬מצב הזיכרון הוא‪:‬‬
‫= ‪head‬‬
‫=‪data‬‬
‫‪5‬‬
‫= ‪head‬‬
‫‪num=3879‬‬
‫=‪data‬‬
‫‪3‬‬
‫=‪data‬‬
‫‪3879‬‬
‫= ‪p_2_head‬‬
‫‪num= 3879‬‬
‫=‪temp‬‬
‫ושוב הרשימה ממוינת כנדרש‪.‬‬
‫את המעקב אחרי תהליך הוספתו של ‪ 17‬לרשימה אשאיר לכם כתרגיל‪.‬‬
‫לסיכום‪ ,‬אנו רואים שבאמצעות שימוש במצביע למצביע קיצרנו את הפונקציה‬
‫‪ ,insert‬אשר אינה משתמשת עוד בזוג מצביעים ‪ ,rear, front‬ואינה מחייבת‬
‫התייחסות למקרה הקצה בו יש להוסיף איבר בראש הרשימה הממוינת‪ .‬מנגד‬
‫הפונקציה שכתבנו קשה יותר להבנה‪.‬‬
‫‪92‬‬
‫‪ 13.10‬הערה בנוגע ל‪const-‬‬
‫עת למדנו להעביר מערך כפרמטר לפונקציה אמרנו שבכוחה של הפונקציה לשנות‬
‫את ערכם של תאי המערך‪ .‬לכן אם בתכנית הוגדר‪, int a[3]={17,17,17}; :‬‬
‫והוגדרה‪:‬‬
‫)][‪void f1(int arr‬‬
‫{‬
‫} ; ‪arr[0] = 0; arr[1] = 1‬‬
‫אזי ערכו של ]‪ a[0‬לפני הקריאה לפונקציה הוא ‪ ,17‬וערכו אחרי ביצוע הפונקציה‪,‬‬
‫עבור הקריאה‪ f1(a); :‬הוא אפס‪.‬‬
‫בהמשך‪ ,‬ראינו כי אם ברצוננו למנוע מפונקציה לשנות את ערכם של תאי מערך‬
‫המועבר לה כפרמטר‪ ,‬אזי אנו יכולים להוסיף לפני שם הפרמטר את מילת המפתח‬
‫‪ .const‬הוספת מילת המפתח תמנע מהפונקציה לשנות את ערכם של תאי המערך‪.‬‬
‫אמרנו כי האפקט אינו זהה לזה של פרמטר ערך‪ ,‬שכן את ערכו של פרמטר ערך‬
‫הפונקציה רשאית לשנות; )אולם הערך החדש יוכנס ַלפרמטר בלבד‪ ,‬ולא ַלארגומנט‬
‫המתאים(‪.‬‬
‫עוד ראינו כי עת מערך מועבר כפרמטר לפונקציה אנו רשאים לתאר את הפרמטר גם‬
‫באופן הבא‪:‬‬
‫} … { )‪void f2(int *ip‬‬
‫עתה נשאל את עצמנו‪ :‬נניח שברצוננו למנוע מ‪ f2 -‬לשנות את ערכם של תאי המערך‬
‫עליו מצביע )פרמטר הערך( ‪ ,ip‬כיצד נעשה זאת? התשובה היא‪ :‬כמו קודם‪ ,‬כלומר‬
‫נוסיף את מילת המפתח ‪ const‬לפני שם הטיפוס‪:‬‬
‫} … { )‪void f2(const int *ip‬‬
‫אם עתה ‪ f2‬תכלול פקודה כגון‪ ip[0]=0; :‬אזי התכנית לא תתקמפל בהצלחה‪.‬‬
‫שאלה‪ :‬האם בפונקציה ‪ f2‬אנו רשאים לשנות את ערכו של ‪ ,?ip‬כלומר‪ ,‬האם‬
‫פקודה כגון‪ ip = NULL; :‬או ;]‪ִ ip = new int[5‬ת ְמנע מהפונקציה‬
‫להתקמפל בהצלחה? תשובה‪ :‬לא; מילת המפתח ‪ const‬המופיעה לפני שם הטיפוס‬
‫רק מונעת מהפונקציה לשנות את ערכם של תאי המערך עליו מצביע ‪ ,ip‬היא אינה‬
‫מונעת מאתנו לשנות את ערכו של ‪ .ip‬נשים לב כי בפונקציה ‪ f2‬הפרמטר ‪ ip‬הוא‬
‫פרמטר ערך‪ ,‬ועל‪-‬כן שינויים שהפונקציה תכניס לתוכו לא יוותרו בארגומנט‬
‫המתאים ל‪ ip -‬אחרי ש‪ f2 -‬תסתיים‪.‬‬
‫נדגים את מה שהסברנו באמצעות הפונקציה ‪ .strlen‬בעבר ראינו את הגרסה‬
‫הבאה של הפונקציה‪:‬‬
‫{ )‪int strlen(char *s‬‬
‫; ‪int i‬‬
‫)‪for (i = 0; *s != '\0’; i++, s++‬‬
‫;‬
‫; ‪return i‬‬
‫}‬
‫הפונקציה מקבלת מצביע לתו‪ .‬היא מגדירה משתנה עזר ‪ ,i‬ורצה על הסטרינג‬
‫המועבר לה כל עוד התו עליו מצביע ‪ s‬שונה מ‪ .‘\0’ -‬בכל סיבוב בלולאה הפונקציה‬
‫גם מגדילה את ערכו של ‪ ,i‬וכך היא מונה כמה פעמים הסתובבנו בלולאה‪ ,‬במילים‬
‫אחרות מה אורכו של הסטרינג‪ .‬ערך זה הפונקציה מחזירה‪ .‬שימו לב כי גוף הלולאה‬
‫כולל את הפקודה הריקה‪ ,‬וזאת משום שכל העבודה שיש לבצע נעשית בכותרת‬
‫הלולאה‪ ,‬אשר גם מקדמת את ‪ i‬ואת ‪.s‬‬
‫‪93‬‬
‫הפונקציה ‪ strlen‬אינה אמורה לשנות את תוכנו של הסטרינג המועבר לה‪ .‬מאידך‪,‬‬
‫בגרסה שאנו כתבנו הפונקציה משנה את ערכו של הפרמטר ‪ ,s‬שכן בכל סיבוב‬
‫בלולאה היא מסיטה אותו לתא הבא במערך )באמצעות הפקודה ‪ s++‬שהינה‬
‫אריתמטיקה של מצביעים‪ ,‬המקדמת את המצביע ‪ s‬תא אחד הלאה במערך(‪ .‬האם‬
‫אנו מעונינים להוסיף את מילת המפתח ‪ const‬לפני הפרמטר ‪ s‬של ‪ ?strlen‬האם‬
‫אנו רשאים להוסיף את מילת המפתח ‪ const‬לפני הפרמטר ‪ s‬של ‪?strlen‬‬
‫התשובה לשתי השאלות היא‪ :‬כן‪ .‬מילת המפתח ‪ const‬תמנע מהפונקציה לשנות‬
‫את ערכם של תאי המערך‪ ,‬היא לא תמנע ממנה לשנות את ערכו של המצביע‪.‬‬
‫עתה ישאלו השואלים‪ :‬ומה קורה אם ברצוננו למנוע מהפונקציה גם לשנות את‬
‫ערכו של המצביע? התשובה היא שגם לכך יש פתרון‪ .‬ראשית‪ ,‬נזכיר כי שינוי ערכו‬
‫של מצביע שהינו פרמטר ערך אינו משפיע על הארגומנט המתאים‪ ,‬ועל‪-‬כן נזק נוראי‬
‫הוא לא יגרום‪ .‬שנית אם נתאר את הפרמטר באופן הבא‪ char * const s :‬אזי‬
‫בכך אנו מונעים מהפונקציה לשנות את ערכו של המצביע )אך לא את תוכנם של תאי‬
‫הזיכרון עליהם המצביע מורה(‪ .‬אנו רשאים גם לשלב את שתי ההגבלות‪:‬‬
‫‪ ,const char * const s‬ובכך נמנע מהפונקציה לשנות הן את ערכו של‬
‫המצביע‪ ,‬והן את ערכם של תאי הזיכרון עליהם המצביע מורה‪.‬‬
‫עתה נבדוק את התסריט הבא‪) :‬א( פרמטר כלשהו‪ ,‬בפונקציה כלשהי‪ ,‬הוגדר באופן‬
‫הבא‪) ,const int *ip :‬שכן רצינו למנוע מהפונקציה לשנות את ערכו של המערך‬
‫אחר הגדרנו משתנה לוקלי‪ , int *temp = ip; :‬כלומר‬
‫עליו מצביע ‪) ,(ip‬ב( ַ‬
‫הגדרנו מצביע ‪ temp‬והפננו אותו להצביע על המערך עליו מורה ‪) ,ip‬ג( לבסוף‬
‫כתבנו‪ . *temp = 0; :‬מה עשינו? 'עבדנו' על התכנית‪ ,‬ושינינו את ערכו של התא‬
‫מספר אפס במערך לא דרך הפרמטר ‪ ,ip‬אלא באמצעות המצביע ‪ .temp‬האומנם?‬
‫פריֵרית‪ .‬אם ‪ temp‬הוגדר כ‪ ,int *temp -‬אזי השפה תמנע‬
‫לא‪ ,‬ולא! שפת ‪ C‬אינה ַ‬
‫מכם לבצע את ההשמה‪ , temp = ip; :‬וזאת בדיוק כדי למנוע מכם לבצע‬
‫תעלולים )במילים אחרות שטויות( כפי שתיארנו קודם לכן‪ .‬ומה קורה אם ברצונכם‬
‫בכל אופן להכניס את ערכו של ‪ ip‬למשתנה ‪ ?temp‬הגדירו את ‪ temp‬באופן הבא‪:‬‬
‫;‪ . const int *temp‬עתה תוכלו להשים את ערכו של ‪ ip‬לתוך ‪ ,temp‬אך בשל‬
‫הדרך בה הוגדר ‪ temp‬כבר לא תוכלו לבצע השמה‪. *temp = 0; :‬‬
‫העקרונות שחלים ביחס למצביעים‪ ,‬מערכים ו‪ ,const -‬תקפים גם לגבי רשימות‬
‫משורשרות‪ .‬נבחן לדוגמה את הפונקציה ‪ disply_list‬שכתבנו בעבר‪ ,‬ואשר‬
‫אמורה להציג את הערכים השמורים ברשימה מקושרת‪:‬‬
‫{ ) ‪void display_list( struct Node *head‬‬
‫{ )‪while (head != NULL‬‬
‫; ‪cout << head -> _data‬‬
‫; ‪head = head -> _next‬‬
‫}‬
‫}‬
‫ראשית נשים לב כי אם מתכנת לא אחראי יוסיף מייד בתחילת הפונקציה את‬
‫הפקודה‪ head = NULL; :‬אזי הפונקציה לא תפעל כהלכה‪ ,‬אולם היא לא תפגע‬
‫ברשימה ה מקושרת שנבנתה‪ .‬שינוי שהוכנס ְלפרמטר הערך של הפונקציה לא ישפיע‬
‫על הארגומנט המתאים‪ ,‬והאחרון ימשיך להצביע על האיבר הראשון ברשימה‬
‫שבפונקציה אנו מקדמים את הפרמטר ‪ head‬ללא חשש‬
‫שנבנתה; זו גם הסיבה ַ‬
‫שנפגע בארגומנט ִעמו הפונקציה מזומנת‪.‬‬
‫אולם‪ ,‬אם המתכנת הלא אחראי יכתוב את הפקודה‪head-> _next = NULL; :‬‬
‫בעינה גם אחרי ביצוע הפונקציה; וזאת משום ש‪-‬‬
‫אזי הפגיעה שהוא מבצע תישאר ֵ‬
‫‪94‬‬
‫‪ head-> _next‬הוא מצביע המצוי בתאי הרשימה )המוקצים על הערמה(‪ .‬ותאים‬
‫אלה אינם משוכפלים עם הקריאה לפונקציה‪ .‬עם הקריאה לפונקציה משוכפל רק‬
‫הפרמטר ‪ .head‬שינוי שמתבצע על ‪ head-> _next‬חל על המצביע האחד והיחיד‬
‫היוצא מהאיבר הראשון ברשימה‪ ,‬ואין דרך לשחזר את ערכו של מצביע זה‪.‬‬
‫האם יש ביכולתנו לגרום לכך שהוספתה של פקודה כגון‪head-> _next = :‬‬
‫;‪ַ NULL‬לפונקציה ‪ display_list‬תמנע מהתכנית להתקמפל בהצלחה‪ ,‬ועל‪-‬ידי‬
‫כך תשמור עלינו מפני מתכנתים לא אחראיים? התשובה היא‪ :‬כן! את הפרמטר של‬
‫הפונקציה נגדיר כ‪ . const struct Node *head :‬בכך אנו מונעים הכנסת‬
‫שינויים לתאי הזיכרון עליהם ‪ head‬מצביע‪ ,‬אך איננו מונעים הכנסת שינויים לערכו‬
‫‪head‬עצמו‪ ,‬וטוב שכך‪ ,‬שכן בפונקציה אנו משנים את ערכו של ‪head‬‬
‫של‬
‫באמצעות הפקודה‪. head = head -> _next; :‬‬
‫בחלק ניכר מהפונקציות שכתבנו ניתן היה להגדיר את הפרמטר כ‪const -‬‬
‫‪ . struct Node *head‬אתם מוזמנים לסקור את הפונקציות השונות שהוגדרו‬
‫בפרק זה‪ ,‬ולאתר את אותן בהן היה ראוי לקבוע את הפרמטר כמצביע לקבוע‪.‬‬
‫‪ 13.11‬תרגילים‬
‫התרגילים המופיעים להלן נלקחו כולם מבחינות ומתרגילים שניתנו על‪-‬ידי מורים‬
‫במוסדות להשכלה גבוהה שונים‪ .‬על‪-‬כן התרגילים אינם קלים‪ ,‬ומחייבים השקעה‬
‫מרובה הן של מחשבה והן של עבודה‪.‬‬
‫‪13.11.1‬‬
‫תרגיל מספר אחד‪ :‬רשימות משורשרות של משפחות‬
‫בתרגיל זה נדון ברשימות משורשרות של משפחות משולשלות )במובן שושלות(‪.‬‬
‫כל משפחה בתרגיל תהיה משפחה גרעינית הכוללת לכל היותר אם‪ ,‬אב ומספר‬
‫כלשהו של ילדים‪ .‬נתוני משפחה יחידה ישמרו במבנה מהטיפוס‪:‬‬
‫‪struct family‬‬
‫{‬
‫; ‪char *family_name‬‬
‫‪char *mother_name, *father_name ; // parents' names‬‬
‫; ‪child *children_list‬‬
‫‪// list of the children in this family‬‬
‫; ‪family *next_family‬‬
‫‪// pointer to the next family in our database‬‬
‫;}‬
‫נתוני כל ילד ישמרו ב‪:‬‬
‫‪struct child‬‬
‫{‬
‫; ‪char *child_name‬‬
‫; ‪child *next_child‬‬
‫;}‬
‫מבנה הנתונים המרכזי של התכנית יהיה‪family *families :‬‬
‫התכנית תאפשר למשתמש לבצע את הפעולות הבאות‪:‬‬
‫‪ .10‬הזנת נתוני משפחה נוספת‪ .‬לשם כך יזין המשתמש את שם המשפחה )וזהו‬
‫מרכיב חובה(‪ ,‬ואת שמות בני המשפחה‪ .‬במידה והמשפחה אינה כוללת אם יזין‬
‫המשתמש במקום שם האם את התו מקף )‪) (-‬המרכיב ‪ mother_name‬במבנה‬
‫המתאים יקבע אז‪ ,‬כמובן‪ ,‬להיות ‪ ,(NULL‬באופן דומה יצוין שאין אב במשפחה‪,‬‬
‫גם סיום רשימת שמות הילדים יצוין באופן זה )הרשימה עשויה‪ ,‬כמובן‪ ,‬להיות‬
‫‪95‬‬
‫ריקה(‪ .‬לא תיתכן משפחה בה אין לא אם ולא אב )יש לתת הודעת שגיאה במידה‬
‫וזה הקלט מוזן‪ ,‬ולחזור על תהליך קריאת שמות ההורים(‪ .‬אתם רשאים להגדיר‬
‫משתנה סטטי יחיד מסוג מערך של תווים לתוכו תקראו כל סטרינג מהקלט‪ .‬את‬
‫רשימת המשפחות יש להחזיק ממוינת על‪-‬פי שם משפחה‪ .‬את רשימת ילדי‬
‫המשפחה החזיקו בסדר בה הילדים מוזנים‪ .‬הניחו כי שם המשפחה הוא מפתח‬
‫קבוצת המשפחות‪ ,‬כלומר לא תתכנה שתי משפחות להן אותו שם משפחה‬
‫)משמע ניסיון להוסיף משפחה בשם כהן‪ ,‬עת במאגר כבר קיימת משפחת כהן‬
‫מהווה שגיאה(‪ .‬אין צורך לבדוק כי לא מוזנים באותה משפחה שני ילדים בעלי‬
‫אותו שם‪.‬‬
‫דוגמה לאופן קריאת הנתונים‪:‬‬
‫‪Enter family name: Cohen‬‬
‫‪Enter father name: Yosi‬‬
‫ ‪Enter mother name:‬‬‫‪Enter children's names: Dani Dana -‬‬
‫‪ .11‬הוספת ילד למשפחה‪ .‬יוזן שם המשפחה ושמו של הילד‪ .‬הילד יוסף בסוף רשימת‬
‫הילדים‪ .‬במידה ומשפחה בשם הנ"ל אינה קיימת יש להודיע על‪-‬כך למשתמש‬
‫)ולחזור לתפריט הראשי(‪.‬‬
‫‪ .12‬פטירת בן משפחה‪ .‬יש להזין‪) :‬א( את שם המשפחה‪) ,‬ב( האם מדובר באם )ולשם‬
‫כך יוזן הסטרינג ‪ ,(mother‬באב )יוזן ‪ ,(father‬או באחד הילדים )יוזן ‪) (child‬ג(‬
‫במידה ומדובר בילד יוזן גם שם הילד שנפטר‪ .‬במידה ואין במאגר משפחה‬
‫כמצוין‪ ,‬או שבמשפחה אין בן‪-‬משפחה כמתואר )למשל אין אב והתבקשתם‬
‫לעדכן פטירת אב‪ ,‬או אין ילד בשם שהוזן( יש לדווח על שגיאה )ולחזור לתפריט‬
‫הראשי(‪ .‬כמו כן לא ניתן לדווח על פטירת הורה במשפחה חד‪-‬הורית )ראשית יש‬
‫להשיא את המשפחה למשפחה חד‪-‬הורית אחרת‪ ,‬כפי שמתואר בהמשך‪ ,‬ורק אז‬
‫יוכל ההורה המתאים לעבור לעולם שכולו טוב(‪.‬‬
‫‪ .13‬נשואי שתי משפחות קיימות‪ .‬יש לציין את שמות המשפחות הנישאות‪ ,‬ואת שם‬
‫המשפחה החדש שתישא המשפחה המאוחדת‪ .‬פעולה זאת לגיטימית רק אם‬
‫אחת משתי המשפחות אינה כוללת אם בעוד השניה אינה כוללת אב‪ .‬בעקבות‬
‫ביצוע הפעולה תאוחדנה רשימות הילדים )ראוי לעשות זאת ע"י מניפולציה של‬
‫מצביעים‪ ,‬ללא הקצאת מבנים חדשים!(‪ ,‬והמבנה שתיאר את אחת המשפחות‬
‫ישוחרר‪.‬‬
‫‪ .14‬הצגת נתוני המשפחות בסדר עולה של שמות משפחה‪) .‬זהו‪ ,‬כזכור‪ ,‬הסדר בו‬
‫מוחזקת רשימת המשפחות(‪ .‬עבור כל משפחה ומשפחה יש להציג את שם‬
‫המשפחה‪ ,‬ואת שמות בני המשפחה‪.‬‬
‫‪ .15‬הצגת נתוני משפחה בודדת רצויה‪ .‬יש להזין את שם המשפחה ויוצגו פרטי בני‬
‫המשפחה‪) .‬במידה והמאגר אינו כולל משפחה כמבוקש תוצג הודעת שגיאה(‪.‬‬
‫‪ .16‬הצגת נתוני המשפחות בסדר יורד של מספר בני המשפחה‪ .‬יש להציג את שם‬
‫המשפחה ואת מספר בני המשפחה‪ .‬לשם סעיף זה החזיקו רשימה מקושרת‬
‫נוספת‪ .‬כל איבר ברשימה יהיה מטיפוס‪:‬‬
‫‪struct family_size‬‬
‫{‬
‫‪family *the_family ; // pointer to a family at the families linked list‬‬
‫; ‪int size‬‬
‫; ‪// number of children in this family‬‬
‫‪family_size *next_families_sizes_item ; // pointer to next item in the current list‬‬
‫}‬
‫המצביע לראש הרשימה הזאת יהיה ‪ .family_size *families_sizes‬הרשימה‬
‫תמוין בסדר יורד של המרכיב ‪ .size‬על‪-‬ידי סריקה של רשימה זאת ופניה ממנה‬
‫בכל פעם למשפחה המתאימה )תוך שימוש במצביע ‪ (the_family‬תוכלו להציג את‬
‫‪96‬‬
‫המשפחות בסדר יורד של גודל המשפחות‪ .‬שימו לב כי יש גם לתחזק רשימה‬
‫זאת‪ :‬עת חל שינוי במשפחה כלשהי ברשימת המשפחות‪ ,‬ונניח כי המצביע ‪p‬‬
‫מצביע על אותה משפחה‪ ,‬יש לסרוק את הרשימה על ראשה מצביע‬
‫‪ ,families_sizes‬עד שמאתרים מרכיב ברשימה זאת שהמצביע ‪ the_family‬בו‬
‫מצביע לאותו מקום כמו המצביע ‪ ,p‬זוהי המשפחה שגודלה שונה‪ .‬עתה יש‬
‫להזיז את המרכיב המתאים ברשימת גודלי המשפחות למקום המתאים )על‪-‬ידי‬
‫הסרתו מהרשימה‪ ,‬והוספתו במקום חדש(‪ .‬בין כל המשפחות להן אותו גודל אין‬
‫חשיבות לסדר ההצגה‪ .‬תלמיד שלא יענה על סעיף זה ציונו המרבי בתרגיל יהיה‬
‫‪ ,90‬תלמיד שיענה על סעיף זה יקבל בונוס של עד ‪ 7‬נקודות‪ ,‬ובלבד שציונו לא‬
‫יעלה על מאה‪.‬‬
‫‪ .17‬הצגת נתוני שכיחות שמות ילדים‪ .‬יש להציג עבור כל שם ילד המופיע במאגר‪,‬‬
‫כמה פעמים הוא מופיע במאגר‪ .‬אין צורך להציג את הרשימה ממוינת‪.‬‬
‫)רמז\הצעה‪ :‬סרקו את מאגר המשפחות משפחה אחר משפחה‪ ,‬עבור כל משפחה‬
‫עיברו ילד אחר ילד‪ ,‬במידה ושמו של הילד הנוכחי עדיין לא מופיע ברשימת‬
‫הילדים ששמם הודפס סיפרו כמה פעמים מופיע שם הילד במאגר הנתונים‪,‬‬
‫ואחר הוסיפו את שמו של הילד לרשימת שמות הילדים שהוצגו(‪.‬‬
‫‪ .18‬סיום‪ .‬בשלב זב עליכם לשחרר את כל הזיכרון שהוקצה על‪-‬ידכם דינמית‪) .‬עבור‬
‫כל משפחה ומשפחה‪ :‬יש לעבור על רשימת הילדים ולשחרר כל מרכיב ברשימה‬
‫זאת‪ ,‬לפני שמשחררים כל קודקוד ברשימת הילדים יש לשחרר את הזיכרון‬
‫שמחזיק את שמו של הילד‪ ,‬אחרי שמשחררים את רשימת הילדים יש לשחרר‬
‫את הזיכרון שמחזיר את שמות ההורים ואת שם המשפחה(‪.‬‬
‫הערות כלליות‪:‬‬
‫ג‪ .‬התכנית תנהל כלולאה בה בכל שלב המשתמש בוחר בפעולה הרצויה לו‪ ,‬הפעולה‬
‫מתבצעת )או שמוצגת הודעת שגיאה(‪ ,‬והתכנית חוזרת לתפריט הראשי‪.‬‬
‫ד‪ .‬הקפידו שלא לשכפל קוד‪ ,‬למשל כתבו פונקציה יחידה אשר מציגה נתוני‬
‫משפחה‪ ,‬והשתמשו בה במקומות השונים בהם הדבר נדרש‪ .‬וכו'‪.‬‬
‫‪ Needless to say‬שיש להקפיד על כללי התכנות המקובלים‪ ,‬בפרט ובמיוחד‬
‫מודולריות‪ ,‬פרמטרים מסוגים מתאימים )פרמטרי ערך ‪ vs‬פרמטרים משתנים(‪ ,‬ערכי‬
‫החזרה של פונקציות ושאר ירקות‪ .‬תכנית רצה אינה בהכרח גם תכנית טובה! )קל‬
‫וחומר לגבי תכנית מקרטעת(…‬
‫‪13.11.2‬‬
‫תרגיל מספר שתיים‪ :‬איתור פרמידה ברשימה‬
‫בתכנית מוגדר‪:‬‬
‫‪struct struct Node‬‬
‫{‬
‫; ‪int _data‬‬
‫; ‪struct Node *next‬‬
‫; }‬
‫ברשימה מקושרת נגדיר פרמידה באורך ‪ ,2n +1‬כסדרה של איברים רציפים‬
‫ברשימה המסודרים באופן הבא‪ n +1 :‬האיברים הראשונים בפרמידה מקיימים‬
‫שהערך בכל איבר גדול מהערך באיבר שקדם לו‪ ,‬ו‪ n +1 -‬האיברים האחרונים‬
‫‪ ,n +1‬מקיימים שהערך בכל איבר קטן‬
‫בפרמידה‪ ,‬המתחילים בתא שמספרו‬
‫מהערך באיבר שקדם לו‪.‬‬
‫‪97‬‬
‫א‪ .‬כתבו שגרה המקבלת מצביע מטיפוס * ‪ struct Node‬לרשימה מקושרת‬
‫ומחזירה מצביע לפרמידה הארוכה ביותר המוכלת ברשימה‪ ,‬בפרמטר הפניה‬
‫יוחזר אורך הפרמידה‪.‬‬
‫לדוגמה ברשימה‪:‬‬
‫‪0‬‬
‫‪0‬‬
‫‪1‬‬
‫‪1‬‬
‫‪6‬‬
‫‪7‬‬
‫‪10‬‬
‫‪2‬‬
‫‪9‬‬
‫‪5‬‬
‫קיימות פרמידה באורך ‪ 5‬המורכבת מהאיברים‪ ,0 1 7 6 2 :‬ושלוש פרמידות‬
‫באורך ‪ 3‬המורכבות מהאיברים ‪0 1 0 ;1 7 6 ;2 10 9‬‬
‫במידה וקיימות כמה פרמידות מרביות באורכן ניתן להחזיר מצביע לאחת‬
‫מהן‪.‬‬
‫ב‪ .‬מה זמן הריצה של השגרה שכתבתם?‬
‫‪13.11.3‬‬
‫תרגיל מספר שלוש‪ :‬רשימה מקושרת דו‪-‬כיוונית‬
‫נגדיר‪:‬‬
‫‪struct struct Node‬‬
‫{‬
‫; ‪int _data‬‬
‫; ‪struct Node *next, *prev_odd_even‬‬
‫;}‬
‫בתכנית כלשהי ברצוננו לשמור רשימה מקושרת ממוינת אשר תתוחזק באופן‬
‫הבא‪:‬‬
‫המצביע _‪ next‬בכל תא יצביע על האיבר הבא ברשימה‪.‬‬
‫באיבר שהערך בו זוגי יצביע המצביע ‪ prev_odd_even‬לאיבר הזוגי הקודם‬
‫ברשימה; באיבר שהערך בו פרדי יצביע המצביע ‪ prev_odd_even‬לאיבר הפרדי‬
‫הקודם ברשימה‪.‬‬
‫דוגמה לרשימה אפשרית )המצביע _‪ next‬צויר בקו שלם‪ prev_odd_even ,‬בקו‬
‫מרוסק‪ .‬מצביעים שערכם ‪] NULL‬כגון _‪ next‬מהתא של ‪ ,8‬וכן ‪prev_odd_even‬‬
‫מהתאים של ‪ 2‬ושל ‪ [1‬הושמטו מהציור(‪:‬‬
‫‪8‬‬
‫‪4‬‬
‫‪7‬‬
‫‪2‬‬
‫‪1‬‬
‫כתבו פונקציה אשר מקבלת מצביע לרשימה מקושרת בעלת מבנה כמתואר‪,‬‬
‫וערך שלם‪ ,‬ומוסיפה את הערך לרשימה‪.‬‬
‫הערה‪ :‬חשיבות יתרה תינתן לשיקולי יעילות‪.‬‬
‫‪13.11.4‬‬
‫תרגיל מספר ארבע‪ :‬רשימה מקושרת עם מיון כפול‬
‫בתכנית מוגדר‪:‬‬
‫‪struct info‬‬
‫{‬
‫‪98‬‬
‫; ]‪int _data[2‬‬
‫; ]‪info *next[2‬‬
‫;}‬
‫בתכנית הוגדרו גם המצביעים ‪ ,info *head0, *head1‬ונבנתה רשימה מקושרת‪.‬‬
‫הרשימה נבנתה כך שאם נפנה אליה דרך המצביע ‪ head0‬ואחר נתקדם לאורכה‬
‫באמצעות המצביעים _ ]‪ next[0‬בכל תא‪ ,‬נסרוק את הרשימה כשהיא ממוינת על‪-‬‬
‫פי השדה _]‪ .data[0‬מנגד‪ ,‬אם נפנה לרשימה דרך המצביע ‪ head1‬ואחר נתקדם‬
‫לאורכה באמצעות המצביעים _ ]‪ next[1‬בכל תא‪ ,‬נסרוק את הרשימה כשהיא‬
‫ממוינת על‪-‬פי השדה _]‪.data[1‬‬
‫דוגמה לרשימה‪:‬‬
‫‪data[0] =6‬‬
‫‪data[1] =17‬‬
‫= ]‪next[0‬‬
‫= ]‪next[1‬‬
‫‪data[0] =5‬‬
‫‪data[1] =1‬‬
‫= ]‪next[0‬‬
‫= ]‪next[1‬‬
‫‪data[0] =2‬‬
‫‪data[1] =19‬‬
‫= ]‪next[0‬‬
‫= ]‪next[1‬‬
‫‪head0‬‬
‫‪head1‬‬
‫מקושרת כנ"ל‪ ,‬וכן‬
‫כתבו פונקציה המקבלת זוג מצביעים לרשימה‬
‫מערך ]‪ .int new_data[2‬הפונקציה תוסיף לרשימה איבר חדש שיחזיק את ערכי‬
‫][‪ new_data‬אם ורק אם לא קיים ברשימה איבר עם שדה _]‪ data[0‬השווה בערכו‬
‫ל‪ new_data[0] -‬וכן לא קיים ברשימה איבר עם שדה ]‪ data[1‬השווה בערכו ל‪-‬‬
‫]‪. new_data[1‬‬
‫‪13.11.5‬‬
‫תרגיל מספר חמש‪ :‬רשימה מקושרת עם מיון כפול‬
‫בתכנית מוגדר‪:‬‬
‫‪struct info‬‬
‫{‬
‫; ‪int _data‬‬
‫; ]‪info *next[2‬‬
‫;}‬
‫בתכנית הוגדר גם המצביע ‪ ,info *head‬ונבנתה רשימה מקושרת באופן הבא‪:‬‬
‫א‪ .‬המצביע ]‪ next[0‬מורה על הנתון הבא ברשימה‪ ,‬כלומר הנתון שהוכנס בעקבות‬
‫הנתון הנוכחי )במילים אחרות אם נסרוק את הרשימה תוך התקדמות על‬
‫המצביעים ]‪ next[0‬בכל תא‪ ,‬אזי נסרוק את הנתונים בסדר הכנסתם(‪.‬‬
‫ב‪ .‬המצביע ]‪ next[1‬בכל תא מורה על התא הבא ברשימה בו קיים אותו ערך כמו‬
‫בתא הנוכחי )כשאנו אומרים 'התא הבא' כוונתנו תא שנמצא בהמשך הרשימה‬
‫מבחינת סדר המצביעים ]‪ .(next[0‬במידה ולא קיים תא בהמשך בו מצוי אותו‬
‫ערך‪ ,‬יצביע המצביע בתא לאיבר הראשון ברשימה בו קיים הערך )כשאנו‬
‫אומרים 'ראשון ברשימה' אנו מתכוונים‪ :‬ראשון אם נסרוק את הרשימה‬
‫באמצעות ]‪ .(next[0‬בפרט‪ ,‬אם ברשימה קיים איבר יחיד בו יש ערך ‪ x‬כלשהו‪,‬‬
‫אזי ]‪ next[1‬באותו תא יצביע על התא עצמו‪.‬‬
‫‪99‬‬
‫דוגמה לרשימה‪ :‬סדר הכנסת הערכים‪) 2 :‬שבתא השמאלי(‪) 2 ,17 ,‬הימני(‬
‫‪data = 2‬‬
‫= ]‪next[0‬‬
‫= ]‪next[1‬‬
‫‪data = 17‬‬
‫= ]‪next[0‬‬
‫= ]‪next[1‬‬
‫‪data = 2‬‬
‫= ]‪next[0‬‬
‫= ]‪next[1‬‬
‫‪head‬‬
‫כתבו פונקציה המקבלת מצביע לרשימה מקושרת כנ"ל‪ ,‬וערך שלם ‪,val‬‬
‫הפונקציה תוסיף לרשימה איבר חדש שיחזיק את הערך ‪.val‬‬
‫‪13.11.6‬‬
‫תרגיל מספר שש‪ :‬מאגר ציוני תלמידים‬
‫כתבו תכנית המטפלת בציוני תלמידים‪ .‬לכל תלמיד תחזיק התכנית את הנתונים‬
‫הבאים‪) :‬א( שם משפחה‪) ,‬ב( שם פרטי‪) ,‬ג( רשימת הקורסים שהתלמיד לקח‪ ,‬כאשר‬
‫לכל קורס יוחזק‪ (1) :‬שם הקורס‪ (2) ,‬כמות השעות השבועיות המוקצות לקורס )‪(3‬‬
‫ציונו של התלמיד בקורס‪.‬‬
‫התכנית תאפשר למשתמש לבצע את הפעולות הבאות‪:‬‬
‫‪ .1‬תוספת תלמיד חדש לרשימת התלמידים )במידה וברשימה כבר קיים תלמיד‬
‫שזה שמו תתקבל הודעת שגיאה(‪.‬‬
‫‪ .2‬מחיקת תלמיד מרשימת התלמידים )במידה והתלמיד המבוקש לא קיים‬
‫ברשימה תתקבל הודעת שגיאה(‪.‬‬
‫‪ .3‬הוספת קורס נוסף לתלמיד המצוי ברשימה )במידה והתלמיד כבר רשום לאותו‬
‫קורס תינתן הודעת שגיאה(‪.‬‬
‫‪ .4‬ביטול קורס לו רשום תלמיד כלשהו )במידה והתלמיד אינו מופיע ברשימה‪ ,‬או‬
‫שהתלמיד אינו רשום לאותו קורס תינתן הודעת שגיאה(‪.‬‬
‫‪ .5‬עדכון ציון של תלמיד כלשהו בקורס כלשהו )הודעות שגיאה בהתאם(‪.‬‬
‫‪ .6‬הצגת רשימת הקורסים והציונים עבור תלמיד מבוקש כלשהו; הרשימה‬
‫המוצגת תחולק לשניים ראשית יוצגו הקורסים אותם התלמיד עבר‪ ,‬ואחר‬
‫הקורסים בהם התלמיד נכשל‪ .‬מעבר להדפסת הרשימה הנ"ל יודפס גם ממוצע‬
‫ציוני התלמיד )ממוצע משוקלל שישקלל עבור כל קורס את משקלו == מספר‬
‫השעות השבועיות המוקצות לקורס(‪ ,‬ומספר הקורסים בהם התלמיד נכשל‪.‬‬
‫מבנה הנתונים המרכזי בתכנית יוגדר באופן הבא‪:‬‬
‫‪struct course // holds info about a single course‬‬
‫{‬
‫; ‪char *course_name‬‬
‫; ‪int grade‬‬
‫;}‬
‫‪struct stud // holds info about a single student‬‬
‫{‬
‫; ‪char *last, *first‬‬
‫‪course *courses ; // array of courses stud took‬‬
‫‪stud *next_stud ; // pointer to the next student in‬‬
‫‪// the students’ list‬‬
‫; }‬
‫‪100‬‬
‫פי שם משפחה‬-‫התכנית תחזיק רשימה מקושרת ממוינת של התלמידים )ממוינת על‬
‫ רשימת הקורסים של כל תלמיד תוחזק כמערך ממוין )על פי שם‬.(‫ שם פרטי‬+
‫קורס( שיוקצה באופן דינמי ויוגדל בכל פעם שיש להוסיף קורס נוסף ואין די מקום‬
.‫במערך בגודלו הנוכחי‬
:‫התכנית הראשית תראה לערך כך‬
void main()
{
stud *class ; // pointer to the list of students
... prototypes should be declared here...
init(class) ;
do
{
int option = get_option() ;
switch (option)
{
case ADD_STUD : add_stud(&class) ; break ;
case REM_STUD : rem_stud(&class) ; break ;
case ADD_COURSE : add_course(class) ; break ;
case REM_COURSE : rem_course(class) ; break ;
case UPDATE_GRADE : update_grade(class) ; break ;
case DISPLAY_GRADES : display_grades(class) ; break ;
case END : dispose(&class) ; break
}
}
while (option != END) ;
}
:‫ תראה‬add_stud :‫השגרה‬
void add_stud(stud **my_class)
{
void get_stud_name( char **last_name, char **first_name) ;
void add_stud_to_list(stud **a_class, char *a_last, char *a_first) ;
char *last_n, *first_n ;
get_stud_name(&last_n, &first_n) ; // read last/first name from user
add_stud_to_list(my_class, last_n, first_n) ;
}
...‫ואידך זיל גמור‬
‫ מיון הכנסה של רשימה מקושרת‬:‫תרגיל מספר שבע‬
13.11.7
:‫בתכנית מוגדר‬
struct info
{
int _data ;
info *next ;
101
‫;}‬
‫א‪ .‬כתבו פונקציה אשר מקבלת כפרמטר מצביע לראשה של רשימה מקושרת‬
‫כנ"ל‪ .‬על הפונקציה למיין את אברי הרשימה בסדר עולה‪ .‬בתום פעולתה של‬
‫הפונקציה יצביע הפרמטר לראשה של הרשימה הממוינת‪.‬‬
‫מיון הרשימה יתבצע באופן הבא‪ :‬במעבר מספר ‪ i‬על הרשימה )…‪(i = 1, 2, 3,‬‬
‫יאותר האיבר ה‪-i -‬י בקוטנו )לדוגמה‪ :‬במעבר הראשון יאותר האיבר הכי‬
‫קטן‪ ,‬במעבר השני יאותר האיבר השני הכי קטן‪ ,‬וכו'(‪ ,‬ויועבר למקום ה‪-i -‬י‬
‫ברשימה )לדוגמה‪ :‬האיבר השני הכי קטן יועבר למקום השני ברשימה(‪.‬‬
‫הערות‪) :‬א( הקפידו על יעילות הפונקציה שתכתבו‪) .‬ב( עדיף שהפונקציה לא‬
‫תקצה תאים חדשים! )ג( אין להניח דבר על תכולתה של הרשימה המועברת‬
‫לפונקציה‪) .‬ד( כרגיל‪ ,‬תנו דעתכם למודולריות‪.‬‬
‫ב‪ .‬מה זמן הריצה של הפונקציה שכתבתם? הסבירו בקצרה‪.‬‬
‫‪13.11.8‬‬
‫תרגיל מספר שמונה‪ :‬פולינומים‬
‫בפרק שש הצגנו תכנית המטפלת בפולינומים‪ .‬בפרק אחת‪-‬עשרה שינינו את מבנה‬
‫הנתונים באמצעותו יישמרו הפולינומים לכדי מערך של מבנים מטיפוס ‪.monom‬‬
‫עתה ברצוננו להכניס שינוי נוסף במבנה הנתונים‪ ,‬ולייצג כל פולינום כרשימה‬
‫מקושרת של מונומים‪ .‬נוכל לעשות זאת באופן הבא‪:‬‬
‫{ ‪struct monom‬‬
‫; ‪float coef, degree‬‬
‫; }‬
‫כלומר מונום מיוצג כמו בפרק אחת‪-‬עשרה‪.‬‬
‫ייצוגו של פולינום יהיה‪:‬‬
‫{ ‪struct polynom‬‬
‫; ‪monom a_monom‬‬
‫; ‪polynom *next_monom‬‬
‫;}‬
‫סדרת פולינומים תיוצג באופן הבא‪:‬‬
‫{ ‪list_of_polynoms‬‬
‫; ‪polynom *a_polynom‬‬
‫; ‪list_of_polynoms next_poly‬‬
‫; }‬
‫ובתכנית נחזיק משתנה‪list_of_polynoms *database; :‬‬
‫אתם מוזמנים‪ ,‬ראשית‪ ,‬להתעמק במבנה הנתונים ולהבינו‪ ,‬ושנית לכתוב את תכנית‬
‫הפולינומים תוך שימוש במבנה נתונים זה‪.‬‬
‫‪13.11.9‬‬
‫תרגיל מספר תשע‪ :‬הפרדת רשימות משורשרות שהתמזגו‬
‫בתכנית כלשהי מוגדר ‪ struct Node‬כפי שמוכר לנו היטב‪ ,‬והוגדרו‬
‫;‪.Node head1, head2‬‬
‫‪102‬‬
‫‪struct‬‬
‫בתכנית עשוי לקרות מצב בו שתי הרשימות המשורשרות עליהן מצביעים ‪head1,‬‬
‫‪ head2‬התמזגו החל ממקום מסוים‪ ,‬כלומר קיימים איברים השייכים הן לרשימה‬
‫עליה מצביע ‪ head1‬והן לרשימה עליה מצביע ‪.head2‬‬
‫לדוגמה‪:‬‬
‫‪3‬‬
‫‪2‬‬
‫‪5‬‬
‫‪2‬‬
‫‪1‬‬
‫‪1‬‬
‫עליכם לכתוב פונקציה המקבלת שני מצביעים לשתי רשימות‪ ,‬ואשר מפרידה את‬
‫הרשימות באופן הבא‪ :‬כל התאים השייכים )גם( לרשימה עליה מצביע הפרמטר‬
‫הראשון ישויכו‪ ,‬בעקבות ביצוע הפונקציה‪ ,‬אך ורק לרשימה זאת‪ .‬כל האיברים‬
‫השייכים רק לרשימה עליה מצביע הפרמטר השני ישויכו לרשימה עליה מורה‬
‫פרמטר זה‪.‬‬
‫בדוגמה שלנו‪ ,‬תיראנה הרשימות בתום ביצוע הפונקציה באופן הבא‪:‬‬
‫‪3‬‬
‫‪2‬‬
‫‪5‬‬
‫‪2‬‬
‫‪1‬‬
‫‪1‬‬
‫‪13.11.10‬‬
‫תרגיל מספר עשר‪ :‬בדיקת איזון סוגריים‬
‫מחסנית היא מבנה נתונים )במילים אחרות משתנה( המתנהל על‪-‬פי העיקרון הבא‪:‬‬
‫בקצה המחסנית )כלומר בקצה המשתנה‪ ,‬ולא באמצעו(‪ .‬כל‬
‫כל איבר חדש מוסף ְ‬
‫איבר שמוסר מהמחסנית מוסר מאותו קצה אליו מוּספים הנתונים החדשים )ועל כן‬
‫המחסנית מתנהלת בשיטה 'אחרון שנוסף הוא הראשון שמוסר' ובלע"ז ‪last in first‬‬
‫‪ out‬ובקיצור ‪ .LIFO‬נקל לממש מחסנית באמצעות רשימה מקושרת‪ :‬כל איבר חדש‬
‫יתווסף בראש הרשימה; בעת שיש להסיר איבר שולפים את זה המצוי בראש‬
‫הרשימה‪.‬‬
‫כתבו תכנית אשר קוראת ביטוי חשבוני שעשוי להכיל סוגריים ממסר סוגים‪) :‬א( ) (‪,‬‬
‫)ב( ] [‪) ,‬ג( } {‪) ,‬ד( > < ‪ .‬התכנית תבדוק האם הסוגריים בביטוי מאוזנים כהלכה‪.‬‬
‫)התכנית לא תתעניין כלל ביתר מרכיבי הביטוי פרט לסוגריים‪.‬‬
‫לדוגמה הביטוי‪ (5+3)*< (2+2) / ([6 / 7] + 2) > -6 :‬הוא ביטוי תקין‪ ,‬אך הביטויים‬
‫הבאים אינם תקינים‪:‬‬
‫)א( ‪) (5+3*< (2+2) / ([6 / 7] + 2) > -6‬בביטוי זה נשמט הסוגר הימני שהופיע בביטוי‬
‫הראשון מייד אחרי ה‪.(3 -‬‬
‫)ב( )ב( ‪) (5+3)*< (2+2) / ([6 / 7] + 2) ) –6‬בביטוי זה הסוגר הימני הזוויתי שהופיע‬
‫בביטוי המקורי לפני ‪ 6- -‬הוחלף בסוגר מעוגל(‪.‬‬
‫‪103‬‬
‫)ג( )ג( ‪) (5+3)*< (2+2) / ([6 / 7) + 2] > -6‬בביטוי זה הסוגר המרובע שהופיע במקור‬
‫אחרי ה‪ ,7 -‬הומר בסוגר מעוגל‪ ,‬והסוגר המעוגל שהופיע אחרי ה‪ 2 -‬השלישי‬
‫הומר בסוגר מרובע‪.‬‬
‫)ד( (‪.)5+5‬‬
‫האלגוריתם לבדיקת ביטוי אותו עליכם לממש הוא כמתואר‪:‬‬
‫קרא את הביטוי מרכיב אחר מרכיב )כאשר מארכיב עשוי להיות‪ :‬מספר טבעי‬
‫כלשהו‪ ,‬סימן פעולה‪ ,‬או סוגר(‪:‬‬
‫א‪ .‬במידה ונתקלת בסוגר פותח מסוג כלשהו דחף אותו על‪-‬גבי המחסנית‪.‬‬
‫ב‪ .‬במידה ונתקלת בסוגר סוגר מסוג כלשהו בדוק האם בראש המחסנית מצוי סוגר‬
‫פותח מאותו סוג; אם אכן מצוי סוגר כנדרש אזי שלוף אותו מהמחסנית‪,‬‬
‫אחרת‪ :‬הודע על שגיאה‪.‬‬
‫בתום קריאת הביטוי בדוק האם המחסנית ריקה‪ ,‬אם לא הודע על שגיאה‪.‬‬
‫הניחו כי כל ביטוי יוזן בשורה יחידה‪ ,‬וכי לא יוזנו שני ביטויים שונים באותה‬
‫שורה‪.‬‬
‫‪13.11.11‬‬
‫תרגיל מספר אחת‪-‬עשרה‪ :‬המרת תא ברשימה ברשימה‬
‫בתכנית הוגדר המבנה ‪ node‬כמקובל‪.‬‬
‫כתבו את הפונקציה ‪ replace‬אשר מקבלת‪:‬‬
‫א‪ .‬מצביע ‪ head_main‬לראשה של רשימה מקושרת אחת‪.‬‬
‫ב‪ .‬מצביע ‪ head_secondary‬לראשה של רשימה מקושרת שניה‪.‬‬
‫ג‪ .‬מספר שלם ‪.wanted‬‬
‫הפונקציה תחליף כל תא המכיל את הערך ‪ wanted‬ברשימה עליה מצביע‬
‫‪ head_main‬באברי הרשימה ‪.head_secondary‬‬
‫לדוגמה‪ :‬אם ‪ head_main‬מצביע על הרשימה‪:‬‬
‫‪5‬‬
‫‪2‬‬
‫‪7‬‬
‫ו‪ head_secondary -‬מצביע על הרשימה‪:‬‬
‫‪2‬‬
‫‪3‬‬
‫‪9‬‬
‫‪6‬‬
‫וערכו של ‪ waned‬הוא שתיים‪ ,‬אזי בתום פעולה של הפונקציה יצביע ‪head_main‬‬
‫על הרשימה‪:‬‬
‫‪5‬‬
‫‪9‬‬
‫‪6‬‬
‫‪7‬‬
‫‪9‬‬
‫‪6‬‬
‫‪3‬‬
‫‪ 13.11.12‬תרגיל מספר שתים‪-‬עשרה‪ :‬מחיקת איבר מינימלי מסדרות‬
‫המרכיבות רשימה מקושרת‬
‫בתכנית הוגדר ‪ struct struct Node‬כמקובל‪ ,‬ונבנתה רשימה מקושרת‪ .‬אנו‬
‫אומרים כי הרשימה ה מקושרת מורכבת ממספר סדרות של מספרים‪ .‬כל סדרה‬
‫מסתיימת בערך אפס )שאינו חלק מהסדרה(‪ ,‬ובתא שאחר‪-‬כך מתחילה הסדרה‬
‫הבאה‪.‬‬
‫‪104‬‬
‫לדוגמה‪ :‬הרשימה ה מקושרת הבאה‪:‬‬
‫‪0‬‬
‫‪1‬‬
‫‪2‬‬
‫‪5‬‬
‫‪0‬‬
‫‪2‬‬
‫‪9‬‬
‫‪2‬‬
‫‪0‬‬
‫‪5‬‬
‫מורכבת מהסדרות‪) :‬א( ‪) , 5‬ב( ‪) ,2 ,9 ,2‬ג( ‪.1 ,2 ,5‬‬
‫כתבו פונקציה המקבלת מצביע לרשימה מקושרת כנ"ל‪ ,‬ומוחקת את האיבר‬
‫המזערי בכל סדרה‪ .‬במידה ובסדרה כלשהי מופיע האיבר המזערי מספר פעמים יש‬
‫למחוק את המופע הראשון שלו‪ .‬במידה ואחרי מחיקת האיבר המזערי הסדרה‬
‫התרוקנה )שכן הוא היה האיבר היחיד בסדרה(‪ ,‬יש למחוק גם את האיבר המכיל‬
‫את הערך אפס‪ ,‬והמציין את תום הסדרה‪.‬‬
‫לדוגמה‪ :‬אחרי ביצוע הפונקציה תראה הרשימה הנ"ל באופן הבא‪:‬‬
‫‪0‬‬
‫‪2‬‬
‫‪105‬‬
‫‪5‬‬
‫‪0‬‬
‫‪2‬‬
‫‪9‬‬
‫עודכן ‪1/2011‬‬
‫‪ 14‬עצים בינאריים‬
‫עץ בינארי הוא אמצעי לאחסון נתונים‪ .‬אנו אומרים כי עץ בינארי הוא מבנה נתונים‬
‫מופשט )‪ ,(Abstract Data Type, ADT‬אותו נוכל לממש בתכנית מחשב בדרכים שונות‪.‬‬
‫‪ 14.1‬הגדרת עץ בינארי‬
‫עת בינארי מוגדר באופן הבא‪:‬‬
‫א‪ .‬עץ בינארי עשוי להיות ריק‪.‬‬
‫ב‪ .‬עץ בינארי שאינו ריק מכיל‪:‬‬
‫‪ .1‬שורש )שהינו צומת בעץ(‪.‬‬
‫‪ .2‬ילד שמאלי שהינו עץ בינארי‪.‬‬
‫‪ .3‬ילד ימני שהינו עץ בינארי‪.‬‬
‫ג‪ .‬כל צומת )‪ (node‬בעץ בינארי הוא ילד של צומת אחד בדיוק‪ ,‬פרט לצומת יחיד‪,‬‬
‫הקרוי שורש העץ‪ ,‬שאינו ילד של אף צומת אחר‪) .‬יש המשתמשים במילה‬
‫'קודקוד' במקום במילה 'צומת'‪ .‬בעבר‪ ,‬עת העולם היה פחות תקין פוליטית‪,‬‬
‫נהגו להשתמש במונחים אב ובנים בקשר לעץ‪ ,‬כיום הוחלפו הכינויים להורה‬
‫וילדים‪(.‬‬
‫אנו רואים כי הגדרת עץ בינארי היא רקורסיבית‪ :‬עץ בינארי מוגדר תוך שימוש‬
‫במונח עץ בינארי; אולם בשלב זה של חיינו הדבר אינו גורם לנו למורא )קל וחומר‬
‫שלא למשוא פנים(‪ ,‬שכן יש גם מקרה קצה‪ :‬העץ עשוי להיות ריק‪.‬‬
‫נביט בדוגמה של עץ בינארי בה כל צומת מכיל נתון מספרי‪:‬‬
‫‪17‬‬
‫‪77‬‬
‫‪13‬‬
‫‪83‬‬
‫נבדוק האם ומדוע זהו עץ בינארי?‬
‫א‪ .‬הוא אינו ריק‪ ,‬לכן עליו להכיל שורש‪ ,‬ילד שמאלי שהוא עץ בינארי‪ ,‬וילד ימני‬
‫שהינו עץ בינארי‪ .‬הצומת המכיל את ‪ 17‬הוא שורש העץ‪ ,‬ועתה כדי להשלים את‬
‫תהליך ההצדקה יש להראות כי ילדו השמאלי של הצומת של ‪ ,17‬וילדו הימני של‬
‫הצומת של ‪ 17‬הם עצים בינאריים‪ .‬ילדו השמאלי של השורש הוא תת‪-‬העץ‪:‬‬
‫‪13‬‬
‫וילדו הימני של השורש הוא תת‪-‬העץ‪:‬‬
‫‪83‬‬
‫‪106‬‬
‫‪77‬‬
‫ב‪ .‬עתה נראה כי האובייקט‪:‬‬
‫‪13‬‬
‫הוא עץ בינארי‪ .‬הוא כולל שורש‪ ,‬שהינו‬
‫הצומת של ‪ ,13‬ושני הילדים שהינם ריקים וככאלה הם עצים בינאריים‪.‬‬
‫ג‪ .‬עתה נבחן את‪:‬‬
‫‪77‬‬
‫‪83‬‬
‫הוא אינו ריק‪ .‬הוא כולל שורש שהינו הצומת של ‪ ,77‬ילד שמאלי שהינו ריק‬
‫)ועל‪-‬כן הוא עץ בינארי(‪ ,‬ילד ימני שכולל את‪ :‬הצומת של ‪ ,83‬ושני ילדים ריקים‪.‬‬
‫הילד הימני של ‪ 77‬הוא עץ בינארי מאותה סיבה שתת‪-‬העץ שכלל את ‪ 13‬הוא עץ‬
‫בינארי‪ .‬הילד השמאלי של ‪ 77‬הוא עץ בינארי שכן הוא ריק‪ ,‬ולכן גם כל הציור‬
‫שבסעיף ג' הוא עץ בינארי‪.‬‬
‫מעבר למה שבדקנו נשים לב כי כל צומת בעץ הוא ילד של צומת אחד בדיוק‪ ,‬פרט‬
‫לצומת של ‪ 17‬שאינו ילד של אף צומת אחר בעץ‪.‬‬
‫‪107‬‬
‫נשים לב כי ארבעת הבאים אינם עצים בינאריים‪ ,‬ואני מזמין אתכם לבחון מדוע כל‬
‫אחד ואחד מהם אינו עונה על ההגדרה‪:‬‬
‫‪77‬‬
‫‪57‬‬
‫‪83‬‬
‫‪17‬‬
‫‪77‬‬
‫‪17‬‬
‫‪83‬‬
‫‪57‬‬
‫‪77‬‬
‫‪57‬‬
‫‪88‬‬
‫‪83‬‬
‫‪17‬‬
‫‪77‬‬
‫‪83‬‬
‫‪17‬‬
‫עצים בינאריים נקראים 'בינאריים' שכן לכל צומת בהם ייתכנו לכל היותר שני‬
‫ילדים‪.‬‬
‫עץ בינארי יקרא עץ חיפוש בינארי )‪ binary search tree‬או בקיצור ‪ (BST‬אם הוא‬
‫מקיים את התנאי הבא‪ :‬עבור כל צומת ‪ n‬בעץ מתקיים שכל הערכים המצויים בתת‬
‫העץ השמאלי של ‪ n‬קטנים או שווים מהערך המצוי בצומת ‪ ,n‬וכל הערכים המצויים‬
‫בתת‪-‬העץ הימני של ‪ n‬גדולים מהערך המצוי ב‪ .n -‬העץ הראשון שהצגנו הוא עץ‬
‫חיפוש בינארי‪ :‬בתת‪-‬העץ השמאלי של הצומת של ‪ 17‬מצוי ערך קטן מ‪ ,17 -‬ובתת‬
‫העץ הימני ערכים גדולים מ‪ .17 -‬גם עבור הצומת של ‪ 77‬בתת‪-‬העץ הימני מצוי ערך‬
‫גדול מ‪ .77 -‬לו היינו ממירים את הערך ‪ 83‬בערך ‪ 53‬אזי העץ כבר לא היה עץ חיפוש‬
‫שכן בתת‪-‬העץ הימני של ‪ 77‬כבר לא היו רק ערכים גדולים מ‪) .77 -‬אעיר כי מסיבות‬
‫שונות יש המאפשרים לערך השווה לערך המצוי בשורש של עץ חיפוש בינארי‬
‫להימצא בתת העץ השמאלי או בתת העץ הימני; אצלנו ערך שווה יימצא בהכרח‬
‫בתת העץ השמאלי(‪ .‬תלימידים לעתים מכנים עץ חיפוש בינארי בשם עץ ממוין; זה‬
‫אינו שם מופרך‪ ,‬אך גם לא מקובל באופן 'רשמי'‪.‬‬
‫‪ 14.2‬מימוש עץ בינארי בתכנית מחשב‬
‫עד כה הצגנו את העץ הבינארי בצורתו המופשטת‪ ,‬עתה נפנה לשאלה כיצד נייצג את‬
‫העץ בתכנית מחשב בשפת ‪ ?C‬בשלב הנוכחי של חיינו המשותפים רבים מכם ודאי‬
‫יציעו לעשות זאת באמצעות מצביעים‪ .‬זו אכן תשובה מתאימה‪ ,‬ומייד נפנה להציגה‬
‫בצורה מלאה; אולם ראשית נִ ראה כי ניתן לייצג עצים בינאריים גם באמצעות‬
‫‪108‬‬
‫מערכים; כלומר את מבנה הנתונים המופשט 'עץ בינארי' ניתן לממש בתכנית מחשב‬
‫במספר אופנים‪.‬‬
‫כיצד ימומש עץ בינארי באמצעות מערך?‬
‫‪ .I‬תא מספר אחד במערך יכיל את הנתונים שבשורש העץ‪) .‬בתא מספר אפס‬
‫במערך לא נעשה כל שימוש(‬
‫‪ .II‬אם הנתונים שבצומת ‪ n‬של העץ נשמרו בתא מספר ‪ x‬במערך‪ ,‬אזי הנתונים‬
‫שבילדו השמאלי של ‪ n‬יישמרו בתא מספר ‪ ,2*x‬והנתונים שבילדו הימני של ‪n‬‬
‫יישמרו בתא מספר ‪.2*x +1‬‬
‫לדוגמה‪ ,‬העץ שראינו בתחילת הפרק יישמר במערך של מספרים שלמים באופן‬
‫הבא‪:‬‬
‫‪83‬‬
‫‪77‬‬
‫‪13‬‬
‫‪17‬‬
‫‪#9‬‬
‫‪#8‬‬
‫‪#7‬‬
‫‪#6‬‬
‫‪#5‬‬
‫‪#4‬‬
‫‪#3‬‬
‫‪#2‬‬
‫‪#1‬‬
‫‪#0‬‬
‫לתאים בהם לא מצוין ערך יש להתייחס כאל תאים ריקים‪ .‬אנו רואים כי שורש‬
‫העץ‪ ,‬הנתון ‪ ,17‬מצוי בתא ‪ ,#1‬שני ילדיו של השורש שמורים בתאים ‪ .#3 ,#2‬לצומת‬
‫שמכיל את הערך ‪) 13‬ושמצוי בתא ‪ #2‬במערך( אין ילדים‪ ,‬ועל כן התאים ‪#5 ,#4‬‬
‫במערך נותרו ריקים‪ .‬גם לצומת שמכיל את ‪ 77‬אין ילד שמאלי‪ ,‬ועל כן התא ‪ #6‬נותר‬
‫ריק‪ .‬התא ‪ #7‬מכיל את ילדו הימני של הצומת שנתוניו מופיעים בתא ‪.#3‬‬
‫כמובן שגודלו של העץ שניתן לייצג חסום על‪-‬ידי גודלו של המערך‪.‬‬
‫עתה נפנה לייצוג עץ בינארי באמצעות מצביעים‪ .‬לאורך הפרק נניח כי כל צומת בעץ‬
‫מכיל נתון מספרי בודד‪ .‬במקרה הכללי הצומת עשוי להכיל מספר נתונים‪ ,‬שיקובצו‪,‬‬
‫קרוב לודאי‪ ,‬ל‪ , struct -‬ואז במקום השדה ‪ int _data‬יהיה לנו שדה ‪struct‬‬
‫‪ . info‬נציג עתה את המבנה הבסיסי שישמש אותנו לאורך הפרק‪:‬‬
‫{ ‪struct Node‬‬
‫; ‪int _data‬‬
‫; ‪struct Node *_left, *_right‬‬
‫; }‬
‫טיפוס המבנה שהגדרנו כולל חבר שמכיל את הנתון המספרי‪ ,‬ושני מצביעים לשני‬
‫ילדיו של הצומת‪.‬‬
‫‪ 14.3‬בניית עץ חיפוש בינארי‬
‫נניח כי בתכניתנו הוגדר‪ . struct Node *root; :‬המצביע ‪ root‬יצביע לשורשו‬
‫של העץ הבינארי שנבנה בתכנית‪ .‬את הפונקציה ‪ ,build_bst‬שבונה את העץ‪ ,‬אנו‬
‫מזמנים באופן הבא‪ . root = build_bst() ; :‬הפונקציה ‪ build_bst‬תבנה‬
‫עץ חיפוש בינארי‪ ,‬ותחזיר מצביע לשורש העץ שנבנה‪ .‬מצביע זה יוכנס למשתנה‬
‫הבא‪:‬‬
‫באופן‬
‫הפונקציה‬
‫את‬
‫לכתוב‬
‫יכולנו‬
‫לחילופין‬
‫‪.root‬‬
‫;)‪ . other_build_bst(root‬כלומר יכולנו להעביר לפונקציה את המשתנה‬
‫‪ root‬כארגומנט‪ .‬הפונקציה ‪ other_build_bst‬הייתה מקבלת פרמטר הפניה‪:‬‬
‫&* ‪ .struct Node‬כפי שציינו עת דנו בפונ'‪ ,‬מבחינת סגנון תכנותי‪ ,‬הגרסה בה‬
‫מחזירים ערך‪ ,‬עדיפה על‪-‬פני זו המקבלת את המצביע כפרמטר הפניה‪.‬‬
‫נציג את הפונקציה ‪:build_bst‬‬
‫{ )(‪struct Node *build_bst‬‬
‫; ‪struct Node *root = NULL‬‬
‫‪109‬‬
‫; ‪int num‬‬
‫‪cin >> num‬‬
‫{ )‪while (num != 0‬‬
‫; )‪insert_into_bst(num, root‬‬
‫; ‪cin >> num‬‬
‫}‬
‫; ‪return root‬‬
‫}‬
‫בפונקציה ‪ build_bst‬אין רבותא‪ .‬הדבר היחיד עליו ראוי לתת את הדעת הוא‬
‫שהיא מגדירה מצביע‪). struct Node *root; :‬כלומר משתנה המוקצה על גבי‬
‫המחסנית(‪ ,‬מצביע זה יורה על שורש העץ הבינארי שייבנה )ושצמתיו ישכנו ַבערמה(‪.‬‬
‫עם סיום ריצתה מחזירה הפונקציה את המצביע לשורש העץ‪.‬‬
‫‪110‬‬
‫‪ 14.3.1‬הכנסה לעץ חיפוש באמצעות פונ' רקורסיבית‬
‫הפונקציה שתעניין אותנו יותר היא ‪ .insert_into_bst‬נציג שלוש גרסות שונות‬
‫שלה‪.‬‬
‫נתחיל בראשונה‪:‬‬
‫{ )‪void insert_into_bst(int num, struct Node *&root‬‬
‫{ )‪if (root == NULL‬‬
‫; ‪root = new (std::nothrow) struct Node‬‬
‫)‪if (root == NULL‬‬
‫; )‪terminate(“Can not allocate memory\n", 1‬‬
‫; ‪root -> _data = num‬‬
‫; ‪root->_left = root->_right = NULL‬‬
‫}‬
‫)‪else if (num <= root->_data‬‬
‫; )‪insert_into_bst(num, root->_left‬‬
‫‪else‬‬
‫; )‪insert_into_bst(num, root->_right‬‬
‫}‬
‫כפי שנקל לראות‪ ,‬גרסתה הראשונה של ‪ ,insert_into_bst‬כמו מרבית‬
‫הפונקציות לטיפול בעצים‪ ,‬היא פונקציה רקורסיבית‪ .‬היא פועלת באופן הבא‪ :‬אם‬
‫הגענו ְלמצביע שמורה על )תת‪(-‬עץ ריק‪ ,‬אזי יש להוסיף את הערך ‪ num‬כאן ועכשיו‬
‫ְלצומת עליו המצביע יורה‪ .‬אחרת‪ :‬יש להוסיף את הערך החדש או לבנו השמאלי של‬
‫השורש או לבנו הימני )בהתאם לערכו של המספר המוסף(‪ ,‬ועל כן אנו קוראים‬
‫רקורסיבית עם המצביע המתאים‪ .‬כלומר ‪ insert_into_bst‬מוסיפה ערכים‬
‫חדשים בתחתית העץ; במילים אחרות ערך שהוכנס מאוחר יותר לעולם לא יהיה‬
‫הורה של ערך שהוכנס מוקדם יותר‪ ,‬הערך שהוכנס מאוחר יותר יהיה צאצאו של‬
‫הערך שהוכנס מוקדם יותר‪ ,‬או שהוא יוסף על ענף אחר של העץ‪ִ .‬בדקו לעצמכם כי‬
‫לא יתכן שלא יהיה מקום בעץ לערך חדש‪ .‬כל ערך נוסף תמיד יוכל למצוא את‬
‫המקום המתאים לו ַבעץ )גם אם ערך זהה לו כבר מצוי אי‪-‬שם בעץ(‪.‬‬
‫מכיוון ש‪ insert_into_bst -‬מקבלת את המצביע המועבר לה כפרמטר הפניה‪,‬‬
‫אזי שינויים שהפונקציה תכניס למצביע יוותרו בתום פעולתה בארגומנט המתאים‬
‫אשר יעבור להצביע על הצומת שהוסף לעץ‪.‬‬
‫כדרכנו‪ ,‬נבצע סימולציית הרצה לפונקציה‪ .‬נניח שהקלט המוזן הוא‪,13 ,83 ,77 ,17 :‬‬
‫‪ 17) 0‬מוזן ראשון‪ 0 ,‬מוזן אחרון(‪ .‬בתחילה‪ ,‬בתכנית הראשית מוגדר המשתנה ‪root‬‬
‫)אשר אינו מאותחל‪ ,‬ועל‪-‬כן כולל ערך 'זבל'(‪ .‬נציג את מצב המחסנית‪:‬‬
‫=‪root‬‬
‫עת התכנית הראשית מזמנת את ‪ build_bst‬מצב הזיכרון הוא‪:‬‬
‫=‪root‬‬
‫=‪root‬‬
‫=‪num‬‬
‫נזכור כי ‪ root‬של ‪ build_bst‬הוא משתנה לוקלי‪ ,‬ועל‪-‬כן אין קשר בינו לבין‬
‫‪ root‬של ‪ .main‬הפונקציה ‪ build_bst‬מתחילה להתבצע‪ .‬היא קוראת את הערך‬
‫‪ 17‬ומזמנת את ‪) insert_into_bst‬אשר מקבלת את המצביע כפרמטר משתנה(‪,‬‬
‫מצב הזיכרון הוא‪:‬‬
‫‪111‬‬
‫=‪root‬‬
‫=‪root‬‬
‫‪num=17‬‬
‫=‪root‬‬
‫‪num=17‬‬
‫עת ‪ insert_into_bst‬שואלת )‪ if (root == NULL‬אנו הולכים בעקבות‬
‫החץ )עם הראש המשולש השחור(‪ ,‬ובודקים את ערכו של ‪ root‬של ‪.build_bst‬‬
‫ערכו הוא אכן ‪ NULL‬ועל‪-‬כן מוקצית קופסה חדשה על הערמה ו‪ root -‬של‬
‫‪ build_bst‬עובר להצביע על הקופסה‪ .‬כמו כן השדות הרצויים מאותחלים כפי‬
‫שמתואר בקוד‪.‬‬
‫מצב הזיכרון בתום ריצתה של ‪ insert_into_bst‬הוא‪:‬‬
‫‪17‬‬
‫‪a‬‬
‫=‪root‬‬
‫‪b‬‬
‫=‪root‬‬
‫‪num=17‬‬
‫=‪root‬‬
‫‪num=17‬‬
‫)האותיות ‪ a, b‬שהוספו לציור תסייענה לנו בהמשך לזהות את המצביעים‬
‫המתאימים‪ .‬הן אינן חלק ממצב הזיכרון‪ ,‬אלא רק תוספות שהוספנו כדי להקל על‬
‫ההסברים(‪.‬‬
‫אנו שבים ל‪ build_bst -‬אשר קוראת את הערך ‪ ,77‬ומזמנת שוב את‬
‫‪ .insert_into_bst‬גם עתה ‪ build_bst‬מעבירה ל‪ insert -‬כארגומנט את‬
‫‪) root‬של ‪ .(build_bst‬מצב הזיכרון דומה לציור האחרון‪ ,‬פרט לכך שערכו של‬
‫‪ num‬בשתי רשומות ההפעלה הוא ‪) 77‬ולא ‪ 17‬כפי שמופיע בציור האחרון(‪.‬‬
‫‪112‬‬
‫עת ‪ insert_into_bst‬מתחילה להתבצע‪ ,‬ובודקת את ערכו של ‪ ,root‬היא מגלה‬
‫כי ערכו שונה מ‪ .NULL -‬גם התנאי )‪ (num <= root-> _data‬אינו מתקיים‪ ,‬ועל‬
‫כן ‪ insert_into_bst‬קוראת רקורסיבית לעצמה‪ ,‬ומעבירה לעותק הנקרא‬
‫הרקורסיבית את המצביע ‪) root-> _right‬כלומר את המצביע הימני היוצא‬
‫מהקופסה של ‪ ,17‬זה שסומן באות ‪ .(a‬שימו לב כי מצביע זה מועבר ְלפונקציה‬
‫המקבלת פרמטר הפניה‪ .‬לכן אם וכאשר הפונקציה תשנה את ערכו של הפרמטר‬
‫שלה )ותפנה אותו להצביע על קופסה כלשהי שתוקצה על‪-‬גבי הערמה(‪ ,‬אזי‬
‫השינויים שהפונקציה תבצע יוותרו במצביע‪ ,‬המסומן באות ‪ ,a‬המועבר כארגומנט‬
‫)ושערכו עתה הוא ‪ .(NULL‬נציג את מצב הזיכרון בעקבות הקריאה הרקורסיבית‬
‫המזומנת‪:‬‬
‫‪17‬‬
‫=‪root‬‬
‫‪b‬‬
‫‪a‬‬
‫=‪root‬‬
‫‪num=77‬‬
‫=‪root‬‬
‫‪num=77‬‬
‫=‪root‬‬
‫‪num=77‬‬
‫‪113‬‬
‫העותק השני של ‪ insert_into_bst‬מתחיל להתבצע‪ .‬עת הוא שואל האם ערכו‬
‫של הפרמטר ‪ root‬שלו הוא ‪ ,NULL‬המחשב הולך בעקבות החץ השחור‪ ,‬מגיע‬
‫למצביע ‪ ,a‬ומגלה שערכו הוא אכן ‪ .NULL‬לכן עתה המחשב מקצה קופסה חדשה‬
‫ומפנה את המצביע ‪ a‬להצביע עליה‪ .‬שימו לב שוב שהשינוי קורה על המצביע ‪ a‬שכן‬
‫מצביע זה הועבר כארגומנט לפונקציה המקבלת פרמטר הפניה; ועל‪-‬כן שינויים‬
‫שהפונקציה עורכת לפרמטר חלים למעשה על הארגומנט‪ .‬בעקבות ההקצאה גם‬
‫מוכנסים ערכים לקופסה שהוקצתה‪ .‬מצב הזיכרון הוא‪:‬‬
‫‪17‬‬
‫=‪root‬‬
‫‪b‬‬
‫‪a‬‬
‫=‪root‬‬
‫‪num=77‬‬
‫‪77‬‬
‫‪c‬‬
‫=‪root‬‬
‫‪num=77‬‬
‫=‪root‬‬
‫‪num=77‬‬
‫בכך העותק השני של ‪ insert_into_bst‬מסתיים‪ .‬כתובת החזרה שלו היא סיומו‬
‫של העותק הראשון של ‪ ,insert_into_bst‬ולכן גם העותק הראשון מסתיים‪.‬‬
‫אנו שבים ל‪ build_bst -‬אשר קוראת את הערך ‪ ,83‬ומזמנת שוב את‬
‫‪ insert_into_bst‬תוך שהיא מעביר לה את ‪ root‬של ‪.build_bst‬‬
‫מצב הזיכרון הוא‪:‬‬
‫‪17‬‬
‫=‪root‬‬
‫‪b‬‬
‫‪a‬‬
‫=‪root‬‬
‫‪num=83‬‬
‫‪77‬‬
‫‪c‬‬
‫=‪root‬‬
‫‪num=83‬‬
‫הפונקציה ‪ insert_into_bst‬מתחילה להתבצע‪ .‬ערכו של ‪ root‬אינו ‪ .NULL‬גם‬
‫‪ num <= root-> _data‬אינו מתקיים ולכן ‪insert_into_bst‬‬
‫התנאי‪:‬‬
‫קוראת רקורסיבית לעצמה‪ ,‬תוך שהיא מעבירה לקריאה הרקורסיבית את המצביע‬
‫המסומן באות ‪ .a‬מצב הזיכרון בעקבות הקריאה הוא‪:‬‬
‫‪17‬‬
‫=‪root‬‬
‫‪b‬‬
‫‪a‬‬
‫=‪root‬‬
‫‪num=83‬‬
‫‪77‬‬
‫‪c‬‬
‫=‪root‬‬
‫‪num=83‬‬
‫=‪root‬‬
‫‪num=83‬‬
‫‪114‬‬
‫העותק השני של ‪ insert_into_bst‬מתחיל להתבצע‪ .‬גם בו ערכו של ‪ root‬אינו‬
‫‪ ,NULL‬וגם בו לא נכון ש‪) num <= root->_data :‬שכן ערכו של ‪ num‬הוא ‪,83‬‬
‫וערכו של ‪ root->_data‬הוא ‪ .(77‬ולכן העותק הנוכחי של ‪insert_into_bst‬‬
‫מזמן קריאה לעותק שלישי של ‪ .insert_into_bst‬העותק השני מעביר לעותק‬
‫השלישי את ‪ root->_ _right‬שהינו המצביע המסומן באות ‪ c‬בציור שלנו‪ .‬מצב‬
‫הזיכרון‪:‬‬
‫‪17‬‬
‫=‪root‬‬
‫‪b‬‬
‫‪a‬‬
‫=‪root‬‬
‫‪num=83‬‬
‫‪77‬‬
‫‪c‬‬
‫=‪root‬‬
‫‪num=83‬‬
‫=‪root‬‬
‫‪num=83‬‬
‫=‪root‬‬
‫‪num=83‬‬
‫עת העותק השלישי של ‪ insert_into_bst‬מתחיל להתבצע‪ ,‬ובוחן את ערכו של‬
‫‪ root‬שלו‪ ,‬הוא מגלה כי ערכו הוא ‪ .NULL‬על כן העותק השלישי יוצר קופסה חדשה‬
‫ומפנה את הארגומנט שהועבר לו‪ ,‬כלומר את המצביע ‪ ,c‬להורות על הקופסה‬
‫החדשה‪ .‬אחר העותק השלישי גם מכניס ערכים לקופסה כפי שמתואר בקוד‪ .‬מצב‬
‫הזיכרון הוא עתה‪:‬‬
‫‪17‬‬
‫=‪root‬‬
‫‪b‬‬
‫‪a‬‬
‫=‪root‬‬
‫‪num=83‬‬
‫‪77‬‬
‫‪c‬‬
‫=‪root‬‬
‫‪num=83‬‬
‫‪83‬‬
‫=‪root‬‬
‫‪num=83‬‬
‫=‪root‬‬
‫‪num=83‬‬
‫שלושת העותקים של ‪ insert_into_bst‬מסתיימים בזה אחר זה‪ ,‬ואנו שבים ל‪-‬‬
‫‪ build_bst‬אשר קוראת את הערך ‪ ,13‬ומזמנת שוב את ‪ insert‬כדי להוסיף ערך‬
‫זה לעץ‪ .‬אני משאיר לכם כתרגיל לצייר את השתלשלות העניינים בזיכרון‪ .‬אחרי ש‪-‬‬
‫‪) insert_into_bst‬באמצעות קריאה רקורסיבית אחת נוספת( תוסיף את ‪13‬‬
‫לעץ נשוב ל‪ .build_bst -‬עתה ‪ build_bst‬תקרא את הערך אפס‪ ,‬ולכן תצא‬
‫מלולאת קריאת הערכים‪ build_bst .‬תתקדם לפקודת ה‪ return -‬שלה בה היא‬
‫מחזירה את ערכו של ‪ root‬שלה‪ .‬הערך המוחזר יושם למשתנה ‪ root‬של התכנית‬
‫הראשית )להזכירכם ‪ build_bst‬זומנה מה‪ main -‬באמצעות הפקודה‪:‬‬
‫‪115‬‬
‫;)(‪ .( root = build_bst‬לכן עתה ‪ root‬של התכנית הראשית יעבור להצביע‬
‫על העץ ש‪ build_bst -‬בנתה‪.‬‬
‫‪ 14.3.2‬הכנסה לעץ חיפוש באמצעות פונ' איטרטיבית המקבלת פרמטר‬
‫הפניה‬
‫הפונקציה ‪ insert_into_bst‬שכתבנו פעלה באופן רקורסיבי‪ .‬הפונקציה‬
‫התקדמה על הענף המתאים של העץ עד קצהו‪ ,‬כלומר עד שהיא הגיע ְלמצביע שערכו‬
‫הוא ‪ ,NULL‬באותו מקום הפונקציה הוסיפה את הצומת החדש‪ .‬עת פונקציה‬
‫מתקדמת על ענף יחיד של העץ‪ ,‬ואינה סורקת מספר ענפים‪ ,‬ניתן לכתבה גם באופן‬
‫איטרטיבי‪ ,‬תוך שימוש בלולאות‪ .‬לכן עתה נציג גרסה איטרטיבית של‬
‫‪ .insert_into_bst‬אקדים את המאוחר ואציין שהגרסה אומנם איטרטיבית‬
‫ולא רקורסיבית‪ ,‬וזו הקלה‪ ,‬אך היא מסורבלת למדי‪ ,‬וזו מגרעת‪ ,‬ועל כן בשלב‬
‫שלישי נציג גרסה איטרטיבית ולא מסורבלת‪ ,‬אך‪ ,‬למרבה הצער)?( כזו העושה‬
‫שימוש במצביע למצביע‪ .‬אני תקווה שהשוואת שתי הגרסות גם תאפשר לכם ללמוד‬
‫על היתרונות הגלומים בשימוש במצביע למצביע‪:‬‬
‫)‪void insert_into_bst(int num, struct Node *&root‬‬
‫{‬
‫‪struct Node *temp = new (std::nothrow) struct Node,‬‬
‫;‪*run = root‬‬
‫)‪if (temp == NULL‬‬
‫; )‪retminate("cannot allocate memoty", 1‬‬
‫)‪//(4‬‬
‫; ‪temp -> _data = num‬‬
‫; ‪temp -> _left = temp ->_right = NULL‬‬
‫{ )‪if (toor == NULL‬‬
‫; ‪root = temp‬‬
‫; ‪return‬‬
‫}‬
‫)‪// (1‬‬
‫)‪// (2‬‬
‫)‪// (3‬‬
‫{ )‪while(true‬‬
‫)‪if (num <= run -> _data‬‬
‫)‪if (run -> _left != NULL‬‬
‫; ‪run = run -> _left‬‬
‫{ ‪else‬‬
‫; ‪run -> _left = temp‬‬
‫; ‪break‬‬
‫}‬
‫‪else‬‬
‫)‪if (run -> _right != NULL‬‬
‫;‪run = run -> _ right‬‬
‫{ ‪else‬‬
‫; ‪run -> _right = temp‬‬
‫; ‪break‬‬
‫}‬
‫}‬
‫}‬
‫נסביר את הפונ'‪.‬‬
‫‪116‬‬
‫נניח שהפונ' מזומנת כבר אחרי שנבנה העץ כמתואר בציור )מצביעים שערכם ‪NULL‬‬
‫הושמטו מהציור‪ ,‬פרט למצביע המתויג באות ‪: (a‬‬
‫‪17‬‬
‫=‪root‬‬
‫‪num=83‬‬
‫‪12‬‬
‫‪...‬‬
‫‪14‬‬
‫‪10‬‬
‫=‪root‬‬
‫‪num=13‬‬
‫=‪temp‬‬
‫=‪run‬‬
‫‪a‬‬
‫ראשית מוקצה איבר חדש )קופסה חדשה( עליו מצביע משתנה העזר ‪ .temp‬הערך‬
‫‪ 13‬מוכנס למרכיב הנתונים בקופסה‪ ,‬ושני המצביעים מקבלים את הערך ‪.NULL‬‬
‫עתה מצביע העזר ‪ run‬מקבל את ערכו של ‪) root‬של הפונ'( שהינו פרמטר הפניה‪,‬‬
‫ולכן ‪ run‬עובר להצביע גם הוא על הקופסה של ‪ ,17‬כלומר על שורש העץ(‪.‬‬
‫מכאן פונים ללולאה‪ ,‬שהתנאי שלה מתקיים תמיד )לכאורה זו לולאה אינסופית‪,‬‬
‫ובפועל פקודות ה‪ break -‬בגוף הלולאה הופכות אותה ללולאה סופית(‪.‬‬
‫התנאי המסומן בהערת התיעוד )‪ (1‬מתקיים‪ ,‬וכך גם זה המסומן בהערה )‪ (2‬ולכן‬
‫‪ , run = run -> _left‬כלומר ‪ run‬עובר להצביע על הקופסה של ‪ ,12‬ובזאת‬
‫תם הסיבוב הראשון בלולאה‪.‬‬
‫בסיבוב השני בלולאה‪ ,‬התנאי )‪ (1‬אינו מתקיים‪ ,‬והתנאי )‪ (3‬כן מתקיים ולכן ‪run‬‬
‫עובר להצביע על הקופסה של ‪.14‬‬
‫בסיבוב השלישי בלולאה התנאי )‪ (1‬מתקיים‪ ,‬והתנאי )‪ (2‬אינו מתקיים‪ ,‬ולכן‬
‫‪ , run-> _left‬שהינו המצביע המסומן ב‪ a -‬בציור‪ ,‬עובר להצביע על הקופסה‬
‫החדשה שהוקצתה בתחילת הפונ'‪ ,‬כלומר ‪ 13‬משולב בעץ במקום המתאים; ובזאת‬
‫ביצוע הפונ' מסתיים‪.‬‬
‫שאלות למחשבה‪:‬‬
‫א‪ .‬האם ולמה יש צורך בהעברת ‪root‬כפרמטר הפניה )ולא כפרמטר ערך(‪.‬‬
‫ב‪ .‬האם הפונ' הייתה שגויה לו בלולאה היינו מרצים את ‪) root‬ולא את ‪run‬כפי‬
‫שאנו עושים(?‬
‫ג‪ .‬האם ולמה יש חשיבות בהשמה המסומנת ב‪ (4) -‬בפונ'?‬
‫‪117‬‬
‫נסכם‪ :‬ראינו גרסה רקורסיבית של פונ' ההוספה לעץ חיפוש בינארי‪ ,‬וגירסה‬
‫איטרטיבית )אך מעט מסורבלת(‪ .‬שתי הגרסות קיבלו את המצביע לשורש העץ‬
‫כפרמטר הפניה‪.‬‬
‫‪ 14.3.3‬הכנסה לעץ חיפוש באמצעות פונ' איטרטיבית המקבלת מצביע‬
‫למצביע‬
‫עתה נציג גרסה איטרטיבית לא מסורבלת אשר תקבל כפרמטר שלה מצביע ַלמצביע‬
‫לשורש העץ )וכמבן‪ ,‬את הערך שיש להוסיף(‪ .‬הגרסה שנראה עתה תזומן על ידי‬
‫‪ build_bst‬באופן הבא‪ .insert_into_bst(num, &root); :‬מכיוון ש‪-‬‬
‫‪ root‬הוא מצביע‪ ,‬אזי ‪ &root‬הוא מצביע למצביע‪ .‬נציג את קוד הפונקציה‪:‬‬
‫{ )‪void insert_into_bst(int num, struct Node **p_2_root‬‬
‫)‪while (*p_2_root != NULL‬‬
‫) ‪if ( num <=(*p_2_root)-> _data‬‬
‫; ) ‪p_2_root = &( (*p_2_root)-> _left‬‬
‫; ) ‪else p_2_root = &( (*p_2_root)-> _right‬‬
‫; ‪NULL‬‬
‫; ‪*p_2_root = new (std::nothrow) struct Node‬‬
‫‪if (*p2root == NULL) ...‬‬
‫; ‪(*p_2_root)-> _data = num‬‬
‫= ‪(*p_2_root)-> _left = (*p_2_root)-> _right‬‬
‫}‬
‫בפונקציה אנו מקדמים בלולאה את המצביע למצביע על הענף הרצוי‪ ,‬עד אשר‬
‫המצביע עליו המצביע‪-‬למצביע מורה ערכו ‪ .NULL‬בשלב זה אנו מקצים קופסה‬
‫ומפנים את המצביע עליו המצביע‪-‬למצביע מורה להצביע על הקופסה‬
‫ְ‬
‫חדשה‪,‬‬
‫החדשה‪.‬‬
‫נציג דוגמת הרצה של הפונקציה על העץ הבא‪ .‬נניח כי עתה בנוסף לערכים ‪,77 ,17‬‬
‫‪ 83‬עלינו להוסיף את ‪ 70‬תוך שימוש גרסה השלישית של פונ' ההכנסה‪ .‬כאמור‪,‬‬
‫‪ build_bst‬מזמנת את הפונקציה באופן הבא‪:‬‬
‫)‪ .insert_into_bst (num, &root‬מצב הזיכרון עם הקריאה לפונקציה הוא‪:‬‬
‫‪17‬‬
‫=‪root‬‬
‫‪b‬‬
‫‪a‬‬
‫=‪root‬‬
‫‪num=70‬‬
‫‪77‬‬
‫‪c‬‬
‫=‪p_2_root‬‬
‫‪num=70‬‬
‫‪83‬‬
‫אנו שמים לב כי ‪ p_2_root‬הוא מצביע‪ ,‬ועל‪-‬כן מצויר עם חץ בעל ראש בצורת ‪.v‬‬
‫מכיוון שהוא מצביע למצביע )ולא מצביע לקופסה( הוא מצויר בקו מקווקוו‪ .‬בעת‬
‫הקריאה ‪ p_2_root‬מצביע על המצביע ‪ root‬של ‪.build_bst‬‬
‫עת הפונקציה מתחילה להתבצע היא פונה ללולאה‪:‬‬
‫)‪while (*p_2_root != NULL‬‬
‫‪118‬‬
‫) ‪if ( num <=(*p_2_root)-> _data‬‬
‫; ) ‪p_2_root = &( (*p_2_root)-> _left‬‬
‫; ) ‪else p_2_root = &( (*p_2_root)-> _right‬‬
‫נזכור כי‪ *p_2_root :‬הוא המצביע עליו ‪ p_2_root‬מורה‪ ,‬ובתחילה זהו ‪root‬‬
‫של ‪ build_bst‬שערכו שונה מ‪ .NULL -‬לכן אנו נכנסים לסיבוב ראשון בלולאה‪.‬‬
‫התנאי‪ ( num <=(*p_2_root)-> _data ) :‬אינו מתקיים‪ ,‬שכן‬
‫‪ (*p_2_root)-> _data‬הוא שדה ה‪ data -‬בקופסה עליה מורה המצביע‬
‫)‪ ,(*p_2_root‬כלומר שדה ה‪ _data -‬בקופסה עליה מורה המצביע עליו מצביע‬
‫‪ ,p_2_root‬במילים פשוטות זהו שדה ה‪ _data -‬בקופסה עליה מורה ‪ root‬של‬
‫‪ ,build_bst‬כלומר זה הערך ‪ .17‬לכן אנו מבצעים את הפקודה הכפופה ל‪,else -‬‬
‫ומשימים למצביע ‪ p_2_root‬את כתובתו של ‪ ,root-> _left‬כלומר את כתובתו‬
‫של המצביע ‪ ,a‬מבחינה ציורית המצב הוא‪:‬‬
‫‪17‬‬
‫=‪root‬‬
‫‪b‬‬
‫‪a‬‬
‫=‪root‬‬
‫‪num=70‬‬
‫‪77‬‬
‫‪d‬‬
‫‪c‬‬
‫=‪p_2_root‬‬
‫‪num=70‬‬
‫‪83‬‬
‫לקראת כניסה לסיבוב שני בלולאה אנו בודקים את התנאי בכותרתה‪*p_2_root .‬‬
‫הוא עתה המצביע ‪) a‬המצביע עליו מורה ‪ ,(p_2_rrot‬ולכן התנאי מתקיים‪ .‬אנו‬
‫נכנסים לגוף הלולאה‪ ,‬ובו התנאי‪( num <=(*p_2_root)-> _data :‬‬
‫)מתקיים‪ ,‬שכן ‪ (*p_2_root)-> _data‬הוא שדה ה‪ _data -‬בקופסה עליה‬
‫מצביע ‪ ,a‬כלומר זהו הערך ‪ .77‬לכן עתה ‪ p_2_root‬מקבל את הכתובת של הבן‬
‫השמאלי של התא עליו מצביע המצביע עליו ‪ ,p_2_root‬במילים פשוטות‪:‬‬
‫‪ p_2_root‬מקבל את כתובתו של המצביע ‪ .d‬והציור המתאים‪:‬‬
‫‪17‬‬
‫=‪root‬‬
‫‪b‬‬
‫‪a‬‬
‫=‪root‬‬
‫‪num=70‬‬
‫‪77‬‬
‫‪c‬‬
‫‪d‬‬
‫=‪p_2_root‬‬
‫‪num=70‬‬
‫‪83‬‬
‫לקראת כניסה לסיבוב נוסף בלולאה אנו בוחנים שוב האם‪*p_2_root!=NULL :‬‬
‫והפעם התשובה היא 'לא'‪ ,‬ערכו של המצביע עליו מורה ‪ p_2_root‬הוא ‪ .NULL‬אנו‬
‫הפקודה‪:‬‬
‫הלולאה‪.‬‬
‫שאחרי‬
‫לפקודות‬
‫מתקדמים‬
‫‪ *p_2_root = new struct Node‬מקצה קופסה חדשה על הערמה‪ ,‬ושולחת‬
‫את המצביע עליו ‪ p_2_root‬מורה להצביע על קופסה זאת‪ .‬הציור המתאים‪:‬‬
‫‪119‬‬
‫‪17‬‬
‫=‪root‬‬
‫‪b‬‬
‫‪a‬‬
‫=‪root‬‬
‫‪num=70‬‬
‫‪77‬‬
‫‪d‬‬
‫‪c‬‬
‫=‪p_2_root‬‬
‫‪num=70‬‬
‫‪83‬‬
‫שלוש הפקודות הבאות מכניסות ערכים לקופסה שהוקצתה‪ .‬לדוגמה‪ ,‬הפקודה‪:‬‬
‫;‪ (*p_2_root)-> _data = num‬מכניסה את הערך ‪ 80‬לשדה ה‪_data -‬‬
‫בקופסה עליה מורה )‪ , (*p_2_root‬כלומר מכניסה את הערך ‪ 80‬לשדה ה‪_data-‬‬
‫בקופסה עליה מורה המצביע ‪) d‬שעליו מצביע עתה ‪.(p_2_root‬‬
‫כדי לחדד את הבנתנו את הפונקציה שהצגנו‪ ,‬נבחן מה היה קורה לו היינו כותבים‬
‫אותה באופן הבא‪:‬‬
‫{ )‪void iterative_insert(int num, struct Node *&root‬‬
‫)‪while (root != NULL‬‬
‫) ‪if ( num <= root-> _data‬‬
‫; ‪root = root-> _left‬‬
‫; ) ‪else root = root-> _right‬‬
‫; ‪root = new (std::nothrow) struct Node‬‬
‫‪if (root == NULL) ...‬‬
‫; ‪root-> _data = num‬‬
‫; ‪root-> _left = root->_right = NULL‬‬
‫}‬
‫במקום מצביע למצביע‪ ,‬פרמטר משתנה‬
‫הגרסה הנוכחית של הפונקציה מקבלת ְ‬
‫שהנו מצביע‪ .‬שוב נניח כי יש להוסיף את ‪ ,70‬לעץ הכולל את ‪ .83 ,77 ,17‬הקריאה מ‪-‬‬
‫(‪iterative_insert‬‬
‫‪ build_bst‬לפונקציה‪ ,‬בגרסתה הנוכחית תהא‪:‬‬
‫;)‪ num, root‬מצב הזיכרון בעקבות הקריאה יהא‪:‬‬
‫‪17‬‬
‫=‪root‬‬
‫‪b‬‬
‫‪a‬‬
‫=‪root‬‬
‫‪num=70‬‬
‫‪77‬‬
‫‪c‬‬
‫‪d‬‬
‫=‪root‬‬
‫‪num=70‬‬
‫‪83‬‬
‫עת הפונקציה מתחילה להתבצע ושואלת האם ‪ ,root!=NULL‬אנו הולכים בעקבות‬
‫החץ )עם המשולש השחור(‪ ,‬ובודקים את ערכו של ‪ root‬של ‪ ,build_bst‬שערכו‬
‫אכן אינו ‪ .NULL‬לכן אנו נכנסים ללולאה‪ .‬בגוף הלולאה אנו בודקים האם‪num <= :‬‬
‫‪ ,root-> _data‬והתשובה לכך היא שלילית‪ .‬לכן אנו מבצעים את ההשמה‬
‫‪120‬‬
‫הפטלית‪ .root = root-> _right; :‬מדוע השמה זאת הינה פטלית? שכן אנו‬
‫מפנים את המצביע ‪ root‬של ‪) build_bst‬כלומר את הארגומנט שהועבר‬
‫ְ‬
‫לפונקציה( להצביע כמו המצביע ‪ , root-> _right‬כלומר כמו המצביע ‪ .a‬בזאת‬
‫הפננו את ‪ root‬של ‪ build_bst‬להצביע על הקופסה של ‪ ,77‬ואיבדנו לעולמים את‬
‫הקופסה של ‪!17‬‬
‫‪ 14.3.4‬זמן הריצה של בניית עץ חיפוש בינארי‬
‫עתה נשאל את עצמנו כמה עולה בניית עץ חיפוש בינארי במקרה הגרוע ביותר? אנו‬
‫זוכרים שכל נתון חדש נוסף כעלה‪ ,‬אחרי שאנו יורדים לאורך המסלול המתאים‬
‫בעץ‪ .‬לכן זמן הבניה יהיה הגדול עת כל עלה חדש יתווסף מעבר לקודמו‪ ,‬ויאריך את‬
‫המסלול היחיד הקיים‪ ,‬כלומר עת העץ 'יתנוון' לכדי רשימה מקושרת‪ ,‬למשל מפני‬
‫שהנתונים יוזנו ממוינים‪ .17, 20, 23, 25, 30, ... :‬העץ יראה‪:‬‬
‫‪17‬‬
‫‪20‬‬
‫‪23‬‬
‫‪25‬‬
‫‪30‬‬
‫במצב זה‪ ,‬הכנסת הנתון ה‪-n -‬י תעלה ‪ n‬צעדים‪ ,‬ולכן עלות בניית העץ השלם תהיה‪:‬‬
‫‪ 1 + 2 + …+n‬כלומר סד"ג של ‪ n2‬צעדים‪.‬‬
‫לעומת זאת‪ ,‬אם העץ הינו מאוזן‪' ,‬יפה'‪ ,‬אזי אורך המסלול הארוך ביותר בו‬
‫מהצומת לעלה העמוק ביותר הוא לכל היותר ‪ lg2(n+1)-1‬צעדים‪ ,‬או סד"ג של )‪.lg2(n‬‬
‫לא נוכיח זאת‪ ,‬אך נביט בכמה דוגמות שיעזרו לנו להשתכנע בכך‪:‬‬
‫בעץ השמאלי יש צומת יחיד‪ .‬יש לבצע אפס צעדים מהשורש לעלה העמוק ביותר‪,‬‬
‫כלומר מספר הצעדים הוא ‪ lg2(1+1)-1‬כפי שמורה נוסחה שלנו‪ .‬בעץ האמצעי יש‬
‫שלושה צמתים‪ .‬מספר הצעדים הדרוש כדי להגיע מהשורש לעלה העמוק ביותר הוא‬
‫‪121‬‬
‫אחד‪ ,‬ולכן‪ ,‬שוב בהתאמה לנוסחה‪ .lg2(3+1)-1 :‬בעץ הימני‪ lg2(7+1)-1 = 2 :‬ואכן יש‬
‫להתקדם שני צעדים מהשורש לעלה העמוק ביותר‪.‬‬
‫מכיוון שפעולת ההכנסה מחייבת להתקדם מהשורש לעלים‪ ,‬אזי ‪ n‬פעולות הכנסה‬
‫יחייבו עבודה בשיעור של לכל היותר )‪ n*lg2(n‬צעדים‪.‬‬
‫‪ 14.4‬ביקור בעץ חיפוש בינארי‬
‫עתה ברצוננו לכתוב פונקציה אשר מציגה את כל הנתונים השמורים בעץ החיפוש‬
‫שבנינו‪ .‬נרצה להציג את הנתונים ממוינים‪ .‬כמובן שכדי להציג את כל הנתונים עלינו‬
‫לסרוק את כל צמתי העץ‪) ,‬ולא די בסריקת ענף יחיד(‪ .‬מסיבה זאת הפונקציה‬
‫שנכתוב תהא בהכרח רקורסיבית‪.‬‬
‫עת בנינו את העץ הכנסנו נתון שערכו קטן או שווה מהנתון המצוי ַבשורש לתת‪-‬העץ‬
‫השמאלי; נתון שערכו גדול מהשורש הוכנס לתת‪-‬העץ הימני‪ .‬על‪-‬כן גם ַבביקור בעץ‪,‬‬
‫ראשית נבקר בתת‪-‬העץ השמאלי‪ ,‬המכיל נתונים קטנים או שווים מאלה שבשורש‪,‬‬
‫אחר נבקר בשורש‪ ,‬ולבסוף נבקר בתת העץ הימני‪ ,‬המכיל נתונים גדולים מאלה‬
‫המצויים ַבשורש‪ .‬אם נחזיר על עקרון זה עבור כל תת‪-‬עץ שהוא אזי הנתונים יוצגו‬
‫תוֹכי או באנגלית‪:‬‬
‫ממוינים מקטן לגדול‪ .‬לביקור שכזה בעץ אנו קוראים ביקור ִ‬
‫‪ in-order‬יען כי אנו מבקרים בשורש בין ביקורנו בילד השמאלי‪ ,‬לביקורנו בילד‬
‫הימני‪ .‬אל מול ‪ in-order‬עומדים‪) :‬א( ביקור תחילי או ‪ :pre-oreder‬בו אנו מבקרים‬
‫ראשית בשורש‪ ,‬אחר בילד השמאלי‪ ,‬ולבסוף בילד הימני‪) ,‬ב( ביקור סופי או ‪post-‬‬
‫‪ :order‬בו אנו מבקרים ראשית בילד השמאלי‪ ,‬שנית בימני‪ ,‬ורק לבסוף בשורש‪ .‬שימו‬
‫לב כי בכל שלושת הביקורים אנו מבקרים בילד השמאלי לפני הביקור בילד הימני‪.‬‬
‫ההבדל בין הביקורים השונים הוא בתשובה לשאלה‪ :‬מתי מבקרים בשורש? לעת‬
‫עתה נדון בביקור ‪ .in order‬ביקורים אחרים יעזרו לנו במשימות אחרות )למשל‪ ,‬עת‬
‫נשחרר את העץ נרצה ראשית לשחרר את הילדים‪ ,‬ורק לבסוף את השורש‪ ,‬ולכן‬
‫ננקוט בביקור סופי(‪.‬‬
‫פונקצית הביקור‪ ,‬וההצגה הינה‪:‬‬
‫{ )‪void display(const struct Node * root‬‬
‫{ )‪if (root != NULL‬‬
‫; )‪display(root-> _left‬‬
‫; ‪cout << root-> _data‬‬
‫; )‪display(root-> _right‬‬
‫}‬
‫}‬
‫קוד הפונקציה פשוט למדי‪ :‬אם )תת( העץ בו אנו מבקרים אינו ריק )‪(root!=NULL‬‬
‫אזי אנו פונים ראשית לבקר בילדו השמאלי‪ ,‬אחר אנו מציגים את הנתון שבשורש‪,‬‬
‫ולבסוף אנו מבקרים בילד הימני‪.‬‬
‫‪122‬‬
‫נבצע סימולציית הרצה מעט מקוצרת על עץ החיפוש הבא‪ ,‬עליו מצביע המשתנה‬
‫‪ root‬של התכנית הראשית‪:‬‬
‫‪17‬‬
‫=‪root‬‬
‫ב‬
‫א‬
‫‪17‬‬
‫‪35‬‬
‫ד‬
‫ג‬
‫ה‬
‫ו‬
‫‪35‬‬
‫ז‬
‫ח‬
‫עם הקריאה לפונקציה מצב הזיכרון הוא שעל גבי המחסנית נוספת רשומת ההפעלה‬
‫של הפונקציה‪:‬‬
‫=‪root‬‬
‫עתה הפונקציה מתחילה להתבצע‪ .‬ערכו של הפרמטר ‪) root‬של הפונקציה( אינו‬
‫‪ NULL‬ועל כן הפונקציה קוראת רקורסיבית לעותק שני של הפונקציה‪ ,‬ומעבירה לו‬
‫את ‪ root-> _left‬כלומר עותק של המצביע ב' )המועבר כפרמטר ערך(‪ .‬על גבי‬
‫המחסנית נוספת רשומת ההפעלה‪:‬‬
‫=‪root‬‬
‫העותק השני של הפונקציה מתחיל להתבצע‪ .‬גם בו ערכו של ‪ root‬אינו ‪ ,NULL‬ועל‪-‬‬
‫כן העותק השני קורא רקורסיבית לעותק שלישי של הפונקציה ומעביר לעותק‬
‫השלישי את בנו השמאלי‪ ,‬כלומר את המצביע ו'‪ .‬בעותק השלישי ערכו של ‪root‬‬
‫הוא ‪ ,NULL‬ועל כן העותק השלישי אינו מבצע דבר‪ ,‬ומסיים מיידית‪ .‬אנו שבים‬
‫לעותק השני אשר עתה מציג את ערכו של ‪ root-> _data‬כלומר את הערך ‪.17‬‬
‫אחר מזמן העותק השני קריאה רקורסיבית שנסמנה כקריאה הרביעית‪ ,‬ומעביר לה‬
‫עותק של המצביע ה'‪ .‬בקריאה הרביעית ערכו של ‪ root‬הוא ‪ ,NULL‬על‪-‬כן היא‬
‫מסיימת בלי לבצע דבר‪ ,‬אנו שבים לעותק השני אשר מסתיים בזאת‪ .‬את העותק‬
‫השני זימן העותק הראשון )עת האחרון קרא רקורסיבית עם ילדו השמאלי(‪ .‬עתה‬
‫העותק הראשון יכול להציג את ‪ root-> _data‬שלו‪ ,‬כלומר את הערך ‪ .17‬לאחר‬
‫מכן קורא עותק הראשון רקורסיבית עם ילדו הימני‪ ,‬כלומר עם עותק של המצביע‬
‫א'‪ .‬נסמן את הקריאה שזומנה עם עותק של המצביע א' כקריאה החמישית‪ .‬ערכו‬
‫של ‪ root‬בה שונה מ‪ NULL -‬ועל כן היא קוראת רקורסיבית עם ילדה השמאלי‪,‬‬
‫כלומר עם המצביע ד'‪ ,‬אחר הקריאה החמישית תציג את הערך ‪ ,35‬ולבסוף הקריאה‬
‫החמישית תקרא רקורסיבית עם המצביע ג'‪ .‬הקריאה עם ג' תקרא רקורסיבית‬
‫לילדה השמאלי )הריק(‪ ,‬אחר תציג את הערך ‪ ,35‬ולבסוף תקרא רקורסיבית עם‬
‫ילדה הימני )הריק(‪ .‬בזאת תסתיים הקריאה עם ג'‪ .‬היא תחזור לקריאה עם א'‪ ,‬אשר‬
‫תסתיים בזאת‪ ,‬ותחזור לקריאה הרקורסיבית הראשונה‪ .‬גם קריאה זאת תסתיים‪,‬‬
‫וזאת אחרי שהנתונים בעץ הוצגו ממוינים מקטן לגדול כנדרש‪.‬‬
‫הערת תזכורת‪ :‬את הפרמטר של פונקציה הגדרנו באופן הבא‪:‬‬
‫‪ . const struct Node * root‬המילה ‪ const‬מונעת מהפונקציה לשנות את‬
‫ערכו של האיבר עליו מצביע המצביע ‪.root‬‬
‫‪123‬‬
‫‪ 14.4.1‬זמן הריצה של סריקת העץ‬
‫זמן הריצה של סריקת העץ הוא לינארי במספר הצמתים בעץ‪ ,‬שכן עבור כל צומת‬
‫אנו קוראים לפונ' פעם יחידה‪ ,‬ועושים אז עבודה בישעור קבוע )בכל ביקור בכל‬
‫צומת(‪ .‬מעבר לכך אנו קוראים לפונ' פעם יחידה גם עבור כל המצביעים שערכם הוא‬
‫‪) NULL‬שכן אם המציע לשורש תת‪-‬עץ כלשהו אינו ‪ NULL‬אזי אנו קוראים לפונ' עם‬
‫שני תתי‪-‬העצים‪ ,‬כלומר עם שני הילדים‪ ,‬ואלה עשויים להיות ‪ .(NULL‬משפט קטן‪,‬‬
‫שלא נוכיח‪ ,‬טוען שבעץ בינארי שאינו ריק‪ ,‬מספר המצביעים שערכם הוא ‪ NULL‬הוא‬
‫לכל היותר כמספר הצמתים‪ ,‬ולכן זימון הפונ' גם עבור מצביעים אלה מכפיל את זמן‬
‫הריצה‪ ,‬אך לא הופך אותו להיות לא לינארי במספר הצמתים‪.‬‬
‫‪ 14.5‬חיפוש ערך מבוקש בעץ בינארי כללי ובעץ חיפוש בינארי‬
‫‪ 14.5.1‬חיפוש ערך בעץ שאינו עץ חיפוש‬
‫עתה ברצוננו לכתוב פונקציה אשר מקבלת מצביע לשורשו של עץ בינארי )שאינו‬
‫דווקא עץ חיפוש(‪ ,‬וערך שלם‪ .‬הפונקציה תחזיר את הערך ‪ true‬אם ורק אם הערך‬
‫מצוי בעץ‪ .‬הקריאה לפונקציה תהיה למשל‪:‬‬
‫) )‪if (search(num, root‬‬
‫; ”‪cout << “It exists in data\n‬‬
‫הגדרת הפונקציה‪:‬‬
‫{ )‪bool search(int num, const struct Node * root‬‬
‫; ‪bool found‬‬
‫)‪if (root == NULL‬‬
‫; ) ‪return( false‬‬
‫)‪if (root -> _data == num‬‬
‫;) ‪return( true‬‬
‫; )‪found = search(root-> _left‬‬
‫)‪if (found‬‬
‫; ‪return true‬‬
‫; )‪found = search(root-> _right‬‬
‫; ) ‪return( found‬‬
‫}‬
‫הסבר הפונקציה‪ :‬במידה ולפונקציה מועבר מצביע ל)תת(‪-‬עץ ריק אזי ברור שהערך‬
‫לא נמצא בעץ‪ ,‬ויש להחזיר את הערך ‪ .false‬מנגד‪ ,‬אחרי שוידאנו שהעץ אינו ריק‪,‬‬
‫אנו רשאים לבדוק את ערכו של ‪ ,root-> _data‬ואם ערכו שווה לערך המבוקש‬
‫ניתן להחזיר מיידית את הערך ‪ ,true‬ואין צורך להמשיך ולחפש בילדיו של השורש‬
‫הנוכחי‪ .‬במידה ושני התנאים הראשונים לא התקיימו אנו קוראים רקורסיבית‬
‫לפונקציה על‪-‬מנת לבדוק האם הערך המבוקש נמצא בילד השמאלי‪ .‬את הערך‬
‫המוחזר על‪-‬ידי הקריאה הרקורסיבית אנו מכניסים למשתנה ‪ .found‬במידה וערכו‬
‫של המשתנה הוא ‪ ,true‬ניתן להחזיר מייד איתות על מציאה‪ ,‬ואין צורך להמשיך‬
‫ולבדוק את הילד הימני‪ .‬לבסוף‪ ,‬אם העץ אינו ריק‪ ,‬הערך המבוקש לא נמצא לא‬
‫בשורש ולא בילד השמאלי‪ ,‬אזי נבדוק האם הוא נמצא בילד הימני‪ ,‬ותוצאת‬
‫הבדיקה היא שתוחזר על‪-‬ידי העותק הנוכחי של הפונקציה; שכן אם הערך נמצא‬
‫בילד הימני‪ ,‬אזי ערכו של ‪ found‬יהיה ‪ ,true‬וזה הערך שיש להחזיר‪ .‬מנגד אם‬
‫הערך לא נמצא גם בילד הימני‪ ,‬אזי ערכו של ‪ found‬יהיה ‪ ,false‬וזה הערך שיש‬
‫להחזיר‪.‬‬
‫‪124‬‬
‫פונקציה זאת מדגימה לנו מצב שכיח למדי בפונקציות שפועלות על עצים ומחזירות‬
‫ערך‪ :‬בפונקציה אנו בודקים האם השורש מקיים תכונה רצויה כלשהי )למשל מכיל‬
‫ערך מבוקש(‪ .‬במידה והשורש אכן מקיים את התכונה )או לעיתים במידה והשורש‬
‫אינו מקיים את התכונה(‪ ,‬אזי ניתן לעצור את תהליך הבדיקה ולהחזיר ערך מתאים‪.‬‬
‫אחרת יש לקרוא רקורסיבית עם הילדים בזה אחר זה‪ ,‬לקלוט את הערך המוחזר‬
‫על‪-‬ידי כל‪-‬אחד מהם‪ ,‬ולהחזיר לבסוף ערך המייצג את מה שהוחזר מהילדים‪.‬‬
‫מתכנתים מתחילים שוכחים לעיתים להקפיד על מילוי ההיבטים הללו‪:‬‬
‫‪ .I‬החזרת ערך במידה והשורש מקיים )לא מקיים( התכונה הרצויה‪.‬‬
‫‪ .II‬קליטת הערך המוחזר על‪-‬ידי הקריאה הרקורסיבית לכל אחד משני הילדים‪.‬‬
‫‪ .III‬החזרת ערך 'המסכם' את מה שהוחזר מהילדים‪.‬‬
‫את הפונקציה שהצגנו יכולנו לכתוב גם בצורה קומפקטית יותר‪:‬‬
‫{ )‪bool search(int num, const struct Node * root‬‬
‫)‪if (root == NULL‬‬
‫; ) ‪return( false‬‬
‫)‪if (root -> _data == num‬‬
‫;) ‪return( true‬‬
‫;))‪return(search(root-> _left) || search(root-> _right‬‬
‫}‬
‫בגרסה הנוכחית אנו מחזירים ישירות )בלי שימוש במשתנה עזר( את תוצאת‬
‫הקריאה הרקורסיבית ַלילדים‪ .‬האם הגרסה הנוכחית פחות יעילה מקודמתה? האם‬
‫בגרסה הנוכחית נקרא רקורסיבית גם עם הילד הימני אפילו אם הערך נמצא כבר‬
‫בילד השמאלי )ועל כן הקריאה הרקורסיבית עם הילד השמאלי החזירה את הערך‬
‫‪ ?(true‬התשובה היא‪ :‬לא‪ .‬מכיוון ששפת ‪ C‬מבצעת הערכה מקוצרת של ביטויים‬
‫בולאניים‪ ,‬ומכיוון שאנו מחזירים את תוצאת ה‪ or -‬בין שתי הקריאות‬
‫הרקורסיביות‪ ,‬אזי אם הקריאה עם הילד השמאלי תחזיר את הערך ‪ ,true‬ברור‬
‫שערכו של הביטוי השלם יהיה ‪ true‬ואין צורך לבדוק את המרכיב הימני בביטוי‬
‫הבולאני שבפקודת ה‪.return -‬‬
‫‪ 14.5.2‬חיפוש ערך בעץ חיפוש‬
‫כיצד נחפש ערך מבוקש בעץ חיפוש בינארי? האם גם כאן אנו עלולים להידרש‬
‫לסרוק את כל העץ )תוך שימוש בפונקציה רקורסיבית(? לא‪ .‬בעץ חיפוש בינארי‬
‫איננו חייבים לסרוק את כל העץ‪ ,‬אנו יכולים להתקדם על ענף מתאים עד שנאתר‬
‫את הערך‪ ,‬או עד שנגיע לתת‪-‬עץ ריק‪ .‬במקרה השני נוכל להסיק כי הערך אינו מצוי‬
‫בעץ‪ .‬נציג את הפונקציה ואחר נסבירה‪:‬‬
‫{ )‪bool search(int num, const struct Node * root‬‬
‫)‪while (root != NULL‬‬
‫)‪if (root -> _data == num‬‬
‫;) ‪return( true‬‬
‫)‪else if (num < root -> _data‬‬
‫; ‪root = root-> _left‬‬
‫‪else‬‬
‫; ‪root = root-> _right‬‬
‫; ) ‪return( false‬‬
‫}‬
‫‪125‬‬
‫ַבלולאה אנו מתקדמים עם המצביע ‪ root‬כל עוד ערכו אינו ‪ .NULL‬אם הגענו‬
‫לצומת המקיים שערכו של ‪ root-> _data‬שווה לערך המבוקש ניתן לקטוע את‬
‫ביצוע הפונקציה‪ ,‬ולהחזיר מייד את הערך ‪ .true‬אחרת‪ ,‬על‪-‬פי היחס בין הערך‬
‫המבוקש לערך המצוי בצומת הנוכחי אנו מקדמים את ‪ root‬שמאלה או ימינה‪ .‬אם‬
‫מיצינו את הלולאה בלא שהחזרנו את הערך ‪ true‬בשלב כל שהוא‪ ,‬אות הוא וסימן‬
‫שהערך המבוקש לא מצוי בעץ‪ ,‬ויש‪ ,‬על‪-‬כן‪ ,‬להחזיר את הערך ‪.false‬‬
‫שימו לב כי תודות לכך ש‪ root -‬מועבר כפרמטר ערך איו בעיה בכך שבפונ' אנו‬
‫משנים את ערכו‪ .‬הדבר לא יגרום לאובדן העץ בתכנית הראשית‪ .‬הצורך ב‪const -‬‬
‫הוא על מנת למנוע מהפונ' לשנות את הערכים שבעץ‪.‬‬
‫שאלה לא קשה‪ :‬מהו זמן הריצה של חיפוש ערך בעץ בינארי כללי‪ ,‬ובעץ חיפוש‬
‫בינארי במקרה הגרוע ביותר?‬
‫‪ 14.6‬ספירת מספר הצמתים בעץ בינארי‬
‫עתה נרצה לכתוב פונקציה אשר מקבלת מצביע לשורשו של עץ בינארי‪ ,‬ומחזירה את‬
‫מספר הצמתים בעץ‪ .‬הפונקציה דומה לפונקציית הביקור שראינו בסעיף ‪ ,4‬גם בה‬
‫עלינו לבקר בכל צמתי העץ‪ ,‬אולם הפעם איננו מתעניינים בערך המצוי בכל צומת‪,‬‬
‫אלא רק מוסיפים אחד למניה שאנו עורכים‪ .‬קוד הפונקציה יהיה על‪-‬כן‪:‬‬
‫{ )‪unsigned int count_nodes(const struct Node * root‬‬
‫; ‪unsigned int lcount, rcount‬‬
‫)‪if (root == NULL‬‬
‫; ) ‪return( 0‬‬
‫; )‪lcount = count_nodes( root-> _left‬‬
‫; )‪rcount = count_nodes(root -> _right‬‬
‫; )‪return(lcount + rcount + 1‬‬
‫}‬
‫הסבר‪ :‬הפונקציה פשוטה למדי‪ :‬בעץ ריק יש אפס צמתים‪ .‬בעץ שאינו ריק נמנה את‬
‫מספר הצמתים בילד השמאלי‪ ,‬נמנה את מספר הצמתים בילד הימני‪ ,‬ואז מספר‬
‫הצמתים בעץ שהצומת בו אנו נמצאים הוא שורשו הוא סכום הצמתים בשני‬
‫הילדים ועוד אחד )עבור הצומת המשמש כשורש )תת( העץ(‪.‬‬
‫יכולנו לכתוב את הפונקציה באופן מעט יותר קצר‪ ,‬ללא שימוש במשתני העזר‬
‫‪ .lcount, rcount‬אני מזמין אתכם לחשוב כיצד‪.‬‬
‫שימו לב שכפי שהזכרנו קודם‪:‬‬
‫‪ .I‬אנו מקפידים להחזיר ערך עבור מקרה הקצה‪.‬‬
‫‪ .II‬אנו מקפידים לקלוט את הערך המוחזר על‪-‬ידי הקריאות הרקורסיביות‬
‫שמזמנת הקריאה הנוכחית‪.‬‬
‫‪ .III‬אנו מקפידים להחזיר ערך שמסכם את מה שהוחזר מהקריאות‬
‫הרקורסיביות ואת 'תוצאת הבדיקה' שנערכה עבור השורש‪.‬‬
‫זמן הריצה הוא כמו בכל סריקה של העץ‪ :‬לינארי במספר הצמתים בעץ‪.‬‬
‫‪126‬‬
‫‪ 14.7‬מציאת עומקו של עץ בינארי‬
‫עומקו של עץ בינארי מוגדר באופן הבא‪:‬‬
‫‪ .I‬עומקו של עץ ריק הוא ‪.-1‬‬
‫‪ .II‬עומקו של עץ שאינו ריק הוא אחד ועוד המקסימום בין עומקם של שני בניו של‬
‫השורש‪.‬‬
‫במילים פשוטות עומקו של עץ בינארי הוא כמספר הצעדים שיש להתקדם מהשורש‬
‫אל העלה העמוק ביותר‪) .‬עלה בעץ בינארי הוא צומת שאין לו בנים(‪.‬‬
‫לדוגמה‪ :‬העץ הבא הוא בעומק ‪) 3‬לא ציירנו את הערכים בכל צומת שכן אין להם כל‬
‫משמעות עת דנים בעומק העץ‪ .‬כמו כן מצביעים שערכם ‪ NULL‬הושמטו מהציור(‪:‬‬
‫הפונקציה לחישוב עומקו של עץ בינארי תסתמך על פונקציה אשר מחזירה את‬
‫הערך הגדול בין שני המספרים הטבעיים שהועברו לה‪:‬‬
‫{ )‪int max(int num1, int num2‬‬
‫; ) ‪return( (num1 > num2) ? num1 : num2‬‬
‫}‬
‫הגדרת הפונקציה לחישוב עומק‪:‬‬
‫{ )‪int depth(const struct Node * root‬‬
‫)‪if (root == NULL‬‬
‫; ) ‪return( -1‬‬
‫; )‪int ldepth = depth(root-> _left‬‬
‫; )‪int rdepth = depth(root-> _right‬‬
‫; ) ‪return( max(ldepth, rdepth) + 1‬‬
‫}‬
‫הסבר הפונקציה‪ :‬הפונקציה היא מימוש ישיר של הגדרת עומק עץ‪ .‬עומקו של עץ‬
‫ריק הוא ‪ ,-1‬ועומקו של עץ שאינו ריק הוא אחד ועוד המקסימום בין עומקי‬
‫הילדים‪.‬‬
‫זמן הריצה של הפונ' הוא לינארי במספר הצמתים בעץ‪ ,‬שכן כל צומת אנו פוקדים‬
‫פעם יחידה; במילים אחרות‪ ,‬הפונ' פוקדת כל צומת פעם יחידה‪ ,‬ועושה עבודה‬
‫בשיעור קבוע עבור הצומת‪.‬‬
‫‪127‬‬
‫‪ 14.8‬האם עץ בינארי מקיים שלכל צומת פנימי יש שני בנים‬
‫עבור הפונקציה שנכתוב בסעיף זה נגדיר מספר מושגים הקשורים לעת בינארי‪:‬‬
‫‪ .I‬עלה )‪ (leaf‬בעץ בינארי הוא צומת שאין לו ילדים כלל‪.‬‬
‫‪ .II‬צומת בעץ בינארי שאינו עלה נקרא צומת פנימי )‪.(inner node‬‬
‫)אעיר כי עת העץ כולל רק שורש‪ ,‬אזי השורש הוא גם עלה‪ ,‬ולכן שורש ועלה אינם‬
‫מושגים מנוגדים זה לזה‪ .‬לעומת זאת עלה וצומת פנימי הם כן מושגים מנוגדים‪ :‬כל‬
‫צומת בעץ הוא צומת פנימי או עלה‪ ,‬אך לא שני המושגים גם יחד‪(.‬‬
‫עתה נציג פונקציה אשר בודקת האם לכל צומת פנימי בעץ בינארי יש שני ילדים‪ .‬על‬
‫הפונקציה להחזיר את הערך ‪ true‬עבור שני העצים השמאליים בציור‪ ,‬ואת הערך‬
‫‪ false‬על העץ הימני )שכן בעץ הימני‪ ,‬הילד הימני של השורש הוא צומת פנימי עם ילד‬
‫יחיד(‪.‬‬
‫{ )‪bool non_leaf_2_sons( const struct Node * root‬‬
‫; ‪bool ok‬‬
‫)‪if (root == NULL‬‬
‫; ) ‪return( true‬‬
‫|| )‪if ( (root->_left == NULL && root-> _right != NULL‬‬
‫) )‪(root-> _right == NULL && root->_left != NULL‬‬
‫; ) ‪return( false‬‬
‫; ) ‪ok = non_leaf_2_sons( root->_left‬‬
‫)‪if (!ok‬‬
‫; )‪return(false‬‬
‫; ) ‪ok = non_leaf_2_sons( root->_left‬‬
‫; )‪return(ok‬‬
‫}‬
‫בפונקציה אין רבותא‪ ,‬והיא דומה לפונקציות שכבר כתבנו בעבר‪ :‬אם הגענו לתת‪-‬עץ‬
‫ריק‪ ,‬אזי הוא 'תקין' וניתן להחזיר את הערך ‪ .true‬מנגד‪ ,‬אם הגענו לצומת עם ילד‬
‫יחיד )כלומר שילדו השמאלי הוא ‪ NULL‬וילדו הימני אינו ‪ ,NULL‬או להפך(‪ ,‬אזי יש‬
‫להחזיר את הערך ‪ .false‬בכל מקרה אחר )לצומת שני ילדים ריקים‪ ,‬או שני ילדים‬
‫שאינם ריקים( אנו קוראים רקורסיבית ראשית עבור הילד השמאלי‪ .‬אם בילד זה‬
‫התגלתה 'תקלה' יש להחזיר ‪ .false‬לבסוף‪ ,‬אם הילד השמאלי אינו בעייתי אזי יש‬
‫להחזיר את תוצאת הבדיקה של הילד הימני‪.‬‬
‫‪128‬‬
‫זמן הריצה של הפונ' הינו לינארי במספר הצמתים בעץ‪ ,‬שכן עבור כל צומת הפונ'‬
‫נקראת פעם יחידה )וכן היא נקראת גם עבור הילדים הריקים של עלי העץ‪ ,‬כלומר‬
‫המצביעים שערכם ‪ NULL‬בעלים‪ ,‬אולם כאלה יש לכל היותר כמספר צמתי העץ(‬
‫‪ 14.9‬האם עץ בינארי מקיים שעבור כל צומת סכום הערכים‬
‫בבת‪-‬העץ השמאלי קטן או שווה מסכום הערכים בתת‪-‬העץ‬
‫הימני‬
‫עתה נרצה להשלים את המשימה הבאה‪ :‬נתון עץ בינארי שאינו עץ חיפוש‪ ,‬ואין זה‬
‫מעניינו עתה כיצד העץ נבנה‪ .‬הפונ' שנכתוב מקבלת מצביע לשורשו של עץ זה‪ ,‬ועליה‬
‫להחזיר האם שורש העץ השלם‪ ,‬וכל צומת אחר בעץ מקיימים שסכום הערכים בתת‬
‫העץ השמאלי של הצומת קטן או שווה מסכום הערכים בתת‪-‬העץ הימני‪.‬‬
‫לדוגמה‪ :‬עבור העץ הבא‪ ,‬הפונ' אמורה להחזיר את הערך ‪:true‬‬
‫‪1‬‬
‫‪21‬‬
‫‪7‬‬
‫‪3‬‬
‫‪0‬‬
‫‪4‬‬
‫‪2‬‬
‫‪4‬‬
‫נסביר‪ :‬לדוגמה‪ ,‬תת‪-‬העץ השמאלי של שלוש הוא ריק‪ ,‬ולכן סכומו אפס‪ ,‬וסכום זה‬
‫קטן משתיים‪ ,‬שהינו סכמו של תת‪-‬העץ הימני של שלוש‪ .‬עבור הצמות של ‪ ,7‬סכומו‬
‫של תת‪-‬העץ השמאלי הוא חמש‪ ,‬בעוד סכומו של תת‪-‬העץ הימני הוא שמונה‪ .‬וכך‬
‫הלאה‪ .‬לו‪ ,‬למשל ה‪ 4-‬השמאלי היה מוחלף ב‪ ,5-‬אזי עבור הצומת של אפס סכום‬
‫הערכים בתת‪-‬העץ השמאלי היה גדול מסכום הערכים בתת‪-‬העץ הימני‪ ,‬ולכן על‬
‫הפונ' היה להחזיר את הערך ‪.false‬‬
‫זימון הפונ' יהיה‪:‬‬
‫))‪if (sum_left_leq_sum_right(root‬‬
‫; "‪cout << "sum left <= sum right\n‬‬
‫‪else‬‬
‫; "‪cout << "sum left > sum right for some subtree\n‬‬
‫נפנה עתה להגדרת הפונ'‪ ,‬ונציג שתי דרכים שונות להשלמת המשימה‪ :‬הדרך‬
‫הראשונה יותר פשוטה ופחות יעילה‪ ,‬הדרך השניה פחות פשוטה ויותר יעילה‪ .‬נפתח‬
‫בדרך הראשונה‪.‬‬
‫רעיון האלגוריתם ַבפתרון הראשון הוא לסרוק את העץ )בסריקה תחילית(‪ .‬עבור כל‬
‫צומת אליו מגיעים לסכום את תת‪-‬העץ השמאלי של הצומת‪ ,‬לסכום את תת‪-‬העץ‬
‫‪129‬‬
‫הימני של הצומת )וזאת על‪-‬ידי זימון פונ' אחרת‪ ,‬כזו היודעת לסכום עץ(‪ .‬אם סכומו‬
‫של תת‪-‬העץ השמאלי גדול מזה של תת‪-‬העץ הימני אזי גילינו הפרה של הדרישה‪,‬‬
‫ועל כן נחזיר מייד את הערך ‪ .false‬אחרת‪ ,‬נמשיך בבדיקה עם תת העץ השמאלי )ע"י‬
‫קריאה רקורסיבית לפונ' עם תת העץ השמאלי(‪ ,‬ואם הוא יתגלה כתקין אזי נמשיך‬
‫בבדיקה גם עם תת‪-‬העץ הימני‪ .‬אם גם הוא יתגלה כתקין אזי נחזיר את הרך ‪.true‬‬
‫כמובן שאם אחד מתתי‪-‬העצים יתגלה כלא תקין אזי נחזיר את הערך ‪.false‬‬
‫כלומר עבור הפתרון אנו זקוקים לפונ' עזר אשר סוכמת עץ‪ .‬זוהי פונ' פשוטה מאוד‬
‫ונציג אותה עתה‪:‬‬
‫)‪int sum_tree(const struct Node *root‬‬
‫{‬
‫)‪if (root == NULL‬‬
‫; ‪return 0‬‬
‫‪return(sum_tree(root -> _left) +‬‬
‫‪sum_tree(root -> _right) +‬‬
‫; )‪root -> _data‬‬
‫}‬
‫הפונ' דומה מאוד לפונ' שראינו בעבר‪ ,‬ועל כן לא נסביר אותה‪ .‬נשתמש בה לשם‬
‫השלמת המשימה עימה אנו מתמודדים בסעיף זה‪ .‬נציג עתה את הגרסה הראשונה‬
‫של הפונ' ‪ ,sum_left_leq_sum_right‬כלומר הפונ' המרכזית בסעיף זה‪.‬‬
‫)‪bool sum_left_leq_sum_right(const struct Node *root‬‬
‫{‬
‫; ‪int suml, sumr‬‬
‫; ‪bool ok‬‬
‫)‪if (root == NULL‬‬
‫; ‪return true‬‬
‫)‪// (1‬‬
‫)‪// (2‬‬
‫; )‪suml = sum_tree(root -> _left‬‬
‫; )‪sumr = sum_tree(root -> _right‬‬
‫)‪if (suml > sumr‬‬
‫; ‪return false‬‬
‫; )‪ok = sum_left_leq_sum_right(root -> _left‬‬
‫)‪if (!ok‬‬
‫)‪// (3‬‬
‫; ‪return false‬‬
‫; )‪ok = sum_left_leq_sum_right(root -> _right‬‬
‫; ‪return ok‬‬
‫}‬
‫נדגים את התנהלות הפונ' על העץ שמופיע בדוגמה מעל‪ :‬עת הפונ' נקראת לראשונה‪,‬‬
‫עבור שורש העץ‪ ,‬התנאי ‪ root==NULL‬אינו מתקיים‪ ,‬ולכן לא ניתן להחזיר‬
‫מיידית את הערך ‪ .true‬על כן אנו מתקדמים להשמה המסומנת ב‪ (1) -‬וקוראים‬
‫לפונ' אשר תסכום את תת‪-‬העץ השמאלי‪ .‬בדוגמה שלנו יוחזר הערך ‪ ,20‬אשר יוכנס‬
‫למשתנה ‪ .suml‬באופן דומה‪ ,‬בהשמה )‪ (2‬אנו סוכמים את ת‪-‬העץ הימני של הצומת‬
‫של ‪ ,1‬וסכומו כמובן ‪ .21‬על כן התנאי ‪ suml>sumr‬אינו מתקיים‪ ,‬ואין להחזיר‬
‫‪ .false‬בזאת גמרנו לבדוק את השורש )אשר נבדק תחילה‪ ,‬ולכן אמרנו קודם‬
‫שהפונ' סורקת את העץ בביקור תחילי(‪ .‬עת הפונ' שלנו קוראת רקורסיבית עם ת‪-‬‬
‫העץ השמאלי‪ ,‬כלומר עם המצביע לצומת של ‪to make a long story .7‬‬
‫‪ short‬קריאה זאת תייצר עוד קריאות רקורסיביות רבות )תריסר‪ ,‬ליתר דיוק‪.‬‬
‫‪130‬‬
‫בדקו( ובסופו של גבר תחזיר את הערך ‪true‬אשר ייכנס למשתנה ‪ .ok‬התנאי )‪(3‬‬
‫לא יתקיים‪ ,‬ועל כן לא יוחזר הערך ‪ .false‬עתה תזמן הקריאה הרקורסיבית‬
‫הראשונה קריאה רקורסיבית לפונ' ‪ sum_left_leq_sum_right‬עם תת‪-‬העץ‬
‫הימני‪ ,‬כלומר עם המצביע לצומת של ‪ .21‬גם קריאה זאת תחזיר את הערך ‪true‬‬
‫)אחרי שהיא‪ ,‬מצדה‪ ,‬תזמן עוד שתי קריאות רקורסיביות(‪ .‬ולכן הפונ' שלנו תחזיר‬
‫את הערך ‪ ,true‬כראוי‪.‬‬
‫נשאל עתה את עצמנו מהו זמן הריצה של הפונ' שכתבנו? קל לראות שזמן הריצה של‬
‫‪ sum_tree‬הוא לינארי במספר הצמתים בעץ )בדומה ל‪.(non_leaf_2_sons -‬‬
‫הפונ' שלנו‪ ,‬עבור כל צומת קוראת ל‪ sum_tree -‬עם שני ילדיו של הצומת‪ .‬נניח‬
‫לרגע שהעץ שלנו 'התנוון' לכדי רשימה מקושרת )בת ‪ n‬איברים(‪ ,‬ולכן הוא נראה‬
‫באופן‬
‫הבא‪:‬‬
‫‪1‬‬
‫‪2‬‬
‫‪...‬‬
‫‪n‬‬
‫עבור שורש העץ נקרא ל‪ sum_tree -‬עם הילד השמאלי‪ ,‬נעשה עבודה בשיעור‬
‫קבוע‪ ,‬וכן נקרא ל‪ sum_tree -‬עם הילד הימני‪ ,‬ועל כך נשלם סד"ג של ‪n-1‬‬
‫צעדים‪ .‬נגלה שבינתיים הכל בסדר‪ ,‬ולכן נקרא רקורסיבית לפונ' עם ילדו השמאלי‪,‬‬
‫הריק‪ ,‬של הצומת של ‪ ,1‬ואח"כ עם הצומת של ‪ .2‬כאן שוב נקרא פעמיים ל‪-‬‬
‫‪ ,sum_tree‬ונשלם על כך ‪ n-2‬צעדים‪ .‬שוב הכל יהיה בסדר‪ ,‬ולכן נקרא לפונ' עם‬
‫ילדו השמאלי של ‪ ,2‬ומה שחשוב יותר‪ ,‬עם ילדו הימני של ‪ ,2‬וכך הלאה‪ .‬כמות‬
‫העבודה שאנו עושים‪ ,‬בכל הקריאות ל‪ sum_tree -‬גם יחיד היא לפיכך‪:‬‬
‫‪ (n-1)+(n-2)+(n-3)+…+1‬כלומר סד"ג של ‪ n2‬צעדים‪.‬‬
‫כמות העבודה הרבה יחסית נגרמת מכך שעבור כל צומת של העץ אנו קוראים‬
‫רקורסיבית לפונ' ‪ sum_tree‬עם שני תתי‪-‬העצים של הצומת‪ .‬זאת נרצה לחסוך‪ .‬על‬
‫כן עתה נכתוב גרסה שניה‪ ,‬יעילה יותר‪ ,‬של הפונ'‪ .‬רעיון הגרסה השניה הוא שכל‬
‫צומת יחזיר לנו באמצעות ערך ההחזרה האם הוא תקין או לא )כמו בגרסה‬
‫הראשונה(‪ ,‬אולם מעבר לכך‪ ,‬באמצעות פרמטר הפניה‪ ,‬יחזיר לנו הצומת גם מה‬
‫סכום הערכים בתת‪-‬העץ שלו‪ .‬בערך זה נשתמש כדי לבדוק האם השורש בו אנו‬
‫נמצאים בכל קריאה רקורסיבית הינו תקין או לא‪.‬‬
‫הזימון לפונ' הפעם יהיה‪:‬‬
‫))‪if (sum_left_leq_sum_right(root, x‬‬
‫; "‪cout << "sum left <= sum right\n‬‬
‫‪else‬‬
‫; "‪cout << "sum left > sum right for some subtree\n‬‬
‫‪131‬‬
‫כלומר‪ ,‬הפונ' מקבלת גם משתנה )בשם ‪ (x‬לתוכו הפונ' תכניס את סכום הערכים בעץ‬
‫השלם‪ .‬בתכנית הראשית‪ ,‬ערכו של ‪ x‬לא יעניין אותנו‪ .‬נשתמש בפרמטר זה רק‬
‫בקריאות הרקוסיביות‪.‬‬
‫נציג עתה את הפונ'‪ ,‬ואחר נדון בה‪:‬‬
‫‪bool sum_left_leq_sum_right(const struct Node *root,‬‬
‫)‪int &sum‬‬
‫{‬
‫; ‪int suml, sumr‬‬
‫; ‪bool ok‬‬
‫{ )‪if (root == NULL‬‬
‫; ‪sum = 0‬‬
‫; ‪return true‬‬
‫}‬
‫; )‪ok = sum_left_leq_sum_right(root-> _left, suml‬‬
‫)‪if (!ok‬‬
‫; ‪return false‬‬
‫; )‪ok = sum_left_leq_sum_right(root-> _right, sumr‬‬
‫)‪if (!ok‬‬
‫; ‪return false‬‬
‫; ‪sum = suml + sumr + root -> _data‬‬
‫; ) ‪return( (suml <= sumr) ? true : false‬‬
‫}‬
‫נסביר‪ :‬תנאי העצירה של הרקורסיה הוא עת אנו מגיעים לתת‪-‬עץ ריק‪ .‬במצב זה אנו‬
‫מחזירים שני דברים‪ :‬באמצעות פרמטר ההפניה אנו מחזירים שסכום הערכים‬
‫בתת‪-‬העץ 'שלנו' הוא אפס‪ ,‬ובאמצעות ערך ההחזרה אנו מחזירים שתת‪-‬העץ 'שלנו'‬
‫תקין‪.‬‬
‫במקרה הכללי )עת לא הגענו לתת‪-‬עץ ריק(‪ :‬אנו מזמנים את הפונ' עם תת‪-‬נעץ‬
‫השמאלי‪ ,‬ומעבירים לקריאה גם את המשתנה ‪ .suml‬הקריאה עם תת‪-‬העץ השמאלי‬
‫תחזיר לנו שני דברים‪ :‬האחד האם תת‪-‬העץ השמאלי תקין‪ ,‬והשני‪ ,‬באמצעות‬
‫‪ ,suml‬את סכומו של תת‪-‬העץ השמאלי‪ .‬כמובן שאם תת‪-‬העץ השמאלי אינו תקין‪,‬‬
‫אין לנו מנוס אלא להחזיר ‪ .false‬אחרת‪ ,‬נקרא רקורסיבית באופן דומה‪ ,‬עם תת‪-‬‬
‫העץ הימני‪ .‬בהנחה שגם קריאה זו החזירה לנו ‪ ,true‬אנו פונים לטפל בערך שעלינו‬
‫להחזיר למי שקרא לנו‪ :‬ראשית‪ ,‬לפרמטר ההפניה ‪ sum‬נכניס את סכום הערכים‬
‫בתת‪-‬העץ שלנו‪ ,‬שהינו ‪ ,suml + sumr + root -> _data‬ושנית נחזיר את‬
‫הערך הבולאני על‪-‬פי היחס בין ‪ suml‬ל‪) sumr -‬כמובן שאם אנו מחזירים את הערך‬
‫‪ false‬אזי למעשה אין חשיבות לסכום הערכים בתת‪-‬העץ שלנו(‪.‬‬
‫ומהו זמן הריצה של הפונ' בגרסתה נוכחית?‬
‫עתה אנו מבצעים סריקה סופית רגילה של העץ‪ :‬עבור כל צומת‪ ,‬מזמנים את הפונ'‬
‫עם ילדו השמאלי‪ ,‬מזמנים את הפונ' עבור ילדו הימני )כמובן לא שוכחים להתעניין‬
‫בערך שמחזירות שתי הקירות הרקורסיביות(‪ ,‬ולבסוף 'מטפלים' בשורש עצמו‪:‬‬
‫מעדכנים את ‪ sum‬ומחזירים את הערך הרצוי‪ .‬לכן זמן הריצה של הפונ' הוא לינארי‬
‫במספר הצמתים בעץ‪ .‬זהו כמובן שיפור ניכר יחסית לגרסה שראינו קודם‪ ,‬ואשר זמן‬
‫ריצתה עלול היה להתדרדר לכדי ‪.n2‬‬
‫‪132‬‬
‫השיפור הושג ע"י שימוש בפרמטר הפניה אשר מאפשר לנו להחזיר מכל קריאה‬
‫רקורסיבית שני נתונים‪ :‬גם האם תת העץ הנוכחי תקין‪ ,‬וגם את סכומו‪.‬‬
‫‪ 14.10‬שחרור עץ בינארי‬
‫עת איננו זקוקים יותר לעץ הבינארי עלינו לשחררו‪ .‬תהליך השחרור מחייב‬
‫סריקה של העץ‪ .‬הפעם נשתמש בסריקה סופית‪ :‬עבור כל תת‪-‬עץ שאינו ריק‪,‬‬
‫ראשית נשחרר את הילד השמאלי‪ ,‬שנית נשחרר את הילד הימני‪ ,‬ובסוף נשחרר‬
‫את השורש‪.‬‬
‫הקוד‪:‬‬
‫)‪void free_tree(struct Node *root‬‬
‫{‬
‫)‪if (root != NULL‬‬
‫{‬
‫; ) ‪free_tree(root -> _left‬‬
‫; ) ‪free_tree(root -> _right‬‬
‫; ‪delete root‬‬
‫}‬
‫}‬
‫זימון הפונ' )מהתכנית הראשית(‪free_tree(root); :‬‬
‫הסבר הפונ'‪ :‬אם הגענו לתת עץ שאינו ריק אזי נשחחר את תת‪ -‬העץ השמאלי שלו‪,‬‬
‫וזאת ע"י קריאה רקורסיבית עם הילד השמאלי‪ ,‬אחר נשחרר את תת‪-‬העץ הימני של‬
‫השורש )ע"י קריאה רקוסיבית עם הילד הימני(‪ ,‬ולבסוף נשחרר את השורש‪.‬‬
‫שאלה‪ :‬האם יש להעביר את הפרמטר לפונ' כפרמטר הפניה )כלור עם &(?‬
‫תשובה‪ :‬זה לא הכרחי‪ .‬על מנת לשחרר את הזיכרון שהוקצה די שיש לנו מצביע‬
‫לשטח זיכרון זה‪ ,‬כפי שמעמיד לרשותו פרמטר ערך‪.‬‬
‫נשים לב שאחרי הקריאה לפונ' ערכו של המשתנה ‪ root‬של התכנית הראשית נותר‬
‫בעינו‪ ,‬כלומר הוא מצביע לאותו מקום אליו הוא הצביע קודם לכן‪ ,‬אולם שטח‬
‫הזיכרון הזה כבר לא שייך לנו‪ ,‬שכן שחררנו אותו‪ ,‬ובזאת העמדנו אותו שוב לרשות‬
‫מערכת ההפעלה‪ ,‬כך שהיא תוכל להקצותו שוב בפעולות הקצאה עתידיות‪.‬‬
‫‪133‬‬
‫‪ 14.11‬מחיקת צומת מעץ בינארי‬
‫בסעיף זה ברצוננו לכתוב פונקציה אשר מקבלת מצביע לשורשו של עץ חיפוש‬
‫בינארי‪ ,‬וערך שלם‪ .‬הפונקציה מוחקת מהעץ את הצומת בו מצוי הערך‪ .‬במידה‬
‫והערך אינו מצוי בעץ כלל‪ ,‬לא ַתעשה הפונקציה דבר; במידה והערך מצוי בעץ מספר‬
‫תמחק הפונקציה מופע יחיד שלו‪.‬‬
‫פעמים ְ‬
‫‪ 14.11.1‬תיאור האלגוריתם‬
‫נביט בעץ החיפוש הבא‪:‬‬
‫‪17‬‬
‫ב'‬
‫א'‬
‫‪13‬‬
‫‪27‬‬
‫ג'‬
‫‪43‬‬
‫ה'‬
‫ד'‬
‫‪3‬‬
‫‪16‬‬
‫ו'‬
‫‪14‬‬
‫האותיות שלצד המצביעים שבציור תשמשנה אותנו כדי לסמן )לתייג( את‬
‫המצביעים השונים בנוחות‪.‬‬
‫נבחן מקרים שונים של מחיקה‪:‬‬
‫‪ .I‬מחיקת עלה‪ :‬נניח שיש למחוק את הצומת המכיל את הערך ‪ ,43‬או את זה‬
‫המכיל את הערך ‪ .14‬זה מקרה פשוט‪ ,‬שכן יש למחוק צומת שהינו עלה בעץ‪ .‬כל‬
‫שיש לעשות הוא‪ (1) :‬לשמור באמצעות מצביע עזר )נניח ‪ (temp‬מצביע לצומת‬
‫שאותו עלינו למחוק‪ (2) .‬להכניס את הערך ‪ NULL‬למצביע אשר מורה על הצומת‬
‫)המצביע ג' במקרה של ‪ ,43‬או המצביע ו' במקרה של ‪ (3) .(14‬לשחרר את הצומת‬
‫עליו מצביע ‪.temp‬‬
‫‪ .II‬מחיקת צומת בעל ילד יחיד‪ :‬עתה נניח כי עלינו למחוק את הצומת המכיל את‬
‫הערך ‪ ,27‬או את זה המכיל את ‪ .16‬גם זה מקרה פשוט למדי‪ .‬הפעולות הנדרשות‬
‫הפנה את מצביע העזר ‪ temp‬להורות על הצומת שיש למחוק‪.‬‬
‫במקרה זה הן‪ְ (1) :‬‬
‫)‪ (2‬הפנה את המצביע המורה על הצומת שיש למחוק להצביע על ילדו הלא ריק‬
‫נפנֵה את המצביע א'‬
‫של הצומת שיש למחוק‪ַ ) .‬במקרה של הצומת של ‪ְ 27‬‬
‫]המצביע על הצומת של ‪ [27‬להצביע כמו ג' ]ילדו הלא ריק של הצומת של ‪.[27‬‬
‫במקרה של ‪ ,16‬נפנה את המצביע ד' להצביע כמו המצביע ו'‪ ,‬כלומר ד' יעבור‬
‫להצביע על הצומת של ‪ (3) .(14‬שחרר את הצומת עליו מצביע ‪.temp‬‬
‫‪ .III‬מחיקת צומת בעל שני ילדים‪ :‬זהו המקרה המורכב‪ .‬כדי לטפל בו עלינו‬
‫להגדיר את המושג‪ :‬העוקב ַבעץ של ערך ‪ .v1‬העוקב בעץ של ערך ‪ ,v1‬הוא הערך‬
‫הקטן ביותר ‪ v2‬המקיים ש‪ .v2 >= v1 :‬לדוגמה‪ :‬בעץ שהצגנו‪ ,‬העוקב של ‪ 13‬הוא‬
‫‪ ,14‬והעוקב של ‪ 17‬הוא ‪ .27‬שימו לב כי ְלערך המצוי ְבצומת עם בן ימני תמיד‬
‫ימצא בתת‪-‬העץ עליו מצביע בנו הימני של הצומת‬
‫יהיה עוקב בעץ‪ ,‬והעוקב ֵ‬
‫‪134‬‬
‫המכיל את ‪ .v1‬נשים לב כי העוקב של ערך ‪ v1‬בעץ מתקבל על‪-‬ידי ירידה לבנו‬
‫הימני של הצומת של ‪ ,v1‬ואחר כל‪-‬עוד ניתן‪ :‬ירידה לבן השמאלי של הצומת‬
‫הנוכחי‪ .‬לדוגמה‪ :‬לעוקב של ‪ 13‬נגיע על‪-‬ידי ירידה לבנו הימני של הצומת בו מצוי‬
‫‪) 13‬הצומת של ‪ ,(16‬ואחר כך ירידה שמאלה לבנו השמאלי של ‪ .16‬באופן דומה‬
‫לעוקב של ‪ 17‬נגיע על‪-‬ידי שנרד לבנו הימני של הצומת של ‪) 17‬כלומר לצומת של‬
‫‪ ,(27‬ומשם לא ניתן לרדת שמאלה‪ ,‬לכן ַבצומת של ‪ 27‬נעצור‪ .‬שימו לב כי לוּ ל‪27 -‬‬
‫היה בן שמאלי שהכיל את ‪ ,23‬ולאחרון היה בן שמאלי שהכיל את ‪ ,21‬אזי היינו‬
‫יורדים שמאלה לשני בנים אלה‪ ,‬וכך מאתרים את העוקב של ‪ 17‬בעץ‪ .‬עתה נשים‬
‫לב כי העוקב של ‪ַ v1‬בעץ מצוי ְבצומת שאין לו שני בנים‪ :‬העוקב של ‪ v1‬בעץ מצוי‬
‫בעלה‪ ,‬או בצומת שיש לו רק בן ימני )לוּ היה לצומת גם בן שמאלי היינו‬
‫מתקדמים ממנו לבנו השמאלי(‪.‬‬
‫עתה‪ ,‬אחרי שהגדרנו את העוקב של ערך ‪ v1‬בעץ‪ ,‬ואחרי שלמדנו כיצד מגיעים‬
‫מהצומת בו מצוי ‪ v1‬לצומת בו מצוי העוקב שלו‪ ,‬נוכל לתאר את תהליך‬
‫המחיקה של ערך מצומת לו שני בנים‪:‬‬
‫הפנה את‬
‫‪ .I‬אתר את הצומת בו מצוי העוקב בעץ של הערך אותו יש למחוק‪ְ .‬‬
‫מצביע העזר ‪ temp‬להצביע על צומת זה‪.‬‬
‫העתק את הערך ‪ַ temp-> _data‬לצומת בו מצוי הערך שיש למחוק‪.‬‬
‫‪.II‬‬
‫‪.III‬‬
‫מחק מהעץ את הצומת עליו מצביע ‪ .temp‬מכיוון שצומת זה הוא‬
‫עלה‪ ,‬או מכיל בן ימני בלבד‪ ,‬אזי המחיקה תעשה על פי סעיף א' או סעיף ב'‬
‫באלגוריתם המחיקה‪.‬‬
‫לדוגמה‪ :‬כדי למחוק את ‪ 17‬מהעץ המלווה אותנו‪) :‬א( נאתר את הצומת המכיל‬
‫את העוקב של ‪ 17‬בעץ‪ .‬כפי שראינו זהו הצומת של ‪) .27‬ב( נעתיק את הערך ‪27‬‬
‫לצומת של ‪ ,17‬כלומר לשורש העץ‪) .‬ג( נמחק את הצומת של ‪) 27‬כלומר הצומת‬
‫עליו מורה המצביע א'(‪ .‬מכיוון שמדובר בצומת עם בן יחיד נבצע את המחיקה‬
‫על פי המתואר בסעיף ב' באלגוריתם‪ .‬דוגמה שניה‪ :‬כדי למחוק את ‪ 13‬מהעץ‪:‬‬
‫)א( נאתר את הצומת המכיל את העוקב ל‪ 13 -‬בעץ‪ ,‬כלומר את הצומת של ‪) .14‬ב(‬
‫נעתיק את הערך ‪ 14‬לצומת של ‪) .13‬ג( נמחק את הצומת של ‪ 14‬מהעץ‪ .‬מכיוון‬
‫שמדובר בעלה אזי המחיקה תתבצע על‪-‬פי סעיף א' באלגוריתם‪.‬‬
‫עד כאן תיארנו את אלגוריתם המחיקה‪ .‬עתה נציג את מימושו‪.‬‬
‫‪ 14.11.2‬המימוש‬
‫כפי שנראה‪ ,‬כדי שהפונקציה למחיקת ערך מעץ לא תהא מסורבלת‪ ,‬יהיה עלינו‬
‫להעביר לה מצביע ַלמצביע אשר מורה על שורש העץ‪ .‬נבחן ראשית מדוע העברת‬
‫המצביע לשורש העץ כפרמטר משתנה תחייב אותנו לכתוב פונקציה מסורבלת )ועל‬
‫כן לא ננקוט בגישה זאת‪ ,‬אלא נעביר מצביע למצביע לשורש העץ(‪ .‬לכן עתה נניח כי‬
‫אנו מעבירים לפונקציה את המצביע לשורש העץ כפרמטר משתנה‪ ,‬ונראה כיצד‬
‫עלינו לכתוב את פונקצית המחיקה‪ .‬מחיקת צומת מעץ דומה‪ ,‬במובנים רבים‪,‬‬
‫למחיקת צומת מרשימה משורשרת‪ ,‬ועל כן נרצה להתקדם עם שני מצביעים‪ .‬עת‬
‫המצביע הקדמי יצביע על הצומת שמכיל את הערך הרצוי‪ ,‬נַפנה את המצביע לבנו‬
‫המתאים של הצומת עליו מצביע המצביע האחורי ַל ֵבן המתאים של הצומת שיש‬
‫למחוק‪ .‬אם כל כך טוב‪ ,‬אז מה כל‪-‬כך רע? שלכל צומת יתכנו שני בנים‪ ,‬ולא רק אחד‬
‫כמו ברשימה משורשרת‪ .‬על כן יש לנו ארבע אפשרויות שונות של שינויים‪ :‬אנו‬
‫עשויים להידרש להפנות את בנו השמאלי או הימני של הצומת עליו מצביע המצביע‬
‫האחורי‪ ,‬ואת המצביע שיש להפנות אנו עשויים להידרש להפנות לבנו השמאלי או‬
‫הימני של הצומת שיש למחוק‪.‬‬
‫נדגים את הקוד‪:‬‬
‫{ )‪void delete_node(int val, struct Node * &root‬‬
‫‪135‬‬
struct Node *wanted_node = root,
*father_of_wanted = NULL ;
‫ הוא‬:‫ ברשימות משורשרות‬front -‫ הוא האנלוגי ל‬wanted_node ‫המצביע‬
father_of_wanted ‫ המצביע‬.‫שיתקדם עד לצומת שמכיל את הערך שיש למחוק‬
‫ ברשימות‬rear -‫כן האנלוגי ל‬-‫ ויהיה על‬,wanted_node ‫יצביע על אביו של‬
.‫משורשרות‬
:‫ ואביו‬,‫הלולאה הבאה תתקדם עד לאיתור הצומת הרצוי‬
while (wanted_node != NULL) {
// find the node and
// its father
if (wanted_node -> _data == val) break ;
if (wanted_node -> _data > val) {
father_of_wanted = wanted_node ;
wanted_node = wanted_node -> _left ;
}
else {
father_of_wanted = wanted_node ;
wanted_node = wanted_node -> _right ;
}
}
‫ מצביע על הצומת‬wanted_node ‫ אזי‬,‫ אם הערך המבוקש מצוי בעץ‬,‫בשלב זה‬
‫ אם הערך‬,‫ מנגד‬.‫ מצביע על אביו של הצומת‬father_of_wanted ;‫שכולל אותו‬
:‫ לפיכך עתה נבדוק‬.NULL ‫ הוא‬wanted_node ‫ אזי ערכו של‬,‫אינו מצוי בעץ‬
if (wanted_node == NULL) return ;
// val not found
‫ המקרה הפשוט ביותר הינו‬.‫עתה נתקדם לטיפול במצב בו הערך המבוקש מצוי בעץ‬
‫ מקרה זה עלינו לחלק למספר‬.‫זה שבו הצומת בו מצוי הערך המבוקש הינו עלה‬
‫ )ב( הצומת בו מצוי הערך‬,‫ )א( הצומת בו מצוי הערך הוא שורש העץ‬:‫מצבים שונים‬
‫ נציג‬.‫ )ג( הצומת בו מצוי הערך הוא בנו הימני של אביו‬,‫הוא בנו השמאלי של אביו‬
:‫את הקוד‬
if (wanted_node->_left == NULL && // del a leaf
wanted_node-> _right == NULL ) {
if (wanted_node == root) {
root = NULL ;
delete wanted_node ;
}
else { // the wanted val is not in root
if (father_of_wanted -> _left == wanted_node)
father_of_wanted - >_left = NULL ;
else // father_of_wanted -> son == wanted_node
father_of_wanted -> _right = NULL ;
delete wanted_node ;
}
‫ ַלצומת בו שוכן הערך המבוקש יש רק בן‬:‫נפנה עתה לטפל במקרה הפשוט השני‬
:‫ והוא עצמו בן ימני או שמאלי של אביו‬,(‫יחיד )ימני או שמאלי‬
else
// wanted val is not in leaf
136
‫‪if (wanted_node -> _left == NULL || // but has 1 son‬‬
‫{ )‪wanted_node -> _right == NULL‬‬
‫‪if (wanted_node -> _left != NULL) // has a left son‬‬
‫‪// and wanted_node is left son of its father‬‬
‫)‪if (father_of_wanted -> _left == wanted_node‬‬
‫= ‪father_of_wanted -> _left‬‬
‫;‪wanted_node->_left‬‬
‫‪else‬‬
‫‪// wanted is a right son of its dad‬‬
‫= ‪father_of_wanted -> _right‬‬
‫;‪wanted_node->_left‬‬
‫‪else // wanted has only right son‬‬
‫‪// and wanted_node is a left son of its dad‬‬
‫{ )‪if (father_of_wanted -> _left == wanted_node‬‬
‫= ‪father_of_wanted -> _left‬‬
‫;‪wanted_node-> _right‬‬
‫‪else‬‬
‫‪// wanted is a right son of its dad‬‬
‫= ‪father_of_wanted -> _right‬‬
‫;‪wanted_node-> _right‬‬
‫}‬
‫‪else // wanted_node has two sons‬‬
‫בשלב זה‪ ,‬כשאנו כבר יגעים למדי‪ ,‬הגענו למקרה המורכב‪ .‬אשאיר לכם להשלימו‬
‫כתרגיל‪.‬‬
‫כפי שאמרנו בתחילת הסעיף‪ ,‬הדרך האלגנטית יותר לכתוב את פונקצית המחיקה‬
‫היא להעביר לה מצביע למצביע‪ .‬מדוע גישה זאת מקצרת את הפונקציה? כפי‬
‫שראינו עם רשימות משורשרות‪ ,‬עת אנו מטפלים במצביע למצביע אין לנו צורך בזוג‬
‫מצביעים‪ ,‬די לנו באחד; קל וחומר שאיננו מסתבכים עם עיסוק בשאלה האם מדובר‬
‫בבן שמאלי או בבן ימני‪.‬‬
‫בגרסה הנוכחית של פונקצית המחיקה‪ ,‬הפרמטר השני מוגדר כ‪:‬‬
‫‪ struct Node **root_ptr‬כלומר מצביע למצביע‪ .‬הקריאה לפונקציה תהא‬
‫לפיכך‪ delete_node(num, &root); :‬עבור משתנה ‪ root‬של התכנית‬
‫הראשית‪ ,‬אשר הוגדר כ‪ , struct Node *root; :‬ושעתה מצביע לשורש העץ‪.‬‬
‫מסיבה זאת בגוף הפונקציה‪ ,‬עת אנו רוצים להשתמש במצביע ‪ root‬אנו צריכים‬
‫לכתוב ‪ ,*root_ptr‬שכן ‪ root_ptr‬מצביע ל‪ ,root -‬ועל‪-‬כן ‪ *root_ptr‬הוא‬
‫האובייקט עליו ‪ root_ptr‬מצביע‪ ,‬כלומר ‪ .root‬כדי לפנות לשדה ה‪data -‬‬
‫ַבקופסה עליה מצביע ‪ root‬עלינו לכתוב‪ ,(*root_ptr)-> _data :‬שכן כאמור‬
‫‪ *root_ptr‬מביא אותנו למצביע ‪ ,root‬ולכן ‪ (*root_ptr)-> _data‬מביא‬
‫אותנו למרכיב ה‪ data -‬בקופסה עליה מצביע ‪ .root‬בהמשך נראה זאת גם‬
‫בציור‪.‬‬
‫כיצד תיערך קריאה רקורסיבית לפונקציה )מתוך הפונקציה(? ַלקריאה הרקורסיבית‬
‫נרצה להעביר מצביע למצביע לאחד מבניו של שורש תת‪-‬העץ הנוכחי‪ .‬כאמור‬
‫‪ ,(*root_ptr)->_left‬מביא אותנו לבנו השמאלי של הצומת עליו מצביע‬
‫המצביע‪ ,‬שעליו מצביע ‪ .root_ptr‬לכן הביטוי‪:‬‬
‫) ‪ &( (*root_ptr)-> _data‬מחזיר לנו מצביע לבנו השמאלי של הצומת עליו‬
‫מצביע המצביע‪ ,‬שעליו מצביע ‪.root_ptr‬‬
‫נציג עתה את קוד הפונקציה בגרסתו הרצויה‪:‬‬
‫‪137‬‬
void delete_node(int num, struct Node **root_ptr)
{
if (*root_ptr == NULL)
return ;
// if root_ptr points to the pointer
// that points to the wanted node
if ((*root_ptr) -> data == num)
{
// if no _left then move
// pointer so it points to
// _right
if ((*root_ptr) -> _left ==NULL)
{
struct Node *temp = *root_ptr ;
(*root_ptr) = (*root_ptr) -> _right ;
delete temp ;
}
else if ((*root_ptr) -> _right ==NULL)
{
struct Node *temp = *root_ptr ;
(*root_ptr) = (*root_ptr) -> _left ;
delete temp ;
}
else
// wanted node has 2 sons
{
struct Node *temp ;
struct Node **succ_ptr =
find_succ(root_ptr);
(*root_ptr)-> _data =
(*succ_ptr)-> _data;
temp = *succ_ptr ;
(*succ_ptr) = temp -> _right ;
delete temp;
}
}
else if (num < (*root_ptr) -> data)
delete_node(num,
&((*root_ptr) -> _left));
else
delete_node(num, &((*root_ptr) -> _right));
}
//============================================
struct Node **find_succ( struct Node **root_ptr)
{
struct Node **temp =
&( (*root_ptr) -> _right ) ;
while ((*temp) -> _left != NULL)
temp = & ((*temp) -> _left ) ;
return temp ;
138
‫}‬
‫נציג דוגמת הרצה של הפונקציה על העץ המלווה אותנו‪ .‬נניח כי יש למחוק את הערך‬
‫‪ 13‬מהעץ‪ .‬מצב הזיכרון טרם הקריאה לפונקציה הינו‪:‬‬
‫‪17‬‬
‫א'‬
‫ב'‬
‫‪13‬‬
‫‪27‬‬
‫ג'‬
‫‪43‬‬
‫=‪root‬‬
‫ה'‬
‫ד'‬
‫‪3‬‬
‫‪16‬‬
‫ו'‬
‫‪14‬‬
‫עם הקריאה לפונקציה נוספת על המחסנית רשומת‬
‫ההפעלה של הפונקציה‪ .‬המצביע ‪ root_ptr‬מצביע על‬
‫המצביע ‪ root‬של התכנית הראשית‪ .‬נציג את רשומת‬
‫ההפעלה‪:‬‬
‫=‪root_ptr‬‬
‫‪num=13‬‬
‫=‪temp‬‬
‫‪return=main‬‬
‫שימו לב כי בעוד ‪ root_ptr‬הוא מצביע למצביע‪ ,‬כלומר יודע להצביע על מצביע‪,‬‬
‫‪ temp‬הוא מצביע 'פשוט' אשר יודע להצביע על קופסה )לעיתים נשתמש בביטוי‬
‫'מצביע מדרגה ראשונה'‪ ,‬בניגוד למצביע למצביע שהינו מצביע מדרגה שניה(‪.‬‬
‫‪139‬‬
‫של‬
‫הראשון‬
‫העותק‬
‫התנאי‪:‬‬
‫להתבצע‪.‬‬
‫מתחיל‬
‫‪delete_node‬‬
‫)‪(*root_ptr == NULL‬אינו מתקיים‪ ,‬שכן ‪ root_ptr‬מצביע על מצביע שערכו‬
‫אינו ‪ .NULL‬גם התנאי )‪((*root_ptr)-> _data == num‬אינו מתקיים‪ ,‬שכן‬
‫בשדה ה‪ data -‬בקופסה עליה מצביע המצביע ‪ *root_ptr‬יש את הערך ‪) 17‬ולא‬
‫את הערך ‪ .(13‬אנו מתקדמים‪ ,‬לפיכך‪ ,‬לתנאי ‪(num < (*root_ptr)-> _data‬‬
‫) אשר מתקיים‪ ,‬דבר שגורם לפונקציה לקרוא רקורסיבית עם המצביע‬
‫)‪ . &((*root_ptr)->_left‬נבדוק מיהו מצביע זה‪ *root_ptr :‬הוא המצביע‬
‫לשורש העץ‪ ,‬על כן )‪ ((*root_ptr)->_left‬הוא הבן השמאלי של שורש העץ‪,‬‬
‫כלומר המצביע ב'‪ .‬הביטוי‪ &(…) :‬מחזיר לנו מצביע למצביע ב'‪ ,‬ומצביע זה הוא‬
‫שמועבר )כפרמטר ערך( לקריאה הרקורסיבית השניה‪ .‬כתובת החזרה של הקריאה‬
‫הרקורסיבית השניה היא סיומה של הקריאה הראשונה ל‪ . delete_node -‬נציג‬
‫את מצב הזיכרון‪:‬‬
‫‪17‬‬
‫א'‬
‫ב'‬
‫‪13‬‬
‫‪27‬‬
‫ג'‬
‫‪43‬‬
‫=‪root‬‬
‫ה'‬
‫ד'‬
‫=‪root_ptr‬‬
‫‪num=13‬‬
‫=‪temp‬‬
‫‪return=main‬‬
‫‪3‬‬
‫‪16‬‬
‫ו'‬
‫‪14‬‬
‫=‪root_ptr‬‬
‫‪num=13‬‬
‫=‪temp‬‬
‫‪return=delete‬‬
‫העותק השני של ‪ delete_node‬מתחיל להתבצע‪ .‬גם הפעם התנאי‪:‬‬
‫>‪(*root_ptr)-‬‬
‫)‪ (*root_ptr ==NULL‬אינו מתקיים‪ .‬לעומתו התנאי‪:‬‬
‫)‪_data == num‬מתקיים‪ ,‬שכן ‪ *root_ptr‬הוא הפעם המצביע ב'‪ ,‬ולכן‬
‫‪ (*root_ptr)-> _data‬הוא הערך המצוי בקופסה עליה מורה המצביע ב'‪,‬‬
‫כלומר הערך ‪ .13‬לכן אנו נכנסים לגוש בו מתבצעת המחיקה‪ .‬התנאי‪:‬‬
‫‪ (*root_ptr)->_left == NULL‬אינו מתקיים‪ ,‬שכן ערכו של הבן השמאלי של‬
‫הקופסה עליה מצביע המצביע ב' )שהינו‪ ,‬כזכור‪ (*root_ptr ,‬אינו ‪ .NULL‬באופן‬
‫דומה גם התנאי‪(*root_ptr)-> _right == NULL :‬אינו מתקיים‪ ,‬ולכן אנו‬
‫מגיעים ל‪ else -‬האחרון‪ ,‬המטפל במקרה בו לצומת שיש למחוק יש שני בנים‪ .‬עתה‬
‫ַמנים את הפונקציה ‪ find_succ‬אשר תחזיר מצביע‪ ,‬למצביע אשר מורה על‬
‫אנו מז ְ‬
‫הצומת שמכיל את העוקב של ‪ 13‬בעץ‪ .‬אנו מעבירים לפונקציה ‪ find_succ‬עותק‬
‫של ‪) root_ptr‬הפונקציה מקבלת פרמטר ערך מאותו טיפוס כמו ‪:root_ptr‬‬
‫מצביע למצביע(‪ .‬נציג את מצב הזיכרון בעקבות זימון ‪.find_succ‬‬
‫‪140‬‬
‫‪17‬‬
‫א'‬
‫=‪root‬‬
‫ב'‬
‫‪13‬‬
‫‪27‬‬
‫ג'‬
‫ד'‬
‫‪43‬‬
‫ה'‬
‫=‪root_ptr‬‬
‫‪num=13‬‬
‫=‪temp‬‬
‫‪return=main‬‬
‫‪3‬‬
‫‪16‬‬
‫=‪root_ptr‬‬
‫‪num=13‬‬
‫=‪temp‬‬
‫‪return=delete‬‬
‫ו'‬
‫‪14‬‬
‫=‪root_ptr‬‬
‫=‪temp‬‬
‫הפונקציה ‪ find_succ‬מתחילה להתבצע‪ .‬למשתנה ‪) temp‬שהינו מטיפוס מצביע‬
‫למצביע( מוכנס הערך‪ . &( (*root_ptr)-> _right ) :‬נבחן ביטוי זה‪:‬‬
‫‪ *root_ptr‬הוא המצביע ב'; לכן ‪ (*root_ptr)-> _right‬הוא המצביע ד'‬
‫לכן‪:‬‬
‫)בנו הימני של הצומת עליו מצביע המצביע‪ ,‬שעליו מצביע ‪; (root_ptr‬‬
‫) ‪ &( (*root_ptr)-> _right‬הוא מצביע למצביע ד'; וזה‪ ,‬כאמור‪ ,‬הערך‬
‫שמוכנס ל‪ .temp -‬בכך פנינו לבנו הימני של הצומת שברצוננו למחוק‪ .‬נציג את מצב‬
‫רשומת ההפעלה של ‪) find_succ‬התעלמו מהמלבן המקווקו ומהמצביע שבו(‪:‬‬
‫‪17‬‬
‫א'‬
‫ב'‬
‫‪13‬‬
‫‪27‬‬
‫ג'‬
‫‪43‬‬
‫=‪root‬‬
‫ה'‬
‫ד'‬
‫=‪root_ptr‬‬
‫‪num=13‬‬
‫=‪temp‬‬
‫‪return=main‬‬
‫‪3‬‬
‫‪16‬‬
‫ו'‬
‫‪14‬‬
‫=‪root_ptr‬‬
‫‪num=13‬‬
‫=‪temp‬‬
‫‪return=delete‬‬
‫=‪root_ptr‬‬
‫=‪temp‬‬
‫עתה אנו פונים לולאה‪ .‬התנאי‪( (*temp)->_left != :‬‬
‫) ‪ NULL‬מתקיים‪ .‬שכן ‪ *temp‬הוא המצביע ד'‪ ,‬ולכן‬
‫‪ (*temp)->_left‬הוא המצביע ו'‪ ,‬שערכו אינו ‪ .NULL‬לכן אנו‬
‫ומפנים את ‪ temp‬להצביע על ‪(*temp)-‬‬
‫נכנסים לגוף הלולאה‪ְ ,‬‬
‫‪ ,>_left‬כלומר על המצביע ו'‪ .‬רשומת ההפעלה של‬
‫‪ find_succ‬נראית באופן הבא ) ִב ְמקום כפי שהיא צוירה‬
‫קודם‪:‬‬
‫)לא ציירנו שוב את ‪ root_ptr‬שכן ערכו נותר כמו קודם(‪.‬‬
‫‪141‬‬
‫=‪root_ptr‬‬
‫=‪temp‬‬
‫לקראת סיבוב נוסף בלולאה שוב נבדק התנאי‪( (*temp)->_left != NULL) :‬‬
‫אולם הפעם הוא כבר אינו מתקיים‪ ,‬שכן בנו השמאלי של הצומת עליו מצביע‬
‫‪ ,*temp‬כלומר בנו השמאלי של הצומת המצביע ו'‪ ,‬מכיל את הערך ‪.NULL‬‬
‫הפונקציה אינה נכנסת ללולאה‪ ,‬והיא מחזירה את ערכו של ‪ ,temp‬כלומר מצביע‬
‫למצביע ו'‪.‬‬
‫הערך המוחזר על‪-‬ידי‬
‫‪ .delete_node‬מצב הזיכרון הוא איפה‪:‬‬
‫‪find_succ‬‬
‫למשתנה‬
‫מוכנס‬
‫‪succ_ptr‬‬
‫של‬
‫‪17‬‬
‫א'‬
‫=‪root‬‬
‫ב'‬
‫‪13‬‬
‫‪27‬‬
‫ג'‬
‫ד'‬
‫‪43‬‬
‫ה'‬
‫=‪root_ptr‬‬
‫‪num=13‬‬
‫=‪temp‬‬
‫‪return=main‬‬
‫‪3‬‬
‫‪16‬‬
‫=‪root_ptr‬‬
‫‪num=13‬‬
‫=‪temp‬‬
‫=‪succ_ptr‬‬
‫‪return=delete‬‬
‫ו'‬
‫‪14‬‬
‫העותק השני של ‪ delete_node‬מוצא מקיפאונו‪ ,‬וממשיך להתבצע‪ .‬הפקודה‪:‬‬
‫‪ (*root_ptr)-> _data = (*succ_ptr)-> _data‬גורמת לאפקט הבא‪:‬‬
‫‪ *root_ptr‬הוא המצביע ב'‪ ,‬לכן ‪ (*root_ptr)-> _data‬הוא שדה ה‪data -‬‬
‫בקופסה עליה מורה המצביע ב'; לשדה זה אנו מכניסים את הערך של‪:‬‬
‫‪ ; (*succ_ptr)-> _data‬מכיוון ש‪ *succ_ptr -‬הוא המצביע ו'‪ ,‬אזי‬
‫‪ (*succ_ptr)-> _data‬הוא הערך ‪ .14‬לסיכומון‪ :‬הערך ‪ 13‬בקופסה עליה‬
‫מצביע ב' מוחלף בערך ‪ .14‬הפקודה אחר‪-‬כך מפנה את המצביע ‪) ,temp‬שיודע‬
‫להצביע על קופסות‪ ,‬ולא על מצביעים(‪ ,‬להצביע כמו ‪ ,*succ_ptr‬כלומר כמו‬
‫המצביע ו'‪ .‬מצב הזיכרון הוא עתה‪:‬‬
‫‪17‬‬
‫א'‬
‫ב'‬
‫‪14‬‬
‫‪27‬‬
‫ג'‬
‫‪43‬‬
‫=‪root‬‬
‫ה'‬
‫ד'‬
‫=‪root_ptr‬‬
‫‪num=13‬‬
‫=‪temp‬‬
‫‪return=main‬‬
‫‪3‬‬
‫‪16‬‬
‫ו'‬
‫‪14‬‬
‫=‪root_ptr‬‬
‫‪num=13‬‬
‫=‪temp‬‬
‫=‪succ_ptr‬‬
‫‪return=delete‬‬
‫>‪temp-‬‬
‫הפקודה אחר‪-‬כך מפנה את המצביע )‪ (*succ_ptr‬להצביע כמו‬
‫‪ ._right‬ערכו של ‪ temp-> _right‬הוא ‪ ,NULL‬ועל‪-‬כן גם המצביע ‪,*succ_ptr‬‬
‫כלומר המצביע ו'‪ ,‬מקבל ערך זה‪ .‬הפקודה האחרונה בגוש משחררת את הזיכרון‬
‫‪142‬‬
‫עליו מצביע ‪ ,temp‬כלומר את הקופסה בה שכן ‪) 14‬לפני העתקתו גם לצומת של ‪.(13‬‬
‫בכך מסתיים העותק השני של ‪ .delete_node‬עותק זה חוזר לנקודת הסיום של‬
‫העותק הראשון‪ ,‬ולכן בכך הסתיים תהליך המחיקה‪.‬‬
‫‪ 14.12‬כמה זה עולה?‬
‫עץ חיפוש בינארי הינו מבנה נתונים חלופי למערך ממוין‪ ,‬ועל כן מתאים להשוות את‬
‫עלות הפעולות השונות על שני ה‪'-‬מתחרים' הללו‪ .‬הפעולות בהן נתעניין הן הוספת‬
‫נתון‪ ,‬מחיקת נתון‪ ,‬וחיפוש נתון‪ .‬נשווה את עלותן היחסית של הפעולות השונות הן‬
‫במקרה הממוצע והן במקרה הגרוע )כלומר במקרה בו הנתונים מסודרים כך שעלינו‬
‫לעשות הכי הרבה עבודה כדי להשלים את הפעולה הרצויה(‪.‬‬
‫‪ 14.12.1‬הוספת נתון‬
‫נניח שברצוננו להוסיף נתון שערכו ‪ v‬למערך ממוין הכולל ‪ n‬נתונים‪ .‬לשם כך‪,‬‬
‫ראשית‪ ,‬יהיה עלינו לאתר את המקום בו יש להוסיף את הנתון ‪ .v‬איתור המקום‬
‫המתאים יתבצע על‪-‬ידי הרצת חיפוש בינארי של ‪ַ v‬במערך‪ .‬החיפוש הבינארי יעלה‬
‫)‪ log2(n‬פעולות הן במקרה הממוצע )וזאת לא הוכחנו(‪ ,‬והן במקרה הגרוע )וזאת‬
‫הראנו(‪ .‬במידה והחיפוש יסתיים בהצלחה‪ ,‬כלומר הערך ‪ v‬כבר מצוי במערך‪ ,‬נוסיף‬
‫את הערך החדש לצד הערך הקיים‪ .‬במידה והחיפוש יסתיים בכישלון‪ ,‬כלומר הערך‬
‫‪ v‬לא מצוי במערך‪ ,‬אזי המקום בו החיפוש הסתיים הוא המקום בו על הערך ִלשכון‪,‬‬
‫לשם נכניס את הערך‪ .‬בשני המצבים‪) ,‬הן אם הערך כבר מצוי במערך‪ ,‬והן אם‬
‫ולכן ַ‬
‫במקרה הממוצע יהיה עלינו להזיז מחצית מאברי המערך תא אחד ימינה כדי‬
‫לא(‪ִ :‬‬
‫ַ‬
‫לפנות מקום לנתון החדש‪ ,‬ובמקרה הגרוע יהיה עלינו להזיז ‪ n‬נתונים תא אחד‬
‫ימינה(‪ .‬הן במקרה הממוצע‪ ,‬והן במקרה הגרוע‪ ,‬פעולת ההזזה תחייב עבודה בשיעור‬
‫של ‪ n‬צעדים‪ .‬כלומר הוספת איבר למערך ממוין תחייב עבודה בשיעור ‪ n‬צעדים‪ ,‬הן‬
‫במקרה הממוצע )בו הנתון מוכנס באמצע המערך(‪ ,‬והן במקרה הגרוע )בו הנתון‬
‫מוסף בתחילת המערך(‪) .‬מעלוּת החיפוש התעלמנו שכן היא זניחה יחסית ל‪.(n -‬‬
‫עתה נניח כי ברצוננו להוסיף נתון לעץ חיפוש הכולל ‪ n‬נתונים‪ .‬במקרה הממוצע‬
‫יהיה עומקו של העץ בסדר גודל של )‪) lg2(n‬וזאת לא הוכחנו(‪ .‬כדי להוסיף את הנתון‬
‫נצטרך לרדת על הענף המתאים‪ ,‬עד למקום בו יש להוסיף את הנתון‪ .‬ההתקדמות על‬
‫הענף הרצוי תדרוש )‪ lg2(n‬צעדים‪ .‬במקרה הגרוע העץ 'יתנוון' לכדי רשימה מקושרת‪.‬‬
‫מציאת המקום בו יש להוסיף את הנתון ברשימה מקושרת מחייבת אותנו לסרוק‬
‫בממוצע מחצית הרשימה‪ ,‬ובמקרה הגרוע את כל הרשימה‪ ,‬כלומר לבצע עבודה‬
‫בשיעור של ‪ n‬פעולות‪ .‬על‪-‬כן הוספת נתון לעץ חיפוש עלולה לחייב במקרה הגרוע‬
‫ביצוע של ‪ n‬צעדים‪.‬‬
‫לסיכום‪ :‬הוספת איבר לעץ חיפוש יעילה יותר מאשר הוספתו למערך ממוין במקרה‬
‫הממוצע‪ ,‬אך לא במקרה הגרוע‪.‬‬
‫כדי שלא להקלע בעצי חיפוש בינאריים למקרה הגרוע‪ ,‬בו העץ התנוון לכדי רשימה‪,‬‬
‫ישנם עצי חיפוש אשר יודעים 'לאזן את עצמם'‪ ,‬כלומר להישאר בעומק של )‪lg2(n‬‬
‫תמיד‪ .‬דוגמות לעצים כאלה הם עצים אדומים שחורים‪ ,‬או עצי שתיים שלוש‪.‬‬
‫בקורס שלנו לא נכיר עצים אלה‪ .‬רק כדי לסבר את העין‪ ,‬בעץ כזה אם הקלט כולל‬
‫את הנתונים ‪ ,1‬אח"כ‪ ,2 :‬אח"כ‪ ,3 :‬אזי העץ ידע 'לשנות את עצמו' כך שבשורש העץ‬
‫יוצב הערך ‪ ,2‬בנו השמאלי יהיה ‪ ,1‬ובנו הימני ‪ ,3‬והוא יוותר מאוזן‪ .‬כל זאת‪ ,‬כמובן‪,‬‬
‫בלי להשקיע בכך 'יותר מדי עבודה'‪.‬‬
‫‪143‬‬
‫‪ 14.12.2‬מחיקת נתון‬
‫עלות מחיקת נתון דומה לעלות הוספת נתון‪ .‬במערך ממוין נאלץ‪ ,‬ראשית‪ ,‬לאתר את‬
‫הנתון‪ ,‬תוך שימוש בחיפוש בינארי‪ .‬אחר יהיה עלינו 'לצופף' את הנתונים כדי‬
‫'לסגור' את התא שנותר ריק‪ .‬עלות החיפוש היא )‪ log2(n‬פעולות הן במקרה הממוצע‬
‫והן במקרה הגרוע‪ ,‬ועלות ההזזה תא אחד ימינה היא ‪ n‬פעולות הן במקרה הממוצע‬
‫והן במקרה הגרוע‪.‬‬
‫כדי למחוק איבר מעץ חיפוש יהיה עלינו ראשית לאתרו‪ .‬במקרה הממוצע הדבר‬
‫ידרוש )‪ lg2(n‬צעדים‪ ,‬ובמקרה הגרוע ‪ n‬צעדים‪ .‬מחיקת הנתון תחייב הן במקרה‬
‫הממוצע והן במקרה הגרוע איתור של העוקב של הערך‪) .‬שימו לב כי אנו טוענים‬
‫כאן‪ ,‬בלי להוכיח בצורה מסודרת‪ ,‬כי גם ַבמקרה הממוצע נאלץ לאתר את העוקב;‬
‫שבעץ מלא‪ ,‬שהינו הקרוּב ְלעץ ממוצע‪ ,‬מחצית‬
‫האינטואיציה שמאחורי הטענה היא ְ‬
‫מהערכים מצויים בצמתים פנימיים‪ ,‬יש להם שני בנים‪ ,‬ועל‪-‬כן מחיקתם מחייבת‬
‫איתור של העוקב להם(‪ .‬איתור העוקב יעלה במקרה הממוצע )‪ log2(n‬צעדים‪.‬‬
‫במקרה הגרוע ידרוש איתור העוקב סדר גודל של ‪ n‬צעדים‪) ,‬אתם מוזמנים לצייר‬
‫לעצמכם עץ בו זה המצב(‪ .‬אחרי שאיתרנו את העוקב‪ ,‬דורשת פעולת המחיקה רק‬
‫מספר קבוע )ועל כן זניח( של צעדים‪ .‬לכן מחיקת ערך מעץ דורשת במקרה הממוצע‬
‫)‪ lg2(n‬צעדים‪ ,‬ובמקרה הגרוע ‪ n‬צעדים‪.‬‬
‫שוב אנו רואים כי עץ חיפוש שקול )מבחינת כמות העבודה הנדרשת( למערך ממוין‬
‫ַבמקרה הגרוע‪ ,‬אך עדיף על‪-‬פני מערך ממוין ַבמקרה הממוצע‪.‬‬
‫‪ 14.12.3‬חיפוש נתון‬
‫עתה נשאל את עצמנו כמה עולה חיפוש נתון בכל אחד משני מבני הנתונים‪ .‬כבר‬
‫אמרנו כי במערך ממוין ידרוש החיפוש )‪ log2(n‬צעדים הן במקרה הגרוע והן במקרה‬
‫הממוצע‪ .‬לעומת זאת בעץ ידרוש החיפוש )‪ log2(n‬צעדים במקרה הממוצע‪ ,‬אך ‪n‬‬
‫צעדים במקרה הגרוע )בו העץ התנוון לכדי רשימה(‪.‬‬
‫לסיכום‪ ,‬במקרה הממוצע )או בעץ 'שיודע לאזן את עצמו'(‪ :‬עץ יעיל יותר ממערך‬
‫בהוספה ובמחיקה )שתי הפעולות עולות )‪ log2(n‬צעדים בעץ‪ ,‬לעומת ‪ n‬צעדים‬
‫במערך(‪ ,‬ושקול למערך בחיפוש )שתי הפעולות עולות )‪ log2(n‬צעדים בשני מבני‬
‫הנתונים(‪ .‬במקרה גרוע‪ :‬מערך ועץ שקולים בהוספה ומחיקה )שתי הפעולות עולות ‪n‬‬
‫צעדים בשני מבני הנתונים(‪ ,‬ומערך יעיל יותר מעץ בחיפוש )אשר עולה )‪log2(n‬‬
‫צעדים במערך‪ ,‬לעומת ‪ n‬צעדים בעץ(‪.‬‬
‫== מכאן ואילך לא עודכן ‪==2010-11‬‬
‫‪ 14.13‬האם עץ בינארי מלא‬
‫עץ בינארי נקרא מלא אם‪:‬‬
‫‪ .I‬לכל צומת פנימי יש שני בנים‪ ,‬וכן‪:‬‬
‫‪ .II‬כל העלים מצויים באותה רמה‪.‬‬
‫מבין שלושת העצים שהצגנו בציור האחרון )בסעיף הקודם(‪ ,‬רק האמצעי הוא עץ‬
‫בינארי מלא‪ .‬כפי שראינו העץ הימני אינו מקיים את התנאי א'‪ ,‬ובעץ השמאלי לא‬
‫כל העלים מצויים באותה רמה‪.‬‬
‫‪144‬‬
‫עתה ברצוננו לכתוב פונקציה אשר מקבלת מצביע לעץ בינארי‪ ,‬ומחזירה ערך בולאני‬
‫המעיד האם העץ מלא‪ .‬נציג שתי גרסות שונות לפונקציה‪ .‬הגרסה הראשונה תהא‬
‫מימוש ישיר של ההגדרה‪ .‬ראשית נציג אותה‪:‬‬
‫{ )‪bool is_fool(const struct Node * const on_the_hill‬‬
‫; ‪bool ok‬‬
‫; ‪unsigned int the_depth‬‬
‫; ) ‪if (on_the_hill == NULL) return( true‬‬
‫; )‪ok = non_leaf_2_sons(on_the_hill‬‬
‫; ) ‪if (! ok) return( false‬‬
‫)‪the_depth = depth(on_the_hill‬‬
‫; )‪ok = all_leaves_at_depth(on_the_hill, depth, 1‬‬
‫; )‪return(ok‬‬
‫}‬
‫את הפונקציות ‪ non_leaf_2_sons‬ו‪ depth -‬כבר פגשנו‪ .‬נותר לנו עוד לכתוב את‬
‫הפונקציה ‪ .all_leaves_at_depth‬פונקציה זאת מקבלת שלושה פרמטרים‪:‬‬
‫‪ .I‬מצביע לשורשו של )תת( עץ בינארי‪.‬‬
‫‪ .II‬עומקו של העץ המקורי )השלם( עבורו נערכת הבדיקה‪.‬‬
‫‪ .III‬העומק בעץ המקורי בו מצוי שורש תת‪-‬העץ הנוכחי‪ .‬מכיוון ששורש העץ‬
‫השלם מצוי בעומק של אחד‪ ,‬אזי בקריאה הראשונה לפונקציה מועבר לה הערך‬
‫‪ .1‬כל קריאה רקורסיבית לפונקציה‪ ,‬עם בניו של השורש הנוכחי‪ ,‬תעביר ערך‬
‫גדול באחד מהערך שהועבר לה‪.‬‬
‫אנו מניחים כי לא יקראו לפונקציה עם עץ ריק‪ .‬על‪-‬כן בפונקציה ‪ ,if_fool‬במידה‬
‫והעץ ריק אנו מחזירים את הערך ‪ true‬מיידית‪ ,‬ולכן איננו מזמנים את הפונקציה‬
‫‪ .all_leaves_at_depth‬גם בפונקציה ‪ all_leaves_at_depth‬אנו‬
‫מקפידים לקרוא רקורסיבית רק עבור בן שאינו ריק‪.‬‬
‫נציג את הפונקציה‪:‬‬
‫‪bool all_leaves_at_depth(const struct Node * const root,‬‬
‫{ )‪unsigned int tree_depth, unsigned int curr_depth‬‬
‫; ‪bool ok = true‬‬
‫)‪if (root->_left == NULL && root-> _right == NULL‬‬
‫; ) ‪return( curr_depth == tree_depth‬‬
‫)‪if (root->_left != NULL‬‬
‫‪ok = all_leaves_at_depth(root->_left,‬‬
‫)‪// (-‬‬
‫; )‪tree_depth, curr_depth +1‬‬
‫; )‪if (!ok) return(false‬‬
‫)‪if (root-> _right != NULL‬‬
‫‪ok = all_leaves_at_depth(root-> _right,‬‬
‫)‪// (+‬‬
‫; )‪tree_depth, curr_depth +1‬‬
‫; )‪return(ok‬‬
‫‪145‬‬
‫}‬
‫הסבר הפונקציה‪ :‬במידה והצומת הנוכחי הוא עלה )ערכם של המצביעים _‪ left‬ו‪-‬‬
‫_‪ right‬בצומת הוא ‪ (NULL‬אזי יש להחזיר את הערך ‪ true‬במידה ו‪-‬‬
‫‪ ,curr_depth==tree_depth‬כלומר במידה והעלה הנוכחי מצוי ַברמה שינה‬
‫עומק העץ‪ .‬פקודת ה‪ ,return -‬ראשית‪ ,‬תעריך את הביטוי הבולאני‪:‬‬
‫‪ ,curr_depth==tree_depth‬ושנית‪ ,‬תחזיר את ערכו של הביטוי; ולכן יוחזר‬
‫הערך המתאים‪.‬‬
‫אם הבן השמאלי אינו ריק אנו קוראים עבורו‪ .‬במידה ומוחזר ערך ‪ false‬העותק‬
‫הנוכחי מחזיר מייד ערך זה‪ .‬שימו לב כי אם הבן השמאלי הוא ריק‪) ,‬ולכן לא נערכת‬
‫קריאה רקורסיבית(‪ ,‬אזי תודות לכך ש‪ ok -‬אותחל לערך ‪ true‬התנאי‪ (!ok) :‬לא‬
‫יתקיים‪ ,‬ולא יוחזר הערך ‪ false‬באופן נחפז‪ .‬באותו אופן‪ ,‬אם הבן הימני אינו ריק‬
‫אזי נערכת קריאה עבורו‪ ,‬ומוחזר הערך שחזר מהקריאה הרקורסיבית‪ .‬אם הבן‬
‫הימני ריק‪ ,‬אזי לא תיערך קריאה רקורסיבית עבורו‪ ,‬ובמקרה זה יוחזר הערך ‪true‬‬
‫שחזר מהקריאה עם הבן השמאלי‪) .‬בדקו לעצמכם מדוע אם הבן הימני ריק אזי אנו‬
‫מובטחים ש‪) :‬א( נערכה קריאה עם הבן השמאלי‪ ,‬וכן‪) :‬ב( קריאה זאת החזירה את‬
‫הערך ‪ true‬לתוך המשתנה ‪.(ok‬‬
‫מכיוון שמדובר בפונקציה לא טריוויאלית נציג שתי דוגמות הרצה‪.‬‬
‫בדוגמה הראשונה מצב הזיכרון טרם הקריאה לפונקציה ‪ is_fool‬הוא‪:‬‬
‫‪root (of‬‬
‫= )‪main‬‬
‫הפונקציה מקבלת עותק של המצביע לשורש העץ‪ .‬לכן מצב הזיכרון בעקבות זימון‬
‫הפונקציה הוא‪:‬‬
‫‪root (of‬‬
‫= )‪main‬‬
‫=‪on_the_hill‬‬
‫‪146‬‬
‫הפונקציה מתחילה להתבצע‪ .‬ערכו של המצביע ‪ on_the_hill‬אינו ‪.NULL‬‬
‫הפונקציה ‪ non_leaf_2_sons‬מחזירה את הערך ‪ true‬לתוך המשתנה ‪ ,ok‬ועל‪-‬‬
‫כן פקודת ה‪ return -‬העוקבת לקריאה לפונקציה אינה מתבצעת‪ .‬בשלב הבא‬
‫מוכנס למשתנה ‪ the_depth‬עומקו של העץ‪ ,‬שהוא שתיים‪ .‬עתה מזמנת ‪is_fool‬‬
‫את הפונקציה ‪ . all_leaves_at_depth‬היא מעבירה לה את שורש העץ‪ ,‬את‬
‫עומקו של העץ‪ ,‬כלומר את הערך שתיים‪ ,‬ואת הקבוע ‪ .1‬נציג את מצב הזיכרון‪:‬‬
‫‪root (of‬‬
‫= )‪main‬‬
‫=‪on_the_hill‬‬
‫=‪root‬‬
‫‪tree_depth=2‬‬
‫‪curr_depth=1‬‬
‫התנאי בפקודת ה‪ if -‬הראשונה אינו מתקיים‪ ,‬שכן לשורש הנוכחי יש שני בנים‬
‫שאינם ריקים‪ .‬התנאי )‪(root->_left != NULL‬מתקיים‪ ,‬ולכן אנו קוראים‬
‫רקורסיבית עם בנו השמאלי של השורש‪ .‬שימו לב לערכם של הפרמטרים‪ ,‬כפי‬
‫שמופיע בציור‪ .‬כתובת החזרה של הקריאה הרקורסיבית המזומנת היא ההשמה )‪.(-‬‬
‫נציג את מצב הזיכרון‪:‬‬
‫‪root (of‬‬
‫= )‪main‬‬
‫=‪on_the_hill‬‬
‫=‪root‬‬
‫‪tree_depth=2‬‬
‫‪curr_depth=1‬‬
‫‪return=is_fool‬‬
‫=‪root‬‬
‫‪tree_depth=2‬‬
‫‪curr_depth=2‬‬
‫)‪return= (-‬‬
‫‪147‬‬
‫העותק השני של ‪ all_leaves_at_depth‬מתחיל להתבצע‪ .‬הפעם התנאי‪:‬‬
‫)‪(root->_left == NULL && root-> _right == NULL‬מתקיים‪ ,‬ולכן‬
‫מוחזרת תוצאת ההשוואה ‪ . curr_depth == tree_depth‬מכיוון שערכם של‬
‫שני הפרמטרים הוא ‪ 2‬מוחזר הערך ‪ .true‬ערך זה חוזר לעותק הראשון של‬
‫‪ ,all_leaves_at_depth‬למשתנה ‪ .ok‬בשלב זה העותק הראשון 'מופשר‬
‫מקפאונו'‪ .‬התנאי )‪ (!ok‬אינו מתקיים‪ ,‬ולכן אנו מתקדמים לפקודת ה‪if -‬‬
‫הבודקת האם )‪ .(root-> _right != NULL‬התנאי בפקודה מתקיים‪ ,‬ולכן אנו‬
‫מזמנים שוב את ‪ .all_leaves_at_depth‬הפעם כתובת החזרה היא ההשמה )‪(+‬‬
‫בעותק הראשון של הפונקציה‪ .‬נציג את מצב הזיכרון‪ ,‬בפרט את רשומת ההפעלה של‬
‫הפונקציה הנקראת‪:‬‬
‫‪root (of‬‬
‫= )‪main‬‬
‫=‪on_the_hill‬‬
‫=‪root‬‬
‫‪tree_depth=2‬‬
‫‪curr_depth=1‬‬
‫‪return=is_fool‬‬
‫=‪root‬‬
‫‪tree_depth=2‬‬
‫‪curr_depth=2‬‬
‫)‪return= (+‬‬
‫העותק הנוכחי של הפונקציה מתחיל להתבצע‪ .‬התנאי‪:‬‬
‫)‪(root->_left == NULL && root-> _right == NULL‬מתקיים‪ ,‬ולכן‬
‫מוחזרת תוצאת ההשוואה ‪ . curr_depth == tree_depth‬מכיוון שערכם של‬
‫שני הפרמטרים הוא ‪ 2‬מוחזר הערך ‪ .true‬ערך זה חוזר לעותק הראשון של‬
‫‪ ,all_leaves_at_depth‬למשתנה ‪ ok‬בהשמה )‪ .(+‬ערכו של ‪ ,ok‬כלומר הערך‬
‫‪ ,true‬מוחזר על‪-‬ידי העותק הראשון )שנקרא על‪-‬ידי ‪ .(is_fool‬בשלב זה‬
‫‪ is_fool‬יכולה להחזיר את הערך ‪ true‬כראוי‪.‬‬
‫עתה נראה כיצד תתנהל ריצה של ‪ is_fool‬על העץ הבא )עליו מצביע ‪ root‬של‬
‫התכנית הראשית(‪:‬‬
‫מצב הזיכרון עם הקריאה לפונקציה ‪ is_fool‬הוא‪:‬‬
‫‪148‬‬
‫‪root (of‬‬
‫= )‪main‬‬
‫=‪on_the_hill‬‬
‫כפי שראינו בסעיף הקודם‪ ,‬הפונקציה ‪ non_leaf_2_sons‬תחזיר את הערך‬
‫‪ ,true‬כלומר לא היא שתעיד על כך שהעץ אינו מלא‪ .‬הפונקציה ‪ depth‬תחזיר את‬
‫הערך ‪ 3‬לתוך המשתנה ‪ .the_depth‬בשלב הבא אנו מזמנים את הפונקציה‬
‫‪ ,all_leaves_at_depth‬והיא שאמורה להחזיר את הערך ‪ ,false‬אשר יוחזר‬
‫על‪-‬ידי ‪ .is_fool‬נבחן עתה את פעולתה של ‪ .all_leaves_at_depth‬בקריאה‬
‫הראשונה לה מועברים לה ארגומנטים כמתואר בציור‪:‬‬
‫‪root (of‬‬
‫= )‪main‬‬
‫=‪on_the_hill‬‬
‫=‪root‬‬
‫‪the_depth= 3‬‬
‫‪curr_depth= 1‬‬
‫‪return=is_fool‬‬
‫העותק הראשון של ‪ all_leaves_at_depth‬מתחיל להתבצע‪ .‬התנאי‬
‫)‪ (root->_left == NULL && root-> _right == NULL‬אינו מתקיים‪.‬‬
‫בפקודה הבאה‪ ,‬התנאי )‪ (root->_left != NULL‬מתקיים‪ ,‬ולכן מזומנת‬
‫קריאה רקורסיבית‪ .‬ערכי הארגומנטים המועברים לקריאה הרקורסיבית הם כפי‬
‫שמופיע בציור‪:‬‬
‫‪root (of‬‬
‫= )‪main‬‬
‫=‪on_the_hill‬‬
‫=‪root‬‬
‫‪the_depth= 3‬‬
‫‪curr_depth= 1‬‬
‫‪return=is_fool‬‬
‫=‪root‬‬
‫‪the_depth= 3‬‬
‫‪curr_depth= 2‬‬
‫)‪return=(-‬‬
‫‪149‬‬
‫העותק השני של ‪ all_leaves_at_depth‬מתחיל להתבצע‪ .‬עת התנאי‬
‫)‪ (root->_left == NULL && root-> _right == NULL‬מתקיים‪ ,‬ולכן‬
‫הערך המוחזר על‪-‬ידי הקריאה השניה ל‪ all_leaves_at_depth -‬הוא ערכו של‬
‫הביטוי הבולאני‪ . ( curr_depth == tree_depth ) :‬מכיוון שערכם של‬
‫‪ curr_depth‬ו‪ the_depth -‬שונֵה‪ ,‬מוחזר הערך ‪ .false‬ערך זה מוכנס בהשמה‬
‫)‪ (-‬למשתנה ‪ ok‬של העותק הראשון של ‪ .all_leaves_at_depth‬מכאן העותק‬
‫הראשון של הפונקציה מתקדם לפקודה‪ , if (!ok)… :‬אשר גורמת לו להחזיר את‬
‫הערך ‪ .false‬ערך זה מוחזר לפונקציה ‪ is_fool‬כראוי; והיא על‪-‬כן מחזירה‬
‫בתורה את הערך ‪.false‬‬
‫עד כאן הצגנו גרסה ראשונה של הפונקציה הבודקת האם עץ בינארי מלא‪ .‬הגרסה‬
‫הראשונה הינה מימוש ישיר של ההגדרה‪ .‬עתה נרצה להציג גרסה שניה‪ ,‬שלא‬
‫תסתמך ישירות על ההגדרה‪ ,‬אלא על אובזרבציה פשוטה‪ .‬נשים לב כי בעץ מלא‬
‫בעומק ‪ 1‬קיים צומת יחיד; בעץ מלא שעומקו ‪ 2‬קיימים ‪ 3‬צמתים‪ ,‬בעץ מלא שעומקו‬
‫‪ 3‬קיימים ‪ 7‬צמתים‪ ,‬ובאופן כללי בעץ מלא שעומקו ‪ n‬קיימים ‪ 2n -1‬צמתים‪ .‬ומכאן‬
‫נוכל לענות על השאלה האם עץ בינארי מלא באופן הבא‪:‬‬
‫{ )‪bool simple_is_full(const struct Node * const root‬‬
‫; ‪unsigned int the_depth, num_of_nodes‬‬
‫; )‪the_depth = depth(root‬‬
‫; )‪num_of_nodes = count_nodes(root‬‬
‫; ) ‪return( power(2, the_depth) –1 == num_of_nodes‬‬
‫}‬
‫מה עשינו? נאתר את עומקו של העץ‪) ,‬באמצעות פונקציה שכתבנו בעבר(; נספור את‬
‫מספר הצמתים בעץ )באמצעות פונקציה שכתבנו בעבר(; נחזיר את תוצאת‬
‫ההשוואה בין מספר הצמתים בעץ ל‪ :‬שתיים בחזקת עומק העץ‪ ,‬מינוס אחד‪) .‬את‬
‫הפונקציה ‪ power‬המעלה מספר טבעי אחד בחזקת מספר טבעי שני יהיה עלינו‬
‫לממש(‪.‬‬
‫‪ 14.14‬האם בכל הצמתים בעץ השוכנים באותה רמה מצוי אותו‬
‫ערך‬
‫עתה ברצוננו לכתוב פונקציה אשר מקבלת מצביע לעץ בינארי )שאינו עץ חיפוש(‪,‬‬
‫ומחזירה את הערך ‪ true‬אם ורק אם בכל אחת מהרמות בעץ יש אותו ערך בכל‬
‫הצמתים‪ .‬לדוגמה‪ ,‬על הפונקציה להחזיר את הערך ‪ true‬על העץ הימני‪ ,‬אך לא על‬
‫השמאלי‪:‬‬
‫‪7‬‬
‫‪5‬‬
‫‪2‬‬
‫‪7‬‬
‫‪5‬‬
‫‪2‬‬
‫‪5‬‬
‫‪2‬‬
‫‪2‬‬
‫‪150‬‬
‫‪5‬‬
‫‪2‬‬
‫‪1‬‬
‫‪ 14.14.1‬הפונקציה‪equal_vals_at_level :‬‬
‫הפונקציה שנכתוב תכלול לולאה אשר תעבור על רמות העץ בזו אחר זו‪ ,‬החל‬
‫מהרמה מספר אחת )שורש העץ(‪ ,‬ועד רמת העלים‪ .‬עבור כל רמה ראשית נשלוף ערך‬
‫כלשהו המצוי באותה רמה‪ ,‬ושנית נבדוק האם בכל הצמתים באותה רמה מצוי ערך‬
‫המפרה את הדרישות נחזיר מיידית את הערך ‪ .false‬נציג‬
‫ֵ‬
‫זה‪ .‬במידה וגילינו רמה‬
‫את הפונקציה‪:‬‬
‫)‪bool equal_vals_at_level(const struct Node * const root‬‬
‫{‬
‫; ‪unsigend int a_depth‬‬
‫; ‪int val‬‬
‫; ‪bool ok‬‬
‫{ )‪for (a_depth= 1; a_depth <= depth(root); a_depth ++‬‬
‫; )‪ok = get_val_from_level(root, a_depth, 1, val‬‬
‫; )‪ok = check_level(root, a_depth, val, 1‬‬
‫; ) ‪if (!ok) return( false‬‬
‫}‬
‫; )‪return(true‬‬
‫}‬
‫הפונקציה פשוטה למדי‪ ,‬היא כתובה על‪-‬פי עקרונות התכנות המודולרי‪ ,‬ועצוב‬
‫מעלה‪-‬מטה‪ .‬שימו לב כי עבור עץ ריק יוחזר הערך ‪ true‬בלי שהפונקציה תכנס‬
‫ללולאה אף לא פעם אחת )שכן התנאי‪ a_depth <= depth(root) :‬לא יתקיים‬
‫כבר בפעם הראשונה שנגיע ללולאה‪ ,‬וזאת משום שעומקו של עץ ריק הוא אפס(‪.‬‬
‫‪ 14.14.2‬הפונקציה‪get_val_from_level :‬‬
‫נִ פנה עתה להצגת הפונקציות בהן ‪ equal_vals_at_level‬עושה שימוש‪.‬‬
‫לפונקציה ‪ get_val_from_level‬ארבעה פרמטרים‪:‬‬
‫‪ .I‬פרמטר ערך באמצעותו מועבר לה מצביע לשורש העץ ממנו עליה לשלוף את‬
‫הערך הרצוי‪.‬‬
‫‪ .II‬פרמטר ערך המציין מאיזו רמה בעץ השלם יש לשלוף את הערך‪.‬‬
‫‪ .III‬פרמטר ערך באמצעותו מועבר ַלפונקציה העומק בעץ השלם בו מצוי הצומת‬
‫שמצביע אליו מועבר לה בקריאה הנוכחית‪.‬‬
‫‪ .IV‬פרמטר משתנה לתוכו יוכנס ערך כלשהו מהרמה המבוקשת בעץ המבוקש‪.‬‬
‫הפונקציה מחזירה ערך בולאני המורה האם היא הצליחה לאתר ערך כלשהו ברמה‬
‫המבוקשת‪ .‬הערך הבולאני המוחזר ישרת אותנו בקריאות הרקורסיביות‬
‫תקרא לעצמה‪ .‬מכיוון שבקריאה הראשונה לפונקציה מתוך‬
‫ְ‬
‫שהפונקציה‬
‫‪ equal_vals_at_level‬אנו מובטחים שמועבר עומק תקין‪ ,‬כלומר כזה שאינו‬
‫גדול מעומק העץ השלם‪ ,‬אזי בערך המוחזר על‪-‬ידי הפונקציה‬
‫‪ get_val_from_level‬ל‪ equal_vals_at_level -‬איננו עושים שימוש‪.‬‬
‫נציג עתה את קוד הפונקציה‪:‬‬
‫‪bool get_val_from_level(const struct Node * const root,‬‬
‫‪unsigned int wanted_depth, unsigned int‬‬
‫‪curr_depth,‬‬
‫‪151‬‬
‫{ )‪int &val‬‬
‫; ‪bool success‬‬
‫; ) ‪if (root == NULL) return( false‬‬
‫{ )‪if (wanted_depth == curr_depth‬‬
‫; ‪val = root -> data‬‬
‫; ) ‪return( true‬‬
‫}‬
‫‪success = get_val_from_level(root->_left,‬‬
‫)‪// (-‬‬
‫‪wanted_depth, curr_depth +1,‬‬
‫; )‪val‬‬
‫; ) ‪if (success) return( true‬‬
‫‪success = get_val_from_level(root->_left,‬‬
‫)‪// (+‬‬
‫‪wanted_depth, curr_depth +1,‬‬
‫; )‪val‬‬
‫; ) ‪return( success‬‬
‫}‬
‫הסבר‪ :‬ראשית‪ ,‬אם העץ ריק‪ ,‬סימן שההתקדמות הייתה על ענף בו לא מצוי ערך‬
‫ברמה המבוקשת‪ ,‬ועל‪-‬כן הפונקציה מחזירה את הערך ‪ .false‬שנית‪ ,‬אם הגענו‬
‫לרמה המקיימת ש‪(wanted_depth == curr_depth) :‬אזי אנו ַבעומק‬
‫המבוקש; על‪-‬כן יש להכניס לפרמטר המשתנה ‪ val‬את הערך המצוי בצומת זה‪ ,‬ויש‬
‫להחזיר את הערך ‪ ,true‬כדי לציין שמצאנו ערך בעומק המבוקש‪ .‬לבסוף‪ ,‬אם העץ‬
‫אינו ריק‪ ,‬וכן עדיין לא הגענו ַלעומק המבוקש‪ ,‬אזי אנו תחילה קוראים רקורסיבית‬
‫עם הבן השמאלי‪ .‬במידה והקריאה הרקורסיבית מחזירה את הערך ‪ ,true‬סימן‬
‫שהיא הצליחה לאתר ערך בעומק המבוקש )בבן השמאלי(‪ ,‬ולכן הקריאה הנוכחית‬
‫יכולה לסיים‪ ,‬ולהחזיר את הערך ‪ .true‬אם הקריאה עם הבן השמאלי לא הצליחה‬
‫לאתר ערך בעומק המבוקש )ועל כן החזירה את הערך ‪ ,(false‬אזי אנו קוראים‬
‫ֵ‬
‫רקורסיבית עם הבן הימני‪ ,‬ומחזירים את האיתות המוחזר מהקריאה הרקורסיבית‪.‬‬
‫‪152‬‬
‫נדגים הרצה של הפונקציה על העץ הבא‪:‬‬
‫‪7‬‬
‫‪8‬‬
‫‪5‬‬
‫‪6‬‬
‫נניח כי הפונקציה מתבקשת להחזיר ערך המצוי ברמה שלוש בעץ‪ .‬מצב הזיכרון עם‬
‫הקריאה לפונקציה הוא הבא‪:‬‬
‫)‪root (of main‬‬
‫=‬
‫‪7‬‬
‫‪8‬‬
‫‪5‬‬
‫‪root (of‬‬
‫)…‪equal_vals‬‬
‫=‬
‫=‪val‬‬
‫‪return= main‬‬
‫‪6‬‬
‫= ‪root‬‬
‫‪wanted_depth= 3‬‬
‫‪curr_depth= 1‬‬
‫=‪val‬‬
‫‪return=equal..‬‬
‫שימו לב לערכם של הפרמטרים השונים‪ :‬הפרמטר ‪ root‬מצביע על שורש העץ;‬
‫‪ wanted_depth‬מציין את הרמה ממנה יש לשלוף ערך‪ ,‬כלומר ‪curr_depth ;3‬‬
‫מציין את העומק בעץ בו אנו מצויים עתה‪ ,‬כלומר ‪ val ;1‬הוא פרמטר משתנה ולכן‬
‫שלחנו חץ לארגומנט המתאים; כתובת החזרה של העותק הראשון של‬
‫‪ get_val_from_level‬היא הפונקציה ‪.equal_vals_at_level‬‬
‫הפונקציה ‪ get_val_from_level‬מתחילה להתבצע‪ .‬ערכו של ‪ root‬אינו ‪.NULL‬‬
‫גם התנאי )‪ (wanted_depth == curr_depth‬אינו מתקיים‪ ,‬ועל‪-‬כן אנו‬
‫מתקדמים לפקודה אשר מזמנת קריאה רקורסיבית לפונקציה עם הבן השמאלי של‬
‫השורש הנוכחי‪ .‬כתובת החזרה של העותק השני של הפונקציה היא ההשמה )‪(-‬‬
‫בעותק הראשון‪ .‬נציג את מצב המחסנית עם הקריאה לעותק השני‪:‬‬
‫‪153‬‬
‫)‪root (of main‬‬
‫=‬
‫‪7‬‬
‫‪8‬‬
‫‪5‬‬
‫‪root (of‬‬
‫)…‪equal_vals‬‬
‫=‬
‫=‪val‬‬
‫‪return= main‬‬
‫‪6‬‬
‫= ‪root‬‬
‫‪wanted_depth= 3‬‬
‫‪curr_depth= 1‬‬
‫=‪val‬‬
‫‪return=equal..‬‬
‫= ‪root‬‬
‫‪wanted_depth= 3‬‬
‫‪curr_depth= 2‬‬
‫=‪val‬‬
‫)‪return= (-‬‬
‫אני מסב את תשומת לבכם לערכם של הפרמטרים השונים בקריאה השניה‬
‫לפונקציה‪.‬‬
‫הקריאה השניה מתחילה להתבצע‪ .‬התנאים בשתי פקודות ה‪ if -‬הראשונות אינם‬
‫מתקיימים‪ ,‬על כן הפונקציה קוראת רקורסיבית עם הבן השמאלי של הצומת אשר‬
‫מצביע אליו מועבר לה בפרמטר ‪ .root‬הבן השמאלי דנן ערכו ‪ ,NULL‬ועל כן‬
‫הקריאה הרקורסיבית השלישית מחזירה מיידית את הערך ‪ false‬למשתנה‬
‫‪ success‬של הקריאה הרקורסיבית השניה‪ .‬עתה הקריאה הרקורסיבית השניה‬
‫קוראת עם בנה הימני‪ .‬גם ערכו של מצביע זה הוא ‪ ,NULL‬ועל כן גם הקריאה עמו‬
‫מחזירה את הערך ‪ .false‬לקריאה הרקורסיבית השניה לא נותר עוד דבר לעשותו‪,‬‬
‫ואין לה בררה אלא להחזיר את הערך ‪ false‬המעיד שמכיווּן זה בעץ לא הצלחנו‬
‫לשלוף ערך כלשהו מעומק שלוש בעץ‪ .‬הערך המוחזר על‪-‬ידי הקריאה הרקורסיבית‬
‫השניה מוכנס בהשמה )‪ (-‬למשתנה ‪ success‬של הקריאה הרקורסיבית הראשונה‪.‬‬
‫הקריאה הרקורסיבית הראשונה ניסתה‪ ,‬עד כה‪ ,‬לשלוף ערך מעומק שלוש בעץ על‪-‬‬
‫ידי התקדמות לבנה השמאלי; הניסיון נכשל‪ .‬על‪-‬כן עתה הקריאה הרקורסיבית‬
‫הראשונה תנסה לשלוף את הערך מבנה הימני‪ .‬הקריאה הראשונה מתקדמת‬
‫להשמה )‪ (+‬ומזמנת קריאה רקורסיבית נוספת‪ .‬נציג את מצב הזיכרון‪:‬‬
‫‪154‬‬
root (of main)
=
root (of
equal_vals…)
=
val=
return= main
7
8
5
6
root =
wanted_depth= 3
curr_depth= 1
val=
return=equal..
root =
wanted_depth= 3
curr_depth= 2
val=
return= (+)
‫ התנאים בשתי‬.‫נציין את הקריאה הרקורסיבית הנוכחית כקריאה מספר חמש‬
‫כן הקריאה‬-‫ על‬,‫פקודות התנאי הראשונות בקריאה מספר חמש אינם מתקיימים‬
.(6 ‫מספר חמש קוראת רקורסיבית עם בנה השמאלי )כלומר עם מצביע לצומת של‬
:‫ נציג את מצב הזיכרון‬.‫נסמן את הקריאה המזומנת כקריאה מספר שש‬
root (of main)
=
root (of
equal_vals…)
=
val=
return= main
7
8
5
6
root =
wanted_depth= 3
curr_depth= 1
val=
return=equal..
root =
wanted_depth= 3
curr_depth= 2
val=
return= (+)
root =
wanted_depth= 3
curr_depth= 3
val=
return= (-)
155
‫הקריאה מספר שש מתחילה להתבצע‪ .‬התנאי )‪(root == NULL‬אינו מתקיים;‬
‫אך התנאי‪ (tree_depth == curr_depth) :‬מתקיים מאוד‪ .‬על כן הפונקציה‬
‫מכניסה לתוך הפרמטר המשתנה ‪ val‬את ערכו של ‪ ,root-> _data‬כלומר את‬
‫הערך שש‪ .‬מכיוון ש‪ val -‬הוא פרמטר משתנה אנו עוקבים אחר החצים כל הדרך‬
‫אל הבנק עד למשתנה ‪ val‬של הפונקציה ‪ ,equal_vals_at_level‬ושם שמים‬
‫אחר העותק מספר שש מחזיר את הערך ‪ .true‬ערך זה חוזר בהשמה ‪(-‬‬
‫את הערך‪ַ .‬‬
‫) למשתנה ‪ success‬של העותק מספר חמש‪ .‬בזאת מסתיים גם העותק מספר חמש‬
‫תוך שהוא מחזיר את ערכו של ‪ success‬להשמה )‪ (+‬של העותק הראשון‪ .‬עתה‬
‫העותק הראשון יכול להחזיר את הערך ‪ true‬המצוי במשתנה ‪ success‬שלו‬
‫לפונקציה ‪ equal_vals_at_level‬אשר זימנה את העותק הראשון של‬
‫‪ .get_val_from_level‬בזאת השלמנו את המשימה של שליפת ערך כשלהו‬
‫מעומק שלוש בעץ‪.‬‬
‫‪ 14.14.3‬הפונקציה‪check_level :‬‬
‫עתה עלינו לכתוב את הפונקציה השניה בה ‪ equal_vals_at_level‬עושה‬
‫שימוש‪ ,‬את הפונקציה ‪ .check_level‬פונקציה זאת מקבלת ארבעה פרמטרי ערך‪:‬‬
‫‪ .I‬מצביע לשורש )תת‪(-‬העץ המועבר לה‪.‬‬
‫‪ .II‬העומק אותו עליה לבדוק‪) .‬להזכירכם‪ ,‬על הפונקציה לבדוק האם בכל‬
‫הצמתים ַבעומק הרצוי קיים אותו ערך(‪.‬‬
‫‪ .III‬הערך שאמור להימצא בכל הצמתים בעומק הרצוי‪.‬‬
‫‪ .IV‬העומק בעץ השלם בו מצוי הצומת שמצביע אליו מועבר בפרמטר הראשון‪.‬‬
‫בקריאה הראשונה ל‪) ,check_level -‬זו המזומנת על‪-‬ידי‬
‫‪ equal_vals_at_level‬יועבר שורש העץ השלם‪ ,‬והערך ‪ ,1‬שכן שורש העץ‬
‫מצוי בעומק אחד(‪ .‬במהלך הבדיקה יהיה על הפונקציה להתקדם באמצעות‬
‫קריאות רקורסיביות לעומק הרצוי‪ .‬כל קריאה רקורסיבית תעביר לקריאה‬
‫שהיא מזמנת את עומקו בעץ השלם של הצומת המועבר בפרמטר הראשון(‪.‬‬
‫נציג את הפונקציה ‪:check_level‬‬
‫‪bool check_level( const struct Node * const root,‬‬
‫‪unsigned int wanted_depth, int val ,‬‬
‫{ )‪unsigned int curr_depth‬‬
‫; ) ‪if (root == NULL) return( true‬‬
‫)‪if (curr_depth == wanted_depth‬‬
‫; ) ‪return( root-> _data == val‬‬
‫‪return( check_level(root->_left, wanted_depth, val,‬‬
‫)‪curr_depth +1‬‬
‫&&‬
‫‪check_level(root-> _right, wanted_depth, val,‬‬
‫)‪curr_depth +1‬‬
‫; )‬
‫}‬
‫הסבר‪ :‬אם הגענו לתת‪-‬עץ ריק לא גילינו הפרה של הדרישה שבכל הצמתים בעומק‬
‫הרצוי )‪ (wanted_depth‬יהיה הערך המבוקש )‪ ,(val‬ולכן אנו מחזירים את הערך‬
‫‪ .true‬במילים אחרות‪ ,‬אם הגענו לתת‪-‬עץ ריק משמע הענף הנוכחי מסתיים בעומק‬
‫קטן מ‪ ,wanted_depth -‬ועל כן בו אין צומת המצוי בעומק ‪,wanted_depth‬‬
‫‪156‬‬
‫בפרט אין צומת שמפר את הדרישה שבכל הצמתים בעומק המבוקש יהיה את הערך‬
‫ְ‬
‫‪.val‬‬
‫מנגד‪ ,‬אם )‪ (curr_depth == wanted_depth‬אזי הגענו ַלעומק בו אנו‬
‫מתעניינים‪ ,‬ועל‪-‬כן יש להחזיר את הערך ‪ true‬אם ורק אם בצומת קיים הערך ‪.val‬‬
‫פקודת ה‪ return -‬ראשית‪ ,‬משווה את הערך המצוי בצומת עם הערך המבוקש‪,‬‬
‫ושנית‪ ,‬מחזירה את תוצאת ההשוואה‪.‬‬
‫לבסוף‪ ,‬אם אף אחד משני התנאים הקודמים לא התקיים אזי מחד טרם הגענו‬
‫לעומק הדרוש‪ ,‬ומאידך יש עדיין לאן להתקדם על הענף עליו אנו מתקדמים‪ ,‬לכן יש‬
‫לקרוא רקורסיבית עם שני בניו של הצומת הנוכחי‪ ,‬ולהחזיר את תוצאת הבדיקה‬
‫המוחזרת על‪-‬ידם‪ .‬הקוד הבא עושה זאת‪:‬‬
‫‪return( check_level(root->_left, wanted_depth, val,‬‬
‫)‪curr_depth +1‬‬
‫&&‬
‫‪check_level(root-> _right, wanted_depth, val,‬‬
‫)‪curr_depth +1‬‬
‫; )‬
‫נסביר מדוע וכיצד‪ :‬הקוד כולל פקודת ‪ return‬אשר מחזירה את תוצאת הקריאה‬
‫הרקורסיבית עם הבן השמאלי וגם תוצאת הקריאה הרקורסיבית עם הבן הימני‪.‬‬
‫אם לפחות אחת משתי הקריאות תחזיר את הערך ‪ false‬אזי פקודת ה‪return -‬‬
‫תחזיר את הערך ‪ ,false‬ואם שתי הקריאות תחזרנה את הערך ‪ true‬אזי פקודת‬
‫ה‪ return -‬תחזיר את הערך ‪.true‬‬
‫‪ 14.14.4‬זמן הריצה של ‪equal_vals_at_level‬‬
‫כמה עבודה מבצעת הפונקציה ‪ equal_vals_at_level‬עת היא בודקת עץ‬
‫כלשהו? נניח כי על הפונקציה לבדוק עץ בעומק ‪ַ .n‬במקרה הגרוע‪ ,‬כלומר ַבמקרה בו‬
‫על הפונקציה לבצע את מירב העבודה‪ ,‬העץ הינו מלא‪ ,‬ולכן כולל ‪ 2n –1‬צמתים‪,‬‬
‫וכמו כן בצמתים המצויים בכל רמה ורמה יש אותו ערך )ולכן הבדיקה אינה נעצרת‬
‫באמצע התהליך‪ ,‬אלא ִמתמצה(‪ .‬עבוּר כל אחת ואחת מרמות העץ‪ ,‬החל ברמה מספר‬
‫אחת ועד הרמה מספר ‪ n‬מבצעת הפונקציה שתי פעולות‪:‬‬
‫‪ .I‬היא שולפת ערך המצוי בעומק הרצוי )נסמן עומק זה באות ‪.(d‬‬
‫‪ .II‬היא בודקת את הצמתים ברמה מספר ‪.d‬‬
‫עבוּר כל אחת משתי הפעולות על פונקציה לסרוק כל צמתי העץ ברמות ‪,1..d‬‬
‫כלומר לסרוק כ‪ 2d -‬צמתים‪.‬‬
‫כאמור‪ ,‬את הבדיקה הנ"ל יש לבצע עבור ‪ ,d=1,…,n‬ולכן כמות העבודה המתבצעת‬
‫היא בשיעור‪ . 21+…+2n :‬הביטוי שקיבלנו הוא סכומו של טור הנדסי בן ‪ n‬איברים‪,‬‬
‫שהאיבר הראשון בו )‪ (a1‬ערכו שתיים‪ ,‬האיבר האחרון ערכו ‪ ,2n‬והיחס בין כל זוג‬
‫איברים עוקבים )‪ (q‬הוא שתיים‪ .‬סכומו של טור זה מתקבל על‪-‬ידי הנוסחה‪s= :‬‬
‫)‪ . a1*(qn –1)/(q –1‬יישומה של הנוסחה למקרה שלנו ייתן את הביטוי‪:‬‬
‫)‪ , 2*(2n –1)/(2 –1‬עבור ‪ n‬שהינו עומק העץ‪ .‬מספר הצמתים בעץ הוא‪,‬‬
‫להזכירכם‪ ,2n ,‬כלומר כמות העבודה המתבצעת על‪-‬ידי הפונקציה לינארית בכמות‬
‫הצמתים‪.‬‬
‫מה‪ ,‬לעומת זאת‪ ,‬יקרה אם העץ יתנוון לכדי רשימה משורשרת )כלומר לכל צומת‬
‫יהיה רק בן יחיד(? במקרה זה בעץ שעומקו ‪ n‬יש ‪ n‬צמתים‪ .‬עתה כדי לבדוק את‬
‫הצומת )היחיד( מעומק ‪ d‬יש לסרוק ‪ d‬צמתים‪ ,‬וזאת יש לבצע עבור ‪,d=1,…,n‬‬
‫‪157‬‬
‫כלומר כמות העבודה הינה בסדר גודל של‪ ,1+2+…+n :‬כלומר בסדר גודל של ‪,n2‬‬
‫עבור ‪ n‬שמציין את עומקו של העץ‪ ,‬ואת מספר הצמתים בעץ‪.‬‬
‫על‪-‬כן במקרה הגרוע הפונקציה שכתבנו תרוץ בזמן ריבועי במספר הצמתים‪ .‬לא‬
‫נוכיח כאן כי במקרה הממוצע כמות העבודה שהפונקציה תבצע הינה כמו במקרה‬
‫הראשון שראינו )בו העץ מלא( כלומר לינארית במספר הצמתים‪.‬‬
‫‪ 14.14.5‬פתרון חלופי לבעיה‬
‫את הבעיה המוצגת בסעיף זה )האם בכל הצמתים באותה רמה בעץ מצוי אותו ערך(‬
‫ניתן לפתור בצורה שונה‪ ,‬פשוטה יותר‪ .‬נציג כאן את קווי המתאר של הפתרון‬
‫החלופי‪:‬‬
‫‪d‬‬
‫‪ .I‬אתר את עומק העץ‪ ,‬שיסומן כ‪) .d -‬כזכור‪ ,‬בעץ יש לכל היותר ‪ 2 -1‬צמתים‪(.‬‬
‫‪ .II‬הקצה מערך בגודל ‪ 2d –1‬תאים שלמים‪.‬‬
‫‪ .III‬שכן את עץ במערך‪ ,‬כפי שמתואר בסעיף ‪.13.2‬‬
‫‪i‬‬
‫‪i+1‬‬
‫‪ .IV‬סרוק את המערך סדרתית‪ .‬התאים מספר ‪) 2 ,…,2 -1‬עבור ‪(i=1,…,d-1‬‬
‫במערך‪ ,‬שאינם ריקים‪ ,‬מכילים ערכים המצויים באותה רמה בעץ‪ ,‬ועל‪-‬כן בדוק‬
‫האם הם שווים‪.‬‬
‫‪ 14.15‬האם כל הערכים בעץ שונים זה מזה‬
‫עתה ברצוננו לכתוב פונקציה בשם ‪ unique‬אשר מקבלת מצביע לעץ בינארי )שאינו‬
‫דווקא עץ חיפוש( ומחזירה את הערך ‪ true‬אם ורק אם כל הערכים בעץ שונים זה‬
‫מזה‪ ,‬במילים אחרות‪ :‬אם לא קיימים בעץ שני צמתים שיש בהם אותו ערך‪.‬‬
‫תמ ֵצא בצומת ‪node‬‬
‫הפונקציה ‪ unique‬תסרוק את כל צמתי העץ‪ .‬עת הפונקציה ַ‬
‫כלשהו‪ ,‬בו מצוי הערך ‪ ,v‬יהיה עליה לקרוא לפונקציה ‪ count_val‬אשר תספור‬
‫כמה פעמים מופיע הערך ‪ v‬בעץ בשלמותו‪ .‬אם כל ערך מופיע בעץ רק פעם יחידה‪,‬‬
‫אזי הפונקציה ‪ count_val‬תחזיר את הערך אחד )שכן היא תספור גם את המופע‬
‫הנוכחי של ‪ .(v‬לכן אם ‪ count_val‬מחזירה ערך גדול מאחד מצאנו הפרה של‬
‫הדרישה‪ ,‬וניתן לסיים מיידית )את הרצתה של ‪ (unique‬תוך החזרת הערך ‪.false‬‬
‫קל יחסית להבין כי ‪ unique‬תקבל מצביע )בשם ‪ (curr_node‬לצומת כלשהו בעץ‪,‬‬
‫והרקורסיה תתבצע על ‪ ,curr_node‬כלומר ַבקריאות הרקורסיביות השונות‬
‫ַלפונקציה ישתנה ערכו של ‪ .curr_node‬הנקודה העדינה בפונקציה ‪ unique‬היא‬
‫שעל מנת ש‪ unique -‬תוכל להעביר ל‪ count_val -‬את שורש העץ השלם היא‬
‫צריכה לקבל ְפרט למצביע ‪ curr_node‬מצביע לשורש העץ השלם )שיקרא‬
‫‪ .(tree_root‬על מצביע זה לא תיערך רקורסיה‪ ,‬והוא יועבר כמות שהוא בקריאות‬
‫הרקורסיביות השונות ל‪ ,unique -‬וזאת על‪-‬מנת שכל קריאה רקורסיבית תוכל‬
‫להעבירו ל‪.count_val -‬‬
‫נציג‪ ,‬ראשית‪ ,‬את ‪:unique‬‬
‫‪bool unique(const struct Node * const curr_node,‬‬
‫{ )‪const struct Node * const tree_root‬‬
‫; ‪if (curr_node == NULL) return true‬‬
‫)‪if (count_val(tree_root, curr_node-> _data) > 1‬‬
‫‪158‬‬
‫; ‪return false‬‬
‫; )‬
‫&& )‪return( unique(curr_node->_left, tree_root‬‬
‫)‪unique(curr_node-> _right, tree_root‬‬
‫}‬
‫הסבר הפונקציה‪ :‬ראשית‪ ,‬אם תת‪-‬העץ אליו הגענו ריק אזי אין בו ערך המופיע בעץ‬
‫יותר מפעם יחידה‪ ,‬ועל‪-‬כן ניתן להחזיר את הערך ‪ .true‬שנית‪ ,‬אם תת‪-‬העץ אליו‬
‫הגענו אינו ריק‪ ,‬אזי יש לספור כמה פעמים מופיע בעץ השלם הערך >‪curr_node-‬‬
‫‪ ,_data‬ואת זאת אנו עושים על‪-‬ידי קריאה ל‪ count_val -‬אשר מקבלת מצביע‬
‫לשורש העץ השלם וערך רצוי‪ .‬אם הפונקציה מחזירה ערך גדול מאחד‪ ,‬אזי ניתן‬
‫להסיק כי הערך המצוי בצומת הנוכחי אינו ייחודי‪ ,‬ועל‪-‬כן יש להחזיר את הערך‬
‫‪ .false‬לבסוף‪ ,‬אם הערך בצומת הנוכחי התגלה כייחודי‪ ,‬אזי יש להחזיר את‬
‫תוצאת הקריאה הרקורסיבית לבן השמאלי‪ ,‬וגם תוצאת הקריאה הרקורסיבית לבן‬
‫המזוּמנות מקבלת את הבן‬
‫הימני‪ .‬שימו לב כי כל אחת מהקריאות הרקורסיביות ְ‬
‫המתאים‪ ,‬ואת ‪ְ tree_root‬כמות שהוא‪ ,‬כלומר את שורש העץ השלם‪.‬‬
‫שאלות למחשבה והשלמות נדרשות לסעיף זה‪:‬‬
‫‪ .I‬כתבו את הפונקציה )הפשוטה למדי( ‪.count_val‬‬
‫‪ .II‬כמה עבודה מבצעת ‪) unique‬במקרה הגרוע ביותר( עת עליה לבדוק עץ בן ‪n‬‬
‫צמתים?‬
‫‪ .III‬כתבו את ‪ unique‬עבור עץ חיפוש‪.‬‬
‫כמה עבודה מבצעת ‪ unique‬על עץ חיפוש?‬
‫‪159‬‬
‫‪ 14.16‬תרגילים‬
‫‪ 13.14.1‬תרגיל ראשון‪ :‬עץ ‪radix‬‬
‫בהינתן אוסף מחרוזות המורכבות משתי אותיות בלבד ‪ a,b‬נרצה להחזיקן במבנה‬
‫נתונים שיאפשר לנו להשיב ביעילות על 'שאילתות שייכות'‪ ,‬כלומר‪ ,‬בהינתן מחרוזת‬
‫נרצה לדעת האם היא מופיעה באוסף המחרוזות הנתון‪ ,‬או לא מופיעה‪.‬‬
‫לדוגמא עבור אוסף המחרוזות }‪ {a,abb,ba,baa,babb‬אנו עשויים להישאל האם‬
‫המחרוזת ‪ baa‬מופיעה באוסף? )כן( האם המחרוזת ‪ bab‬מופיעה בו? )לא( האם‬
‫המחרוזת ‪ abba‬מופיעה בו? )לא( וכדומה‪.‬‬
‫דרך אפשרית לממש את המטרה היא להחזיק את האוסף הנתון בעץ בינרי הנקרא‬
‫‪ :radix tree‬העץ יבנה‪ ,‬על‪-‬סמך אוסף המחרוזות באופן הבא‪:‬‬
‫‪ .I‬ראשית‪ ,‬בנה עץ הכולל שורש בלבד )לא עץ ריק(‪.‬‬
‫אחר‪ ,‬עבוֹר על כל מחרוזות האוסף‪ ,‬בזו אחר זו‪ ,‬והוסף אותן לעץ באופן הבא‪:‬‬
‫‪ַ .II‬‬
‫אחר אות‪ ,‬משמאל לימין‪.‬‬
‫התחל משורש העץ‪ .‬קרא את המחרוזת הנוכחית אות ַ‬
‫בכל צעד אם התו הבא הוא ‪ a‬רד לבן השמאלי של הקודקוד הנוכחי‪ ,‬אם התו‬
‫נדרשת לרדת לבן‬
‫ַ‬
‫הבא הוא ‪ b‬רד לבן הימני של הקודקוד הנוכחי‪ .‬בכל שלב בו‬
‫שאינו קיים – צור אותו‪ .‬אם המחרוזת הסתימה זה עתה – סמן את הקודקוד בו‬
‫היא הסתימה‪.‬‬
‫העץ שיווצר עבור הדוגמה לעיל‪:‬‬
‫‬‫‪+‬‬
‫‬‫‪-‬‬
‫‪+‬‬
‫‪-‬‬
‫‪+‬‬
‫‪+‬‬
‫‪+‬‬
‫בהינתן מחרוזת‪ ,‬הסיקו כיצד תשתמשו בעץ )בלבד( על‪-‬מנת להשיב על שאילתת‬
‫שייכות )לא להגשה(‪.‬‬
‫‪ִ .I‬כתבו את ההגדרות הדרושות למימוש העץ הנ"ל )כלומר אילו מערכים‪-struct ,‬ים‬
‫או מבני נתונים אחרים יידרשו למימוש עץ כנ"ל(‪.‬‬
‫‪160‬‬
‫‪ .II‬כתבו את הפונקציה ‪ add‬המוסיפה מחרוזת לעץ נתון‪ .‬הפונקציה מקבלת כקלט‬
‫מצביע לשורש העץ‪ ,‬ואת המחרוזת המבוקשת‪) .‬הניחו כי העץ מכיל לכל הפחות‬
‫שורש‪ .‬אתם רשאים להוסיף ַלפונקציה פרמטרים נוספים על‪-‬פי שיקול דעתכם(‪.‬‬
‫‪ .III‬כתבו את הפונקציה ‪ find‬המחפשת מחרוזת בעץ נתון )כנ"ל(‪ .‬הפונקציה מקבלת‬
‫כקלט מצביע לשורש העץ‪ ,‬ואת המחרוזת המבוקשת‪ ,‬ומחזירה ערך אמת‬
‫)‪ (true/false‬מתאים‪) .‬אתם רשאים להוסיף לפונקציה פרמטרים נוספים על‪-‬פי‬
‫שיקול דעתכם(‪.‬‬
‫‪ .IV‬נסמן ב‪ n :‬את מספר המחרוזות באוסף‪ ,‬וב‪ m :‬את אורך המחרוזת הארוכה‬
‫ביותר האפשרית )באוסף‪ ,‬או בשאילתת השייכות(‪ .‬הניחו כי פעולת השוואה של‬
‫זוג תווים )זה עם זה( אורכת יחידת זמן אחת )וכל שאר הפעולות זניחות(‪.‬‬
‫‪ .1‬חשבו והסבירו את זמן הריצה של שאילתת שייכות על העץ‪.‬‬
‫‪ .2‬כנ"ל לגבי חיפוש במערך דו ממדי ממוין הגדול דיו להחזיק את כל המחרוזות‬
‫באוסף‪.‬‬
‫ה‪ .‬בחרו את אחת משתי הפונקציות שכתבתם – וכתבו אותה שוב‪ :‬אם בחרתם‬
‫במימוש איטרטיבי‪ ,‬השתמשו כעת ברקורסיה‪ ,‬ולהפך‪.‬‬
‫הערה‪ :‬בכל המקרים ניתן להניח קלט תקין‪.‬‬
‫‪ 13.14.2‬תרגיל שני‪ :‬מציאת קוטר של עץ‬
‫בתכנית מוגדר‪:‬‬
‫‪struct Node‬‬
‫{‬
‫; ‪int _data‬‬
‫; ‪struct Node *_left, * _right‬‬
‫}‬
‫נגדיר קוטר של עץ בינארי כמספר הקשתות על המסלול הארוך ביותר בין שני‬
‫צמתים בעץ‪.‬‬
‫הקוטר של העץ הריק יוגדר להיות ‪ ,1-‬וקוטרו של עץ הכולל צומת יחיד יוגדר‬
‫להיות ‪.0‬‬
‫דוגמה‪ :‬קוטרם של שני העצים הבאים הוא ‪:5‬‬
‫רמז‪ :‬שימו לב כי בכל עץ קיים צומת )הצבוע בדוגמות שלנו באפור( כך ששני‬
‫הצמתים המצויים בשני קצותיו של הקוטר )שני הצמתים הצבועים בשחור(‬
‫הינם שניהם צאצאיו של אותו צומת‪.‬‬
‫כתבו פונקציה המקבלת מצביע לעץ בינארי )מטיפוס * ‪ (struct Node‬ומחזירה את‬
‫קוטרו )יש לכתוב כל פונקצית עזר בה הפונקציה שלכם משתמשת(‪.‬‬
‫‪161‬‬
‫‪ 13.14.3‬תרגיל שלישי‪ :‬מה עושה התכנית?‬
‫נגדיר‬
‫‪struct Node‬‬
‫{‬
‫;‪int _number‬‬
‫;‪struct Node *_left, * _right‬‬
‫}‬
‫נתונה הפונקציה הבאה‪:‬‬
‫)‪void secret (struct Node *t, float &x, int &n‬‬
‫{‬
‫;‪float x1, x2‬‬
‫;‪int n1, n2‬‬
‫)‪if (t == NULL‬‬
‫{‬
‫;‪x= 0‬‬
‫;‪n= 0‬‬
‫}‬
‫‪else‬‬
‫{‬
‫;)‪secret (t->_left, x1, n1‬‬
‫;)‪secret (t-> _right, x2, n2‬‬
‫;‪n= n1 +n2 +1‬‬
‫;‪x= (t->number + x1*n1 + x2*n2) / n‬‬
‫}‬
‫}‬
‫‪ .I‬אם מריצים את הפונקציה הזו על עץ בינרי‪ ,‬מהם הערכים שיוחזרו בפרמטרים‬
‫‪ x‬ו‪?n-‬‬
‫;)‪secret (t-> _right, x2, n2‬‬
‫‪ .II‬אם במקום השורה‬
‫;‪x2 = 0; n2 = 0‬‬
‫יופיעו שתי הפקודות‪:‬‬
‫מהם הערכים שיוחזרו אז?‬
‫‪ 13.14.4‬תרגיל רביעי‪ :‬בדיקה האם עץ א' משוכן בעץ ב'‬
‫נגדיר‪:‬‬
‫‪struct Node‬‬
‫{‬
‫;‪int _data‬‬
‫; ‪struct Node *_left, *_right‬‬
‫}‬
‫נאמר שעץ בינארי ‪ T1‬משוכן בעץ בינארי שני ‪ T2‬אם ב‪ T2 -‬קיים צומת ‪N‬‬
‫המהווה שורש של תת עץ )של ‪ (T2‬שהינו זהה לחלוטין ל‪.T1 -‬‬
‫לדוגמא העץ‪:‬‬
‫‪2‬‬
‫‪162‬‬
‫‪1‬‬
‫‪3‬‬
‫משוכן בימני מבין שלושת העצים הבאים‪ ,‬אך לא בשנים האחרים‪:‬‬
‫)הערה‪ :‬מצביעים שערכם ‪ NULL‬הושמטו מכל ארבעת הציורים(‬
‫‪5‬‬
‫‪5‬‬
‫‪2‬‬
‫‪7‬‬
‫‪3‬‬
‫‪2‬‬
‫‪7‬‬
‫‪1‬‬
‫‪5‬‬
‫‪7‬‬
‫‪1‬‬
‫‪4‬‬
‫‪2‬‬
‫‪3‬‬
‫‪1‬‬
‫‪4‬‬
‫כתבו את הפונקציה‪:‬‬
‫)‪bool nested(struct Node *small, struct Node *large‬‬
‫הפונקציה תחזיר את הערך אמת אם העץ עליו מצביע הפרמטר ‪ small‬משוכן בעץ‬
‫עליו מצביע הפרמטר ‪ ,large‬ושקר אחרת‪.‬‬
‫‪163‬‬
‫‪ 13.14.5‬תרגיל חמישי‪ :‬תת‪-‬עץ מרבי בו השורש גדול מצאצאיו‬
‫כתבו פונקציה המקבלת מצביע ‪ root‬לשורשו של עץ בינארי‪ ,‬ומחזירה מצביע‬
‫‪ root_larger‬לשורש תת‪-‬העץ הגדול ביותר )כלומר לתת‪-‬העץ שכולל את מספר הגדול‬
‫ביותר של צמתים(‪ ,‬המקיים שעבור כל צומת בתת‪-‬העץ עליו מצביע ‪,root_larger‬‬
‫הערכים המצויים בצאצאיו של הצומת קטנים או שווים מהערך המצוי ַבצומת‬
‫עצמו‪.‬‬
‫במידה וקיימים כמה תתי‪-‬עצים כנ"ל יש להחזיר מצביע לאחד מהם‪.‬‬
‫לדוגמה‪ ,‬עבור העץ הבא‪:‬‬
‫‪4‬‬
‫‪5‬‬
‫‪7‬‬
‫‪0‬‬
‫‪6‬‬
‫‪3‬‬
‫‪2‬‬
‫‪4‬‬
‫יוחזר מצביע לתת‪-‬העץ ששורשו הוא הצומת של שבע‪.‬‬
‫‪164‬‬
‫‪ 13.14.6‬תרגיל שישי‪ :‬בדיקה האם תת‪-‬עץ שמאלי מחלק כל שורש‪ ,‬וכל‬
‫שורש מחלק תת‪-‬עץ ימני‬
‫כתבו פונקציה המקבלת מצביע לשורשו של עץ בינארי ומחזירה את הערך ‪ true‬אם‬
‫ורק אם כל צומת ‪ n‬בעץ מקיים שהערכים בכל הצמתים בתת‪-‬העץ עליו מצביע בנו‬
‫השמאלי של ‪ n‬מחלקים את הערך שבצומת ‪ ,n‬בעוד הערכים בכל הצמתים בתת‪-‬העץ‬
‫עליו מצביע בנו הימני של ‪ n‬מחולקים על‪-‬ידי הערך שבצומת ‪.n‬‬
‫לדוגמה‪ ,‬על העץ הבא יוחזר הערך ‪:true‬‬
‫‪16‬‬
‫‪8‬‬
‫‪64‬‬
‫‪64‬‬
‫‪32‬‬
‫‪4‬‬
‫‪8‬‬
‫‪4‬‬
‫‪ 13.14.7‬תרגיל שביעי‪ :‬בדיקה האם עץ סימטרי‬
‫הגדרה‪ :‬עץ בינארי יקרא סימטרי אם בנו השמאלי הוא תמונת ראי של בנו הימני‪.‬‬
‫כתבו פונקציה המקבלת מצביע לשורשו של עץ בינארי‪ ,‬ומחזירה את הערך ‪ true‬אם‬
‫ורק אם העץ סימטרי‪.‬‬
‫לדוגמה‪ ,‬העץ הבא סימטרי‪:‬‬
‫‪16‬‬
‫‪8‬‬
‫‪8‬‬
‫‪4‬‬
‫‪7‬‬
‫‪7‬‬
‫‪5‬‬
‫‪4‬‬
‫‪5‬‬
‫‪9‬‬
‫‪9‬‬
‫‪165‬‬
‫‪ 13.14.8‬תרגיל שמיני‪ :‬איתור הערך המזערי בעץ‬
‫כתבו פונקציה המקבלת מצביע לשורשו של עץ בינארי שאינו ריק‪ ,‬ומחזירה את‬
‫הערך המזערי בעץ‪.‬‬
‫‪ 13.14.9‬תרגיל תשיעי‪ :‬עץ ביטוי )עבור ביטוי אריתמטי(‬
‫בתרגיל זה נעסוק בביטויים אריתמטיים כדוגמת‪. 5, (5+3), (2*(5+3)) :‬‬
‫הגדרה‪ :‬ביטוי אריתמטי מורכב מ‪:‬‬
‫‪ .I‬מספר טבעי‪ .‬או‬
‫‪ .II‬סוגריים המכילים בתוכם‪ (1) :‬ביטוי אריתמטי‪ (2) ,‬אופרטור )שעשוי להיות אחד‬
‫מהבאים‪ (3) ,(% ,/ ,* ,- ,+ :‬ביטוי אריתמטי‪.‬‬
‫ראשית‪ ,‬בדקו לעצמכם ששלושת הביטויים הנ"ל עונים על ההגדרה‪.‬‬
‫התכנית שנכתוב תבצע את הפעולות הבאות‪:‬‬
‫‪ .I‬התכנית תקרא מהקלט ביטוי אריתמטי ותבנה ממנו עץ ביטוי על‪-‬פי אלגוריתם‬
‫שיתואר בהמשך‪.‬‬
‫‪ .II‬התכנית תעריך את הביטוי תוך שימוש בעץ הביטוי שנבנה‪.‬‬
‫התכנית תשתמש בפונקציה שנהוג לקרוא בשם ‪) tokenizer‬ואשר תיכתב‪ ,‬כמובן‪ ,‬על‪-‬‬
‫ידכם(‪ .‬באחריותה של פונקצית ה‪ tokenizer -‬יהיה לקרוא את הקלט‪ ,‬ולהחזיר‪ ,‬בכל‬
‫פעם שהיא נקראת‪ ,‬את האסימון )‪ (token‬הבא בקלט‪ .‬אסימון הוא מרכיב אטומי‬
‫כלשהו בקלט‪ .‬בדוגמה שלנו האסימונים האפשריים הינם‪ :‬סוגר שמאלי‪ ,‬סוגר ימני‪,‬‬
‫אחד מחמשת האופרטורים‪ ,‬מספר טבעי‪ .‬כדי להחזיר את האסימון הדרוש נשתמש‬
‫במבנה שיוגדר באופן הבא לערך‪:‬‬
‫{ ‪struct op_op‬‬
‫‪bool oprnd_oprtr ; // which of the other 2 fields is‬‬
‫; ‪char oprnd‬‬
‫‪// in use‬‬
‫; ‪unsigned int oprtr‬‬
‫; }‬
‫אלגוריתם לבניית עץ ביטוי‪:‬‬
‫קרא שוב ושוב ל‪ tokenizer -‬עד קריאת הביטוי השלם‪ .‬על‪-‬פי הערך המוחזר על‪-‬ידי‬
‫ה‪ tokenizer -‬בצע‪:‬‬
‫‪ .I‬אם אותר מספר אזי החזר מצביע לשורש עץ הכולל את המספר שנקרא בלבד‪.‬‬
‫‪ .II‬אם הוחזר סוגר פותח )סוגר שמאלי( אזי‪:‬‬
‫‪ .1‬קרא רקורסיבית לאלגוריתם‪ ,‬והכנס את הערך המוחזר למצביע שיהיה בן‬
‫שמאלי של הצומת שמצביע אליו תחזיר‪.‬‬
‫‪ .2‬קרא ל‪ tokenizer -‬שיחזיר את האופרטור שישכון בשורש העץ שמצביע אליו‬
‫תחזיר‪.‬‬
‫‪ .3‬קרא רקורסיבית לאלגוריתם‪ ,‬והכנס את הערך המוחזר למצביע שיהיה בן‬
‫ימני של הצומת שמצביע אליו תחזיר‪.‬‬
‫‪ .4‬קרא ל‪ tokenizer -‬שיחזיר את הסוגר הימני הסוגר את הסוגר השמאלי‬
‫שנקרא בתחילת סעיף ב'‪.‬‬
‫‪ .5‬החזר מצביע לעץ שמבנהו תואר בסעיפים ‪ 1‬עד ‪.3‬‬
‫‪166‬‬
‫לדוגמה‪ :‬עבור הביטוי ))‪(2*(5+3‬יבנה עץ הבא‪:‬‬
‫*‬
‫‪+‬‬
‫‪3‬‬
‫‪2‬‬
‫‪5‬‬
‫בהינתן עץ ביטוי אתם מוזמנים לחשוב בעצמכם כיצד ניתן להעריכו‪ ,‬כדי לקבל את‬
‫ערכו של הביטוי‪ .‬בדוגמה שלנו ערכו של הביטוי הוא ‪. (2*(5+3))=16‬‬
‫‪ 13.14.10‬תרגיל עשירי‪ :‬חישוב מספר עצי החיפוש בני ‪ n‬צמתים‬
‫כתבו פונקציה המקבלת מספר טבעי ‪ ,n‬ומחזירה את מספר עצי החיפוש הבינאריים‬
‫השונים בני ‪ n‬צמתים‪ ,‬עם ‪ n‬ערכים שונים ‪.1..n‬‬
‫דוגמה‪:‬‬
‫‪ .I‬עבור ‪ n=0‬יוחזר הערך אחד‪ ,‬שכן יש רק עץ חיפוש בינארי ריק יחיד‪.‬‬
‫‪ .II‬עבור ‪ n=1‬יוחזר הערך אחד‪ ,‬שכן יש רק עץ חיפוש בינארי יחיד המכיל צומת אחד‬
‫‪ .III‬עבור ‪ n=2‬יוחזר הערך שתיים‪ ,‬שכן קיימים שני עצי חיפוש המכילים שני ערכים‬
‫שונים‪:‬‬
‫‪ .IV‬עבור ‪ n=3‬יוחזר הערך חמש‪.‬‬
‫רמז‪:‬‬
‫‪ .I‬כל אחד מ‪ n -‬הערכים עשוי להיות בשורש העץ‪ ,‬וכל שורש כזה מגדיר‪ ,‬כמובן‪ ,‬עץ‬
‫שונה‪.‬‬
‫‪ .II‬בעקבות ההחלטה כי בשורש ישכון הערך ‪ ,x‬נקבע גם גודלו של הבן השמאלי‪,‬‬
‫והבן הימני של השורש‪.‬‬
‫‪ 13.14.11‬תרגיל מספר אחד‪-‬עשר‪ :‬איתור הצומת העמוק ביותר בעץ‬
‫כתבו פונקציה המקבלת מצביע לעץ בינארי‪ ,‬ומחזירה מצביע לצומת העמוק ביותר‬
‫בעץ‪ ,‬וכן את עומקו של צומת זה‪.‬‬
‫‪167‬‬
‫‪ 13.14.12‬תרגיל מספר שניים‪-‬עשר‪ :‬בדיקה האם עץ כמעט מלא‬
‫עץ בינארי נקרא כמעט מלא אם כל עליו נמצאים ברמה ‪ d‬או ברמה ‪ ,d-1‬ואם לכל‬
‫צומת המקיים שיש לו צאצא ימני ברמה ‪ ,i‬יש גם צאצא שמאלי ברמה זאת‪.‬‬
‫לדוגמה‪ :‬השמאלי במין שני העצים הבאים הוא עץ כמעט מלא‪ ,‬בעוד הימני אינו‪.‬‬
‫*‬
‫הסיבה לכך שהימני אינו כמעט מלא היא שלצומת המסומן בכוכב יש צאצא ימני‬
‫בעומק ארבע‪ ,‬אך אין לו בן שמאלי בעומק זה‪.‬‬
‫כתבו פונקציה המקבלת מצביע לשורשו של עץ בינארי‪ ,‬ומחזירה את הערך ‪ true‬אם‬
‫ורק אם העץ כמעט מלא‪.‬‬
‫‪ 13.14.13‬תרגיל מספר שלושה‪-‬העשר‪ :‬האם כל תת‪-‬עץ מכיל מספר זוגי‬
‫של צמתים‬
‫כתבו פונקציה המקבלת מצביע לשורשו של עץ בינארי‪ .‬על הפונקציה להחזיר את‬
‫הערך ‪ true‬אם ורק אם כל תת‪-‬עץ מכיל מספר זוגי של צמתים‪ ,‬וזאת מבלי לספור‬
‫את מספר הצמתים שבכל תת‪-‬עץ‪.‬‬
‫‪ 13.14.14‬תרגיל מספר ארבעה‪-‬עשר‪ :‬שמירת עומקם של עלי העץ במערך‬
‫כתבו פונקציה המקבלת מצביע לשורשו של עץ בינארי‪ ,‬וכן מערך חד‪-‬ממדי של‬
‫משתנים בולאניים‪ ,‬עליו ידוע כי תאיו אותחלו לערך ‪.false‬‬
‫בתום ביצוע הפונקציה יכיל התא מספר ‪ i‬במערך את הערך ‪ true‬אם ורק אם בעץ‬
‫קיים עלה בעומק ‪) i‬עבור ‪.(i = 0…N‬‬
‫‪168‬‬
‫‪ 13.14.15‬תרגיל מספר חמישה‪-‬עשר‪ :‬חיוט של עץ‬
‫עץ בינארי נקרא מחויט אם כל צומת בו מכיל גם מצביע לצומת שמצוי באותה רמה‬
‫כמו הצומת הנוכחי‪ ,‬והינו השכן הימני של הצומת הנוכחי בעץ‪.‬‬
‫דוגמה לעץ מחויט‪:‬‬
‫החצים המקווקווים הם שמדגימים את החיוט‪.‬‬
‫כדי להחזיק עת מחויט נזדקק למבנה מעט שונה מזה המשמש אותנו בדרך כלל‪ ,‬על‪-‬‬
‫כן נגדיר‪:‬‬
‫{ ‪struct T_node‬‬
‫; ‪int _data‬‬
‫; ‪T_node *_left, * _right, *_brother‬‬
‫; }‬
‫כתבו פונקציה המקבלת מצביע מטיפוס * ‪ t_node‬לשורשו של עץ בינארי שנבנה‬
‫‪ left,‬בכל צומת מחזיקים ערכים‬
‫אך טרם חוייט )המצביעים _‪_right‬‬
‫תקינים‪ ,‬אך ערכו של המצביע ‪ brother‬בכל צומת הוא ‪ .(NULL‬על הפונקציה‬
‫לחייט את העץ‪.‬‬
‫רמזים לפתרון‪:‬‬
‫א‪ .‬הגדירו מבנה‪:‬‬
‫{ ‪struct List_of_p_2_t_node‬‬
‫; ‪struct T_node *the_p‬‬
‫; ‪struct List_of_p_2_t_node *_next‬‬
‫; }‬
‫‪ .III‬בקרו בעץ רמה אחר רמה‪ .‬בכל רמה בקרו כרגיל משמאל לימין‪ .‬עת אתם‬
‫מבקרים ַבצמתים מרמה ‪ l‬כלשהי‪:‬‬
‫מטיפוס‬
‫מאיברים‬
‫המורכבת‬
‫משורשרת‬
‫רשימה‬
‫ְבנו‬
‫‪.1‬‬
‫‪ .List_of_p_2_t_node‬כל תא ַברשימה יחזיק בראש וראשונה מצביע‬
‫לאיבר כלשהו ברמה ‪ l‬בעץ‪ ,‬וכן מצביע לאיבר הבא ברשימה המשורשרת‪.‬‬
‫יתוַסף בסופה‪.‬‬
‫את הרשימה המשורשרת בנו ְכתור‪ ,‬כלומר כל איבר חדש ְ‬
‫השתמשו ברשימה המשורשרת כדי לחייט את העץ‪.‬‬
‫‪.2‬‬
‫‪169‬‬
‫עודכן ‪5/2010‬‬
‫‪ 15‬מצביעים לפונקציות‬
‫בפרקים הקודמים ראינו כי מצביעים עשויים להורות על נתונים השמורים‬
‫במערכים‪ ,‬רשימות או עצים‪ .‬אמרנו כי למעשה עת משתנה מטיפוס מצביע מורה על‬
‫משתנה כלשהו הוא )המצביע( מכיל את כתובתו של המשתנה עליו המצביע מורה;‬
‫במילים אחרות‪ ,‬עת אנו מציירים חץ הנשלח מהמצביע לאובייקט עליו המצביע‬
‫מורה‪ ,‬אנו מדגימים בכך בצורה מוחשית‪ ,‬וקלה יותר להבנה‪ ,‬כי המצביע מכיל את‬
‫הכתובת המתאימה‪ .‬בפרק הראשון אמרנו כי אחד ממאפייני מחשבי פון‪-‬נויימן‪,‬‬
‫בהם אנו עושים שימוש‪ ,‬הוא שזיכרון יחיד מכיל הן תכניות‪ ,‬והן נתונים עליהם‬
‫פועלות התכניות‪ .‬הוספנו כי לכל תא בזיכרון יש כתובת; משמע הן לתא המכיל‬
‫נתונים יש כתובת‪ ,‬והן לתא המכיל קטע תכנית‪ .‬אם זה המצב‪ ,‬אזי כפי שמצביע‬
‫עשוי להצביע על נתון‪ ,‬הוא יכול להצביע על קטע תכנית‪ .‬בפרק זה נכיר את‬
‫ההשתמעויות של אפשרות זאת‪.‬‬
‫‪ 15.1‬דוגמה פשוטה ראשונה למצביע לפונקציה‬
‫נניח שברצוננו לכתוב תכנית אשר‪) :‬א( קוראת מהמשתמש האם ברצונו לקבל את‬
‫המינימום או המקסימום בין זוגות המספרים שהוא יזין בהמשך‪) ,‬ב( קוראת‬
‫מהמשתמש סדרת מספרים‪ ,‬ועבור כל זוג מציגה את המינימום או המקסימום בין‬
‫שני המספרים שהוזנו‪ ,‬על‪-‬פי הבקשה שהוזנה קודם לכן‪ .‬אנו יכולים לכתוב את קטע‬
‫התכנית באופן הבא‪:‬‬
‫‪int min_max,‬‬
‫‪// 0 => min, 1 => max‬‬
‫; ‪num1, num2‬‬
‫; ‪cin >> min_max‬‬
‫; ‪cin >> num1 >> num2‬‬
‫{ )‪while (num1 != 0 || num2 != 0‬‬
‫)‪if (min_max == 0‬‬
‫; )‪cout << min(num1, num2‬‬
‫; )‪else cout << max(num1, num2‬‬
‫; ‪cin >> num1 >> num2‬‬
‫}‬
‫הגדרת הפונקציות ‪ min, max‬פשוטה ביותר‪:‬‬
‫{ )‪int min(int num1, int num2‬‬
‫; ) ‪return( num1 < num2 ? num1 : num2‬‬
‫}‬
‫{ )‪int max(int num1, int num2‬‬
‫; ) ‪return( num1 > num2 ? num1 : num2‬‬
‫}‬
‫בפרק זה נרצה להציג דרך חלופית‪ ,‬אלגנטית יותר‪ ,‬לכתיבת קטע הקוד‪ .‬הדרך‬
‫החלופית תסתמך על שימוש במצביע לפונקציה‪.‬‬
‫בשפת ‪ C‬אנו רשאים להגדיר משתנה‪:‬‬
‫; )‪int (*func_ptr)(int, int‬‬
‫‪170‬‬
‫נסביר‪ :‬הגדרנו משתנה בשם ‪) func_ptr‬וזהו כמובן שם שאנו בחרנו(‪ .‬המשתנה‬
‫הינו מצביע )ולכן מופיעה כוכבית לפני שמו( אשר מסוגל להורות על פונקציה‬
‫המקבלת שני ערכים שלמים )ולכן בסוגריים המופיעים אחרי שם המצביע כתבנו‬
‫‪ ,(int, int‬ומחזירה ערך שלם )ולכן לפני שם המשתנה כתבנו ‪ .(int‬כלומר כפי‬
‫שמצביע 'רגיל' לא מצביע על כל טיפוס נתונים שהוא‪ ,‬אלא עלינו להגדירו כמצביע ל‪-‬‬
‫‪ int‬או מצביע ל‪ ,struct Node -‬ולא ניתן לערבב בין סוגי מצביעים שונים‬
‫)מצביע ל‪ int -‬לא יוכל להצביע על ‪ ,(struct Node‬כך גם מצביע לפונ' לא יוכל‬
‫להצביע על כל פונ' שהיא‪ ,‬אלא בהגדרתו עלינו לקבוע על אילו סוגים של פונ'‬
‫המצביע ידע להצביע‪ :‬על פונ' שבהכרח מקבלות א'‪ ,‬ב'‪ ,‬ג'‪ ,‬ומחזירות ד'‪.‬‬
‫באופן דומה כדי להגדיר מצביע בשם ‪ fp‬שידע להורות על פונקציות אשר מקבלות‬
‫מצביע ל‪ ,float -‬ופרמטר הפניה מטיפוס תו‪ ,‬ומחזירות ערך בולאני נכתוב‪:‬‬
‫; )& ‪bool (*fp)(float *, char‬‬
‫נניח‪ ,‬אם כן‪ ,‬כי הגדרנו משתנה ‪ func_ptr‬כנ"ל‪ ,‬וכן משתנים שלמים‬
‫‪ . num2, min_max‬קטע הקוד המשופר שנציג הינו‪:‬‬
‫‪num1,‬‬
‫; ‪cin >> min >> max‬‬
‫)‪if (min_max == 0‬‬
‫; ‪func_ptr = min‬‬
‫‪else‬‬
‫; ‪func_ptr = max‬‬
‫; ‪cin >> num1 >> num2‬‬
‫{ )‪while (num1 != 0 || num2 != 0‬‬
‫; )‪cout << func_ptr(num1, num2‬‬
‫; ‪cin >> num1 >> num2‬‬
‫}‬
‫נסביר‪ :‬ההשמה ;‪ func_ptr = min‬מכניסה ַלמצביע ‪ func_ptr‬את כתובתה‬
‫של הפונקציה ‪) min‬כפי שהופיעה קודם(‪ .‬במילים אחרות‪ ,‬ההשמה הנ"ל גורמת‬
‫למצביע להצביע על הפונקציה ‪ .min‬אחרי שהפננו את המצביע להורות על הפונקציה‬
‫הפקודה‪:‬‬
‫את‬
‫לכתוב‬
‫הלולאה‪,‬‬
‫בגוף‬
‫יכולים‪,‬‬
‫אנו‬
‫;)‪ cout << func_ptr(num1, num2‬ופקודה זאת מציגה את הערך שמחזירה‬
‫הפונקציה ‪ min‬עת היא מזומנת עם ‪ . num1, num2‬כלומר ‪ func_ptr‬הפך‪,‬‬
‫בעקבות ביצוע ההשמה‪ ,‬לשם נרדף לפונקציה ‪ ,min‬ובכל מקום בו מופיע שם‬
‫המשתנה ‪ func_prt‬מבין המחשב כי אנו מזמנים את הפונקציה ‪.min‬‬
‫אעיר כי במקום לכתוב‪ func_ptr = min ; :‬ניתן‪ ,‬ויש האומרים שגם עדיף‪,‬‬
‫לכתוב‪ func_ptr = &min ; :‬כדי להדגיש את השימוש במצביעים‪ .‬הן אם‬
‫ביצענו את ההשמה באופן הראשון‪ ,‬ובין אם ביצענו אותה באופן השני‪ ,‬את זימון‬
‫הפונ' עליה מצביע ‪ func_ptr‬נוכל עשות באופן‪func_ptr(num1, num2) ; :‬‬
‫)כפי שהופיע בדוגמה מעל( או באופן‪ . (*func_ptr)(num1, num2) ; :‬כמובן‬
‫שכדאי להיות עקביים ואם השתמשנו ב‪ & -‬להשתמש גם ב‪ ,* -‬ולהיפך‪ ,‬אך מבחינת‬
‫הקומפיילר זה לא הכרחי‪.‬‬
‫‪171‬‬
‫‪ 15.2‬דוגמה שניה למצביע לפונקציה‬
‫הדוגמה הקודמת שראינו נועדה לשם הכרות ראשונית עם המצביעים לפונקציות‪.‬‬
‫ככזאת‪ ,‬הייתה זאת דוגמה פשוטה ומעט מאולצת של שימוש במצביעים‬
‫לפונקציות‪ .‬עתה נרצה להציג דוגמה טובה יותר לחשיבותו של הכלי‪.‬‬
‫נניח כי בתכנית כלשהי אנו מטפלים בנתוני תלמידים‪ .‬לשם כך אנו מגדירים את‬
‫המבנה‪:‬‬
‫{ ‪struct Stud‬‬
‫;]‪char _last[NAME_LEN], _first[NAME_LEN‬‬
‫; ‪bool _gender‬‬
‫; ‪unsigned int _shoe_num‬‬
‫; ]‪unsigned int _grades[MAX_GRADE‬‬
‫; }‬
‫כפי שניתן לראות מהגדרת ה‪ ,struct -‬עבור כל תלמיד ברצוננו לשמור את שמו‬
‫הפרטי )‪ ,(_first‬שם משפחתו )‪ ,(_last‬מינו )‪ ,(_gender‬מספר נעליו‬
‫)‪ ,(_shoe_num‬ומערך של ציונים )‪.(_grades‬‬
‫אחר‪ ,‬בתכנית הראשית‪ ,‬נגדיר מערך של נתוני תלמידים‪:‬‬
‫ַ‬
‫; ]‪stud my_class[CLASS_SIZE‬‬
‫נניח כי התכנית קראה נתונים לתוך המערך‪ ,‬ועתה המשתמש יכול לבחור האם‬
‫ברצונו למיין את המערך‪:‬‬
‫א‪ .‬על‪-‬פי שם המשפחה והשם הפרטי‪.‬‬
‫ב‪ .‬על‪-‬פי מספר הנעליים‪.‬‬
‫ג‪ .‬על‪-‬פי ממוצע הציונים‪.‬‬
‫לשדות על‪-‬פיהם אנו ממיינים את הרשומות נקרא מפתח המיון‪.‬‬
‫כדי לציין מהו מפתח המיון הרצוי נגדיר טיפוס‪:‬‬
‫‪enum sort_by_t {LAST_FIRST_SRT,‬‬
‫‪SHOE_NUM_SRT,‬‬
‫; }‪AVG_GRADE_SRT‬‬
‫נניח כי תכניתנו כוללת פונקציה ‪ read_sort_key‬אשר מחזירה אחד משלושת‬
‫הערכים הנ"ל‪ ,‬על‪-‬פי בקשתו של המשתמש‪.‬‬
‫עתה‪ ,‬כדי למיין את המערך )מקטן לגדול( עלינו להשוות בין הרשומות בו על‪-‬פי‬
‫השדות הרצויים )כלומר על‪-‬פי מפתח המיון(‪ .‬לדוגמה‪ ,‬אם יש למיין על‪-‬פי שם‬
‫משפחה ושם פרטי אזי המבנה של ‪ cohen yosi‬צריך להופיע במערך לפני המבנה‬
‫של ‪ ;levi dana‬אולם אם יש למיין את התלמידים על‪-‬פי מספר הנעליים אזי יוסי‬
‫כֹהן שמספר נעליו ‪ 49‬צריך להופיע אחרי דנה לוי שמספר נעליה הוא ‪ ;19‬ואם יש‬
‫למיין את הנתונים על‪-‬פי ממוצע הציונים אזי מי משניהם שממוצע ציוניו גבוה יותר‬
‫צריך להופיע אחרי מי שממוצע ציוניו נמוך יותר‪.‬‬
‫‪15.2.1‬אפשרות א' לכתיבת פונקצית המיון‪ :‬שימוש בדגל בלבד‬
‫לשם הפשטות נניח כי ברצוננו למיין את המערך תוך שימוש במיון בועות )אם כי‬
‫העקרונות שנציג ישימים גם ַלמיונים היעילים יותר(‪ .‬האפשרות הראשונה‪ ,‬והפחות‬
‫מוצלחת‪ ,‬לכתיבת פונקצית המיון תסתמך על הרעיון הבא‪ :‬פונקצית המיון תקבל‬
‫ְפרט למערך התלמידים פרמטר נוסף‪ ,‬באמצעותו נאותת לפונקציה על‪-‬פי אילו שדות‬
‫‪172‬‬
‫פי מפתח‬-‫ הפונקציה תשווה בין כל זוג רשומות במערך על‬.‫עליה למיין את המערך‬
.‫ ותחליף ביניהן אם הן מפרות את סדר המיון‬,‫המיון‬
:‫הקריאה לפונקצית המיון מהתכנית הראשית תעשה בהקשר כדוגמת הבא‬
sort_by_t key = read_sort_key() ;
bubble_sort(my_class, key) ;
‫מזמנים את הפונקציה אשר קוראת‬
ְ
‫ַבפקודה הראשונה מבין השתיים אנו‬
,‫פי אילו שדות ברצונו למיין את רשומות המערך; בפקודה השניה‬-‫מהמשתמש על‬
,‫ אנו מעבירים ַלפונקציה את המערך שעליה למיין‬.‫אנו מזמנים את פונקצית המיון‬
:‫ פונקצית המיון תראה‬.‫ואת מציין מפתח המיון הרצוי‬
void bubble_sort( stud my_class[], sort_by_t key) {
for (int round = 0; round < CLASS_SIZE -1; round++)
for (int i =0; i < CLASS_SIZE – round –1; i++)
{
bool need_to_swap = false ; // should class[i], class[i+1]
// be swapped
switch (key) { // check if class[i], class[i+1]
// violate the correct order
// according to the sorting key
case LAST_FIST:
if ( ( strcmp(my_class[i]._last,
my_class[i+1]._last)> 0) ||
( strcmp(my_class[i]._last,
my_class[i+1]._last)==0 &&
strcmp(my_class[i]._first,
my_class[i+1]._first)> 0))
need_to_swap = true ;
break ;
case SHOE_NUM:
need_to_swap =
my_class[i]._shoe_num >
my_class[i+1]._shoe_num ;
break ;
case AVG_GRADE:
need_to_swap =
avg(my_class[i]._grades) >
avg(my_class[i+1].grades) ;
break ;
}
if (need_to_swap)
swap(my_class[i], my_class[i+1]) ;
}
}
unsigned int avg(const unsigned int grades[MAX_GRADES]) {
int sum = 0 ;
for (int i=0; i < MAX_GRADES; i++)
sum += grades[i] ;
return( sum / MAX_GRADES ) ;
}
void swap( stud &s1, stud &s2) {
stud temp = s1 ;
s1 = s2 ;
s2 = temp ;
}
173
‫הסבר הפונקציה ‪ :bubbble_sort‬כדרכו של מיון בועות‪ ,‬הפונקציה כוללת לולאה‬
‫כפולה‪ .‬בכל סיבוב בלולאה הפנימית מחליפים או לא מחליפים בין שתי רשומות‬
‫סמוכות‪ .‬קטע הקוד הכולל את פקודת ה‪ ,switch -‬והמהווה את עיקר הפונקציה‪,‬‬
‫בודק האם על‪-‬פי מפתח המיון יש צורך להחליף בין שתי הרשומות הסמוכות‪,‬‬
‫]‪ my_class[i‬ו‪ .my_class[i+1] -‬ההחלטה האם יש להחליף בין שתי‬
‫הרשומות או לא נשמרת ַבמשתנה הבולאני ‪ .need_to_swap‬במידה וערכו של‬
‫המשתנה הוא ‪ true‬אזי אנו מחליפים בין שתי הרשומות‪.‬‬
‫הסתייגותנו מקוד זה היא שאם בעתיד נרצה למיין את הרשומות על‪-‬פי מפתח מיון‬
‫אחר )למשל נרצה למיין את הרשומות בסדר יורד של השמות ]מגדול לקטן[‪ ,‬או‬
‫נרצה למיינן לפי שדה המין‪ ,‬דווקא( אזי יהיה עלינו לשוב לפונ' המיון ולעדכנה‪ .‬מצב‬
‫זה אינו רצוי‪ :‬היינו מעדיפים לכתוב את פונ' המיון פעם אחת ולתמיד‪ ,‬ולא להצטרך‬
‫לשנותה גם עת נוצר הצורך למיין את המערך בסידור שונה כלשהו‪.‬‬
‫‪15.2.2‬אפשרות ב' לכתיבת פונקצית המיון‪ :‬שימוש בפונקציות‬
‫השוואה ובדגל‬
‫עתה נציג אפשרות אלגנטית יותר לכתיבת פונקצית המיון‪ .‬עבור אפשרות זאת נגדיר‬
‫פונקציַת השוואה תשווה בין זוג רשומות‬
‫ְ‬
‫שלוש פונקציות השוואה בין רשומות‪ .‬כל‬
‫על‪-‬פי מפתח מיון מסוים‪ .‬פונקציות ההשוואה תכתבנה על‪-‬פי אותו עיקרון כמו‬
‫‪ ,strlen‬כלומר אם ערכו של הפרמטר הראשון קטן )על‪-‬פי מפתח המיון( מערכו של‬
‫הפרמטר השני אזי יוחזר ערך שלילי; אם‪ ,‬לעומת זאת‪ ,‬שתי הרשומות שוות‬
‫)מבחינת ערכו של מפתח המיון( אזי יוחזר ערך אפס; ולבסוף‪ ,‬אם הרשומה השניה‬
‫גדולה מהראשונה אזי יוחזר ערך חיובי‪ .‬נציג‪ ,‬ראשית‪ ,‬את פונקציות ההשוואה‪:‬‬
‫‪int comp_by_last_first(const struct Stud &s1,‬‬
‫{ )‪const sturct Stud &s2‬‬
‫; )‪int temp = strcmp(s1._last, s2._last‬‬
‫; ‪if (temp != 0) return temp‬‬
‫; )‪return strcmp(s1._first, s2._first‬‬
‫}‬
‫‪//-----------------------------------------------‬‬‫‪int comp_by_shoe_num(const struct Stud &s1,‬‬
‫{ )‪const sturct Stud &s2‬‬
‫)‪if (s1._shoe_num < s2._shoe_num‬‬
‫; ‪return –1‬‬
‫)‪if (s1._shoe_num == s2._shoe_num‬‬
‫; ‪return 0‬‬
‫)‪if (s1._shoe_num > s2._shoe_num‬‬
‫; ‪return 1‬‬
‫}‬
‫‪//-----------------------------------------------‬‬‫‪int comp_by_avg_grade(const struct Stud &s1,‬‬
‫{ )‪const sturct Stud &s2‬‬
‫‪unsigned int avg1 = avg(s1._grades),‬‬
‫; )‪avg2 = avg(s2._grades‬‬
‫; ‪is (avg1 > avg2) return –1‬‬
‫‪174‬‬
‫; ‪if (avg1 == avg2) return 0‬‬
‫; ‪if (avg1 < avg2) return 1‬‬
‫}‬
‫הסבר‪ :‬נסביר לדוגמה את ‪ ,comp_by_last_first‬שתי הפונקציות האחרות‬
‫דומות לה למדי‪ .‬ראשית‪ ,‬אנו משווים בין שני שמות המשפחה‪ ,‬כפי שהם שמורים ב‪-‬‬
‫‪ . s1._last, s2._last‬את תוצאת ההשוואה אנו שומרים במשתנה העזר‬
‫‪ .temp‬במידה והשם הראשון קטן מהשני )כלומר ‪ strcmp‬מחזירה ערך שלילי(‪ ,‬או‬
‫שהשם השני גדול מהראשון )ואז ערכו של ‪ temp‬חיובי( אזי כבר שם המשפחה קובע‬
‫את יחס הסדר בין שתי הרשומות‪ ,‬וניתן להחזיר את ערכו של ‪ .temp‬אם‪ ,‬לעומת‬
‫זאת‪ ,‬ערכו של ‪ temp‬הוא אפס‪ ,‬סימן ששני שמות המשפחה זהים‪ ,‬ועל כן יש להחזיר‬
‫את תוצאת ההשוואה בין שני השמות הפרטיים של זוג התלמידים; וזאת אנו‬
‫עושים‪.‬‬
‫הערה נוספת‪ :‬מדוע הגדרנו את הפרמטרים לפונקציות ההשוואה באופן‪const :‬‬
‫‪ . struct Stud &s1‬מדוע לא העברנו את המבנים כפרמטרי ערך? מדוע נזקקנו‬
‫לשימוש במילת המפתח ‪ ?const‬הסיבה לכך היא שמבנה עשוי להיות גדול )למשל‬
‫אם מערך הציונים בו גדול(‪ ,‬במקרה כזה העברתו כפרמטר ערך תחייב השקעה של‬
‫זמן‪ ,‬והקצאה של זיכרון‪ ,‬שכן על המחשב יהיה להעתיק את ערכו של הארגומנט על‬
‫שטח הזיכרון המוקצה לפרמטר‪ .‬כדי לחסוך בזמן ובזיכרון נוהגים להעביר מבנים‬
‫כפרמטרים משתנים‪ ,‬ואז נשלח רק 'חץ' מהפרמטר ַלארגומנט המתאים‪ ,‬ואין צורך‬
‫בהשקעת זמן וזיכרון מרובים‪ .‬עתה משהפכנו את הפרמטר לפרמטר משתנה יש‬
‫תפגע בנתונים המועברים‬
‫חשש שהפונקציה‪ ,‬אם תיכתב על‪-‬ידי מתכנת לא אחראי‪ְ ,‬‬
‫לה; כדי למנוע זאת הוספנו לפני טיפוס הפרמטר את הקביעה שהפרמטר הינו קבוע‪,‬‬
‫ולא ניתן לשנותו‪.‬‬
‫את פונקצית המיון נוכל לכתוב תוך שימוש בפונקציות ההשוואה באופן יותר נקי‪:‬‬
‫{ )‪void bubble_sort(struct Stud my_class[], sort_by_t key‬‬
‫)‪for (int round = 0; round < CLASS_SIZE -1; round++‬‬
‫)‪for (int i =0; i < CLASS_SIZE – round –1; i++‬‬
‫{‬
‫]‪bool need_to_swap = false ; // should class[i], class[i+1‬‬
‫‪// be swapped‬‬
‫]‪switch (key) { // check if class[i], class[i+1‬‬
‫‪// violate the correct order‬‬
‫‪// according to the sorting key‬‬
‫‪case LAST_FIST_SRT:‬‬
‫‪if ( comp_by_last_first(my_class[i],‬‬
‫)‪my_class[i+1]) > 0‬‬
‫; ‪need_to_swap = true‬‬
‫; ‪break‬‬
‫‪case SHOE_NUM_SRT:‬‬
‫)‪if ( comp_by_shoe_num(my_class[i], my_class[i+1]) > 0‬‬
‫; ‪need_to_swap = true‬‬
‫; ‪break‬‬
‫‪case AVG_GRADE_SRT:‬‬
‫)‪if ( comp_by_avg_grade(my_class[i], my_class[i+1])> 0‬‬
‫; ‪need_to_swap = true‬‬
‫; ‪break‬‬
‫}‬
‫)‪if (need_to_swap‬‬
‫; )]‪swap(my_class[i], my_class[i+1‬‬
‫}‬
‫‪175‬‬
‫}‬
‫פונקציַת המיון הנוכחית שונה מקודמתה בפקודת ה‪ַ .switch -‬בפונקציה‬
‫ְ‬
‫הסבר‪:‬‬
‫הנוכחית‪ ,‬על‪-‬פי ערכו של הדגל ‪ ,key‬אנו מזמנים את פונקצית ההשוואה הדרושה‪,‬‬
‫ומעדכנים את ערכו של המשתנה ‪ need_to_swap‬על‪-‬פי הערך שמחזירה פונקצית‬
‫ההשוואה‪ .‬באופן זה פונקצית המיון הפכה מסודרת יותר‪ ,‬במילים אחרות רגולרית‬
‫יותר‪ .‬התוצאה היא שבמידה ובעתיד נרצה למיין את התלמידים על‪-‬פי מפתח מיון‬
‫שונה קל יהיה לנו יותר לעדכן את פונקצית המיון‪.‬‬
‫‪15.2.3‬אפשרות ג' לכתיבת פונקצית המיון‪ :‬שימוש במצביע לפונקציה‬
‫עתה נציג את האפשרות האלגנטית ביותר להשלמת המשימה‪ .‬אפשרות זאת‬
‫תשתמש במצביע לפונקציה‪.‬‬
‫בתכנית הראשית נגדיר את המשתנה‪:‬‬
‫‪int (*comp_func_ptr)(const struct Stud &,‬‬
‫; )& ‪const struct Stud‬‬
‫כלומר הגדרנו מצביע לפונקציה‪ .‬המצביע ידע להצביע על פונקציות שמקבלות שני‬
‫מבנים מטיפוס ‪ Stud‬כפרמטרי הפניה קבועים‪ ,‬ומחזירות ערך שלם‪ .‬במילים‬
‫פשוטות‪ ,‬המצביע ידע להצביע על כל אחת משלוש פונקציות ההשוואה שכתבנו‪.‬‬
‫פרט לכך תכיל התכנית אותן הגדרות כפי שהצגנו קודם לכן‪.‬‬
‫התכנית הראשית תראה עתה לערך כך‪:‬‬
‫; )‪read_data(my_class‬‬
‫; )(‪sort_by key = read_sort_order‬‬
‫{ )‪switch (key‬‬
‫‪case LAST_FIRST_SRT :‬‬
‫;‪comp_func_ptr = comp_by_last_first‬‬
‫; ‪break‬‬
‫‪case SHOE_NUM_SRT :‬‬
‫; ‪comp_func_ptr = comp_by_shoe_num‬‬
‫; ‪break‬‬
‫‪case GRADES_SRT :‬‬
‫; ‪comp_func_ptr = comp_by_avg_grade‬‬
‫; ‪break‬‬
‫}‬
‫; )‪bubble_sort(my_class, comp_func_ptr‬‬
‫מה עשינו עד כה )בתכנית הראשית(? את בחירת פונקצית ההשוואה העברנו לחלוטין‬
‫לתכנית הראשית‪ .‬כפי שתיראו בהמשך‪ ,‬פונקצית המיון פשוט תפעיל את פונקצית‬
‫ההשוואה שמועברת לה ) ַבפרמטר השני(‪ ,‬היא אינה מתעניינת כלל בפונקצית‬
‫ההשוואה‪ .‬מבחינת פונקצית המיון‪ ,‬פונקצית ההשוואה הפכה להיות קופסה שחורה‬
‫שרק מחזירה ערך רצוי‪ ,‬ועל‪-‬פי הערך המוחזר פונקצית המיון מחליפה‪ ,‬או לא‬
‫מחליפה‪ ,‬ביו רשומות‪ .‬אם בעתיד נרצה למיין את הרשומות על‪-‬פי מפתח מיון שונה‪,‬‬
‫לא נאלץ לשנות כלל את פונקצית המיון‪ ,‬וזו תכונה רצויה ביותר של פונקצית מיון‬
‫אשר אמורה להיות כללית ככל שניתן‪ ,‬ולמיין כל מה שמעבירים לה‪.‬‬
‫‪176‬‬
‫נציג את פונקצית המיון‪:‬‬
‫‪void bubble_sort( struct Stud my_class[],‬‬
‫‪int (*comp_func_ptr)(const struct Stud &,‬‬
‫{ ))& ‪const struct Stud‬‬
‫)‪for (int round = 0; round < CLASS_SIZE -1; round++‬‬
‫)‪for (int i =0; i < CLASS_SIZE – round –1; i++‬‬
‫{‬
‫)‪if (comp_func_ptr(my_class[i], my_class[i+1]) > 0‬‬
‫; )]‪swap(my_class[i], my_class[i+1‬‬
‫}‬
‫}‬
‫הסבר‪ :‬פונקצית המיון מקבלת שני פרמטרים‪ :‬מערך של מבנים אותו עליה למיין‪,‬‬
‫ומצביע לפונקציה )אשר מקבלת שני מבנים מסוג ‪ stud‬כפרמטרים משתנים‬
‫קבועים‪ ,‬ומחזירה ערך שלם(‪ .‬פונקצית המיון מתנהלת כמו כל מיון בועות‪ :‬היא‬
‫כוללת לולאה כפולה‪ .‬בכל סיבוב בלולאה אנו משווים בין שני תאים סמוכים ַבמערך‬
‫ובמידת הצורך מחליפים ביניהם‪ .‬כיצד אנו יודעים האם יש להחליף בין שני התאים‬
‫הסמוכים או לא? על‪-‬פי מה שמחזירה לנו פונקצית ההשוואה המועברת בפרמטר‬
‫השני‪ :‬אם היא מחזירה ערך חיובי‪ ,‬סימן שעל פי מפתח המיון הרשומה‬
‫]‪ my_class[i‬גדולה מהרשומה ]‪ ,my_class[i+1‬ועל‪-‬כן יש להחליף בין‬
‫הרשומות‪ .‬באחריות מי שקורא לפונקצית המיון להעביר לה פונקצית השוואה‬
‫מתאימה‪ .‬פשוט‪ ,‬נקי ואלגנטי‪ ,‬האין זאת?‬
‫בדוגמה האחרונה ראינו כי את המשתנה ‪ comp_func_ptr‬אנו מפנים להצביע על‬
‫כגון‪:‬‬
‫השמה‬
‫בעזרת‬
‫הרצויה‬
‫הפונ'‬
‫;‪ . comp_func_ptr = comp_by_last_first‬אעיר כי‪ ,‬למרות שהדבר עשוי‬
‫באופן‪:‬‬
‫גם‬
‫ההשמה‬
‫את‬
‫לכתוב‬
‫ניתן‬
‫מפתיע‪,‬‬
‫להיראות‬
‫;‪ comp_func_ptr = &comp_by_last_first‬כלומר הוספנו & לפני שם‬
‫הפונ'‪ .‬יש המעדיפים צורת כתיבה זאת המדגישה שאנו עוסקים כאן במצביעים‪.‬‬
‫הפרוטוטיפ של )(‪ bubble_sort‬נותר זהה בשני המקרים‪ .‬בלי תלות באופן בו‬
‫ביצענו את ההשמה הקודמת‪ ,‬בגוף הפונ' )(‪ bubble_sort‬עת אנו מזמנים את פונ'‬
‫ההשוואה הדרושה‪ ,‬אנו יכולים לעשות זאת באחת משתי צורות הכתיבה‪:‬‬
‫)]‪ comp_func_ptr(my_class[i], my_class[i+1‬כפי שמופיע בדוגמה‬
‫מעל‪ ,‬או‪ (*comp_func_ptr)(my_class[i], my_class[i+1]) :‬כדי‬
‫להדגיש שמדובר במצביע )לפונ'(‪.‬‬
‫כאשר פונ' ‪ f‬מקבלת פרמטר ‪ pf‬שהינו מצביע לפונ' כלשהי‪ ,‬ניתן בעת הזימון של ‪f‬‬
‫להעביר לה בארגומנט המתאים ערך ‪ ,NULL‬כלומר לא להעביר שום פונ' שהיא‪.‬‬
‫כמובן שאז אל ל‪ f -‬לזמן את הפונ' ‪ .pf‬ייתכן כי במצב בו מועבר ל‪ f -‬הערך ‪NULL‬‬
‫היא תזמן פונ' קבועה כלשהי )לא כזו שמועברת לה כפרמטר(‪.‬‬
‫המונח ‪ callback function‬מתאר פונ' המזומנת באמצעות מצביע אליה שמועבר לפונ'‬
‫שניה; לפיכך הדוגמה האחרונה שכתבנו הינה ‪.callback function‬‬
‫‪ 15.3‬הדפסת מערך תוך שימוש במצביע לפונ'‬
‫אציג עתה דוגמה נוספת לשימוש במצביע לפונ'‪ .‬הדוגמה‪ ,‬כמו גם זו שמופיעה בסעיף‬
‫הבא‪ ,‬אינה מחדשת דבר מעבר למה שכבר ראינו‪ .‬הדוגמות רק מציגות שוב את אותו‬
‫נושא‪.‬‬
‫נניח שבכמה מקומות בתכנית עלינו להדפיס מערך של מספרים שלמים‪ .‬אולם‬
‫במקומות השונים יש להדפיס את נתוני המערך באופן מעט שונה‪:‬‬
‫א‪ .‬במקום אחד יש להפרידם באמצעות פסיקים‪.‬‬
‫‪177‬‬
‫ב‪ .‬במקום אחר יש להדפיסם כך שלפני כל נתון יופיע התו < אם הוא גדול מקודמו‪,‬‬
‫יופיע התו = אם הוא שווה לקודמו‪ ,‬ויופיע התו > אם הוא קטן מקודמו‪.‬‬
‫ג‪ .‬במקום שלישי בתכנית יש להדפיס לצד כל נתון גם את מספר התא במערך בו‬
‫הוא מצוי‪.‬‬
‫על כן נרצה פונ' הדפסה אחת‪ ,‬אשר מפעילה בכל שלושת המקרים אותו מנגנון‬
‫בקרה‪ :‬ריצה על‪-‬פני המערך‪ ,‬אך מזמנת בכל אחד משלושת המקרים פונ' אחרת‬
‫להדפסת הערך המצוי בתא שיש להדפיס בכל סיבוב בלולאה‪.‬‬
‫נכתוב את הפונ' באופן דומה לגרסה המתקדמת ביותר של פונ' המיון כפי שראינו‬
‫בסעיף הקודם‪ ,‬כלומר פונ' הדפסת המערך תקבל מצביע לפונ' אשר יודעת להדפיס‬
‫תא בודד במערך‪.‬‬
‫נכין לנו‪ ,‬על כן‪ ,‬שלוש פונ' בעלות אותו פרוטוטיפ‪ ,‬שכל אחת מהן מדפיסה תא‬
‫במערך על פי הדרישות שהצבנו קודם לכן‪:‬‬
‫{ )‪void print_cell_comma(const int arr[], int index‬‬
‫; " ‪cout << arr[index] << ",‬‬
‫}‬
‫‪//---------------------------------------------------‬‬‫{ )‪void print_cell_sign(const int arr[], int index‬‬
‫; ' ' = ‪char sign‬‬
‫)‪if (index > 0‬‬
‫)]‪if (arr[index] > arr[index -1‬‬
‫; '>' = ‪char‬‬
‫)]‪else if (arr[index] == arr[index -1‬‬
‫; '=' = ‪sign‬‬
‫‪else‬‬
‫'<' = ‪sign‬‬
‫; " " << ]‪cout << sign << arr[index‬‬
‫}‬
‫‪//---------------------------------------------------‬‬‫{ )‪void print_cell_index(const int arr[], int index‬‬
‫; " ‪cout << index << "=" << arr[index] << ",‬‬
‫}‬
‫שלוש הפונ' מקבלות את המערך‪ ,‬ואת מספר התא הרצוי‪ .‬כל אחת מהן מדפיסה את‬
‫ערכו של התא המבוקש בפורמט שהגדרנו‪.‬‬
‫עתה נכתוב את פונ' הדפסת המערך‪ ,‬אשר עושה שימוש בפונ' הדפסת התא שהועברה‬
‫לה‪:‬‬
‫‪void print_arr(const int arr[],‬‬
‫))‪void (*print_cell_func)(const int*, int‬‬
‫{‬
‫)‪for (int i=0; i < N ; i++‬‬
‫; )‪print_cell_func(arr, i‬‬
‫}‬
‫‪178‬‬
‫נסביר‪ :‬הפונ' ‪ print_arr‬מקבלת את המערך שעליה להדפיס )באמצעות הפרמטר‪:‬‬
‫][‪ ,(const int arr‬ומצביע לפונ' הדפסת תא בודד במערך‪ .‬הפרמטר השני של‬
‫הפונ' ‪ print_arr‬הוא מצביע לפונ'‪ .‬שמו של הפרמטר השני הוא‬
‫‪ .print_cell_func‬הפרמטר מצביע על פונקציות שמקבלות מצביע קבוע למערך‬
‫הביטוי‪:‬‬
‫לנו‬
‫מורה‬
‫)וזאת‬
‫שלם‬
‫וערך‬
‫) )‪ ((const int*, int‬ומחזירות כלום )וזאת מורה לנו המילה ‪ void‬שלפני שם‬
‫הפרמטר‪ ,‬והכוכבית שלצדו (‪.‬‬
‫הפונ' רצה סידרתית על תאי המערך‪ ,‬ועבור כל תא מזמנת את הפונ' שהועברה לה‬
‫כפרמטר )ושמורה בפרמטר ‪ (print_cell_func‬ומעבירה לאותה פונ' את המערך‬
‫השלם‪ ,‬ואת מספר התא הרצוי‪.‬‬
‫בהנחה שבתכנית הוגדר מערך‪:‬‬
‫; ]‪int int_arr[N‬‬
‫והוזנו לתוכו נתונים‪ ,‬נוכל לזמן את פונ' ההדפסה בכל אחד משלושת האופנים‬
‫הבאים‪:‬‬
‫; )‪print_arr(int_arr, print_cell_comma‬‬
‫; )‪print_arr(int_arr, print_cell_sign‬‬
‫; )‪print_arr(int_arr, print_cell_index‬‬
‫בכל אחד משלושת הזימונים אנו מעבירים לפונ' את המערך‪ ,‬ומצביע לפונ' שונה‬
‫להדפסת תא במערך‪.‬‬
‫‪ 15.4‬מציאת הערך המזערי של פונ' בעזרת מצביע לפונ'‬
‫נניח כי בתכנית הוגדרו מספר פונ' ממשיות של משתנה אחד )במובן המתמטי של‬
‫המילה(‪ ,‬כלומר פונ' המקבלות ערך ממשי ‪ x‬ומחזירות את ערך ה‪ y -‬הממשי‬
‫המתאים לו‪ .‬לדוגמה‪:‬‬
‫{ )‪double f1(int x‬‬
‫; ‪return 3*x + 2‬‬
‫}‬
‫‪//--------------------------------‬‬‫{ )‪double f2(int x‬‬
‫; ‪return 3*x*x -5*x + 4‬‬
‫}‬
‫‪//--------------------------------‬‬‫{ )‪double f3(int x‬‬
‫; )‪return 3*sin(x) – 2*cos(x‬‬
‫}‬
‫‪//---------------------------------‬‬
‫במקומות שונים בתכנית ברצונו למצוא את ערך המינימום של הפונקציות הנ"ל‬
‫בקטעים שונים של הישר‪ .‬לדוגמה‪ :‬ברצוננו למצוא את המינימום של ‪ f1‬בקטע‬
‫]‪ , [-3, 2‬או שברצוננו למצוא את המינימום של ‪ f3‬בקטע ]‪ ,[0, 5‬וכן הלאה‪.‬‬
‫כמו כן‪ ,‬נרצה לפתור את הבעיה לא בשיטות אנליטיות‪ ,‬ע"י גזירת כל אחת מהפונ'‬
‫ובדיקת הנגזרת‪ ,‬אלא בשיטות חישוביות‪ :‬בהינתן פונ' ‪ f‬וקטע שבין ‪ x0‬ל‪ ,x1 -‬נרצה‬
‫‪179‬‬
‫לחשב את ערכה של ‪ f‬ב‪ N -‬נקודות בקטע )עבור ‪ N‬שהינו קבוע של התכנית(‪:‬‬
‫הנקודות‪:‬‬
‫‪x0, x0 +(x1-x0)/N, x0 +(x1-x0)/N*2, ... , x1‬‬
‫וכך לאתר את המינימום של הפונ'‪.‬‬
‫נכתוב‪ ,‬על כן‪ ,‬פונ' )שתקרא ‪ (find_min‬המקבלת מצביע לפונ' כדוגמת שלוש הפונ'‬
‫שהגדרנו מעל )פרמטר זה יקרא ‪ ,(func_ptr‬ושני ערכים ממשיים המציינים קטע‬
‫של הישר )‪ .(x0, x1‬הפונ' ‪ find_min‬תזמן את ‪ N func_ptr‬פעמים‪ ,‬עם ‪N‬‬
‫נקודות בקטע )כפי שתואר מעל(‪ ,‬ותחזיר את הערך המזערי שהוחזר ע"י‬
‫‪func_ptr‬בכל ‪ N‬הקריאות‪ .‬הקוד‪:‬‬
‫‪double find_min( double (*func_ptr)(double),‬‬
‫)‪double x0, double x1‬‬
‫{‬
‫‪double min = func_ptr(x0),‬‬
‫; ‪delta = (x1 – x0)/N‬‬
‫)‪for (double x = x0 + delta; x <= x1; x+= delta‬‬
‫)‪if (func_ptr(x) < min‬‬
‫; )‪min = func_ptr(x‬‬
‫; ‪return min‬‬
‫}‬
‫עתה‪,‬‬
‫כדי‬
‫למצוא‬
‫את‬
‫המינימיום‬
‫של‬
‫‪f3‬‬
‫בקטע‬
‫]‪1‬‬
‫‪[0,‬‬
‫נזמן‪:‬‬
‫)‪.find_min(f3, 0, 1‬‬
‫למותר לציין שאם בעתיד נגדיר בתכנית פונ' ממשית נוספת במשתנה יחיד כלשהי‪,‬‬
‫ונרצה לאתר גם את המינימום שלה בקטע כלשהו‪ ,‬נוכל לעשות זאת בנקל בלי‬
‫שנצטרך לשנות את ‪ find_min‬שכתבנו עתה‪.‬‬
‫‪ 15.5‬תרגילים‬
‫‪ 15.3.1‬תרגיל מספר אחד‪ :‬האם פונקציה אחת גדולה יותר משניה בתחום‬
‫כלשהו‬
‫נניח שבתכנית מסוימת קיימות מספר פונקציות שכל אחת מהן מקבלת שני‬
‫מספרים שלמים )נסמנם ‪ x‬ו‪ (y-‬ומחזירה מספר שלם )נסמנו ‪ .(z‬עליכם לכתוב‬
‫פונקציה ‪ greater‬המקבלת שישה פרמטרים‪:‬‬
‫א‪ .‬זוג פונקציות כנ"ל )נסמנן ‪ f1‬ו‪.(f2-‬‬
‫ב‪ .‬זוג ערכי גבול ‪ x0‬ו‪ x1-‬עבור ‪.x‬‬
‫ג‪ .‬זוג ערכי גבול ‪ y0‬ו‪ y1-‬עבור ‪.y‬‬
‫על הפונקציה ‪ greater‬לקבוע האם עבור כל ערך ‪ x‬כך ש‪ ,x0 ≤ x ≤ x1-‬ועבור כל ערך‬
‫של ‪ y‬כך ש‪ f1 y0 ≤ y ≤ y1-‬הינה 'גדולה יותר' מ‪ f2-‬במובן ש‪ f1-‬מחזירה ערך גדול‬
‫יותר מהערך שמחזירה ‪ f2‬עבור זוג הערכים‪ .‬השגרה ‪ greater‬תבצע את משימתה‬
‫באופן הבא‪ N :‬פעמים תגריל ‪ greater‬זוג ערכים ‪ x‬ו‪ַ y-‬בתחום הרצוי‪ ,‬ותקרא ל‪ f1-‬ול‪-‬‬
‫‪ f2‬עבור זוג הערכים‪ .‬במידה ובכל ‪ N‬הפעמים החזירה ‪ f1‬ערך גדול יותר מכפי‬
‫שהחזירה ‪ ,f2‬תסיק ‪ greater‬ש‪ f1-‬אכן גדולה מ‪ f2-‬בתחום הנ"ל ותחזיר את הערך‬
‫‪ ,true‬אם לפחות באחד המקרים לא החזירה ‪ f1‬ערך גדול יותר מזה שהוחזר על‪-‬ידי‬
‫‪ f2‬תחזיר ‪ greater‬את הערך ‪.false‬‬
‫‪180‬‬
‫‪ 15.3.2‬תרגיל מספר שתיים‪ :‬מציאת שורש של פונקציה‬
‫נניח כי בתכנית כלשהי הוגדרו מספר פונקציות של משתנה ממשי יחיד )כלומר‬
‫כאלה המקבלות ארגומנט ממשי יחיד‪ ,‬ומחזירות ערך ממשי יחיד(‪ .‬כתבו פונקציה‬
‫המקבלת‪:‬‬
‫א‪ .‬מצביע ‪ f‬לפונקציה כנ"ל‪.‬‬
‫ב‪ .‬ערך ממשי ‪ x0‬המקיים כי ערכה של ‪ f‬שלילי בנקודה ‪.x0‬‬
‫ג‪ .‬ערך ממשי ‪ x1‬המקיים כי ערכה של ‪ f‬חיובי בנקודה ‪.x1‬‬
‫על הפונקציה שתכתבו לאתר שורש של ‪.f‬‬
‫הפונקציה תפעל באופן הבא‪:‬‬
‫א‪ .‬במידה ו‪ f(x0) -‬אינו שלילי יוחזר ערך קטן מ‪.x0 -‬‬
‫ב‪ .‬במידה ו‪ f(x1) -‬אינו חיובי יוחזר ערך גדול מ‪.x1 -‬‬
‫ג‪ .‬חזור על תהליך חיפוש בינארי‪:‬‬
‫‪ .1‬קבע‪.mid = (x0+x1)/2 :‬‬
‫‪ .2‬אם ערכו המוחלט של )‪ f(mid‬קטן מ‪ EPSILON -‬אזי החזר את הערך ‪.mid‬‬
‫‪ .3‬אם ‪ f(mid)>0‬אזי עדכן‪. x1 = mid :‬‬
‫‪ .4‬אחרת עדכן‪. x0 = mid :‬‬
‫‪181‬‬
‫עודכן ‪5/2010‬‬
‫‪ .16‬מצביעים גנריים )* ‪(void‬‬
‫עת דנו במצביעים עד כה הדגשנו שמצביע הינו משתנה המכיל כתובת של משתנה‬
‫אחר‪ .‬אולם אם נדייק אזי המצביע מכיל לא רק כתובת אלא גם את טיפוס הנתונים‬
‫עליהם הוא מצביע‪ ,‬זו הסיבה שאנו מבחינים בין * ‪ int‬ל‪ ,double * -‬לדוגמה; וזו גם‬
‫הסיבה שעת אנו כותבים‪ cout << *ip :‬יודע המחשב כמה בתים בזיכרון עליו להציג‪,‬‬
‫וכיצד יש לפרש את הבתים הללו )כמספר שלם? כמספר ממשי? כמחרוזת?(‪ .‬מאותה‬
‫סיבה‪ ,‬עת אנו כותבים ‪ ip++‬יודע המחשב כיצד לקדם את המצביע כך שהוא יצביע‬
‫על האיבר הבא במערך‪ ,‬בין אם מדובר במערך של תווים )בו כל תא צורך בית יחיד(‪,‬‬
‫ובין אם מדובר במערך של מבנים )בו כל תא עשוי להיות גדול(‪.‬‬
‫בפרק זה נדון במצביעים גנריים )‪ (generic pointers‬כלומר כלליים‪ ,‬כאלה שמסוגלים‬
‫להצביע לכל טיפוס של נתונים מצד אחד‪ ,‬אולם טיפוס הנתונים המוצבע על‪-‬ידם‬
‫אינו ידוע להם מצד שני‪ .‬במלים אחרות‪ ,‬המצביע הגנרי כולל רק כתובת של תא‬
‫בזיכרון‪ ,‬ולא כולל את טיפוס הנתונים השמור בבית זה )ובבתים העוקבים לו‪,‬‬
‫ומרכיבים יחד נתון בודד מטיפוס כלשהו(‪ .‬על כן את טיפוס הנתונים יהיה עלינו‬
‫לשמור‪ ,‬כך או אחרת בנפרד‪.‬‬
‫המצביעים הגנריים נותנים לנו אפשרות לטפל בטיפוסי נתונים רב‪-‬צורניים‬
‫)‪ ,(polymorphic data type‬כלומר לכתוב קוד שיטפל במגוון של טיפוסי נתונים‪ .‬אולם‪,‬‬
‫וזו נקודה שמאוד חשוב להפנים‪ ,‬כדי לטפל בטיפוסי הנתונים השונים יהיה עלינו‬
‫ראשית להמיר את המצביע הגנרי )חזרה( לכדי מצביע מטיפוס מסוים‪ .‬אבהיר‪ :‬נניח‬
‫משתנה‪ ,‬או פרמטר‪ , void *p :‬אזי ‪ p‬יכול להצביע הן על משתנים מטיפוס ‪ ,int‬הן על‬
‫משתנים מטיפוס ‪ ,double‬והן על משתנים מכל טיפוס שהוא‪ .‬אולם ניסיון לבצע‬
‫פעולות כגון‪ cout << *p; :‬או‪ p++; :‬לא יתקמפל‪ ,‬בדיוק מהסיבה שהמחשב אינו יודע‬
‫איזה סוג נתון עליו להדפיס עת יש פניה ל‪ ,*p :‬ובאיזה שיעור יש להסיט עת ‪ p‬עת‬
‫מבצעים‪ . p++ :‬לכן‪ ,‬כך או אחרת‪ ,‬כדי להשתמש ַבמצביע ניאלץ ראשית להמירו‬
‫חזרה לטיפוס מסוים כלשהו‪ ,‬ורק אז לעשות בו שימוש; כלומר לכתוב‪:‬‬
‫; ‪ cout << * (int *) p‬או‪ ; p= (int *)p + 1; :‬כלומר יהי עלינו לשמור 'בצד' גם מידע‬
‫אודות טיפוס הנתונים עליהם מצביע ‪ p‬עתה‪.‬‬
‫כדרכנו‪ ,‬נתחיל מדוגמה פשוטה‪ .‬נניח כי בתכניתנו הוגדרו המערכים‪:‬‬
‫; ]‪int iarr[N1‬‬
‫; ]‪double darr[N2‬‬
‫ואולי גם מערכים נוספים שתאיהם מטיפוסים שונים‪.‬‬
‫נניח כי ברצוננו לכתוב קטע קוד הקורא נתונים למערכים השונים‪ .‬בכלים שעמדו‬
‫לרשותנו עד‪-‬היום נאלצנו לכתוב פונ' נפרדת עבור כל אחד משני המערכים השונים;‬
‫עתה נרצה‪ ,‬בהדרגה‪ ,‬לצמצם כפילות הזאת‪.‬‬
‫‪ 16.1‬דוגמה ראשונה‪ :‬פונ' המשתמשת במצביע גנרי‬
‫אציג תחילה אפשרות פשוטה יותר‪ ,‬ומוצלחת פחות‪ ,‬המשתמשת במצביע גנרי‪ ,‬וע"י‬
‫כך מאפשרת לנו לכתוב פונ' יחידה לקריאת נתוני שני המערכים‪ .‬על‪-‬פי גישה זאת‪,‬‬
‫נכתוב פונ' המקבלת מצביע גנרי ‪ void *arr‬אשר יוכל להורות על כל אחד משני‬
‫המערכים‪ .‬בזאת צעדנו צעד קדימה‪ ,‬במובן זה שתהיה לנו פונ' אחת‪ ,‬שתקבל את‬
‫המערך בין אם מדובר במערך של שלמים‪ ,‬ובין אם מדובר מערך של ממשיים )ובין‬
‫‪182‬‬
‫אם מדובר במערך של תאים מכל סוג שהוא(‪ .‬פרמטר נוסף שיהיה מטיפוס ‪char‬‬
‫יורה לפונ' על איזה טיפוס למעשה מצביע הפרמטר ‪ ,arr‬כלומר לאיזה טיפוס יש‬
‫לעשות ‪ cast‬לפרמטר ‪ .arr‬ולבסוף‪ ,‬אם לא כל המערכים מכילים אותו מספר תאים‪,‬‬
‫אזי פרמטר שלישי יחזיק את גודל המערך )כלומר כמה תאים הוא כולל(‪.‬‬
‫‪//-----------------------------------------------------‬‬‫{ )‪void read_data(void *arr, char type, int arr_size‬‬
‫)‪for (int i = 0; i < arr_size; i++‬‬
‫{ )‪switch (type‬‬
‫; ]‪case 'i' : std::cin >> ((int *) arr)[i‬‬
‫; ‪break‬‬
‫; )‪case 'd' : std::cin >> ((double *) arr) +i‬‬
‫; ‪break‬‬
‫‪default:‬‬
‫; "‪std::cerr << "Error: wrong type\n‬‬
‫; )‪exit(1‬‬
‫}‬
‫}‬
‫קוד הפונ' פשוט למדי‪ :‬הפונ' מנהלת לולאה בה היא קוראת את הנתונים תא אחר‬
‫תא‪ ,‬בכל סיבוב בלולאה פקודת ה‪ switch -‬בודקת מהו טיפוס המערך‪ ,‬ועל‪-‬פי ערכו‬
‫של הפרמטר ‪ type‬עושה ‪ cast‬למצביע ‪ arr‬לטיפוס המתאים‪ .‬אחרי שהמצביע‬
‫הומר לטיפוס רצוי‪ ,‬לדוגמה‪ (int *) arr :‬ניתן להוסיף לו את ‪ i‬בתור ההסטה‬
‫הרצויה‪ ,‬כלומר התא המתאים במערך‪ ,‬ומכיוון שעתה כבר מוגדר שהמערך הוא‬
‫מערך של שלמים גם ברור מה צריך להיות שיעור ההסטה בבתים כדי להגיע לתא‬
‫מספר ‪ .i‬לכן הפעולה‪ ((int *) arr) +i :‬מחזירה לנו מצביע לתא מספר ‪i‬‬
‫במערך‪ .‬לבסוף פניה ל‪ *(((int *) arr) +i) :‬פונה לאותו תא‪ ,‬ועל כן‪cin :‬‬
‫;)‪ >> *(((int *) arr) +i‬קוראת נתונים לתא המתאים ְבמערך של‬
‫שלמים‪ .‬לחילופין‪ ,‬ניתן לקרוא לתא רצוי במערך בעזרת הכתיבה הפשוטה יותר‪:‬‬
‫]‪ ; ((int *) arr)[i‬עתה‪ ,‬המרנו את ‪) arr‬רק לצורך ביצוע הפעולה הנוכחית‪,‬‬
‫לא באופן גורף!( לכדי * ‪ ,int‬ולכן אנו יכולים לפנות לתא מספר ‪ i‬במערך עליו‬
‫‪ arr‬מצביע‪.‬‬
‫זימון הפונ' מהתכנית הראשית יעשה באופן הבא‪:‬‬
‫; )‪read_data(iarr, 'i', N1‬‬
‫; )‪read_data(darr, 'd', N2‬‬
‫מגבלתה של הפונ' שכתבנו היא שכדי להכלילה‪ ,‬כך שהיא תהיה מסוגלת לקרוא‬
‫נתונים גם לתוך מערך של תווים עלינו לחזור לפונ'‪ ,‬ולשנות את הקוד שלה‪ :‬להוסיף‬
‫‪ case‬נוסף בפקודת ה‪ ,switch -‬ולכך איננו ששים‪ :‬היינו רוצים לכתוב אחת‬
‫ולתמיד פונ' אשר תדע לקרוא נתונים לתוך כל מערך שהוא‪ ,‬בלי שנצטרך לשנותה כל‬
‫פעם שנרצה להכלילה לטיפוס נתונים נוסף‪ .‬נרצה שהשינויים‪ ,‬או הרחבות‪ ,‬יבוצעו‬
‫מחוץ לפונ'‪ ,‬ע"י מי שקורא לפונ'‪.‬‬
‫‪ 16.2‬דוגמה שניה‪ ,‬משופרת‪ :‬פונ' המשתמשת במצביע גנרי‬
‫עתה נציג גרסה שניה של פונ' המסוגלת לקרוא נתונים למערכים ממגוון טיפוסים‪.‬‬
‫כמו בגרסה הראשונה‪ ,‬גם בגרסה השניה הפונ' תקבל מצביע גנרי ‪ void *arr‬אשר‬
‫יורה על המערך הרצוי‪ .‬אולם בגרסה זאת‪ ,‬בניגוד לקודמתה‪ ,‬לא נעביר פרמטר‬
‫שמציין את טיפוס המערך‪ ,‬אלא נעביר מצביעים לזוג פונ'‪:‬‬
‫א‪ .‬פונ' ‪ get_p2_wanted_cell‬המקבלת את המצביע הגנרי ומספר של תא רצוי‬
‫במערך‪ ,‬ומחזירה מצביע לאותו תא‪.‬‬
‫‪183‬‬
‫ב‪ .‬פונ' ‪ read_into_wanted_cell‬המקבלת מצביע גנרי לתא רצוי במערך‪,‬‬
‫וקוראת נתונים לאותו תא אחרי שהיא ממירה את המצביע הגנרי לטיפוס רצוי‪.‬‬
‫באחריות מי שירצה לזמן את הפונ' ‪ read_data‬יהיה לספק גם שתי פונ' פשוטות‬
‫אלה‪ .‬הפונ' ‪ read_data‬תעשה בהן שימוש‪ :‬בכל סיבוב בלולאה היא תזמן את‬
‫הפונק' ‪ get_p2_wanted_cell‬תעביר לה את המצביע לתחילת המערך ומספר‬
‫תא רצוי‪ ,‬ותקבל חזרה מצביע גנרי אך לתא הרצוי; מצביע זה תעביר ‪read_data‬‬
‫לפונק' ‪) read_into_wanted_cell‬אותה היא קיבלה כפרמטר(‪ .‬הפונ'‬
‫‪ read_into_wanted_cell‬אותה סיפק מי שקרא ל‪ ,read_data -‬כלומר מי‬
‫שיודע מהו טיפוסו האמיתי של המערך ‪ ,arr‬תבצע למצביע הגנרי לתא הרצוי המרת‬
‫טיפוס לטיפוס 'האמיתי'‪ ,‬ותקרא נתונים לתא המתאים‪.‬‬
‫נציג עתה את הקוד של ‪:read_data‬‬
‫‪//------------------------------------------------------‬‬‫‪void read_data(void *arr,‬‬
‫‪void *(*get_p2_wanted_cell)(void *, int),‬‬
‫{ ) )* ‪void (*read_into_wanted_cell)(void‬‬
‫{ )‪for (int i = 0; i < N; i++‬‬
‫; )‪void *cell = get_p2_wanted_cell(arr, i‬‬
‫; )‪read_into_wanted_cell(cell‬‬
‫}‬
‫}‬
‫נסביר‪ :‬הפונ' מקבלת שלושה פרמטרים‪:‬‬
‫א‪ .‬הפרמטר‪ void *arr :‬הינו מצביע גנרי המורה על התא מספר אפס במערך‪.‬‬
‫ב‪ .‬הפרמטר‪) void *(*get_p2_wanted_cell)(void *, int) :‬שמו הוא‬
‫‪ (get_p2_wanted_cell‬הוא מטיפוס מצביע לפונ' המקבלת שני פרמטרים‪:‬‬
‫)‪ (void *, int‬המציינים את המצביע לתחילת המערך ומספר תא רצוי;‬
‫הפונ' מחזירה * ‪ :void‬מצביע )גנרי( לתא המבוקש‪.‬‬
‫)* ‪) void (*read_into_wanted_cell)(void‬שמו הוא‬
‫ג‪ .‬הפרמטר‪:‬‬
‫‪ (read_into_wanted_cell‬הוא מצביע לפונ' המקבלת מצביע גנרי מסוג‬
‫* ‪ ,void‬ומחזירה ‪ .void‬הפונ' עושה למצביע המרת טיפוס לטיפוס מסויים‬
‫כלשהו‪ ,‬וקוראת נתונים לתוך התא עליו המצביע מורה‪.‬‬
‫גוף הפונ' כולל לולאה פשוטה‪ :‬בכל סיבוב בלולאה נקראים נתונים לתוך תא יחיד‬
‫במערך )הפונ' מניחה שבמערך ‪ N‬תאים(‪ .‬הדבר נעשה ע"י שראשית מזמנים את הפונ'‬
‫שהועברה כפרמטר ‪ ,get_p2_wanted_cell‬ושנית את הפונ' )שהועברה‬
‫כפרמטר(‪.read_into_wanted_cell :‬‬
‫יופיה של הפונ'‪ ,‬לעומת זאת שהצגנו בסעיף הקודם‪ ,‬הוא שהיא מסוגלת לקרוא‬
‫ערכים לתוך כל מערך שהוא )בן ‪ N‬תאים(‪ ,‬ובלבד שהיא תקבל את הפרמטרים‬
‫הדרושים‪ :‬מצביע לתחילת המערך‪ ,‬מצביע לפונ' שיודעת להמיר מצביע גנרי ‪ +‬מספר‬
‫תא במצביע לאותו תא‪ ,‬ומצביע לפונ' שבהינתן תא יודעת לקרוא לתוכו נתונים‪.‬‬
‫למעשה ‪ read_data‬שלנו מספקת את הבקרה על התהליך‪ :‬את הלולאה‪ ,‬ומזמנת‬
‫את הפונ' הספציפיות לכל טיפוס וטיפוס‪.‬‬
‫כיצד ינהג מי שרוצה לזמן את הפונ' ‪?read_data‬‬
‫נניח שבתכנית הוגדר‪:‬‬
‫; ]‪int iarr[N‬‬
‫כדי שניתן יהיה להשתמש בפונ' ‪ read_data‬עלינו להגדיר עוד זוג פונ'‪:‬‬
‫‪184‬‬
‫{ )‪void *get_wanted_int_cell(void *p, int i‬‬
‫; )‪return (void *)((int *)p + i‬‬
‫}‬
‫‪//------------------------------------------------------‬‬‫{ )‪void read_into_wanted_int_cell(void *p‬‬
‫; ‪cin >> *(int *) p‬‬
‫}‬
‫הראשונה בין השתיים מחזירה מצביע גנרי לתא רצוי במערך‪ ,‬והשניה מקבלת‬
‫מצביע גנרי‪ ,‬ממירה אותו למצביע ל‪ int -‬וקוראת נתונים לתא עליו המצביע מורה‪.‬‬
‫אחרי כתיבת שתי הפונ' הללו נוכל לזמן את ‪:read_data‬‬
‫‪read_data(iarr,get_wanted_int_cell,‬‬
‫; )‪read_into_wanted_int_cell‬‬
‫נסביר‪ :‬אנו מעבירים ל‪ read_data -‬את המערך‪ ,‬ואת שתי הפונ' הדרושות לה‪.‬‬
‫עתה נניח כי הגדרנו בתכניתנו‪:‬‬
‫{ ‪struct Stud‬‬
‫; ‪unsigned _id‬‬
‫; ]‪char _name[MAX_NAME_LEN‬‬
‫; }‬
‫; ]‪struct Stud studs[N‬‬
‫כיצד נוכל להשתמש בפונ' ‪ read_data‬שלנו על‪-‬מנת לקרוא נתונים גם למערך‬
‫‪?studs‬‬
‫ראשית‪ ,‬כאמור עלינו לכתוב את שתי פונ' העזר‪:‬‬
‫האחת שמחזירה מצביע לתא רצוי במערך‬
‫{ )‪void *get_wanted_Stud_cell(void *p, int i‬‬
‫; )‪return (void *)(( struct Stud *)p + i‬‬
‫}‬
‫והשניה שקוראת נתונים לתוך תא רצוי‪:‬‬
‫‪//------------------------------------------------------‬‬‫{ )‪void read_into_wanted_Stud_cell(void *p‬‬
‫; ‪cin >> (( struct Stud *) p) -> _id‬‬
‫>> )‪cin >> setw(MAX_NAME_LEN‬‬
‫; ‪(( struct Stud *) p) -> _name‬‬
‫}‬
‫עתה נוכל לזמן את ‪:read_data‬‬
‫‪read_data(studs, get_wanted_Stud_cell,‬‬
‫; )‪read_into_wanted_Stud_cell‬‬
‫כמובן שכדי להרחיב את ‪ read_data‬כך שהיא תקרא נתונים גם למערך של‬
‫תלמידים לא שינינו כלל את גוף הפונ'‪.‬‬
‫עת תלמדו תכנות מונחה עצמים תגלו שאת הפונ' הללו‪ ,‬לטיפול במבנה הנתונים‬
‫‪ struct Stud‬ניתן לכלול בתוך הגדרת ה‪ ,struct -‬וכך לקבל 'עצם' )‪(object‬‬
‫הכולל הן נתונים‪ ,‬והן את הפונ' לטיפול בנתונים )בגישת התכנות מונחה העצמים‬
‫קוראים לפונ' אלה בדרך‪-‬כלל בשם 'שיטות' ‪ .(methods‬עת העצם מועבר כפרמטר‬
‫לפונ' מועברים הן הנתונים והן השיטות כאחת‪.‬‬
‫‪185‬‬
‫‪ 16.3‬דוגמה שלישית‪ :‬פונ' מיון גנרית‬
‫כפי שציינו בדוגמה הקודמת‪ ,‬תכונתה של הפונ' הגנרית הינה שהיא מספקת את‬
‫מנגנון הבקרה )בדוגמה הקודמת זו הייתה לולאה פשוטה(‪ ,‬ומשתמשת בפונ'‬
‫המועברות לה כדי למממש את הפעולות עצמן‪ .‬בפונ' פשוטה כגון ‪ read_data‬מנגנון‬
‫הבקרה הוא פשוט למדי‪ ,‬ועל‪-‬כן התועלת משימוש בפונ' גנרית מוגבלת‪ .‬על‪-‬כן עתה‬
‫נציג משימה מורכבת יותר‪ :‬מיון; בה תהליך הבקרה מורכב יותר‪ ,‬וככזה השמוש‬
‫בפונ' גנרית נראה מוצדק יותר‪.‬‬
‫לשם הפשטות אציג מימוש של מיון בועות‪ ,‬אולם לצורך ענייננו אין לכך כל חשיבות‪,‬‬
‫ומימוש של פונ' מיון יעילות יותר נעשה באופן דומה לגמרי‪.‬‬
‫כפי שאנו זוכרים)?( מיון בועות מריץ לולאה כפולה‪ .‬בכל סיבוב בלולאה הוא משווה‬
‫בין שני תאים סמוכים במערך‪ ,‬ובמידת הצורך )במידה והם מפרים את סדר המיון‬
‫הרצוי( האלג' מחליף בין ערכי התאים‪ .‬על‪-‬כן‪ ,‬פונ' המיון הגנרית שלנו תקבל את‬
‫הפרמטרים הבאים‪:‬‬
‫א‪ .‬המערך אותו עליה למיין )‪.(void *arr,‬‬
‫ב‪ .‬פונ' המקבלת מצביע לתחילת המערך ומספר של תא רצוי‪ ,‬ומחזירה מצביע ַלתא‬
‫הקודמת‪.‬‬
‫בדוגמה‬
‫ראינו‬
‫כבר‬
‫כזו‬
‫)פונ'‬
‫)‪( void *(*get_p2_wanted_cell)(void *, int‬‬
‫ג‪ .‬פונ' המקבלת מצביעים לשני תאים )סמוכים( במערך‪ ,‬משווה בין שני התאים‬
‫במערך‪ ,‬ומחזירה האם שני התאים בהתאמה עם הסדר הרצוי או לא‪.‬‬
‫))* ‪( bool (*cmp)(void *, void‬‬
‫ד‪ .‬פונ' המקבלת מצביעים לזוג תאים )סמוכים( במערך ומחליפה בין ערכיהם‬
‫)* ‪.void (*swap)(void *, void‬‬
‫נציג את קוד הפונ ואחר נסבירו‪:‬‬
‫‪//------------------------------------------------------‬‬‫‪void bubble_sort(void *arr,‬‬
‫‪void *(*get_p2_wanted_cell)(void *, int),‬‬
‫‪bool (*cmp)(void *, void *),‬‬
‫{ ))* ‪void (*swap)(void *, void‬‬
‫)‪for (int round = 0; round < N -1; round++‬‬
‫{ )‪for (int place = 0; place < N -round -1; place++‬‬
‫‪if (!cmp(get_p2_wanted_cell(arr, place),‬‬
‫) ) )‪get_p2_wanted_cell(arr, place +1‬‬
‫‪swap(get_p2_wanted_cell(arr, place),‬‬
‫; ) )‪get_p2_wanted_cell(arr, place +1‬‬
‫}‬
‫}‬
‫הפונ' מנהלת את הלולאה הכפולה 'הרגילה' המורצת על‪-‬ידי מיון בועות‪ .‬בכל סיבוב‬
‫בלולאה היא מזמנת את פונ' ההשוואה ‪) cmp‬אשר הועברה לה כפרמטר( על שני תאי‬
‫המערך ]‪ arr[place], arr[place+1‬אשר מצביעים אליהם מחזירה לה הפונ'‬
‫‪ .get_p2_wanted_cell‬במידה ופונ' ההשוואה מורה כי זוג התאים מפרים את‬
‫הסדר )כלומר )]‪ ( if (!cmp(arr[place], arr[place+1‬אזי יש להחליף‬
‫בין ערכי התאים‪ ,‬וזה נעשה ע"י הפונ' )שהועברה כפרמטר( ‪ swap‬אשר מקבלת‬
‫מצביעים לשני התאים‪ ,‬ומחליפה בין ערכיהם‪.‬‬
‫‪186‬‬
‫על‪-‬כן‪ ,‬כדי להשתמש בפונ' ‪ bubble_sort‬הנ"ל יש צורך לממש את הפונ'‬
‫הדרושות‪ .‬נדגים כיצד נעשה הדבר‪ ,‬ראשית עבור מערך של מספרים שלמים‪.‬‬
‫נניח שהגדנו‪:‬‬
‫; ]‪int idata[N‬‬
‫וקראנו נתונים למערך‪.‬‬
‫נציג את הפונ' אותן יש לממש כדי לזמן את פונ' מיון הבועות‪ .‬ראשית‪ ,‬את הפונ'‬
‫)‪ void *get_wanted_int_cell(void *p, int i‬כבר ראינו‪ ,‬והיא‬
‫שתועבר כארגומנט שני ל‪.bubble_sort -‬‬
‫עתה אציג את פונ' ההשוואה‪ ,‬אותה נעביר כארגומנט שלישי‪:‬‬
‫‪//------------------------------------------------------‬‬‫{ )‪bool cmp_int( void *p1, void *p2‬‬
‫; ‪return *(int *)p1 <= *(int *)p2‬‬
‫}‬
‫הפונ' ממירה את שני המצביעים הגנריים שהועברו לה לכדי מצביעים על ‪ ,int‬ואז‬
‫מחזירה את תוצאת ההשוואה בין השלם עליו מצביע הפרמטר הראשון‪ ,‬למספר‬
‫השלם עליו מצביע הפרמטר השני‪.‬‬
‫פונ' ההחלפה‪ ,‬המועברת כארגומנט הרביעי והאחרון ל‪ bubble_sort -‬הינה‪:‬‬
‫‪//------------------------------------------------------‬‬‫{ )‪void swap_int(void *p1, void *p2‬‬
‫; ‪int temp = *(int *) p1‬‬
‫; ‪*(int *) p1 = *(int *)p2‬‬
‫; ‪*(int *) p2 = temp‬‬
‫}‬
‫גם פונ' זו מקבלת שני מצביעים גנריים‪ .‬היא ממירה אותם למצביעים ל‪,int -‬‬
‫ומכאן מעבירה ערכים בין תאי הזיכרון כפי שאנו כבר מכירים מימים ימימה‪.‬‬
‫עתה‪ ,‬כשבאמתחתנו שלוש פונ' עזר הדרושות נוכל לזמן את ‪ bubble_sort‬כדי‬
‫למיין את המערך ‪:idata‬‬
‫‪bubble_sort(iarr, get_wanted_int_cell,‬‬
‫‪cmp_int,‬‬
‫; )‪swap_int‬‬
‫נניח שעתה ברצוננו למיין את המערך‪ ,‬אולם כל שאנו רוצים הוא שהמספרים‬
‫השליליים ישכנו בתחילתו‪ ,‬והחיוביים )או האי שליליים( בסופו‪ .‬מה יהיה בדיוק‬
‫הסדר בין השליליים לבין עצמם‪ ,‬או בין החיוביים לבין עצמם לא משנה לנו‪ .‬כדי‬
‫להשיג מיון זה עלינו רק לכתוב פונ' השוואה חדשה‪ .‬פונ' ההשוואה תקבע ששני‬
‫שליליים הם בהתאמה לסדר הדרוש‪ ,‬ואין צורך להחליף ביניהם‪ ,‬כך גם שני‬
‫חיוביים‪ ,‬וכך גם שלילי וחיובי‪ .‬הזוג היחיד שמפר את הסדר הוא עת מספר חיובי‬
‫מופיע לפני מספר שלילי‪ ,‬ולכן רק במקרה זה על פונ' ההשוואה להחזיר ערך‬
‫‪ .false‬הפונ' תכתב לפיכך‪:‬‬
‫‪//------------------------------------------------------‬‬‫{ )‪bool cmp_neg_pos( void *p1, void *p2‬‬
‫)‪if (*(int *)p1 >= 0 && *(int *)p2 < 0‬‬
‫; ‪return false‬‬
‫; ‪return true‬‬
‫}‬
‫עתה נוכל למיין את המערך שוב‪ ,‬בלי שנכניס כל שינוי בפונ' המיון עצמה‪ ,‬ע"י‬
‫שנעביר ל‪ bubble_sort -‬את ‪ cmp_neg_pos‬כפונ' ההשוואה בה יש לעשות‬
‫שימוש‪:‬‬
‫‪187‬‬
‫‪bubble_sort(iarr, get_wanted_int_cell,‬‬
‫‪cmp_neg_pos,‬‬
‫; )‪swap_int‬‬
‫באופן דומה‪ ,‬כדי ש‪ bubble_sort -‬תוכל למיין מערך של ‪) struct Stud‬כפי‬
‫שהוצג בסעיף הקודם(‪ ,‬עלינו למממש את שלוש פונ' העזר הדרושות על משתנים‬
‫מטיפוס זה‪ .‬את הפונ' ‪ get_wanted_Stud_cell‬כבר מימשנו בסעיף הקודם‪.‬‬
‫את הפונ' ‪ cmp_struct_Stud‬נממש עתה‪ .‬נניח שברצוננו למיין את התלמידים‬
‫על‪-‬פי מספר הזהות )שדה ה‪ ,(id -‬ובין שני תלמידים להם אותו מספר זהות )ולרגע‬
‫נניח שהדבר אפשרי(‪ ,‬נקבע את הסדר על‪-‬פי שדה השם )‪ .(name‬על כן פונ' ההשוואה‬
‫בין שני תלמידים תהיה‪:‬‬
‫{ )‪bool cmp_stuct_Stud( void *p1, void *p2‬‬
‫אם מספר הזהות של הראשון קטן משל השני החזר ‪:true‬‬
‫< ‪if ((struct Stud *)p1 -> _id‬‬
‫) ‪(struct Stud *)p2 -> _id‬‬
‫; ‪return true‬‬
‫אם מספרי הזהות שלהם שווים‪ ,‬ושמו של הראשון קטן משמו של השני‬
‫אזי החזר ‪:true‬‬
‫== ‪_id‬‬
‫&& ‪_id‬‬
‫‪*)p1 -> _name,‬‬
‫)‪*)p2 -> _name) <= 0‬‬
‫>‪if ((struct Stud *)p1 -‬‬
‫>‪(struct Stud *)p2 -‬‬
‫‪strcmp((struct Stud‬‬
‫‪(struct Stud‬‬
‫; ‪return true‬‬
‫בכל מקרה אחר החזר ‪:false‬‬
‫; ‪return false‬‬
‫}‬
‫הפונ' ‪ swap_struct_Stud‬דומה מאוד לזו המחליפה שלמים‪:‬‬
‫‪//------------------------------------------------------‬‬‫{ )‪void swap_struct_Stud(void *p1, void *p2‬‬
‫; ‪struct Stud temp = *( struct Stud *) p1‬‬
‫; ‪*( struct Stud *) p1 = *( struct Stud *)p2‬‬
‫; ‪*( struct Stud *) p2 = temp‬‬
‫}‬
‫עתה נוכל לזמן את ‪:bubble_sort‬‬
‫‪bubble_sort(studs, get_wanted_ struct_Stud _cell,‬‬
‫‪cmp_struct_Stud,‬‬
‫; )‪swap_struct_Stud‬‬
‫כדי לא להעמיס על הדוגמות הקודמות הסרתי מהן את כל נושא הפרמטרים‬
‫הקבועים )‪ ,(const‬מעשה‪ ,‬שכמובן‪ ,‬לא ראוי לעשות‪ .‬הפונ' המחזירה מצביע לתא‬
‫במערך‪ ,‬כמו גם זו המשווה בין שני תאי מערך‪ ,‬יכולה וצריכה להיכתב תוך שימוש‬
‫בפרמטרים קבועים‪ .‬על‪-‬כן‪ ,‬לסיום‪ ,‬אציג את הפרוטוטיפים של הפונ' השונות כשהן‬
‫כתובות כהלכה‪ ,‬ועם פרמטרים קבועים‪:‬‬
‫‪//------------------------------------------------------‬‬‫שתי הפונ' המחזירות תא במערך מקבלות ‪:const void *p‬‬
‫; )‪void *get_wanted_int_cell(const void *p, int i‬‬
‫; )‪void *get_wanted_double_cell(const void *p, int i‬‬
‫בהתאמה‪ ,‬גם ‪ read_data‬מקבלת כפרמטר שני פונ' המקבלת מצביע שהינו קבוע‪:‬‬
‫‪void read_data(void *arr,‬‬
‫‪188‬‬
‫)‪void *(*get_p2_wanted_cell‬‬
‫‪(const void *, int),‬‬
‫; ) )* ‪void (*read_into_wanted_cell)(void‬‬
‫; )‪void read_into_wanted_int_cell(void *p‬‬
‫; )‪void read_into_wanted_double_cell(void *p‬‬
‫פונ' ההשוואה יכולות לקבל מצביע קבוע‪:‬‬
‫; )‪bool cmp_int(const void *p1,const void *p2‬‬
‫; )‪bool cmp_double(const void *p1,const void *p2‬‬
‫; )‪void swap_int(void *p1, void *p2‬‬
‫; )‪void swap_double(void *p1, void *p2‬‬
‫הפרוטוטיפ של מיון בועות משקף את הפרוטוטיפים של הפונ' שהוא מקבל‪:‬‬
‫‪void bubble_sort(void *arr,‬‬
‫)‪void *(*get_p2_wanted_cell‬‬
‫‪(const void *, int),‬‬
‫‪bool (*cmp)(const void *,const void *),‬‬
‫; ))* ‪void (*swap)(void *, void‬‬
‫‪ 16.4‬דוגמה שלישית‪ :‬בניית רשימה מקושרת ממוינת גנרית‬
‫נניח שבתכנית הגדרנו מבנה בעזרתו נשמור נתוני תלמיד בודד )מספר הזהות שלו‪,‬‬
‫שמו‪ ,‬ומינו(‪ ,‬ומבנה שני בעזרתו נשמור נתוני קורס בודד )קוד הקורס‪ ,‬שמו‪ ,‬ושנת‬
‫הלימודים לה הוא מיועד(‪:‬‬
‫{ ‪struct Stud‬‬
‫; ‪unsigned long _id‬‬
‫; ]‪char _name[MAX_NAME_LEN‬‬
‫; ‪enum gender_t _gender‬‬
‫; }‬
‫{ ‪struct Course‬‬
‫; ‪unsigned int _course_code‬‬
‫; ]‪char _name[MAX_NAME_LEN‬‬
‫; ‪char _for_year‬‬
‫; }‬
‫נשים לב כי עת הגדרנו מבנים אלה לא נתנו דעתנו לשאלה האם נרצה לאחסן את‬
‫נתוני התלמידים או הקורסים במערך‪ ,‬ברשימה מקושרת‪ ,‬בעץ‪ ,‬או בכל מבנה נתונים‬
‫אחר‪ .‬מבני הנתונים כוללים רק את המידע אותו ברצוננו לאחסן‪ .‬במילים אחרות‪,‬‬
‫המבנים אינם כוללים מצביעים שיתמכו ברשימה או בעץ‪.‬‬
‫עתה נניח כי החלטנו שבתכנית כלשהי ברצוננו לבנות רשימה מקושרת ממוינת אחת‬
‫של תלמידים )שתמוין על‪-‬פי מספר הזהות(‪ ,‬ושניה של קורסים )שתמוין על‪-‬פי שם‬
‫הקורס(‪ .‬אדגיש כי כל רשימה תכיל מבנים מסוג יחיד )לא נכניס לרשימה אחת הן‬
‫תלמידים והן קורסים(‪.‬‬
‫את המשימה של בניית הרשימה הממוינת נרצה לממש באמצעות פונ' גנרית‪ ,‬אשר‬
‫תממש עבורנו את הלולאות הדרושות‪ ,‬תוך שהיא עושה שימוש בפונ' שנממש עבור‬
‫‪189‬‬
‫כל אחד ואחד ממבני הנתונים‪ ,‬בפרט פונ' השוואה בין שני תלמידים‪ ,‬ופונ' השוואה‬
‫בין שני קורסים‪.‬‬
‫‪190‬‬
‫לשם ביצוע המשימה נגדיר מבנה שלישי‪:‬‬
‫{ ‪struct List_node‬‬
‫; ‪void *_item‬‬
‫; ‪struct List_node *_next‬‬
‫; }‬
‫מבנה זה הוא שיתמוך בקיומה של רשימה מקושרת‪ .‬נסביר‪ :‬כל איבר מסוג‬
‫‪ List_node‬כולל מצביע למבנה של תלמיד בודד או קורס בודד )ולכן זהו מצביע‬
‫מסוג * ‪ ,void‬על מנת שהוא יוכל להצביע על כל מבנה שהוא‪ ,‬ומצביע שני לאיבר‬
‫הבא ברשימה‪ ,‬איבר שהוא תיד ובהכרח מסוג ‪ .List_node‬מבחינה ציורית‬
‫הרשימה נראית‪:‬‬
‫‪_next‬‬
‫‪_next‬‬
‫‪_next‬‬
‫‪_next‬‬
‫‪_item‬‬
‫‪_item‬‬
‫‪_item‬‬
‫‪_item‬‬
‫תלמיד או‬
‫קורס בודד‬
‫תלמיד או‬
‫קורס בודד‬
‫תלמיד או‬
‫קורס בודד‬
‫תלמיד או‬
‫קורס בודד‬
‫נסביר‪ :‬בשורה העליונה בציור מופיעים איברים מטיפוס ‪ List_node‬הכוללים שני‬
‫מצביעים בכל איבר‪ .‬את המצביעים מסוג * ‪ List_node‬ציירתי בקו שלם‪ .‬בשורה‬
‫מתחת מופיעים האיברים הכוללים את הנתונים שיש לשמור )נתוני תלמידים או‬
‫לחילופין קורסים(‪ .‬כל איבר מסוג ‪ List_node‬מצביע )עם מצביע שמצויר בקו‬
‫מקווקו( על איבר בודד של תלמיד או של קורס‪ .‬אברי התלמידים\קורסים כמובן‬
‫שאינם משורשרים זה לזה ישירות‪ ,‬הרי אין בהם כלל מצביעים‪ ,‬רק שדות של‬
‫נתונים‪.‬‬
‫כדי להשלים את המשימה יהיה עלינו לכתוב שלוש פונ' עבור ‪,struct Stud‬‬
‫ושלוש מקבילות עבור ‪ .struct Node‬שלוש הפונ' יבצעו‪:‬‬
‫א‪ .‬הקצאת איבר של תלמיד\קורס והחזרת מצביע אליו )מצביע זה נכניס למרכיב‬
‫‪ _item‬בקופסה מסוג ‪.List_noe‬‬
‫ב‪ .‬קריאת נתוני תלמיד\קורס בודד )לתוך איבר עליו מורה מצביע גנרי‪ ,‬כלומר כזה‬
‫מטיפוס * ‪.(void‬‬
‫ג‪ .‬השוואה בין שני תלמידים )או בין שני קורסים( עליהם מורים מצביעים גנריים‪.‬‬
‫בנוסף להן אציג גם פונ' הדפסת נתוני תלמיד בודד‪ .‬לפונ' זו נזדקק עת נרצה להציג‬
‫את הרשימות הממוינות שבנינו‪.‬‬
‫אציג את שלוש הפונ' עבור ‪ ,struct Stud‬אלה של קורס דומות לגמרי‪:‬‬
‫פונ' זו‬
‫‪//-----------------------------------------------------------‬‬‫מקצה איבר מסוג ‪ struct Stud‬ומחזירה מצביע אליו‬
‫{ )(‪void* alloc_stud‬‬
‫; ‪struct Stud *p = new (std::nothrow) struct Stud‬‬
‫{ )‪if (p == NULL‬‬
‫; "‪cerr << "cannot allocate a new sturct Stud\n‬‬
‫; )‪exit(EXIT_FAILURE‬‬
‫}‬
‫; ‪return p‬‬
‫}‬
‫‪//------------------------------------------------------------‬‬
‫פונ' זו מקבלת מצביע גנרי‪ ,‬אשר אנו מובטחים כי מצביע לאיבר של תלמיד‪ ,‬וקוראת‬
‫את נתוני התלמיד‪:‬‬
‫‪191‬‬
‫{ )‪void read_stud(void *p‬‬
‫; ‪int temp‬‬
‫; ‪cin >> ((struct Stud *) p) -> _id‬‬
‫; ‪cin >> setw(MAX_NAME_LEN) >> ((struct Stud *) p) -> _name‬‬
‫; ‪cin >> temp‬‬
‫? )‪((struct Stud *) p) -> _gender = ((temp == 0‬‬
‫;)‪FEMALE_GNDR : MALE_GNDR‬‬
‫}‬
‫‪//------------------------------------------------------------‬‬
‫פונ' זו מקבלת מצביע גנרי‪ ,‬אשר אנו מובטחים שמצביע אל מבנה של תלמיד‪,‬‬
‫ומציגה את נתוני התלמיד )נזדקק לה לשם הצגת הרשימות שנבנה(‪:‬‬
‫{ )‪void print_stud(const void *p‬‬
‫; " " << ‪cout << ((struct Stud *) p) -> _id‬‬
‫; ‪cout << ((struct Stud *) p) -> _name‬‬
‫? ‪cout << (((struct Stud *) p) -> _gender == FEMALE_GNDR‬‬
‫; )"‪"F" : "M‬‬
‫; ‪cout << endl‬‬
‫}‬
‫‪//------------------------------------------------------------‬‬
‫פונ' זו מקבלת זוג מצביעים גנריים‪ ,‬אשר אנו מובטחים שמצביעים על מבנים של‬
‫תלמידים )כל אחד על מבנה יחיד(‪ ,‬ומחזירה את תוצאת ההשוואה בין שני המבנים‪.‬‬
‫שרירותית‪ ,‬ולשם הפשטות בחרתי להשוות בין שני תלמידים על‪-‬פי מספר הזהות‬
‫שלהם‪ ,‬אולם לכך אין‪ ,‬כמובן‪ ,‬חשיבות‪:‬‬
‫{ )‪bool cmp_stud(const void *p1, const void *p2‬‬
‫=< ‪return ((struct Stud *) p1) -> _id‬‬
‫; ‪((struct Stud *) p2) -> _id‬‬
‫}‬
‫עתה‪ ,‬כשבצקלוננו הפונ' הפשוטות הללו נוכל לכתוב את הפונ' הגנרית‬
‫‪ .build_sorted_list‬הפונ' תקבל מצביעים לשלוש פונ'‪ :‬פונ' הקצאת איבר‬
‫מהטיפוס הרצוי‪ ,‬פונ' קיראת נתונים לתוך איבר מהטיפוס הרצוי‪ ,‬ופונ' להשוואה‬
‫בין שני איברים מהטיפוס הרצוי‪ .‬הפונ' תבנה רשימה מקושרת ממוינת‪ ,‬ותחזיר‬
‫מצביע לראש הרשימה‪ .‬מצביע מטיפוס * ‪ .List_node‬אציג את קוד הפונ'‪:‬‬
‫‪//-----------------------------------------------------------‬‬‫(‪struct List_node *build_sorted_list‬‬
‫‪void* alloc_struct() ,‬‬
‫‪void read_struct(void *p) ,‬‬
‫{ ) )‪bool cmp_struct(const void *p1, const void *p2‬‬
‫; ‪struct List_node *head = NULL‬‬
‫; '‪char another_one = 'y‬‬
‫{ )'‪while (another_one == 'y‬‬
‫; )(‪void *p = alloc_struct‬‬
‫; )‪read_struct(p‬‬
‫; )‪insert(head, p, cmp_struct‬‬
‫; " ?‪cout << "insert another item‬‬
‫; ‪cin >> another_one‬‬
‫}‬
‫; ‪return head‬‬
‫}‬
‫נסביר את הפונ'‪ :‬כפי שאנו רואים מכתורת הפונ' היא אכן מקבלת מצביעים לשלוש‬
‫פונ'‪ :‬האחת מקצה איבר ומחזירה מצביע גנרי אליו‪ ,‬השניה מקבלת מצביע גנרי‬
‫וקוראת נתונים לתוך האיבר )תוך שהיא‪ ,‬כמובן‪ ,‬מבצעת המרת טיפוס למצביע‬
‫הגנרי לכדי הטיפוס המתאים(‪ ,‬והשלישית משווה בין איברים כדי לקבוע את‬
‫מקומם ברשימה הממוינת‪.‬‬
‫‪192‬‬
‫הפונ' מגדירה מצביע‪ struct List_node *head = NULL ; :‬אשר יורה על אברי‬
‫הרשימה‪ .‬היא מנהלת לולאה בה כל עוד המשתמש מעוניין להוסיף איברים נוספים‬
‫לרשימה אותה הוא בונה עתה‪ :‬א‪ .‬מזמנים את הפונ' להקצאת איבר בודד‪) ,‬ועליו‬
‫מצביע המשתנה ‪ (p‬ב‪ .‬קוראים נתונים לתוך אותו איבר )זה שעליו מצביע ‪ ,(p‬ג‪.‬‬
‫מכניסים את האיבר הבודד )עליו מצביע ‪ (p‬לרשימה השלמה הנבנית )עליה מצביע‬
‫‪ (head‬במקום המתאים )תוך שימוש בפונ' ההשוואה(‪.‬‬
‫נפנה עתה להצגת הפונ' ‪ insert‬אשר מוסיפה איבר בודד לרשימה‪:‬‬
‫‪//-----------------------------------------------------------‬‬‫‪void insert(struct List_node *&head,‬‬
‫‪void *p,‬‬
‫{ ) )‪bool cmp_struct(const void *p1, const void *p2‬‬
‫= ‪struct List_node *new_item‬‬
‫; ‪new (std::nothrow) struct List_node‬‬
‫{ )‪if (new_item == NULL‬‬
‫; "‪cerr << "Cannot allocate a new List_node\n‬‬
‫; )‪exit(EXIT_FAILURE‬‬
‫}‬
‫; ‪new_item -> _item = p‬‬
‫אם הרשימה ריקה‪ ,‬זה האיבר הראשון המוסף לה‪:‬‬
‫{ )‪if (head == NULL‬‬
‫; ‪head = new_item‬‬
‫; ‪head -> _next = NULL‬‬
‫}‬
‫אם האיבר החדש צריך להיות מוסף בראש הרשימה )פונ' ההשוואה מורה לנו שהוא‬
‫קטן מהאיבר שבראש הרשימה(‪:‬‬
‫{ ))‪else if (cmp_struct(new_item -> _item, head -> _item‬‬
‫; ‪new_item -> _next = head‬‬
‫; ‪head = new_item‬‬
‫}‬
‫אם האיבר החדש צריך להיות מוסף באמצע או בסוף הרשימה‪:‬‬
‫‪else‬‬
‫{‬
‫‪struct List_node *rear = head,‬‬
‫; ‪*front = head->_next‬‬
‫&& ‪while (front != NULL‬‬
‫{ ))‪cmp_struct(front->_item, new_item->_item‬‬
‫; ‪rear = front‬‬
‫; ‪front = front -> _next‬‬
‫}‬
‫; ‪new_item -> _next = front‬‬
‫; ‪rear -> _next = new_item‬‬
‫}‬
‫}‬
‫נסביר את הפונ'‪ :‬הפונ' מקבלת שלושה פרמטרים‪ :‬הראשון‪ head ,‬מצביע על‬
‫הרשימה הנבנית‪ .‬הוא מועבר כפרמטר הפניה שכן הפונ' עשויה לשנות את ערכו )בעת‬
‫הוספת איבר ראשון‪ ,‬או עת מוסף יבר חדש שקטן מהאיבר המצוי בראש הרשימה(‪.‬‬
‫המצביע הוא מסוג * ‪ List_node‬שכן אברי הרשימה הם כולם מסוג זה‪ .‬בכל‬
‫איבר יהיה מצביע גנרי שיצביע על תלמיד בודד או על קורס בודד‪ ,‬ואותו מצביע‬
‫)השדה ‪ _item‬הוא מצביע גנרי(‪ .‬הפרמטר השני ‪) p‬של הפונ'(‪ ,‬מצביע על האיבר‬
‫הבודד אותו יש להוסיף לרשימה )איבר זה עשוי להיות מסוג ‪ struct Stud‬או‬
‫מסוג ‪ struct Course‬ולכן המצביע הוא גנרי(‪ .‬הפרמטר השלישי הוא פונ'‬
‫השוואה בין שני איברים‪ ,‬בעזרתה ממוינת הרשימה‪.‬‬
‫‪193‬‬
‫קוד הפונ' דומה לזה שראינו עת בנינו רשימה מקושרת ממוינת 'רגילה'‪ .‬המשתנה‬
‫‪ new_item‬יצביע על האיבר שיוּסף ַלרשימה )איבר מסוג ‪ ,(List_node‬על כן‬
‫ראשית אנו מקצים לו זיכרון‪ ,‬ושנית ְ‬
‫מפנים את המצביע ‪ _item‬להצביע על נתוני‬
‫התלמיד\קורס החדש‪ .‬עתה עלינו לשלב את האיבר עליו מצביע ‪ַ new_item‬ברשימה‬
‫עליה מצביע ‪ ,head‬ולשם כך אנו מבחינים בין שלושה מקרים‪:‬‬
‫נפנה את ‪head‬‬
‫א‪ .‬אם הרשימה ריקה‪ ,‬כלומר זהו האיבר הראשון המוסף לה‪ ,‬אזי ְ‬
‫להצביע כמו ‪.new_item‬‬
‫ב‪ .‬אם על‪-‬פי פונ' ההשוואה שהועברה לנו עולה כי האיבר הנוכחי קטן מזה שניצב‬
‫בראש הרשימה‪ ,‬אזי יש להוסיף את האיבר הנוכחי בראש הרשימה‪ ,‬וזה מה‬
‫שאנו עושים‪.‬‬
‫ג‪ .‬במקרה הכללי‪ :‬אנו מתקדמים על‪-‬פני הרשימה עם זוג מצביעים ‪rear,‬‬
‫‪ front‬עד אשר ‪ front‬מצביע על איבר גדול יותר מהאיבר שיש להוסיף‪ .‬עתה‬
‫)אחרי שגמרנו להתקדם(‪ ,‬אנו משלבים את האיבר החדש ברשימה בין האיבר‬
‫עליו מצביע ‪ rear‬לאיבר עליו מצביע ‪.front‬‬
‫התכנית הראשית תראה‪:‬‬
‫‪//-----------------------------------------------------------‬‬‫{ )(‪int main‬‬
‫; ‪struct List_node *studs‬‬
‫; ‪struct List_node *courses‬‬
‫; )‪studs = build_sorted_list(alloc_stud, read_stud, cmp_stud‬‬
‫‪courses = build_sorted_list(alloc_course, read_course,‬‬
‫; )‪cmp_course‬‬
‫; )‪print_list(studs, print_stud‬‬
‫; )‪print_list(courses, print_course‬‬
‫; ‪return EXIT_SUCCESS‬‬
‫}‬
‫את מימוש הפונ' ‪ print_list‬אני משאיר לכם כתרגיל‪.‬‬
‫‪194‬‬
‫עודכן ‪5/2010‬‬
‫‪ .17‬חלוקת תכנית לקבצים‬
‫עת התכניות שאנו כותבים גדלות קורה לא אחת שאנו מעוניינים לחלק את התכנית‬
‫לכמה קבצים שיקומפלו יחד לכדי תכנית אחת שלמה )לעתים נאמר‪ :‬לכדי‬
‫אפליקציה אחת(‪ .‬סיבות שונות מביאות אותנו לחלוקת התכנית לכמה קבצים‪:‬‬
‫א‪ .‬מתכנתים שונים יכולים לעבוד במקביל על חלקים שונים בתכנית; כל חלק‬
‫ייכתב בקובץ נפרד‪.‬‬
‫ב‪ .‬פונ' אשר מבצעות משימה דומה )למשל פונ' המבצעות משימות מתמטיות שונות‬
‫בתכניתנו( יושמו בקובץ יחיד‪ .‬עת נזדקק גם בתכנית אחרת למשימות אלה‪ ,‬נוכל‬
‫לשלב את הקובץ ַבתכנית האחרת בנקל )בלי שנצטרך לבצע 'העתק\הדבק'‬
‫מקובץ לקובץ ַלפונ' הדרושות לנו(‪.‬‬
‫ג‪ .‬עת מתגלית שגיאה בחלק כלשהו של התכנית‪ ,‬או עת אנו רוצים לשנות חלק‬
‫כלשהו בתכנית איננו צריכים לקמפל את כל התכנית )דבר שעלול לקחת זמן רב‬
‫אם התכנית כוללת אלפים רבים של שורות קוד(‪ ,‬אלא אנו מקמפלים )במובן‬
‫הצר והמדויק של המילה( רק את הקובץ שמכיל את הקוד שהשתנה‪ ,‬ואחר אנו‬
‫כורכים )עושים ‪ (linking‬לכלל הקבצים המקומפלים )אלה שלא שונו‪ ,‬וזה ששונה(‬
‫לכדי תכנית שלמה חדשה‪.‬‬
‫לסיכום‪ :‬מגוון של סיבות מביאות אותנו לרצות לחלק את תכניתנו למספר קבצים‪.‬‬
‫בפרק זה נלמד כיצד לעשות זאת‪.‬‬
‫‪ 17.1‬הנחיית הקדם מהדר ‪#define‬‬
‫אתחיל בסטייה לנושא מעט שונה‪ ,‬אשר נזדקק לו בהמשך‪ ,‬עת נתקדם עם המשימה‬
‫של חלוקת התכנית לקבצים‪.‬‬
‫עת דנו בתרגומה של התכנית משפה עילית לשפת מכונה אמרנו שלמרות שבשפה‬
‫חופשית אנו קוראים לתהליך קומפילציה )או הידור( הרי ליתר דיוק התהליך‬
‫מתחלק לשני שלבים מרכזיים‪ :‬שלב הקומפילציה‪ ,‬בו הקומפיילר מתרגם את‬
‫התכנית משפה עילית לשפת מכונה )אלא אם הוא מצא בה שגיאות תחביריות‪ ,‬ואז‬
‫הוא מודיע עליהן‪ ,‬ולא מתרגם את התכנית(‪ ,‬ושלב הלינקינג )או הכריכה( בה נוספים‬
‫לתכנית חלקי קוד נוספים‪ ,‬בפרט של פונ' סיפריה שונות אותן זימנו בתכניתנו‬
‫)כדוגמת‪ cin, sqrt :‬וכולי( כך שמתקבלת תכנית שלמה‪ ,‬ניתנת להרצה‪.‬‬
‫עתה נוסיף ונפרט‪ ,‬ונאמר שגם שלב ההידור מתחלק לשני שלבים‪ :‬שלב ה‪'-‬קדם‬
‫מהדר' )‪ ,(pre-processor‬ושלב ההידור המרכזי‪ .‬בסעיף זה נתאר חלק מפעולותיו של‬
‫קדם המהדר‪.‬‬
‫קדם המהדר סורק את קובץ התכנית שלנו‪ .‬עת הוא נתקל בפקודה המתחילה בסימן‬
‫הסולמית )‪ (#‬הוא מבצע אותה‪ ,‬ובכך משנֵה את הקובץ שלנו‪ ,‬באופן אותו נתאר‬
‫מייד‪) .‬אם נדייק‪ ,‬השינוי אינו מבוצע על קובץ המקור‪ ,‬קובץ המקור אינו משתנה‪,‬‬
‫השינוי הוא בכך שהפלט של קדם המהדר כולל את קובץ המקור שלנו‪ ,‬אחרי‬
‫שינויים(‪.‬‬
‫לדוגמה‪ :‬אם בתכנית שלנו נכלול את השורה‪:‬‬
‫‪17‬‬
‫‪195‬‬
‫‪#define MAX‬‬
‫אזי הפקודה ‪) #define‬כמו כל ההנחיות המתחילות ב‪ (# -‬הינה פקודה המופנית‬
‫לקדם המהדר‪ .‬הפקודה מורה לקדם המהדר שבהמשך התכנית‪ ,‬בכל מקום בו תופיע‬
‫המחרוזת ‪ MAX‬יש להמירה במספר ‪ .17‬הפקודה גם מדגימה לנו את אופן פעולתו של‬
‫קדם המהדר באופן כללי‪ :‬קדם המהדר הוא רכיב 'טיפש' אשר יודע לבצע פעולות‬
‫המרה של מחרוזות‪ .‬מבחינה עניינית‪ ,‬הפקודה הנ"ל היא הדרך המקובלת בשפת ‪C‬‬
‫)בניגוד לשפת ‪ (C++‬להגדיר קבועים‪ .‬דרך זו נחשבת לפחות מוצלחת או רצויה‬
‫מהדרך המקובלת ב‪ C++ -‬שכן אין בה אפשרות לבדוק טיפוסים—המהדר עצמו‪ ,‬עת‬
‫מטפל בתכנית‪ ,‬מוצא את המספר הקבוע ‪) .17‬כרגיל‪ ,‬אנו נוהגים לכתוב קבועים‬
‫באות גדולה‪(.‬‬
‫אם נדייק אזי הפקודה ‪ #define‬תביא להמרת המחרוזת שמופיעה אחריה‬
‫בשארית השורה‪ .‬מסיבה זאת אנו יכולים לכתוב גם את פקודות ה‪#define -‬‬
‫הבאות‪:‬‬
‫{‬
‫}‬
‫‪#define BEGIN‬‬
‫‪#define END‬‬
‫‪#define THEN‬‬
‫אם בהמשך התכנית שלנו יופיע הקוד‪:‬‬
‫‪if (a < b) THEN BEGIN a = b; b = 0; END‬‬
‫‪if (a == 0) THEN BEGIN ; END BEGIN ; END‬‬
‫אזי בעקבות פעולת הקדם מעבד יתקבל הקוד‪:‬‬
‫} ;‪if (a < b) { a = b; b = 0‬‬
‫} ; { } ; { )‪if (a == 0‬‬
‫נסביר‪ :‬כל מופע של המחרוזת ‪ BEGIN‬מומר במחרוזת {‪ ,‬באופן דומה כל מופע של‬
‫‪ END‬מוחלף בשארית השורה‪ ,‬כלומר ב‪ , } :‬ולבסוף‪ ,‬כל מופע של ‪ THEN‬מוחלף‬
‫בכלום‪ ,‬במילים פשוטות‪ ,‬נמחק מהקוד‪.‬‬
‫שימו לב שהקדם מעבד מבצע פעולה על מחרוזות‪ ,‬במובן זה ניתן להפעילו גם על‬
‫קובץ שאינו כולל קוד‪ ,‬והוא יבצע את פעולות ההמרה המתבקשות ממנו‪.‬‬
‫במהדר ‪ g++‬נוכל לבקש לראות את פלט קדם ההידור של התכנית שלנו מבלי‬
‫שהמהדר ימשיך ביתר תהליך הקומפילציה‪ ,‬בעזרת הפקודה‪:‬‬
‫‪g++ -E my_prog.cc‬‬
‫הדגל ‪ -E‬מורה ל‪ g++ -‬לבצע רק את שלב הקדם מהדר‪.‬‬
‫הפלט של קדם המהדר יוצג על‪-‬גבי המסך‪.‬‬
‫נציג עתה את פעולת הקדם מעבד הקרויה 'מאקרו'‪ .‬המאקרו מגדיר מספר פעולות‬
‫שיש לבצע עת 'מזמנים' אותו‪ ,‬ליתר דיוק עת 'פורשים' אותו‪ .‬לדוגמה המאקרו‪:‬‬
‫; ‪#define ZERO num1 = 0; num2 = 0; num3 = 0‬‬
‫אומר שכל פעם שנכתוב בהמשך התכנית את המילה ‪ ZERO‬היא תוחלף על‪-‬ידי קדם‬
‫המהדר בשלוש פעולות ההשמה‪num1 = 0; num2 = 0; num3 = 0 ; :‬‬
‫כלומר נוכל בתחילת הקוד שלנו להגדיר את המאקרו הנ"ל )באמצעות פעולת ה‪-‬‬
‫‪ ,(#define‬ובהמשך לכתוב כל פעם ‪ ZERO‬ואז בקוד ישתלו שלוש ההשמות הנ"ל‪.‬‬
‫לפני שנפנה לדוגמה נוספת נציג ונסביר את הקוד הבא‪:‬‬
‫; ‪num1 = num1 + num2‬‬
‫; ‪num2 = num1 – num2‬‬
‫; ‪num1 = num1 – num2‬‬
‫‪196‬‬
‫קוד זה מהווה דוגמה טובה ַלדרך בה נקל לכתוב קוד פשוט בדרך קשה להבנה‪ .‬אם‬
‫תסמלצו את הקוד תגלו שהוא מחליף בין ערכי ‪ num1‬ו‪ num2 -‬בלי להשתמש‬
‫במשתנה עזר‪.‬‬
‫עתה נציג את המאקרו הבא‪:‬‬
‫;‪#define SWAP(_A, _B) _A = _A +_B; _B= _A- _B; _A = _A - _B‬‬
‫זהו מאקרו 'מתקדם'‪ ,‬כזה המקבל פרמטרים‪ _A :‬ו‪ . _B -‬בהמשך התכנית‪ ,‬אחרי‬
‫הגדרת המאקרו נוכל לכתוב‪ , SWAP(num1, num2) :‬והקדם מהדר ימיר כתיבה‬
‫זאת בשלוש הפקודות‪:‬‬
‫;‪num1 = num1 +num2; num2= num1- num2; num1 = num1 - num2‬‬
‫תוך שהוא ממיר כל ‪ _A‬ב‪ ,num1 -‬וכל ‪ _B‬ב‪ .num2 -‬התוצאה היא מעין קריאה‬
‫לפונ'‪ ,‬אולם זו לא באמת קריאה לפונ' עם הסתעפות לקוד הפונ'‪ ,‬ותוך בדיקה של‬
‫המהדר שהזימון הינו תקין; כל שנעשה פה הוא שהמחרוזת‬
‫)‪ SWAP(num1, num2‬מומרת על‪-‬ידי קדם המעבד בשלוש ההשמות‪:‬‬
‫;‪num1 = num1 +num2; num2= num1- num2; num1 = num1 - num2‬‬
‫אגב‪ ,‬שימו לב שאחרי שם המאקרו איננו כותבים נקודה‪-‬פסיק‪ ,‬שכן המאקרו אינו‬
‫פקודה בתכנית שיש לסיים בנקודה‪-‬פסיק‪ ,‬הוא הנחיה לקדם‪-‬מהדר‪.‬‬
‫נביט עתה במאקרו פשוט אחר‪:‬‬
‫‪#define DOUBLE(_A) _A+_A‬‬
‫אם נכתוב בהמשך התכנית‪:‬‬
‫; )‪cout << DOUBLE(num1‬‬
‫אזי המאקרו ייפרש ונקבל שהמהדר יקמפל את הקוד‪:‬‬
‫; ‪cout << num1 + num1‬‬
‫אך מה יקרה אם נכתוב‪:‬‬
‫; )‪cout << num2 * DOUBLE(num1‬‬
‫אזי תוצאת פרישת המאקרו תהיה‪:‬‬
‫; ‪cout << num2 * num1 + num1‬‬
‫ובשל סדר הפעולות האריתמטיות קרוב לוודאי שלא זה מה שהמתכנת רצה להשיג‪.‬‬
‫הפתרון הוא להגדיר את המאקרו באופן‪:‬‬
‫)‪#define DOUBLE(_A) (_A+_A‬‬
‫תוצאת פרישת המאקרו‪:‬‬
‫; )‪cout << num2 * DOUBLE(num1‬‬
‫תהיה‪:‬‬
‫; )‪cout << num2 * (num1 + num1‬‬
‫מין הסתם‪ ,‬התוצאה הרצויה‪.‬‬
‫לבסוף‪ ,‬אנו רשאים להגדיר את המאקרו‪:‬‬
‫\ )‪#define SWAP(_TYPE, _A, _B‬‬
‫} ;‪{_TYPE temp = _A; _A= _B; _B = temp‬‬
‫ראשית‪ ,‬אעיר שהגדרת מאקרו צריכה לשכון על שורה יחידה‪ ,‬עת המאקרו גדול מדי‬
‫אנו רשאים לחלקו למספר שורות‪ ,‬אך בסוף כל שורה עלינו לכתוב את התו‬
‫‪ (\) backslash‬שמורה למהדר להתייחס לשורה הבאה כאילו היא המשכה של השורה‬
‫הנוכחית‪ .‬כאילו לא שברנו שורה‪.‬‬
‫אם בהמשך הקוד נכתוב‪:‬‬
‫)‪SWAP(int, num1, num2‬‬
‫אזי תוצאת הפרישה של המאקרו תהיה‪:‬‬
‫} ;‪{int temp = num1; num1= num2; num2 = temp‬‬
‫‪197‬‬
‫הפרמטר ‪ _TYPE‬במאקרו מוחלף במילה ‪ ,int‬הפרמטרים ‪ _A, _B‬מוחלפים ב‪:‬‬
‫‪ num1, num2‬בהתאמה‪ ,‬והמאקרו יוצר בלוק‪ ,‬בו הוא מגדיר משתנה עזר בשם‬
‫‪ temp‬ובעזרתו מחליף בין ערכי ‪.num1, num2‬‬
‫‪ 17.2‬הנחיית הקדם מהדר ‪#ifdef‬‬
‫הנחיית המהדר‪ #ifdef :‬שולטת על קימפולו או אי קימפולו של קטע קוד כלשהו‪.‬‬
‫נראה דוגמה קטנה‪:‬‬
‫‪#ifdef SYMBOL1‬‬
‫; "!‪cout << "Wow‬‬
‫; ‪num = 0‬‬
‫‪#endif‬‬
‫עת הקדם מהדר יגיע לקטע קוד זה הוא ישאל את עצמו‪ :‬האם כבר הוגדר לי‬
‫)באמצעות פקודת ‪ (#define‬הסימן ‪) SYMBOL1‬ולא משנה איזה ערך הוא מייצג(?‬
‫במידה והתשובה לשאלה היא כן אזי שתי הפקודות‪:‬‬
‫; "!‪cout << "Wow‬‬
‫; ‪num = 0‬‬
‫תכללנה בפלט של קדם‪-‬המהדר‪ ,‬כלומר תקומפלנה על‪-‬ידי המהדר עצמו‪ .‬אם לעומת‬
‫זאת הסימן ‪ SYMBOL1‬לא הוגדר עד כה אזי שתי הפקודות הללו )ובמקרה הכללי עד‬
‫ה‪ (#endif -‬לא תשלחנה לפלט‪ ,‬ועל כן לא תקומפלנה‪ ,‬כלומר יתקבל מצב כאילו הן‬
‫לא נכללו כלל בתכנית שלנו‪.‬‬
‫נראה דוגמה שנייה‪ .‬נניח את הקוד‪:‬‬
‫‪#define A‬‬
‫{ )(‪int main‬‬
‫; ‪int num = 0‬‬
‫‪#ifdef A‬‬
‫; ‪num = 1‬‬
‫‪#endif‬‬
‫‪#ifdef B‬‬
‫; ‪num = 2‬‬
‫‪#endif‬‬
‫; ‪cout << num‬‬
‫; ‪return EXIT_SUCCESS‬‬
‫}‬
‫ונשאל‪ :‬מה יהיה פלט התכנית? התשובה‪ :‬הפלט יהיה‪ ,1 :‬שכן ההשמה‪:‬‬
‫;‪ num = 1‬תיכלל בקוד המקומפל‪ ,‬אך ההשמה‪ num= 2 ; :‬לא תיכלל‪ .‬אם‪,‬‬
‫לעומת זאת‪ ,‬נוסיף לצד ההנחיה‪ #define A :‬את ההנחיה‪) #define B :‬לפני‬
‫או אחרי ההנחיה ‪ ,#define A‬אין לכך חשיבות(‪ ,‬אזי פלט התכנית יהיה ‪.2‬‬
‫נציג דוגמה בה הנחיית המהדר עשויה להיות שימושית‪ .‬נביט בקטע הקוד‪:‬‬
‫)‪for (int div = 2; div <= sqrt(num); d++‬‬
‫‪198‬‬
‫{‬
‫‪#ifdef DEBUG‬‬
‫; ‪cout << num << div << num%div << endl‬‬
‫‪#endif‬‬
‫)‪if (num % div == 0‬‬
‫; ‪break‬‬
‫}‬
‫קטע קוד זה נכלל בפונ' הבודקת האם המספר ‪ num‬הינו ראשוני או פריק‪.‬‬
‫חשיבותו לצורך ענייננו הנוכחי היא שבמידה והסימן ‪ DEBUG‬הוגדר כבר ַל ְקדם‬
‫המהדר אזי בכל סיבוב בלולאה יבוצעו שתי פקודות‪ :‬פקודת פלט )העוזרת לנו לבחון‬
‫את ערכי המשתנים‪ ,‬ועל‪-‬ידי כך את הקורה בלולאה(‪ ,‬ופקודת התנאי‪ .‬אם‪ ,‬לעומת‬
‫זאת‪ ,‬הסימן ‪ DEBUG‬לא הוגדר עד כה לקדם המהדר‪ ,‬אזי בכל סיבוב בלולאה תבוצע‬
‫רק פקודת התנאי‪ ,‬פקודת הפלט לא תיכלל כלל בקוד שיקומפל‪.‬‬
‫וכיצד הדבר ישמש אותנו? דרך אחת היא שבזמן העבודה על התכנית‪ ,‬עת יש לבדוק‬
‫את תקינותה‪ ,‬נוסיף לפני הקוד הנ"ל את השורה‪:‬‬
‫‪#define DEBUG‬‬
‫ובכך אנו מגדירים את הסימן ‪) DEBUG‬וקובעים שיש להחליפו בשום דבר‪ ,‬אולם לכך‬
‫אין חשיבות לצורך העניין(‪ ,‬ולכן גם פקודת הפלט תופיע בקוד שלנו‪ .‬בתום העבודה‬
‫על התכנית נסיר את השורה‪ #define DEBUG :‬ועל כן בתכנית הגמורה כבר לא‬
‫תופיע פקודת הפלט‪.‬‬
‫דרך חלופית‪ ,‬פשוטה יותר‪ ,‬היא במהלך העבודה על התכנית לקמפלה ַבאופן‪:‬‬
‫‪g++ -Wall –DDEBUG my_prog.cc‬‬
‫הדגל‪ -DDEBUG :‬כולל ראשית את הסימן ‪ –D‬ובעקבותיו את המחרוזת ‪.DEBUG‬‬
‫מרכיב זה בפקודת ההידור שקול בדיוק לכתיבת‪ַ #define DEBUG :‬בקוד; כלומר‬
‫הוא מגדיר לקדם מהדר את הסימן ‪ .DEBUG‬התוצאה תהיה כמו בדרך שתוארה‬
‫קודם‪ :‬קוד התכנית יכלול גם את פקודת הפלט‪ .‬לעומת זאת‪ ,‬עת נסיים לעבוד על‬
‫התכנית‪ ,‬נקמפלה 'כרגיל'‪:‬‬
‫‪g++ -Wall my_prog.cc‬‬
‫בפקודת הידור זו איננו מגדירים את הסמל ‪ ,DEBUG‬ולכן קוד התכנית לא יכלול את‬
‫פקודת הפלט‪.‬‬
‫לפקודה ‪ #ifdef‬יש 'אחות תאומה' והיא פקודת ה‪ #ifndef :‬אשר שואלת את‬
‫השאלה ההפוכה‪ :‬האם לא הוגדר הסימן המופיע בהמשך השורה? רק אם הסימן‬
‫לא הוגדר קטע הקוד המופיע בין ההנחיה הנ"ל לבין ה‪ #endif -‬יקומפל‪.‬‬
‫בהמשך‪ ,‬עת נציג כיצד מחלקים את תכנית למספר קבצים‪ ,‬נראה שימושים נוספים‪,‬‬
‫מובהקים יותר להנחיות קדם המהדר‪. #ifdef, #ifndef :‬‬
‫‪ 17.3‬הנחיית הקדם מהדר ‪#include‬‬
‫ההנחיה‪ #include "my_file" :‬גורמת לקדם המהדר להכניס לקובץ בו מופיע‬
‫ההנחיה‪ַ ,‬במקום בו מופיעה ההנחיה‪ ,‬את הקובץ ‪ my_file‬אשר אמור לשכון‬
‫במדריך הנוכחי )אחרת יש לציין נתיב מלא לקובץ(‪.‬‬
‫נראה דוגמה‪:‬‬
‫נניח את ההנחות הבאות‪:‬‬
‫הקובץ ‪ f1‬כולל את השורה )ורק אותה‪ ,‬כלומר זהו כל הקובץ(‪:‬‬
‫‪199‬‬
‫‪I am f1‬‬
‫הקובץ ‪ f2‬כולל את השורות )ורק אותן(‪:‬‬
‫‪I am f2 1‬‬
‫"‪#include "f1‬‬
‫‪I am f2 2‬‬
‫הקובץ ‪ f3‬כולל את השורות‪:‬‬
‫‪I am f3 1‬‬
‫"‪#include "f1‬‬
‫"‪#include "f2‬‬
‫‪I am f3 2‬‬
‫כיצד יראה פלט קדם ההידור של הקובץ ‪:f3‬‬
‫ראשית‪ ,‬השורה‪ I am f3 1 :‬תופיע בפלט קדם ההידור‪ ,‬כמות שהיא‪.‬‬
‫שנית‪ ,‬השורה‪ #include "f1" :‬תגרום להכנסת הקובץ ‪ f1‬כמות שהוא‪,‬‬
‫כלומר תוסיף לפלט קדם ההידור את השורה‪.I am f1 :‬‬
‫שלישית‪ ,‬השורה‪ #include "f2" :‬תגרום להכללת הקובץ ‪ f2‬בפלט‪ ,‬ולכן תופיע‬
‫בפלט קדם ההידור השורה‪ ,I am f2 1 :‬אחריה תופעל פעולת ההכללה של ‪f1‬‬
‫כפי שמופיעה ב‪ ,f2 -‬ולכן תופיע בפלט השורה‪ ,I am f1 :‬ולבסוף תופיע השורה‪I :‬‬
‫‪.am f2 2‬‬
‫רביעית תופיע בפלט קדם ההידור השורה‪.I am f3 2 :‬‬
‫התוצאה הכוללת תהיה‪:‬‬
‫‪1‬‬
‫‪1‬‬
‫‪2‬‬
‫‪2‬‬
‫‪200‬‬
‫‪f3‬‬
‫‪f1‬‬
‫‪f2‬‬
‫‪f1‬‬
‫‪f2‬‬
‫‪f3‬‬
‫‪am‬‬
‫‪am‬‬
‫‪am‬‬
‫‪am‬‬
‫‪am‬‬
‫‪am‬‬
‫‪I‬‬
‫‪I‬‬
‫‪I‬‬
‫‪I‬‬
‫‪I‬‬
‫‪I‬‬
‫עתה נניח את ההנחות הבאות‪:‬‬
‫הקובץ ‪ f1‬כולל את ההגדרה‪:‬‬
‫{ ‪struct S1‬‬
‫; ‪double _x‬‬
‫; }‬
‫הקובץ ‪ f2‬כולל את השורות הבאות‪:‬‬
‫"‪#include "f1‬‬
‫{ ‪struct S2‬‬
‫; ‪struct S1 s1, s2‬‬
‫; }‬
‫הקובץ ‪ f3‬כולל את הקוד‪:‬‬
‫"‪#include "f1‬‬
‫"‪#include "f2‬‬
‫כיצד יראה פלט קדם ההידור של הקובץ ‪?f3‬‬
‫ראשית יוכלל בו ‪ f1‬ולכן בפלט יופיע‪:‬‬
‫{ ‪struct S1‬‬
‫; ‪double _x‬‬
‫; }‬
‫שנית‪ ,‬יוכלל בו ‪' f2‬שמביא איתו' שוב את ‪ f1‬ולכן לפלט יתווסף‪:‬‬
‫{‬
‫; ‪_x‬‬
‫{‬
‫; ‪S1 s1, s2‬‬
‫‪struct S1‬‬
‫‪double‬‬
‫; }‬
‫‪struct S2‬‬
‫‪struct‬‬
‫; }‬
‫כלומר הפלט יכלול את ההגדרה }…{ ‪ struct S1‬פעמיים‪.‬‬
‫אם פלט זה אמור לעבור לשלב ההידור המרכזי‪ ,‬כפי שצפוי שיקרה‪ ,‬אזי המהדר‬
‫'יצעק' עלינו שאנו מגדירים את ‪ struct S1‬פעמיים )גם אם בשתי הפעמים זו‬
‫בדיוק אותה הגדרה(‪.‬‬
‫כיצד נמנע מתקלה בלתי רצויה זאת? התשובה היא‪ :‬בעזרת פקודות ‪.#ifdef‬‬
‫נסביר‪ :‬את הקובץ ‪ f1‬נשנה באופן הבא‪:‬‬
‫‪#ifndef STRUCT_S1‬‬
‫‪#define STRUCT_S1‬‬
‫{ ‪struct S1‬‬
‫; ‪double _x‬‬
‫; }‬
‫‪#endif‬‬
‫אנו אומרים ש‪':‬אנו מגנים על המבנה ‪ s1‬מפני הכללה כפולה'‪..‬‬
‫נסביר מה הועילו חכמים בתקנתם‪ :‬כיצד יראה עתה פלט קדם ההידור של הקובץ‬
‫‪ :f3‬ראשית‪ ,‬ההנחיה "‪ #include "f1‬תביא להכללת הקובץ ‪ f1‬בפלט‪ .‬עת קדם‬
‫המהדר יעבור על ‪ f1‬הוא ישאל את עצמו‪ :‬האם לא הוגדר לי עדיין )בריצתי‬
‫הנוכחית( הסימן ‪ ? STRUCT_S1‬והתשובה תהיה‪' :‬כן‪ .‬עד כה לא הוגדר לקדם‬
‫המהדר הסימן ‪ .'STRUCT_S1‬על כן השורות שעד ה‪ #endif -‬תשלחנה לפלט‪ ,‬וכן‬
‫הסימן ‪ STRUCT_S1‬כבר יוגדר לקדם המהדר‪.‬‬
‫עת נתקדם בקובץ ‪ f3‬לשורה השנייה‪ #include "f2" :‬נפנה לכלול את הקובץ‬
‫‪ .f2‬בקובץ ‪ f2‬נמצא את ההנחיה‪ ,#include f1 :‬ולכן קדם המהדר יפנה לקובץ‬
‫‪ .f1‬הוא ישאל את עצמו שוב‪ :‬האם לא הוגדר לי עדיין )בריצתי הנוכחית( הסימן‬
‫‪201‬‬
‫‪ ? STRUCT_S1‬הפעם התשובה תהיה 'לא‪ .‬כבר הוגדר לי הסימן ‪ .'STRUCT_S1‬על‬
‫כן כל הקוד עד ה‪ #endif -‬לא ישלח לפלט‪ ,‬בפרט הגדרת ה‪ struct S1 -‬לא תופיע‬
‫פעמיים בפלט קדם ההידור‪ .‬קדם המהדר יחזור לקובץ ‪ ,f2‬ויוסיף את הגדרת‬
‫המבנה ‪ struct S2‬לפלט שלו‪.‬‬
‫התוצאה המרכזית‪ ,‬כאמור‪ ,‬היא שבתוצאת קדם ההידור‪ ,‬המבנה ‪ S1‬מוגדר פעם‬
‫יחידה‪ ,‬ועל כן המהדר כבר לא יתלונן על הגדרה כפולה של המבנה‪ .‬נחזור לנושא‬
‫בהמשך‪ ,‬עם תכנית שלמה‪.‬‬
‫כמה מכם ודאי שאלו את עצמם מה הקשר בין הפקודה‪:‬‬
‫>‪#include <iostream‬‬
‫‪ #include‬כפי שמוסברת בסעיף‬
‫שאנו כותבים בכל תכנית שלנו‪ ,‬לבין הנחיית ה‪-‬‬
‫זה? התשובה היא‪ ,‬כמובן‪ ,‬שמדובר באותו דבר‪ .‬ההבדל היחיד הוא שאת הקובץ‬
‫‪ iostream‬לא אנחנו יצרנו‪ ,‬ובהתאמה הוא אינו שוכן בתיקיה שלנו‪ .‬סוגרי הזווית‬
‫)> <( שעוטפים את שם הקובץ מורים לקדם המהדר שקובץ זה עליו לחפש לא‬
‫במדריך הנוכחי‪ ,‬אלא במדריך )המוכר לו( הכולל את כל קובצי ה‪include -‬‬
‫הסטנדרטיים‪ .‬מעבר לכך‪ ,‬הקובץ ‪ iostream‬הוא קובץ הכולל הצהרות חיוניות‪,‬‬
‫אשר הקדם מהדר 'דוחף' בקובץ שלנו‪ ,‬בדיוק כפי שתיארנו את פעולת ה‪.include -‬‬
‫ההצהרות המופיעות בקובץ ‪ iostream‬גורמות למהדר להכיר את פקודות‬
‫הספרייה הסטנדרטית‪ cin, cout, ... :‬ולא 'לצעוק' עלינו עת אנו כוללים‬
‫פקודות אלה )שאינן חלק משפת ‪ c++‬הבסיסית( בתכניתנו‪.‬‬
‫בהתאמה‪ ,‬אנו יכולים לצור קובץ בשם ‪ . my_includes.h‬בהזדמנות זאת אעיר‬
‫שהסיומת ‪ .h‬שמציינת ‪ header‬או 'כותר' בעברית‪ ,‬היא סיומת מקובלת לקבצים‬
‫הכוללים הגדרות שונות; קבצים אשר עושים להם ‪ ,include‬ולא מקמפלים אותם‬
‫)כלומר לעולם לא נכתוב‪ g++ -Wall my_file.h :‬עבור ‪ my_file.h‬שהינו‬
‫קובץ כותר‪ ,‬הכולל הצהרות מועילות שונות‪ (.‬הקובץ ‪ my_include.h‬עשוי‬
‫להראות‪:‬‬
‫>‪<iostream‬‬
‫>‪<iomanip‬‬
‫>‪<cmath‬‬
‫>‪<cstdlib‬‬
‫‪#include‬‬
‫‪#include‬‬
‫‪#include‬‬
‫‪#include‬‬
‫; ‪using std::cin‬‬
‫; ‪using std::cout‬‬
‫; ‪using std::endl‬‬
‫בכל תכנית שלנו נוכל לעשות ‪ include‬לקובץ זה‪ ,‬והדבר יחסוך לנו את הצורך‬
‫לכתוב שוב ושוב‪ ,‬בראש כל תכנית‪ ,‬את השורות הללו‪ .‬אעיר שאני לא בטוח שאני‬
‫ממליץ על גישה זאת‪ ,‬שכן לבצע ‪ include‬בכל מקרה‪ ,‬לקבצים בהם אין לנו צורך‬
‫מעמיס על המהדר‪ ,‬ומאריך את משך ההידור; גודלה של התכנית הנוצרת בסופו של‬
‫דבר‪ ,‬התכנית בשפת מכונה‪ ,‬לא יגדל‪ ,‬אולם גם הכבדה שלא לצורך על המהדר אינה‬
‫דבר ראוי‪ .‬לכן‪ ,‬למרות שהרעיון שהצגתי כאן הינו אפשרי ותקין‪ ,‬אני לא בטוח שהוא‬
‫גם מומלץ‪.‬‬
‫‪ 17.4‬חלוקת תכנית לקבצים‪ ,‬דוגמה א'‬
‫‪ 17.4.1‬הקבצים שנכללים בתכנית‬
‫נדון בתכנית הפשוטה הבאה‪:‬‬
‫‪202‬‬
//---------- include and using sections -------#include <iostream>
using std::cin ;
using std::cout ;
//-------------- prototypes section -----------void read_int(int &n) ;
void print_int(int n) ;
int add_int(int n1, int n2) ;
int sub_int(int n1, int n2) ;
//-------------- main -----------int main()
{
int num1, num2,
result ;
read_int(num1) ;
read_int(num2) ;
result = add_int(num1, num2) ;
print_int(result) ;
result = sub_int(num1, num2) ;
print_int(result) ;
return(EXIT_SUCCESS) ;
}
//-----------------------------------void read_int(int &n)
{
cin >> n ;
}
//-----------------------------------void print_int(int n)
{
cout << n << " " ;
}
//-----------------------------------int add_int(int n1, int n2)
{
return n1+n2 ;
}
//-----------------------------------void sub_int(int n1, int n2)
{
Return n1 – n2 ;
}
//------------------------------------
203
‫מדובר בתכנית קצרה ופשוטה‪ ,‬אולם לצורך דיוננו 'נשחק בכאילו'—כאילו מדובר‬
‫בתכנית גדולה‪ ,‬ועל כן ברצוננו לחלקה למספר קבצים‪ .‬נתחיל בכך שנחלק את‬
‫התכנית לשלושה קבצים‪:‬‬
‫א‪ .‬קובץ שייקרא ‪ ex17g.cc‬ויכלול את התכנית הראשית‪.‬‬
‫ב‪ .‬קובץ שייקרא ‪ io.cc‬ויכלול את הפונ' העוסקות בקלט פלט )‪ read_int‬ו‪-‬‬
‫‪(print_int‬‬
‫ג‪ .‬קובץ שייקרא ‪ arithmetic.cc‬ויכלול את הפונ' העוסקות באריתמטיקה‬
‫)‪ add_int‬ו‪.(sub_int -‬‬
‫החלוקה מלמדת אתכם על האופן בו אנו מחלקים תכנית לקבצים‪ :‬את אוסף הפונ'‬
‫העוסקות בהיבט מסוים אנו מרכזים בקובץ אחד‪.‬‬
‫הקובץ ‪ , io.cc‬כאמור‪ ,‬יכלול את שתי הפונ' ‪ read_int‬ו‪ .sub_int -‬פונ' אלה‬
‫עושות שימוש בפקודות ‪ cin, cout‬ועל כן יש צורך לכלול בקובץ גם את פקודות‬
‫ה‪ include -‬וה‪ using -‬הדרושות‪ .‬על כן תכולתו של ‪ io.cc‬הינה‪:‬‬
‫>‪#include <iostream‬‬
‫; ‪using std::cin‬‬
‫; ‪using std::cout‬‬
‫‪//-----------------------------------‬‬‫)‪void read_int(int &n‬‬
‫{‬
‫; ‪cin >> n‬‬
‫}‬
‫‪//-----------------------------------‬‬‫)‪void print_int(int n‬‬
‫{‬
‫; " " << ‪cout << n‬‬
‫}‬
‫‪//------------------------------------‬‬
‫הקובץ ‪ arithmetic.cc‬יכיל את שתי הפונ' האריתמטיות‪ ,‬פונ' אלה אינן עושות‬
‫שימוש בפונ' ספריה כלשהן‪ ,‬ולכן בקובץ זה אין צורך בכל פקודת ‪ include‬או‬
‫‪ .using‬תכולתו של ‪ arithmetic.cc‬הינה לפיכך‪:‬‬
‫‪//-----------------------------------‬‬‫)‪int add_int(int n1, int n2‬‬
‫{‬
‫; ‪return n1+n2‬‬
‫}‬
‫‪//-----------------------------------‬‬‫)‪int sub_int(int n1, int n2‬‬
‫{‬
‫; ‪return n1 – n2‬‬
‫}‬
‫‪//------------------------------------‬‬
‫בקובץ ‪ ex17g.cc‬שוכנת התכנית הראשית‪ .‬התכנית הראשית אינה מזמנת פונ'‬
‫ספריה‪ ,‬אולם משתמשת ב‪ EXIT_SUCCESS -‬המוגדר ב‪ .cstdlib -‬התכנית‬
‫הראשית מזמנת את ארבע הפונ' שכתבנו‪ ,‬ועל כן‪ ,‬על מנת שהמהדר לא 'יצעק' עלינו‬
‫‪204‬‬
‫בעת הידורה‪ ,‬יש לכלול בה את ההצהרה על הפרוטוטיפים של פונ' הללו‪ .‬תכולתו של‬
‫‪ ex17g.cc‬הינה לפיכך‪:‬‬
‫>‪#include <cstdlib‬‬
‫‪//-------------- prototypes section -----------‬‬‫; )‪void read_int(int &n‬‬
‫; )‪void print_int(int n‬‬
‫; )‪int add_int(int n1, int n2‬‬
‫; )‪int sub_int(int n1, int n2‬‬
‫‪//-------------- main -----------‬‬‫)(‪int main‬‬
‫{‬
‫‪int num1, num2,‬‬
‫; ‪result‬‬
‫; )‪read_int(num1‬‬
‫; )‪read_int(num2‬‬
‫; )‪result = add_int(num1, num2‬‬
‫; )‪print_int(result‬‬
‫; )‪result = sub_int(num1, num2‬‬
‫; )‪print_int(result‬‬
‫; )‪return(EXIT_SUCCESS‬‬
‫}‬
‫‪ 17.4.2‬הידור התכנית‬
‫עד כאן הכנו את שלושת הקבצים‪ .‬עתה נשאל‪ :‬כיצד 'נתפור' את שלושת הקבצים‬
‫לכדי תכנית שלמה אחת‪ .‬נציג מספר דרכים לעשות זאת בעזרת המהדר ‪g++‬‬
‫)בסביבת יוניקס(‪.‬‬
‫אפשרות א'‪ ,‬הפשוטה ביותר‪ ,‬היא לכתוב‪:‬‬
‫‪g++ -Wall ex17g.cc io.cc arithmetic.cc –o ex17g‬‬
‫בשיטת הידור זאת אנו מציינים בפקודת ההידור את שלושת הקבצים‪ .‬המהדר‬
‫יעבור על שלושתם יקמפלם‪ ,‬ויכרוך אותם לכדי תכנית ניתנת להרצה אחת‪ ,‬אשר על‪-‬‬
‫פי הוראתנו תשכון בקובץ ‪.ex17g‬‬
‫חסרונה של גישה זאת הוא שאם עתה עלינו לשנות רק את אחד הקבצים אנו‬
‫מקמפלים מחדש גם את שני הקבצים האחרים‪ ,‬ובכך משקיעים זמן ועבודה‬
‫מיותרים של המהדר‪ .‬היינו מעדיפים שאם רק אחד הקבצים השתנה אזי נהדר‬
‫)במובן הצר של המילה( רק את הקובץ שהשתנה‪ ,‬ואז נכרוך )נבצע לינקינג( של‬
‫שלושת הקבצים לכדי ‪ executable‬חדש‪.‬‬
‫כדי להשיג מטרה רצויה זאת ננקוט בגישה הבאה‪ ,‬שהינה אפשרות ב' לקימפול‬
‫התכנית‪ .‬לפי שיטה זאת אנו‪ ,‬ראשית‪ ,‬מקמפלים )במובן הצר והמדויק של המילה(‬
‫כל אחד משלושת הקבצים‪ ,‬ומקבלים את תרגום הקובץ לשפת מכונה; אנו אומרים‬
‫שאנו מקבלים קובץ ‪ .object‬בשלב השני אנו כורכים את שלושת קובצי ה‪-‬‬
‫‪205‬‬
‫‪ object‬לכדי תכנית שלמה ניתנת להרצה )‪ g++ .(executable‬יעשה עבורנו הן‬
‫את עבודת הקימפול‪ ,‬והן את עבודת הלינקינג‪ ,‬והדבר יתבצע באופן הבא‪ :‬הפקודה‪:‬‬
‫‪g++ -Wall –c io.cc‬‬
‫מפעילה את ‪ g++‬ומורה לו‪ ,‬באמצעות הדגל ‪ –c‬להסתפק רק בקומפילציה )במובן‬
‫הצר של המילה(‪ ,‬כלומר רק לתרגם את התכנית לשפת מכונה‪ ,‬בלי להמשיך לשלב‬
‫הלינקינג‪ ,‬ולנסות לייצר תכנית ניתנת להרצה )דבר שלא ניתן לעשות‪ ,‬למשל מפני‬
‫שאין לנו בקובץ זה כל תכנית ראשית‪(.‬‬
‫באופן דומה‪ ,‬נקמפל את שני הקבצים האחרים‪:‬‬
‫‪g++ -Wall –c arithmetic.cc‬‬
‫‪g++ -Wall –c ex17g.cc‬‬
‫‪io.o,‬‬
‫שלוש פקודות הידור אלה ייצרו שלושה קובצי ‪:object code‬‬
‫‪) arithmetic.o, ex17g.o‬המהדר בעצמו יודע שעת יש רק לקמפל קובץ‪,‬‬
‫ולייצר ממנו קובץ ‪ object‬יש לתת לקובץ ה‪ object -‬אותו שם כמו לקבוץ‬
‫המקור‪ ,‬עם סיומת ‪.(o‬‬
‫את שלושת קובצי ה‪ object -‬נכרוך יחד לכדי תכנית שלמה ניתנת להרצה באופן‬
‫הבא‪:‬‬
‫‪g++ io.o arithmetic.o ex17g.o –o ex17g‬‬
‫שוב אנו עושים שימוש ב‪ ,g++ -‬אולם הפעם כלינקר )ולא כקומפיילר(‪ .‬אנו מורים לו‬
‫לכרוך את שלושת הקבצים‪ io.o arithmetic.o ex17g.o :‬לכדי תכנית‬
‫שלמה‪ ,‬אשר תשכון בקובץ‪) ex17g :‬הסיומת ‪ –o ex17g‬מורה שזה יהיה שמו של‬
‫הקובץ שיכיל את התכנית הניתנת להרצה‪ .‬לו היינו משמיטים את סיומת‪ ,‬הייתה‬
‫התכנית הניתנת להרצה שוכנת תמיד בקובץ‪ ,‬בעל השם המוזר‪(.a.out ,‬‬
‫אם עתה אנו משנים את אחד מקובצי המקור‪ ,‬אזי נקמפל שוב )במובן הצר של‬
‫המילה( רק קובץ זה‪ ,‬ואחר‪-‬כך נכרוך יחדיו שוב את שלושת קובצי ה‪ .object -‬בכך‬
‫נחסוך לנו את הצורך לקמפל שוב את כל שלושת הקבצים‪ ,‬עת רק אחד מהם‬
‫השתנה‪.‬‬
‫עת הפרויקט שלנו כולל שלושה קבצים קטנים‪ ,‬קל יחסית לעקוב אחר השינויים‬
‫שאנו עורכים בכל קובץ‪ ,‬ולקמפל בדיוק את מה שצריך )לא פחות‪ ,‬ולא יותר(‪ .‬אולם‬
‫עת האפליקציה )התכנית( שלנו כוללת מספר גדול של קבצים אזי יקשה עלינו לזכור‬
‫אילו קבצים השתנו‪ ,‬ולפיכך איזה קבצים יש לקמפל‪ .‬כאן באה לעזרתנו פקודת ה‪-‬‬
‫‪ Shell‬של יוניקס הנקראת ‪.make‬‬
‫פקודת ה‪ make -‬עושה שימוש בקובץ הנקרא ‪) makefile‬זה בדיוק צריך להיות‬
‫שמו של הקובץ; ללא סיומת(‪ .‬הקובץ ‪ makefile‬מתאר‪ ,‬לפקודת ה‪ ,make -‬את‬
‫תהליך ההידור שעליה לבצע‪ .‬אציג כאן צורה בסיסית ביותר לבניית קובץ ה‪-‬‬
‫‪ ,makefile‬המתעניינים מוזמנים‪ ,‬כמובן‪ ,‬להרחיב את ידיעותיהם בנושא‪.‬‬
‫‪206‬‬
‫תכולת הקובץ היא‪:‬‬
‫‪ex17g: ex17g.o io.o arithmetic.o‬‬
‫‪g++ ex17g.o io.o arithmetic.o –o ex17g‬‬
‫"‪echo "a new ex17g was created‬‬
‫‪ex17g‬‬
‫‪ex17g.o: ex17g.cc‬‬
‫‪g++ -Wall –c ex17g.cc‬‬
‫‪io.o: io.cc‬‬
‫‪g++ -Wall –c io.cc‬‬
‫‪arithmetic.o: arithmetic.cc‬‬
‫‪g++ -Wall –c arithmetic.cc‬‬
‫אדגיש כי בשורות שאינן מתחילות בעמודה הראשונה )כדוגמת השורה השנייה‪,‬‬
‫השלישית‪ ,‬הרביעית‪ ,‬השישית וכולי( יש להקיש על מקש ה‪ tab -‬בתחילת השורה‬
‫)ולא על מקשי רווח(‪.‬‬
‫נסביר את הקובץ‪ :‬כאמור‪ ,‬הקובץ הנ"ל הינו הקלט לפקודת ה‪ .make -‬הפקודה‬
‫עוברת על הקובץ משורתו הראשונה ב‪'-‬רברס'‪ .‬היא מתחילה בקובץ ‪ ex17g‬כפי‬
‫שמופיע משמאל לנקודותיים בשורה הראשונה‪ .‬מימין לנקודותיים מתוארים‬
‫)שלושת( הקבצים בהם קובץ זה תלוי—אם אחד הקבצים הללו עודכן במועד‬
‫מאוחר יותר מהמועד בו עודכן ‪ ex17g‬אזי יש לבצע את הפקודות המופיעות בשלוש‬
‫השורות הבאות )שכל אחת מהן מתחילה‪ ,‬כאמור‪ ,‬ב‪ .(tab -‬שלוש הפקודות הן‬
‫ראשית לינקינג של שלושת קובצי ה‪ object -‬לכדי תכנית ניתנת להרצה‪ ,‬ומעבר‬
‫לכך‪ ,‬כדי לסבר את העין‪ ,‬הוספנו שיש לעשות עוד שני דברים‪ :‬לשלוח למסך פלט‬
‫האומר ‪ ,a new ex17g was created‬ולסיום גם להריץ את התכנית‪ .‬בד"כ‪,‬‬
‫קובץ ה‪ makefile -‬לא יכיל הרצה של התכנית‪ ,‬וגם הודעה למסך ספק אם יש‬
‫צורך להוציא‪.‬‬
‫כאמור‪ ,‬פקודת ה‪ make -‬מתקדמת על הקובץ מראשיתו כלפי סופו‪ :‬אחרי שהיא‬
‫גילתה ש‪ ex17g :‬תלוי ב‪ ex17g.o io.o arithmetic.o :‬היא עוברת לבדוק‬
‫עבור כל אחד ואחד משלושת הקבצים הללו במי הוא תלוי‪ .‬כך היא מגלה‪ ,‬למשל‪,‬‬
‫שהקובץ ‪ io.o‬תלוי ב‪) io.cc -‬זאת מורה לה השורה‪ (io.o: io.cc :‬ועל כן אם‬
‫‪ io.cc‬מאוחר משל ‪ io.o‬אזי יש לבצע‪:‬‬
‫תאריך עדכונו של‬
‫‪.g++ -Wall –c io.cc‬‬
‫‪207‬‬
‫לכן פקודת ה‪ make -‬ראשית תהדר את ‪ io.cc‬ותייצר ‪ io.o‬חדש‪ ,‬ורק שנית‬
‫תכרוך את ‪ io.o‬החדש עם קובצי האובג'קט האחרים לכדי ‪ executable‬חדש‪.‬‬
‫כלומר‪ ,‬למרות שבקובץ פקודת הלינקינג מופיעה בשורה קודמת לפקודת ההידור‪,‬‬
‫ַבפועל‪ ,‬בשל ההתקדמות 'ברברס'‪ ,‬פקודת ההידור תבוצע לפני פקודת הכריכה‪.‬‬
‫פקודת ה‪ make -‬למעשה מייצרת מעין 'עץ תלויות' שבמקרה שלנו נראה באופן‬
‫הבא‪:‬‬
‫‪ex17g‬‬
‫‪g++ ex17g.o io.o arithmetic.o –o ex17g‬‬
‫‪arithmetic.o‬‬
‫‪g++ -Wall –c arithmetic.cc‬‬
‫‪arithmetic.c‬‬
‫‪ex17g.o‬‬
‫‪o.io‬‬
‫‪g++ -Wall –c io.cc‬‬
‫‪io.c‬‬
‫‪g++ -Wall –c ex17g.cc‬‬
‫‪ex17g.c‬‬
‫שורש העץ הוא התכנית שיש לייצר‪ ,‬כפי שמופיעה משמאל לנקודותיים בשורה‬
‫הראשונה‪ .‬חץ מצומת אחד לשני מעיד שהצומת השני תלוי בצומת הראשון‪ ,‬ואם‬
‫הצומת הראשון עדכני יותר אזי יש לעדכן גם את הצומת השני‪ .‬הפעולה שיש לבצע‬
‫כתובה לצד החץ או החיצים‪ .‬פקודת ה‪ make -‬בונה את העץ מהשורש כלפי העלים‪,‬‬
‫אך מבצעת את הפעולות מהעלים כלפי השורש )כפי שהגיוני לעשות(‪.‬‬
‫‪ 17.4.3‬שימוש בקובצי כותר )‪(header‬‬
‫נמשיך בחלוקת התכנית שלנו לקבצים‪ .‬כאמור‪ ,‬אחת המטרות של חלוקת התכנית‬
‫לקבצים היא שאם בהמשך נרצה לכתוב תכנית הזקוקה לפונקציות המתמטיות‬
‫שכתבנו )‪ (add_int, sub_int‬נוכל לצרף פונקציות אלה בנקל לאותה תכנית‪ ,‬ע"י‬
‫שנכרוך את הקובץ ‪ add_sub.o‬עם קובצי האובג'קט האחרים שירכיבו את אותה‬
‫תכנית‪ .‬נניח שבאותה תכנית קיים גם הקובץ ‪ .prog.cc‬נניח שבקובץ זה ברצוננו לזמן‬
‫את הפונ'‪ .add_int, sub_int :‬על כן עלינו להצהיר על הפרוטוטיפ שלהן באותו קובץ‪.‬‬
‫כדי להקל עלינו בכך‪ ,‬נהוג לצרף לכל קובץ מקור‪ ,‬הכולל הגדרה של פונ' )כדוגמת‬
‫‪ (add_sub.cc‬גם קובץ כותר אשר כולל הצהרות על הפרוטוטיפים של הפונקציות‬
‫הנכללות בקובץ המקור‪) .‬נהוג ששמו של קובץ הכותר מסתיים ב‪, .h :‬האות ‪ h‬היא‬
‫עבור‪.(header :‬‬
‫הדוגמה שלנו‪ ,‬כוללת את קובץ המקור ‪:io.cc‬‬
‫>‪#include <iostream‬‬
‫; ‪using std::cin‬‬
‫‪208‬‬
using std::cout ;
//-----------------------------------void read_int(int &n)
{
cin >> n ;
}
//-----------------------------------void print_int(int n)
{
cout << n << " " ;
}
//------------------------------------
:‫ שיראה‬io.h ‫עתה נכלול בתכנית גם את קובץ הכותר‬
void read_int(int &n) ;
void print_int(int n) ;
‫ עתה נוסיף‬.arithmetic.cc ‫ התכנית שלנו כוללת את קובץ המקור‬,‫באופן דומה‬
:arithmetic.h ‫לתכנית את קובץ הכותר‬
int add_int(int n1, int n2) ;
int sub_int(int n1, int n2) ;
‫ כבר לא תמנה את הפרוטוטיפים של ארבע הפונ' הללו באופן‬,‫התכנית הראשית‬
:‫ התכנית הראשית תראה‬.‫ אלא תכליל את קובצי הכותר המתאימים‬,‫מפורש‬
#include <cstdlib>
#include "io.h"
#include "arithmetic.h"
//-------------- main -----------int main()
{
int num1, num2,
result ;
read_int(num1) ;
read_int(num2) ;
result = add_int(num1, num2) ;
print_int(result) ;
result = sub_int(num1, num2) ;
print_int(result) ;
return(EXIT_SUCCESS) ;
}
‫( יתבצע‬make ‫ באופן ישיר )שלא באמצעות‬,‫הידור התכנית במתכונתה הנוכחית‬
:‫בדיוק כמו קודם; דבר לא משתנה‬
g++ -Wall –c io.cc
g++ -Wall –c arithmetic.cc
g++ -Wall –c ex17g.cc:
209
‫‪g++ io.o arithmetic.o ex17g.o –o ex17g‬‬
‫עת אנו משתמשים בפקודת ה‪ make -‬יש לתקן את קובץ ה‪ makefile -‬כך שהוא‬
‫יורה שגם עת חל שינוי בקובץ כותר כלשהו יש לקמפל את קובצי המקור אליהם‬
‫קובץ הכותר מתייחס )למשל‪ ,‬אם חל שינוי ב‪ io.h -‬אזי יש לקמפל את ‪,(io.cc‬‬
‫וכן יש לקמפל את הקבצים בהם נכלל קובץ הכותר המתאים )בדוגמה שלנו‪ ,‬עת חל‬
‫שינוי ב‪ io.h -‬יש לקמפל גם את ‪ ex17g.cc‬הכולל אותו(‪ .‬לכן קובץ ה‪-‬‬
‫‪ makefile‬יראה‪:‬‬
‫‪ex17g: ex17g.o io.o arithmetic.o‬‬
‫‪g++ ex17g.o io.o arithmetic.o –o ex17g‬‬
‫"‪echo "a new ex17g was created‬‬
‫‪ex17g‬‬
‫‪ex17g.o: ex17g.cc io.h arithmetic.h‬‬
‫‪g++ -Wall –c ex17g.cc‬‬
‫‪io.o: io.cc io.h‬‬
‫‪g++ -Wall –c io.cc‬‬
‫‪arithmetic.o: arithmetic.cc arithmetic.h‬‬
‫‪g++ -Wall –c arithmetic.cc‬‬
‫נסביר‪ :‬שינוי ב‪ io.h -‬מעיד על שינוי בפרוטוטיפים של הפונ' הממומשות ב‪-‬‬
‫‪ ,io.c‬על כן יש לקמפל קובץ זה מחדש‪ .‬באופן דומה‪ ,‬שינוי בקובץ זה אשר נכלל‬
‫בקובץ ‪ ex17g.cc‬משמעו‪ ,‬מבחינת המהדר‪ ,‬שינוי בקובץ ‪ ,ex17g.cc‬ולכן גם את‬
‫הקובץ האחרון יש לקמפל‪ .‬כמובן שקימפול כל אחד מהקבצים הללו יביא גם‬
‫לכריכה מחדש של כלל קובצי האובג'קט לכדי תכנית ניתנת להרצה חדשה‪.‬‬
‫‪ 17.5‬חלוקת תכנית לקבצים‪ ,‬דוגמה ב'‪ :‬תרומתה של הנחיית‬
‫המהדר ‪ifndef‬‬
‫נניח כי עתה עלינו לכתוב תכנית המטפלת בנקודות‪ ,‬ובמלבנים אשר צלעותיהם‬
‫מקבילות לצירים‪ .‬לשם כך‪ ,‬בפרט עלינו להגדיר את המבנים הבאים‪:‬‬
‫{ ‪struct Point‬‬
‫; ‪double _x, _y‬‬
‫}‬
‫נקודה אנו מייצגים באמצעות הקואורדינאטות שלה‪ ,‬ומלבן באמצעות פינתו‬
‫השמאלית העליונה‪ ,‬ופינתו הימנית התחתונה‪:‬‬
‫{ ‪struct Rectangle‬‬
‫; ‪struct Point _top_left, _bottom_right‬‬
‫}‬
‫כמו כן‪ ,‬יש להגדיר פונ' שונות אשר פועלות על נקודות ועל מלבנים‪.‬‬
‫מעבר לאלה תהייה לנו תכנית ראשית אשר תשתמש בנקודות ובמלבנים‪.‬‬
‫במצב זה התכנית שלנו תורכב מחמשת הקבצים הבאים‪:‬‬
‫א‪ .‬קובץ כותר בשם ‪ Point.h‬אשר כולל את הגדרת המבנה ‪ ,Point‬ואת ההצהרה על‬
‫הפונ' הפועלות על נקודות‪.‬‬
‫ב‪ .‬קובץ מקור בשם ‪ Point.cc‬הכולל את מימוש הפונ' אשר עליהן הצהרנו ב‪:‬‬
‫‪ .Point.h‬מכיוון שהפונ' עושות שימוש בהגדרת המבנה ‪ Point‬אזי בקובץ זה נכלול‬
‫)בין היתר( את הקובץ ‪Point.h‬‬
‫‪210‬‬
‫ג‪ .‬קובץ כותר בשם ‪ Rectangle.h‬הכולל את הגדרת המבנה ‪ ,Rectangle‬ואת ההצהרה‬
‫על הפונ' הפועלות על מלבנים‪ .‬מכיוון שהגדרת מלבן‪ ,‬כפי שראינו‪ ,‬מסתמכת על‬
‫הגדרת נקודה אזי קובץ זה יכלול את הקובץ ‪.Point.h‬‬
‫ד‪ .‬קובץ מקור בשם ‪ Rectangle.cc‬הכולל את מימוש הפונ' עליהן הצהרנו ב‪:‬‬
‫‪ .Rectangle.h‬מכיוון שהפונ' עושות שימוש בהגדרת המבנה ‪ Rectangle‬אזי בקובץ‬
‫זה נכלול )בין היתר( את הקובץ ‪.Rectangle.h‬‬
‫ה‪ .‬קובץ בשם ‪ my_prog.cc‬בו תשכון התכנית הראשית‪ .‬מכיוון שהתכנית הראשית‬
‫תגדיר משתנים מהטיפוסים ‪ Point, Rectangle‬אזי עלינו לכלול בקובץ ‪my_prog.cc‬‬
‫את ‪ Point.h‬ואת ‪ .Rectangle.h‬נעיר כאן שמישהו עשוי לטעון‪ ,‬ובצדק‪ ,‬שאין צורך‬
‫לכלול בקובץ זה גם את ‪ Point.h‬שכן ‪ Rectangle.h‬כבר מכליל בתוכו קובץ זה‪ .‬זה‬
‫נכון‪ ,‬אולם פעמים רבות מתכנתים שונים כותבים חלקים שונים בתכנית; )כפי‬
‫שעוד נרחיב בהמשך( המתכנת שמקודד את התכנית הראשית עשוי שלא לדעת‬
‫שמלבן מיוצג באמצעות שתי נקודות‪ ,‬ועל כן ש‪ Rectangle.h -‬כבר הכליל את‬
‫‪ .Point.h‬המתכנת שכותב את התכנית הראשית יודע שהוא משתמש הן בנקודות‬
‫והן במלבנים‪ ,‬ועל כן הוא‪ ,‬באופן טבעי‪ ,‬מכליל את שני קובצי הכותר‪ .‬בבעיות‬
‫שהדבר גורם‪ ,‬והפתרונות להן‪ ,‬נדון בהמשך‪) .‬למעשה הבעיות והפתרונות כבר‬
‫נסקרו עד דנו בהנחיית המהדר ‪(.#ifndef‬‬
‫נציג עתה את הקבצים השונים‪ .‬אתחיל בכך שאציג את הקבצים כפי שמתכנת שלמד‬
‫את החומר עד כה‪ ,‬אך לא הפנים את נושא ה‪ ,#ifndef -‬היה כותב אותם‪ .‬אחר כך‬
‫אסביר את בעיית הקומפילציה שמתעוררת עת כותבים את התכנית באופן זה‪,‬‬
‫ולבסוף אסביר מה יש להוסיף לתכנית על מנת להתגבר על הבעיה‪.‬‬
‫‪ 17.5.1‬הקובץ ‪Point.h‬‬
‫כפי שאמרנו‪ ,‬נניח שאנו מגדירים נקודה באופן הבא‪:‬‬
‫{ ‪struct Point‬‬
‫; ‪double _x, _y‬‬
‫}‬
‫עוד נניח שעל נקודה אנו מגדירים את הפעולות הבאות‪:‬‬
‫א‪ .‬קריאת נקודה מהמשתמש‪.‬‬
‫ב‪ .‬הדפסת נקודה לפלט הסטנדרטי‪..‬‬
‫ג‪ .‬החזרת הרביע בו מצויה הנקודה‪.‬‬
‫ד‪ .‬החזרת המרחק בין שתי נקודות‪.‬‬
‫כל אחת‪ ,‬כמובן‪ ,‬תמומש באמצעות פונ'‪.‬‬
‫לכן הקובץ ‪ Point.h‬יראה באופן הבא‪:‬‬
‫{ ‪struct Point‬‬
‫; ‪double _x, _y‬‬
‫}‬
‫; )‪void read_point(struct Point &p‬‬
‫; )‪void print_point(const struct Point &p‬‬
‫; )‪int quarter((const struct Point &p‬‬
‫‪double distance(const struct Point & p1,‬‬
‫; )‪const struct Point &p2‬‬
‫אעיר שעת פונ' אמורה רק להשתמש בנקודה כלשהי‪ ,‬אך לא לשנותה‪ ,‬אנו מעבירים‬
‫את הפרמטר כפרמטר הפניה קבוע )‪ .(const struct Point &p‬אנו לא מעבירים את‬
‫המבנה כפרמטר ערך כדי לחסוך את הזמן והזיכרון הדרושים לשם העתקת המבנה‪,‬‬
‫‪211‬‬
‫העתקה שהייתה נדרשת לו הפרמטר היה פרמטר ערך‪ .‬דנו בכך בהקשר לש מבנים‪,‬‬
‫ולא ארחיב על כך כאן‪.‬‬
‫‪ 17.5.2‬הקובץ ‪Point.cc‬‬
‫הקובץ ‪ Point.cc‬כולל‪ ,‬כאמור‪ ,‬את המימוש של הפונ' המטפלות בנקודות‪ ,‬פונ' עליהם‬
‫הצהרנו בקובץ ‪ .Point. h‬על כן הוא יכלול למשל את הפונ'‪:‬‬
‫)‪void read_point(struct Point &p‬‬
‫{‬
‫; ‪cin >> p. _x >> p._y‬‬
‫}‬
‫‪//-----------------------------------‬‬‫)‪void print_point(const struct Point &p‬‬
‫{‬
‫; ')' << ‪cout << '(' << p._x << ", " << p._y‬‬
‫}‬
‫על מנת שקימפולו של קובץ זה יעבור בהצלחה על הקומפיילר להכיר את הפרמטר‬
‫של הפונ'‪ ,‬מטיפוס ‪ struct Point‬הגדרת המבנה ‪ Point‬מצויה בקובץ‬
‫‪ Point.h‬על‪-‬כן יש לכלול קובץ זה בקובץ ‪ .Point.cc‬מכיוון שהפונ'‬
‫‪ read_point‬כוללת פקודת ‪ , cin‬והפונ' ‪ print_point‬כוללת פקודת ‪,cout‬‬
‫אזי יש לכלול גם את ‪ iostream‬בקובץ‪ .‬הכללתו יכולה להתבצע מפורשות בקובץ‬
‫‪ Point.cc‬או בקובץ הכותר ‪ Point.h‬המוכלל ב‪ .Point.cc -‬כמובן‪ ,‬במידה‬
‫ורוצים בכך יש צורך בפקודות ‪.using‬‬
‫באותו אופן‪ ,‬נממש בקובץ ‪ Point.cc‬את הפונ' האחרות‪.‬‬
‫כפי שתיארתי את קובץ הכותר נתקן לכדי‪:‬‬
‫>‪#include <iostream‬‬
‫>‪#include <cmath‬‬
‫; ‪using std::cin‬‬
‫; ‪using std::cout‬‬
‫{ ‪struct Point‬‬
‫; ‪double _x, _y‬‬
‫}‬
‫; )‪void read_point(struct Point &p‬‬
‫; )‪void print_point(const struct Point &p‬‬
‫; )‪int quarter((const struct Point &p‬‬
‫‪double distance(const struct Point & p1,‬‬
‫; )‪const struct Point &p2‬‬
‫הקובץ ‪ Point.cc‬יראה‪:‬‬
‫"‪#include "Point.h‬‬
‫‪//-----------------------------‬‬‫)‪void read_point(struct Point &p‬‬
‫{‬
‫‪212‬‬
cin >> p. _x >> p._y ;
}
//-----------------------------void print_point(const struct Point &p)
{
cout << '(' << p._x << ", " << p._y << ')' ;
}
//---------------------------------... implementation of the other two functions
Rectangle.h ‫ הקובץ‬17.5.3
‫ נניח שעבור מלבן אנו‬.‫עתה נטפל במלבן באופן דומה לדרך בה נהגנו עם נקודה‬
:‫מגדירים את הפעולות‬
.‫ קריאת נתונים מלבן מהמשתמש‬.‫א‬
.‫ הדפסת נתוני מלבן למשתמש‬.‫ב‬
.‫ חישוב והחזרת שטח מלבן‬.‫ג‬
.‫ חישוב והחזרת היקף מלבן‬.‫ד‬
.‫ בדיקה האם נקודה מצויה בתוך מלבן‬.‫ה‬
:‫קובץ הכותר ייראה‬
#include <iostream>
#include "Point.h"
using std::cin ;
using std::cin ;
struct Rectangle {
struct Point _top_left,
_bottom_right ;
}
void read_rectangle(struct Rectangle &r) ;
void print_rectangle(const struct Rectangle &r) ;
double rectangle_area(const struct Rectangle &r) ;
double rectangle_circumference(
const struct Rectangle &r) ;
bool is_point_in_rect(const struct Point &p,
const struct Rectangle &r) ;
‫ שכן מלבן מוגדרת תוך שימוש‬Point.h ‫ את‬Rectangle.h ‫אנו מכלילים בקובץ‬
‫ בעת שהוא מקמפל את‬struct Point ‫ ועל כן על הקומפיילר להכיר את‬,‫בנקודה‬
‫ )למרות שהוא מוכלל כבר ע"י‬iostream ‫ אנו מכלילים גם את‬.‫הקוד הקשור למלבן‬
‫( מתוך הנחה שהמתכנת שכותב את הקוד הקשור למלבן הוא מתכנת‬Point.h
213
‫ והוא אינו יודע מה כלל עמיתו בקוד‬,‫אחר מזה שכותב את הקוד הקשור לנקודה‬
‫ הוא מכלילו‬iostream -‫ מכיוון שהמתכנת שכותב את קוד של מלבן זקוק ל‬.‫שלו‬
.‫בקובץ הכותר שלו בעצמו‬
Rectangle.cc ‫ הקובץ‬17.5.4
:Point.cc -‫ יהיה דומה במבנהו ל‬Rectangle.cc ‫הקובץ‬
#include "rectangle.h"
//----------------------------------------------void read_rectangle(struct Rectangle &r) {
read_point(r._top_left) ;
read_point(r._bottom_right) ;
}
//----------------------------------------------void print_rectangle(const struct Rectangle &r) {
cout << "[ " '
print_point(r._top_left) ;
cout << " – " ;
print_point(r._bottom_right) ;
cout << " ]" ;
}
//----------------------------------------------... the other three functions
my_prog.cc ‫ הקובץ‬17.5.5
‫ אשר עושה שימוש בנקודות‬,‫ יכיל את התכנית הראשית‬my_prog.cc ‫הקובץ‬
‫ תחת ההנחה שהמתכנת שכתב אותו אינו יודע מה הכלילו עמיתיו‬,‫ על כן‬.‫ובמלבנים‬
Point.h, ‫ קובץ זה צריך לכלול בתוכו את שני קובצי הכותר‬,‫בקבצים שהם כתבו‬
.‫פי הצורך‬-‫ על‬,‫ ואולי קובצי כותר אחרים‬,Rectangle.h
:‫הקובץ יראה באופן הבא‬
#include <iostream>
#include "Point.h"
#include "Rectangle.h"
using std::cout ;
//----------------------------------------int main()
{
struct Point p1, p2 ;
struct Rectangle r ;
read_point(p1) ;
read_point(p2) ;
214
‫; )‪read_rectangle(r‬‬
‫)‪if (distance(p1, p2) == 0‬‬
‫; "‪cout << "It is the same point\n‬‬
‫))‪if (is_point_in_rect(p1, r‬‬
‫; "‪cout << "p1 is in r\n‬‬
‫‪...‬‬
‫}‬
‫‪ 17.5.6‬בעיות ההידור המתעוררות והתיקונים הדרושים בעזרת ‪#ifndef‬‬
‫נניח שעתה ברצוננו להדר )לקמפל( את התכנית על מנת לקבל קובץ ניתן להרצה‪.‬‬
‫בפרט יהיה עלינו להדר )במובן הצר של המילה( את הקובץ ‪ .my_prog.cc‬פקודת‬
‫ההידור היא‪:‬‬
‫‪g++ -Wall –c my_prog.cc‬‬
‫נבחן מה יקרה בתהליך ההידור‪ ,‬ואיזה בעיה תתעורר‪ :‬כזכור לנו‪ ,‬ההידור מתחיל‬
‫בהפעלת קדם המהדר‪ ,‬אשר 'שותל' את הקבצים המוכללים במקום בו הם‬
‫מוכללים‪ .‬על‪-‬כן‪:‬‬
‫א‪ .‬ראשית‪' ,‬ישתול' קדם המהדר את ‪ iostream‬בקובץ שלנו;‬
‫ב‪ .‬שנית‪ ,‬הוא ישתול את ‪ ,Point.h‬ועל‪-‬כן הגדרת ‪ struct Point‬תוכנס לקובץ‬
‫‪ ,my_prog.cc‬וטוב שכך‪ ,‬שכן אחרת המהדר )המרכזי( בשלב ההידור העיקרי לא‬
‫יכיר את ‪ ,struct Point‬ו‪'-‬יצעק' עלינו‪ ,‬כלומר יוציא הודעת שגיאה‪.‬‬
‫ג‪ .‬שלישית‪ ,‬הוא ייגש 'לשתול' את ‪ Rectangle.h‬בקובץ הנוכחי‪ Rectangle.h .‬כולל‬
‫בתוכו את ההנחיה "‪ #include "Point.h‬מה שיגרום לקדם מהדר 'לשתול' שוב את‬
‫‪ Point.h‬בקובץ שלנו‪ ,‬ואחר‪-‬כך 'לשתול' את יתר הקובץ ‪ Rectangle.h‬בקובץ‬
‫‪my_prog.cc‬‬
‫התוצאה‪ :‬הגדרת ‪ struct Point‬מופיעה בקובץ ‪ my_prog.cc‬פעמיים‪ .‬אומנם בשתי‬
‫הפעמים זו בדיוק אותה הגדרה‪ ,‬אך‪ ,‬כאמור‪ ,‬היא מצויה בקובץ פעמיים‪ .‬עת המהדר‬
‫)המרכזי( ייתקל פעמיים בהגדרת המבנה הוא 'יצעק' עלינו שזו שגיאה; וזה כמובן‬
‫רע מאוד‪.‬‬
‫כיצד נתקן את התקלה‪ ,‬ונאפשר לתכניתנו להתקמפל כהלכה?‬
‫הכלי שיעזור לנו בכך הוא הנחיית הקדם מהדר ‪.#ifndef‬‬
‫‪215‬‬
:‫ ואחר אסביר את התיקון‬,‫אציג את קובצי הכותר המעודכנים‬
The file: Point.h
#ifndef POINT
#define POINT
struct Point {
double _x, _y ;
}
void read_point(struct Point &p) ;
void print_point(const struct Point &p) ;
int quarter((const struct Point &p) ;
double distance(const struct Point & p1,
const struct Point &p2) ;
#endif
The file: Rectangle.h
#ifndef RECTANGLE
#define RECTANGLE
#include <iostream>
#include "Point.h"
using std::cin ;
using std::cin ;
struct Rectangle {
struct Point _top_left,
_bottom_right ;
}
void read_rectangle(struct Rectangle &r) ;
void print_rectangle(const struct Rectangle &r) ;
double rectangle_area(const struct Rectangle &r) ;
double rectangle_circumference(
const struct Rectangle &r) ;
bool is_point_in_rect(const struct Point &p,
const struct Rectangle &r) ;
#endif
. #ifndef :‫ 'הגנו' על כל אחד מקובצי הכותר באמצעות השאלה‬:‫כפי שניתן לראות‬
‫ עת הוא עובר על‬,‫ תוך שאנו חוזרים על פעולתו של קדם המהדר‬,‫אסביר ביתר פירוט‬
:‫ עבור הקבצים המתוקנים‬my_prog.cc ‫הקובץ‬
‫ כאן לא‬.(my_prog.cc) ‫ בקובץ שלנו‬iostream ‫ 'ישתול' קדם המהדר את‬,‫ ראשית‬.‫א‬
.ifndef ‫ בה לא הוספנו עדיין את ההנחיות‬,‫חל כל שינוי יחסית לדוגמה הקודמת‬
:‫ כלומר‬,#ifndef POINT :‫ בפרט הוא יישאל‬.Point.h ‫ הוא ייגש לשתול את‬,‫ שנית‬.‫ב‬
‫ ? התשובה תהיה‬POINT ‫ לא הוגדר לי הסימן‬,‫ בהרצה נוכחית שלי‬,‫האם עד כה‬
‫ ייכלל‬,‫ כלומר בדוגמה שלנו עד סוף הקובץ‬,endif -‫ ועל כן כל מה שמופיע עד ה‬,‫כן‬
struct ‫ הגדרת‬,‫ בפרט‬.(‫בפלט של קדם המהדר )ולכן יקומפל ע"י המהדר המרכזי‬
‫ מגדיר‬#define POINT :‫ בשורה‬,‫ מעבר לכך‬.my_prog.cc ‫ תוכנס לקובץ‬Point
.‫לעצמו קדם המהדר סימן זה‬
216
‫ג‪ .‬שלישית‪ ,‬הוא ייגש 'לשתול' את ‪ Rectangle.h‬בקובץ הנוכחי‪ Rectangle.h .‬כולל‬
‫בתוכו את ההנחיה "‪ #include "Point.h‬מה שיגרום לקדם מהדר לפנות שוב ל‪-‬‬
‫‪ Point.h‬על מנת להכלילו בקובץ ‪ .my_prog.cc‬שוב ישאל עצמו קדם המהדר‪ :‬האם‬
‫עד כה‪ ,‬בהרצה נוכחית שלי‪ ,‬לא הוגדר לי הסימן ‪ ? POINT‬הפעם‪ ,‬התשובה תהיה‬
‫לא‪ ,‬שכן בסעיף ב' שמעל כבר הגדיר לעצמו קדם המהדר את הסימן הזה‪ ,‬על כן‬
‫כל מה שמופיע עד ה‪ #endif -‬לא 'יישתל' בקובץ שלנו‪ ,‬בפרט הגדרת ‪struct Point‬‬
‫לא תופיע פעמיים‪ ,‬וההידור יעבור בהצלחה )לכל הפחות מהיבט זה(‪ .‬לא‬
‫התייחסתי בדברי לשאלה המקבילה שקדם המהדר שואל‪:‬‬
‫‪#ifndef‬‬
‫‪ ? RECTANGLE‬שכן הדיון בה זהה לזה שערכנו עבור ‪ .POINT‬בתכנית הנוכחית‬
‫הגנה זאת מפני הכללה כפולה של הגדרת ‪ struct Rectangle‬אינה חיונית‪ ,‬ולכן‬
‫בתכנית הנוכחית יכולנו לוותר על ההנחיות‪#ifndef RECTANGLE, #define :‬‬
‫‪ . RECTANGLE, #endif‬אנו כותבים אותן לטובת מקרה בן בעתיד מישהו עשוי‬
‫לכלול בתכניתו את ‪ rectangle.h‬יותר מפעם אחת‪.‬‬
‫‪ 17.5.7‬קובץ ה‪ makefile -‬לפרויקט‬
‫קובץ ה‪ makefile -‬לבניית תכנית ניתנת להרצה עבור הקבצים שלנו דומה בעיקרון‬
‫לזה שראינו בדוגמה הקודמת‪:‬‬
‫‪my_prog: my_prog.o Point.o Rectangle.o‬‬
‫‪g++ my_prog.o Point.o Rectangle.o –o my_prog‬‬
‫‪my_prog.o: my_prog.cc Point.h Rectangle.h‬‬
‫‪g++ -Wall –c my_prog.cc‬‬
‫‪Rectangle.o: Rectangle.cc Rectangle.h Point.h‬‬
‫‪g++ -Wall –c Rectangle.cc‬‬
‫‪Point.o: Point.cc Point.h‬‬
‫‪g++ -Wall –c Point.cc‬‬
‫נסביר‪:‬‬
‫‪ Rectangle.o‬תלוי כמובן ב‪ .Rectangle.cc :‬מכיוון ש‪ Rectangle.cc :‬כולל בתוכו את‬
‫שני קובצי הכותר ‪ Rectangle.h Point.h‬אזי לצורך הקומפילציה )המרכזית(‬
‫שני קבצים אלה הם חלק מ‪ Rectangle.cc :‬ולכן ‪ Rectangle.o‬תלוי גם בהם‪ .‬דין דומה‬
‫חל על ‪ .my_prog.o‬כמובן שה‪ ,executable -‬הקובץ ‪ ,my_prog‬תלוי בשלושת קובצי ה‪-‬‬
‫‪ object‬ולכן אם אחד מהם עדכני יותר ממנו יש לבצע לינקינג מחדש לשלושת‬
‫הקבצים על מנת לקבל תכנית ניתנת להרצה חדשה‪.‬‬
‫‪ 17.6‬קבועים גלובליים‬
‫נניח שאנו כותבים תכנית המורכבת מכמה קבצים‪ .‬לשם הפשטות נניח ששמות‬
‫הקבצים הם ‪ .f1.cc, f2.cc‬עוד נניח כי ברצוננו להגדיר קבועים בהם נעשה‬
‫שימוש‪.‬‬
‫אם נגדיר בכל אחד משני הקבצים את הקבועים‪:‬‬
‫; ‪const int N = 1‬‬
‫אזי הקבוע ‪ N‬יוכר רק בקובץ בו הוא מוגדר‪ .‬לכן‪ ,‬מבחינת תקינות התכנית‪ ,‬נוכל‬
‫להגדיר בקובץ ‪ f1.cc‬את הקבוע‪:‬‬
‫; ‪const int N = 1‬‬
‫ובקובץ ‪f2.cc‬את הקבוע‪:‬‬
‫; ‪const int N = 2‬‬
‫‪217‬‬
‫ומבחינת הקומפיילר לא תהיה בכך כל בעיה )מבחינת הסגנון התכנותי זה עלול‬
‫להיות מבלבל‪ ,‬אך זו סוגיה אחרת(‪ .‬כמובן בכל קובץ עת יתעניינו בערכו של ‪N‬‬
‫יתקבל הערך שהוגדר באותו קובץ‪.‬‬
‫אולם ישנם מצבים בהם ברצוננו שאותו קבוע‪ ,‬אשר יוגדר פעם אחת בלבד‪ ,‬יוכר‬
‫בקבצים שונים‪ .‬כיצד נשיג אפקט זה?‬
‫התשובה היא שבקובץ מקור אחד בלבד )קובץ ‪ , .cc‬לא קובץ כותר( נגדירו באופן‪:‬‬
‫; ‪extern const int N = 1‬‬
‫בקובצי המקור האחרים‪ ,‬או בקובץ כותר אותו מכללים בקובצי מקור אחרים‬
‫נצהיר עליו באופן‪:‬‬
‫; ‪extern const int N‬‬
‫כלומר בלי לקבוע את ערכו‪.‬‬
‫מילת המפתח ‪ extern‬בקובץ בו הקבוע הוגדר‪ ,‬ונקבע לו ערך‪ ,‬אומרת שעליו להיות‬
‫מוכר בכל הקבצים המרכיבים את התכנית )ולא רק בקובץ בו הוא מוגדר‪ ,‬כפי‬
‫שקורה עת לא כותבים את מילת המפתח ‪.(extern‬‬
‫ההצהרה‪ extern const int N ; :‬מורה למהדר שהקבוע הוגדר‪ ,‬ונקבע לו‬
‫ערך‪ ,‬בקובץ אחר‪ ,‬וללינקר היא מורה שעליו לאתר את ערכו של הקבוע עת הוא כורך‬
‫את קובצי האובג'קט השונים לכדי תכנית שלמה אחת‪.‬‬
‫נראה דוגמה קטנה ביותר‪:‬‬
‫הקובץ ‪ f1.cc‬הינו‪:‬‬
‫>‪#include <iostream‬‬
‫; ‪extern const int N = 1‬‬
‫; )(‪void f‬‬
‫{ )(‪int main‬‬
‫; " " << ‪std::cout << N‬‬
‫; )(‪f‬‬
‫; ‪return 0‬‬
‫}‬
‫הקובץ ‪ f2.cc‬הינו‪:‬‬
‫>‪#include <iostream‬‬
‫;‬
‫‪extern const int N‬‬
‫{ )(‪void f‬‬
‫; ‪std::cout << N‬‬
‫}‬
‫‪218‬‬
‫‪ 17.7‬מבוא לתכנות מונחה עצמים‪ :‬עקרון הכימוס‬
‫בדוגמה האחרונה ראינו תכנית אשר משתמשת בנקודות ובמלבנים‪ .‬לטיפוסי‬
‫משתנים של מבנים )כדוגמת נקודות ומלבנים( אנו קוראים לעתים אובייקטים‪ .‬אחד‬
‫העקרונות התכנותיים המקובלים ביותר‪ ,‬עת תכנית משתמשת באובייקט‪ ,‬כדוגמת‬
‫נקודה או מלבן‪ ,‬הוא עיקרון הכימוס )‪ .(encapsulation‬עקרון הכימוס קובע שהתכנית‬
‫הראשית כלל לא צריכה לדעת כיצד המתכנת שהגדיר את ‪ struct Point‬בחר לייצג‬
‫נקודה‪ .‬האם הוא עושה זאת באמצעות קואורדינאטת ה‪ x -‬וה‪ y -‬של הנקודה )כפי‬
‫שאכן עשינו בדגומה מעל(‪ ,‬כלומר בייצוג קרטזי? או שמא הוא בחר לייצג נקודה‬
‫באמצעות המרחק שלה מראשית הצירים )מה שקרוי הרדיוס של הנקודה(‪ ,‬והזווית‬
‫בין הישר המחבר אותה לראשית הצירים‪ ,‬לבין הכיוון החיובי של ציר ה‪) x -‬מה‬
‫שקרוי זווית אלפה של הנקודה(‪ ,‬כלומר בייצוג קוטבי? לפי עקרון הכימוס‪ ,‬התכנית‬
‫הראשית‪ ,‬אמורה לקבל אוסף של פעולות על נקודה ולהשתמש בהן‪ ,‬ורק בהן‪ ,‬עת‬
‫היא מגדירה משתנים מטיפוס נקודה‪ .‬כלומר‪ ,‬התכנית הראשית לא תוכל לבצע‬
‫פעולה כגון‪) p. _x = 0; :‬עבור נקודה ‪ ,(p‬שכן התכנית הראשית כלל לא אמורה לדעת‬
‫שנקודה מיוצגת באמצעות קואורדינאטת ה‪ x -‬שלה‪ .‬כיצד אם כן תוכל התכנית‬
‫הראשית לבצע פעולה פשוטה ובסיסית זאת? התשובה היא‪ :‬בעזרת פונקציות‬
‫שהמתכנת שכתב את ‪ struct Point‬העמיד לרשות התכנית הראשית‪ ,‬למשל באמצעות‬
‫פעולה‪ . set_x(p, 0) :‬באותו אופן‪ ,‬בתכנית הראשית נוכל לכתוב‪ set_radius(p, 1) :‬בלי‬
‫להתעניין כיצד בפועל מיוצגת נקודה; וזאת‪ ,‬כאמור‪ ,‬מתוך הנחה שהמתכנת שכתב‬
‫את הקוד של נקודה העמיד לרשותנו את שתי הפונ' הללו‪.‬‬
‫באופן דומה‪ ,‬התכנית הראשית לא תוכל לכלול פקודה‪ ,cout << p._x :‬אלא היא‬
‫תכלול פקודה‪ cout << get_x(p) :‬או פקודה‪ cout << get_radius(x) :‬ובאחריות‬
‫המתכנת שכתב את הקוד של נקודה להעמיד לרשות התכנית הראשית את שתי‬
‫הפונ' הללו‪.‬‬
‫על כן‪ ,‬סביר להניח שעבור נקודה‪ ,‬שנניח שהוגדרה כפי שהדגמנו קודם‪:‬‬
‫{ ‪struct Point‬‬
‫; ‪double _x, _y‬‬
‫}‬
‫תסופקנה לנו‪ ,‬מעבר לפעולות שראינו )החזרת הרבע בו מצויה נקודה‪ ,‬החזרת מרחק‬
‫בין שתי נקודות( גם הפעולות‪:‬‬
‫)‪void set_x(struct Point &p, double x‬‬
‫} ; ‪{ p. _x = x‬‬
‫)‪void set_y(struct Point &p, double y‬‬
‫} ;‪{ p. _x = y‬‬
‫‪void set_radius_and_alpha(struct Point &p,‬‬
‫)‪double r, double alpha‬‬
‫;‪{ p. _x = r / cos(alpha‬‬
‫‪p._y = r/ sin(alpa} ; :‬‬
‫)‪double get_x(const struct Point &p‬‬
‫} ; ‪{ return p._x‬‬
‫)‪double get_y(const struct Point &p‬‬
‫} ; ‪{ return p._y‬‬
‫‪219‬‬
‫)‪double get_raduis(const struct Point &p‬‬
‫} ; )‪{ return sqrt(p._x*p._x + p._y*p._y‬‬
‫)‪double get_alpha(const struct Point &p‬‬
‫} ; )‪{ return atan(y/x‬‬
‫התכנית הראשית‪ ,‬תוכל להגדיר משתנה‪:‬‬
‫; ‪struct Point p‬‬
‫כדי להציב את הנקודה בראשית הצירים תבצע התכנית הראשית‪:‬‬
‫; )‪set_x(p, 0‬‬
‫; )‪set_y(p, 0‬‬
‫וכדי לבדוק האם הנקודה נמצאת על הישר ‪ y = x‬תשאל התכנית הראשית‪:‬‬
‫))‪if (get_x(p) == get_y(p‬‬
‫‪...‬‬
‫על דרך השלילה‪ ,‬התכנית הראשית לא תוכל לבצע את הפעולות הבאות‪:‬‬
‫; ‪p._x = 0‬‬
‫)‪if (p._x == p._y‬‬
‫‪...‬‬
‫‪ 17.8‬יצירת ספריה סטאטית‬
‫נחזור לשני זוגות הקבצים ששימשו אותנו בדוגמה הראשונה בה חילקנו את‬
‫תכניתנו לקבצים‪. io.h, io.cc, arithmetic.h, arithmetic.cc :‬‬
‫נניח שאנו סוברים כי הפונ' שכתבנו בקבצים אלה מאוד שימושיות‪ ,‬ועל כן נרצה‬
‫להשתמש בהן בתכניות רבות שנכתוב בעתיד‪ .‬כדי לייעל את תהליך הקומפילציה של‬
‫אותן תכניות נרצה לצור משני הקבצים הללו ספריה )‪ ,(library‬ולא רק לכרוך את‬
‫שני הקבצים הללו עם תכניות עתידיות שנכתוב‪ ,‬כפי שעשינו בדוגמות הקודמות‪.‬‬
‫יצירת ספריה עדיפה על‪-‬פני כריכת הקבצים ‪ io.o arithmetic.o‬עם התכנית‬
‫המזמנת את הפונ' הנכללות באותם קבצים שכן היא מקצרת את זמן ההידור‪.‬‬
‫בתכניות שלכם‪ ,‬ההידוק לוקח הרף עין‪ ,‬אולם בתכניות גדולות ההידור עלול לקחת‬
‫זמן רב )סדר גודל של דקות ואפילו שעות(‪ ,‬ועל כן המוטיבציה לקצרו רבה‪.‬‬
‫הסיבה לכך שמשך ההידור מתקצר היא שהלינקר צריך לפתוח פחות קבצים )קובץ‬
‫ספריה יחיד‪ ,‬במקום שני קובצי אובג'קט(‪ .‬לקובץ הספרייה יש בדרך כלל אינדקס‪,‬‬
‫ולכן נקל לאתר בו פונ' אותה יש לכרוך עם התכנית‪.‬‬
‫בסעיף זה נלמד כיצד יוצרים ספריה סטאטית‪ .‬תכונתה של ספריה סטאטית היא‬
‫שבזמן הכריכה הלינקר שולף מהספרייה את קטעי הקוד הדרושים‪ ,‬ומצרף אותם‬
‫לקובץ הניתן להרצה שהוא יוצר‪ .‬עת התכנית רצה כבר אין יותר פניה לקובץ‬
‫הספרייה‪ ,‬ולכן‪ ,‬למשל‪ ,‬ניתן להעביר את הקובץ הניתן להרצה למחשב אחר‪ ,‬בלי‬
‫להעביר איתו את קובץ הספרייה‪.‬‬
‫יצירת הספרייה מתחילה‪ ,‬כמובן בקימפול שני הקבצים‪ ,‬וקבלת קובצי אובג'קט‪:‬‬
‫‪g++ -Wall –c add_sub.cc‬‬
‫‪g++ -Wall –c io.cc‬‬
‫‪220‬‬
‫עתה‪ ,‬כדי לצור משני קובצי האובג'קט הללו קובץ ספריה‪ ,‬נשתמש בפקודת ה‪-‬‬
‫‪ Shell‬של יוניקס הקרויה ‪ ar) .ar‬הוא קיצור של ‪ archive‬או קובץ ארכיב(‪.‬‬
‫אופן השימוש בפקודה‪:‬‬
‫‪ar rc libadd_sub_io.a add_sub.o io.o‬‬
‫נסביר‪:‬‬
‫א‪ .‬כאמור הפקודה אותה אנו מפעילים היא הפקודה ‪.ar‬‬
‫ב‪ .‬זוג האותיות ‪ rc‬הנכתבות מייד אחרי שם הפקודה הן קיצור של‪reaplce :‬‬
‫‪ create‬והן מורות למערכת ההפעלה לייצר את הספרייה אם היא אינה קיימת‪,‬‬
‫או להחליף את הספרייה הקיימת אם כבר הייתה אחת כזאת‪.‬‬
‫ג‪ .‬המרכיב השלישי ‪ libadd_sub_io.a :‬מציין את שם הספרייה שתווצר‪ .‬שם‬
‫ספרייה חייב להתחיל בשלוש האותיות ‪ ,lib‬ולהסתיים בזוג התווים‪) .a :‬התו‬
‫‪ a‬עומד בשביל‪.(archive :‬‬
‫ד‪ .‬המשך הפקודה מציין את שמתו הקבצים שנכלול בספרייה‪ .‬במקרה שלנו אלה‬
‫הקבצים ‪.add_sub.o io.o‬‬
‫קובץ הספרייה שנוצר הוא חסר אינדקס‪ .‬אינדקס בקובץ ספריה‪ ,‬כמו אינדקס‬
‫המוכר לכם מספר‪ ,‬עוזר לאתר במהירות פונ' רצויה בקובץ הספרייה )כפי שהוא‬
‫עוזר לאתר במהירות נושא רצוי בספר(‪ .‬כדי לייצר אינדקס לקובץ הספרייה‬
‫)אינדקס שיכלל בקובץ הספרייה עצמו( ניתן להריץ את פקודת ה‪ ar -‬באופן הבא‪:‬‬
‫‪ar rcs lib_add_sub_io.a add_sub.o io.o‬‬
‫הדגל‪ s :‬מורה שיש לייצר את הספרייה עם אינדקס‪.‬‬
‫לחילופין‪ :‬ניתן להוסיף לספריה הקיימת אינדקס באמצעות הפקודה‪:‬‬
‫‪ranlib libadd_sub_io.a‬‬
‫הפקודה‪nm –s libadd_sub_io.a :‬‬
‫מציגה לכם את תכולתו של האינדקס שנוצר‪.‬‬
‫‪ 17.9‬יצירת ספריה דינאמית )משותפת(‬
‫עת יצרנו ספריה סטאטית הלינקר‪ ,‬בשלב הכריכה‪ ,‬צירף לתכנית שלנו את‬
‫הפונקציות אותן זימנו מתוך הספרייה הסטאטית‪ ,‬כך שהתקבל קובץ ניתן להרצה‬
‫)‪ (executable‬שכבר אינו תלוי בספרייה‪ .‬לדוגמה‪ :‬אם נעביר קובץ זה למחשב‬
‫אחר )כמובן אחד עם אותה שפת מכונה(‪ ,‬התכנית תוכל לרוץ ַבמחשב האחר‪ ,‬גם אם‬
‫לא נעביר ַלמחשב האחר את קובץ הספרייה‪ .‬צדו השני של המטבע‪ :‬אם במחשב‬
‫שלנו רצות תכניות רבות שנכרכו באופן סטאטי עם אותה פונ' ספרייה )לדוגמה‪:‬‬
‫‪ ,(cin‬אזי כל אחת מהן גדלה בנפחה בשיעור הנפח של אותה פונ' ספרייה‪ .‬מכיוון‬
‫שהזיכרון הראשי הוא משאב מוגבל‪ ,‬אזי יש טעם לפגם בכך שאנו מגדילים כל אחת‬
‫ואחת מהתכניות באותו קוד )לדוגמה‪ :‬בפונ' ‪ .(cin‬על כן‪ ,‬במצבים בהם פונ'‬
‫הספרייה נכרכת לתכניות רבות‪ ,‬נעדיף לכרכה באופן שקוד הפונ' לא ייכרך עם כל‬
‫קובץ הרצה בנפרד‪ ,‬נעדיף שקוד הפונ' יוחזק פעם יחידה בזיכרון‪ ,‬עבור כלל התכניות‬
‫הזקוקות לקוד זה‪ .‬ספרייה דינאמית היא הכלי שיעזור לנו להשיג זאת‪.‬‬
‫עת אנו יוצרים ספרייה דינאמית‪ ,‬במלים אחרות ספרייה משותפת‪ ,‬הלינקר‪ ,‬בשלב‬
‫הכריכה‪ ,‬לא מצרף את קוד הפונ' הדרושה לקובץ הניתן להרצה שלנו‪ ,‬אלא שותל‬
‫בקובץ רק בדל )‪ ,(stub‬כלומר מעין 'קצה חוט'‪ ,‬אשר יאפשר‪ ,‬בעת ריצת התכנית‪,‬‬
‫לזמן פונ' שאינה חלק מקובץ התכנית שלנו‪ ,‬פונ' שמצויה בספרייה דינאמית אשר‬
‫שוכנת בזיכרון פעם יחידה עבור כל התכניות שמריצות אותו קוד‪ .‬לדוגמה‪ :‬בהנחה‬
‫ש‪ cin -‬שוכנת בספרייה דינאמית‪ ,‬הקוד של ‪ cin‬נמצא בזיכרון פעם יחידה בלבד‪.‬‬
‫‪221‬‬
‫עת תכנית א' מזמנת את הפונ' פונים להריץ את קוד הפונ'‪ ,‬כפי שמצוי בזיכרון )גם‬
‫אם לא בקובץ התכנית הניתנת להרצה של תכנית א'(; עת תכנית ב' מזמנת את הפונ'‬
‫פונים לאותו קוד‪ ,‬אשר כאמור מצוי בזיכרון פעם יחידה‪ .‬התוצאה‪ :‬מצד אחד קובץ‬
‫התכנית )ה‪ (executable -‬שלנו כבר לא עצמאי‪ ,‬הוא חייב שבזיכרון יהיה גם קובץ‬
‫הספרייה עת התכנית רצה; אולם מצד שני‪ ,‬גם אם תכניות רבות הרצות במקביל‪,‬‬
‫ומצויות בזיכרון המחשב‪ ,‬כוללות את הפונ'‪ ,‬הפונ' נמצאת בזיכרון רק פעם אחת‪,‬‬
‫וכך נחסך בזבוז של הזיכרון )בזבוז שנגרם עת אותה פונ' מצויה בזיכרון פעמים‬
‫רבות‪ ,‬כחלק מתכניות רבות(‪.‬‬
‫לשימוש בספרייה דינאמית יש עוד מחיר‪ :‬טעינת התכנית לזיכרון לשם ריצתה‪,‬‬
‫ובהמשך הסתעפות לפונ' שאינה חלק מהתכנית‪ ,‬הינם איטיים יותר‪.‬‬
‫כדי לצור מזוג הקבצים שראינו בעבר ספרייה דינאמית נקמפלם באופן‪:‬‬
‫‪g++ -Wall –c –fPIC add_sub.cc‬‬
‫‪g++ -Wall –c –fPIC add_sub.cc‬‬
‫פקודה ההידור כולל את הדגל המוכר לנו ‪ –c‬המורה שאין לייצר תכנית ניתנת‬
‫להרצה‪ ,‬אלא רק קובץ אובג'קט‪ ,‬ומעברל ו את הדגל )החדש לנו(‪ . -fPIC :‬דגל זה‬
‫הוא שמאפשר לכלול את האובג'קט בספרייה דינאמית כפי שמייד נייצר‪.‬‬
‫את הספרייה עצמה נייצר באמצעות הפקודה‪:‬‬
‫‪g++ -shared -o libadd_sub_io.so add_sub.o io.o‬‬
‫הספרייה תקרא‪ libadd_sub_io.so :‬שמה בהכרח יתחיל ב‪ lib :‬וייגמר ב‪ ,so :‬היא תכלול‬
‫את הקבצים כמתואר‪ ,‬ותהיה משותפת‪ so ) .‬הוא קיצור של ‪(shared object‬‬
‫את התכנית הראשית נהדר‪ ,‬כרגיל‪ ,‬באופן‪:‬‬
‫‪g++ -Wall –c main.cc‬‬
‫פקודת הכריכה תהייה‪:‬‬
‫‪g++ main.o -L. -ladd_sub_io –o my_prog‬‬
‫הדגל ‪ -L‬מורה היכן יש לחפש ספריות‪ ,‬והמיקום הוא‪ . :‬כלומר גם במדריך הנוכחי‬
‫)מעבר למקום בו בד"כ הכורך יודע לחפש את הספריות הסטנדרטיות(‪ .‬הדגל ‪–l‬‬
‫)האות אנגלית ‪ L‬קטנה( מורה שיש לכלול את הספרייה‪ libadd_sub_io.so :‬כאשר‬
‫את הרישא )‪ (lib‬והסיפא )‪ (.so‬אין כותבים‪.‬‬
‫במידה ויש לנו הן ספריה סטאטית ‪ ,libadd_sub_io.a‬והן ספריה דינאמית‪:‬‬
‫‪ libadd_sub_io.so‬יעדיף הכורך לכרוך את תכניתנו עם הספריה הדינאמית‪.‬‬
‫כדי להריץ את התכנית יש‪ ,‬ראשית‪ ,‬להורות לטוען הדינמי היכן הוא יימצא את‬
‫הספרייה הדינמית שלנו‪ .‬בהנחה שאנו עובדים ביוניקס על ה‪ tcsh :Shell -‬נעשה‬
‫זאת על‪-‬ידי‪:‬‬
‫‪setenv LD_LIBRARY_PATH /cs/teach/yoramb/intro2cs2/dividing_program/example1/dynamic_lib‬‬
‫כלומר‪ :‬אנו מכניסים למשתנה ‪ LD_LIBRARY_PATH‬את הנתיב המלא לספריה‬
‫שלנו‪ .‬כזכור‪ ,‬פקודת ה‪ pwd :Shell -‬תוכל לתת לנו נתיב זה )בהנחה שאנו במדריך‬
‫בו מצויה הספרייה(‪.‬‬
‫הרצת התכנית היא כרגיל‪ ,‬בדוגמה מעל‪. my_prog :‬‬
‫‪222‬‬
‫במידה ואנו מתעניינים בכך‪ ,‬הפקודה‪ ldd a.out :‬תלמד אותנו היכן מצויות הספריות‬
‫בהן תוכניתנו עושה שימוש‪ ,‬בפרט הספרייה שיצרנו‬
‫‪223‬‬