שיעור במבני נתונים ויעילות אלגוריתמים מיום ד

‫מכללת אורט כפר‪-‬סבא‬
‫מבני נתונים‬
‫ויעילות אלגוריתמים‬
‫מחסנית (‪)Stack‬‬
‫כתיב תוכי (‪ ,)infix‬תחילי (‪ ,)prefix‬סופי (‪)postfix‬‬
‫משפט לפתרון נוסחאות נסיגה‬
‫‪05.11.14‬‬
‫אורי וולטמן‬
‫‪[email protected]‬‬
‫חידה לחימום‬
‫‪‬‬
‫‪‬‬
‫‪‬‬
‫‪‬‬
‫‪‬‬
‫נתון לוח שחמט ועליו צריח (‪ ,)rook‬המונח על משבצת כלשהי‪.‬‬
‫כזכור‪ ,‬צריח יכול לנוע מספר משבצות כרצונו לאורך עמודה או לרוחב שורה‪.‬‬
‫שני שחקנים משחקים משחק של הזזת צריח‪ .‬הם מזיזים‪ ,‬כל אחד בתורו‪ ,‬את‬
‫הצריח כמה משבצות שהם רוצים‪ ,‬בכיוונים 'למטה' ו‪'-‬שמאלה' בלבד‪ .‬כלומר‪,‬‬
‫לא ניתן להזיז את הצריח ימינה או למעלה‪.‬‬
‫כל שחקן חייב להזיז את הצריח בתורו‪.‬‬
‫השחקן המנצח הוא זה שמביא את הצריח‬
‫למשבצת השמאלית‪-‬התחתונה של הלוח‪.‬‬
‫פתחו אלגוריתם המקבל כקלט שני מספרים‬
‫טבעיים המציינים את מיקומו ההתחלתי של‬
‫הצריח‪ .‬האלגוריתם יבחר האם להיות‬
‫השחקן הפותח או השני‪ ,‬והוא ישחק כך‬
‫שהוא ינצח תמיד‪.‬‬
‫מחסנית‬
‫‪‬‬
‫מחסנית (‪ )Stack‬היא סוג של טיפוס נתונים (‪ )data type‬התומך‬
‫בפעולות הכנסה והוצאה‪ ,‬כך שמתקיימת התכונה הבאה‪:‬‬
‫‪‬‬
‫‪‬‬
‫הוצאת ערך ממחסנית אפשרית רק כאשר היא אינה ריקה‪ ,‬והיא מחזירה‬
‫תמיד את הערך שהוכנס אחרון‪ ,‬מבין הערכים הקיימים במחסנית‪.‬‬
‫לדוגמא‪:‬‬
‫הכנס ‪9‬‬
‫הכנס ‪5‬‬
‫הכנס ‪3‬‬
‫הוצא‬
‫הוצא‬
‫‪3‬‬
‫►‬
‫ראש המחסנית‬
‫‪5‬‬
‫►‬
‫ראש המחסנית‬
‫‪9‬‬
‫►‬
‫ראש המחסנית‬
‫מחסנית‬
‫‪‬‬
‫‪‬‬
‫‪‬‬
‫מחסנית הינה טיפוס נתונים המאפשר הוצאת והכנסת נתונים אך ורק מצידה‬
‫האחד‪ .‬לפתח זה נקרא 'ראש המחסנית'‪.‬‬
‫ניתן לגשת רק לאיבר הנמצא בראש המחסנית‪ ,‬והוא מסתיר את כל האיברים‬
‫שמתחתיו‪.‬‬
‫אם נדחוף למחסנית איבר ומיד לאחר מכן נבצע שליפה‪ ,‬אז יתקיים ש‪:‬‬
‫‪‬‬
‫‪‬‬
‫‪‬‬
‫האיבר שנשלוף יהיה אותו איבר שזה עתה דחפנו‪.‬‬
‫מצב המחסנית לאחר השליפה יהיה זהה למצבה לפני הדחיפה‪.‬‬
‫המבנה המיוחד של המחסנית גורם לכך שבמהלך הכנסה והוצאה של איברים‬
‫למחסנית מתקיימת בה התכונה הנקראת‪ ,Last In First Out (LIFO) :‬כלומר –‬
‫האיבר שנכנס אחרון הוא היוצא ראשון‪.‬‬
‫מחסנית‬
‫‪‬‬
‫‪‬‬
‫‪‬‬
‫אילו פעולות נגדיר על מחסנית?‬
‫כדי שאפשר יהיה להוסיף איברים למחסנית ולהוציא אותם ממנה‪ ,‬נגדיר את‬
‫הפעולות דחוף‪-‬למחסנית ו‪-‬שלוף‪-‬ממחסנית‪.‬‬
‫המחסנית עשויה להיות ריקה‪ ,‬אך אז אסור לבצע עליה פעולת שליפה‪ .‬לכן‪ ,‬נגדיר‬
‫פעולה בשם מחסנית‪-‬ריקה?‪ ,‬המחזירה 'אמת' אם המחסנית ריקה‪ ,‬ו‪'-‬שקר' אם‬
‫יש במחסנית איברים‪.‬‬
‫‪‬‬
‫‪‬‬
‫לעיתים נרצה להציץ לראש המחסנית‪ ,‬כדי לדעת את ערכו של האיבר שנמצא‬
‫שם‪ ,‬אך מבלי לשלוף אותו‪ .‬איך אפשר לעשות זאת?‬
‫‪‬‬
‫‪‬‬
‫‪‬‬
‫בפעולה זו נשתמש לפני שנשלוף מהמחסנית‪ ,‬כדי למנוע מצב שבו אנחנו מנסים‬
‫לשלוף איבר ממחסנית שהיא ריקה‪.‬‬
‫דרך אחת היא לבצע פעולת שליפה‪ ,‬ואז לדחוף בחזרה למחסנית את האיבר ששלפנו‪.‬‬
‫אפשרות אחרת‪ ,‬היא להגדיר את הפעולה הצץ‪-‬למחסנית שמחזירה לנו את האיבר‬
‫שבראש המחסנית‪ ,‬אך מבלי לשלוף אותו (מבלי להוציא אותו החוצה)‪.‬‬
‫לבסוף‪ ,‬נגדיר פעולה בשם אתחל‪-‬מחסנית‪ ,‬שיוצרת מחסנית ריקה‪.‬‬
‫מחסנית‬
‫‪‬‬
‫הממשק של טיפוס הנתונים מחסנית‪:‬‬
‫מחסנית‬
‫‪‬‬
‫תארו את תכולת המחסניות ‪ S1‬ו‪ S2-‬במהלך סדרת הפעולות הבאה‪:‬‬
‫אתחל‪-‬מחסנית ‪S1 ‬‬
‫דחוף‪-‬למחסנית (‪)S1,7‬‬
‫דחוף‪-‬למחסנית ( ‪)S1,9‬‬
‫אתחל‪-‬מחסנית ‪S2 ‬‬
‫שלוף‪-‬ממחסנית (‪i  )S1‬‬
‫דחוף‪-‬למחסנית (‪)S2,i‬‬
‫דחוף‪-‬למחסנית (‪)S1,6‬‬
‫שלוף‪-‬ממחסנית (‪i  )S2‬‬
‫שלוף‪-‬ממחסנית (‪i  )S1‬‬
‫דחוף‪-‬למחסנית (‪)S1,8‬‬
‫מחסנית‬
‫‪‬‬
‫תארו את תכולת המחסניות ‪ S1‬ו‪ S2-‬במהלך סדרת הפעולות הבאה‪:‬‬
‫אתחל‪-‬מחסנית ‪S2 ‬‬
‫אתחל‪-‬מחסנית ‪S1 ‬‬
‫דחוף‪-‬למחסנית (’‪)S1,’a‬‬
‫דחוף‪-‬למחסנית (’‪)S1,’b‬‬
‫דחוף‪-‬למחסנית (’‪)S2,’c‬‬
‫שלוף‪-‬ממחסנית (‪ch  )S1‬‬
‫דחוף‪-‬למחסנית (‪)S2,ch‬‬
‫שלוף‪-‬ממחסנית (‪ch  )S1‬‬
‫אם לא מחסנית‪-‬ריקה? (‪ )S1‬אזי‬
‫הצג כפלט הצץ‪-‬למחסנית (‪)S1‬‬
‫אם לא מחסנית‪-‬ריקה? (‪ )S2‬אזי‬
‫הצג כפלט הצץ‪-‬למחסנית (‪)S2‬‬
‫‪Undo‬‬
‫‪‬‬
‫‪‬‬
‫‪‬‬
‫דוגמא לשימוש במחסנית‪ ,‬ניתן למצוא בתוכנות עריכה גראפיות‪ ,‬שמאפשרות‬
‫למעצב לבטל את הפעולה האחרונה (‪.)Undo‬‬
‫פעולת ‪ Undo‬מבטלת את הפעולה האחרונה שבוצעה (פרט לפעולת ‪Undo‬‬
‫עצמה)‪ .‬המשתמש יכול לחזור ולהפעיל פעולה זו ללא הגבלה עד לביטול הפעולה‬
‫הראשונה שעשה‪.‬‬
‫איך נממש פעולה זו?‬
‫‪‬‬
‫‪‬‬
‫בכל פעם שהמשתמש יבצע‬
‫איזושהי פעולת עיצוב (למשל‪,‬‬
‫ציור קו או נקודה)‪ ,‬נשמור את‬
‫פרטי הפעולה (למשל‪ :‬מהו צבע‬
‫הנקודה‪ ,‬מאיפה לאיפה מותחים‬
‫את הקו‪ ,‬וכו')‪.‬‬
‫את התיאורים הללו יש לשמור כך‬
‫שניתן יהיה לגשת אליהם בסדר‬
‫הפוך לסדר בו הפעולות התבצעו‪.‬‬
‫‪ ‬נשתמש במחסנית‪ :‬בכל פעם שמתבצעת פעולה (פרט לפעולת ‪ Undo‬עצמה)‪,‬‬
‫נדחוף למחסנית את תיאורה ואת הפרמטרים שלה‪ .‬כאשר המשתמש ירצה לבטל‬
‫פעולה‪ ,‬נשלוף את הפעולה שבראש המחסנית‪ ,‬ונבצע פעולה המבטלת אותה‪.‬‬
‫‪Undo‬‬
‫‪‬‬
‫כיצד תיראה המחסנית במהלך ביצוע סדרת הפעולות הבאה‪:‬‬
‫‪‬‬
‫‪‬‬
‫‪‬‬
‫‪‬‬
‫‪‬‬
‫‪‬‬
‫‪‬‬
‫צייר‪-‬קו (‪)...‬‬
‫צייר‪-‬נקודה (‪)...‬‬
‫צייר‪-‬קו (‪)...‬‬
‫‪Undo‬‬
‫צייר‪-‬מצולע (‪)...‬‬
‫‪Undo‬‬
‫‪Undo‬‬
‫‪Undo‬‬
‫‪‬‬
‫נכתוב את האלגוריתם הפותר את הבעיה‪:‬‬
‫(‪ )1‬אתחל‪-‬מחסנית ‪S ‬‬
‫(‪ )2‬כל עוד המשתמש מבצע פעולות‪ ,‬בצע‪:‬‬
‫(‪ )2.1‬קרא פעולה ‪x‬‬
‫(‪ )2.2‬אם ‪ x‬איננה הפעולה ‪ ,Undo‬אזי‪:‬‬
‫(‪ )2.2.1‬דחוף‪-‬למחסנית (‪)S,x‬‬
‫(‪ )2.2.2‬בצע את הפעולה ‪x‬‬
‫(‪ )2.3‬אחרת‪:‬‬
‫(‪ )2.3.1‬אם מחסנית‪-‬ריקה? (‪ )S‬אזי‪:‬‬
‫(‪ )2.3.1.1‬הצג כפלט‪" :‬אין פעולות בזיכרון"‬
‫(‪ )2.3.2‬אחרת‪:‬‬
‫(‪ )2.3.2.1‬שלוף‪-‬ממחסנית (‪y  )S‬‬
‫(‪ )2.3.2.2‬בצע את הפעולה המבטלת את ‪y‬‬
‫מחסנית זמן ריצה‬
‫‪‬‬
‫‪‬‬
‫‪‬‬
‫דוגמא נוספת לשימוש במחסנית‪ ,‬זהו רכיב הקיים במערכת ההפעלה‪ ,‬הנקרא‬
‫מחסנית זמן הריצה (‪ ,)run time stack‬או מחסנית הקריאות (‪.)call stack‬‬
‫בכל פעם שמזמנים פונקציה‪ ,‬במהלך ריצת התוכנית‪ ,‬מערכת ההפעלה דוחפת‬
‫לתוך מחסנית זמן הריצה נתונים שישמשו אותה כדי לדעת לאיזו נקודה בתכנית‬
‫עליה לחזור‪ ,‬לאחר שהפונקציה תסיים את ריצתה‪ .‬נתונים אלו כוללים את‬
‫הפרמטרים שאיתם זימנו את הפונקציה‪ ,‬וכן את הכתובת אליה יש לחזור בסופה‪.‬‬
‫לאחר סיום ביצוע הפונקציה‪ ,‬שולפים מהמחסנית את הרשומה שבראש‬
‫המחסנית‪ ,‬וביצוע התכנית ממשיך לפי הנתונים המופיעים שם‪.‬‬
‫‪‬‬
‫‪‬‬
‫אם מחסנית הקריאות מתמלאת‪ ,‬בעקבות קינון עמוק מדי של פונקציה‪-‬בתוך‪-‬פונקציה‪,‬‬
‫מתרחשת שגיאה הנקראת 'גלישת מחסנית' (‪ ,)Stack overflow‬המביאה בדרך כלל‬
‫לשגיאת זמן ריצה‪ ,‬ולסיום התכנית‪.‬‬
‫שגיאה כזו מתרחשת‪ ,‬למשל‪ ,‬אם כותבים פונקציה רקורסיבית‪ ,‬המזמנת את עצמה‬
‫שוב ושוב‪ ,‬ללא תנאי עצירה‪.‬‬
‫בדיקת תקינות סוגריים‬
‫‪‬‬
‫נגדיר את המושג 'ביטוי חשבוני תקין מבחינת סוגריים'‪:‬‬
‫‪‬‬
‫‪‬‬
‫‪‬‬
‫זהו ביטוי שיכול להכיל סוגריים במספר לא מוגבל‪ ,‬ובלבד שיהיו מאוזנים‪.‬‬
‫איזון הסוגריים מחייב שמספר הפותחים והסוגרים יהיה שווה בדיוק‪.‬‬
‫לדוגמא‪ ,‬הביטויים האלה תקינים‪:‬‬
‫‪‬‬
‫‪‬‬
‫‪‬‬
‫))‪((a‬‬
‫)‪( b + a – 2 * 7‬‬
‫‪( + 32 * ( 37 * ) / ( 5 + 1 ) ) – 4‬‬
‫נשים לב כי הביטוי האחרון תקין מבחינת הסוגריים‪ ,‬על אף שכביטוי חשבוני הוא אינו תקין‪.‬‬
‫‪‬‬
‫ואילו הביטויים האלה אינם תקינים‪:‬‬
‫‪‬‬
‫‪‬‬
‫‪‬‬
‫‪‬‬
‫‪a+((c‬‬
‫)‪((x+y‬‬
‫‪z+)t‬‬
‫פתחו אלגוריתם המקבל כקלט ביטוי חשבוני‪ ,‬ומחזיר 'אמת' אם הוא‬
‫תקין מבחינת סוגריים‪ ,‬ו‪'-‬שקר' אם לא‪ .‬נסו לפתור את הבעיה ללא‬
‫שימוש במחסנית‪...‬‬
‫בדיקת תקינות סוגריים‬
‫‪‬‬
‫נכתוב את האלגוריתם הבא‪ ,‬הפותר את הבעיה‪ ,‬תוך שימוש במונה‪:‬‬
‫‪counter  0‬‬
‫כל עוד יש עדיין תווים בקלט‪ ,‬בצע‪:‬‬
‫קרא תו ‪ ch‬מהקלט‬
‫אם ‪ ch‬שווה לתו ’(‘ ‪ ,‬אזי‪:‬‬
‫הגדל ב‪ 1-‬את ‪counter‬‬
‫אם ‪ ch‬שווה לתו ’)‘ ‪ ,‬אזי‪:‬‬
‫הקטן ב‪ 1-‬את ‪counter‬‬
‫אם ‪ ,counter < 0‬אזי‪:‬‬
‫החזר 'שקר'‬
‫אם ‪ ,counter > 0‬אזי‪:‬‬
‫החזר 'שקר'‬
‫אחרת‪:‬‬
‫החזר 'אמת'‬
‫בדיקת תקינות סוגריים‬
‫‪‬‬
‫כעת‪ ,‬נגדיר מחדש את המושג 'ביטוי חשבוני תקין מבחינת סוגריים'‪ ,‬כך‬
‫שיכלול גם ביטויים המכילים סוגריים מסוגים שונים‪:‬‬
‫‪‬‬
‫‪‬‬
‫‪‬‬
‫כעת הביטוי יכול להכיל‪ ,‬בנוסף לסוגריים רגילים‪ ,‬גם סוגריים מסולסלים‪ ,‬מרובעים‪,‬‬
‫וכו'‪ ,‬במספר לא מוגבל‪ ,‬ובלבד שיהיו מאוזנים‪.‬‬
‫איזון הסוגריים מחייב שמספר הפותחים והסוגרים יהיה שווה בדיוק‪ ,‬וכן שכנגד כל‬
‫פותח יימצא הסוגר המתאים מאותו סוג במקום המתאים‪.‬‬
‫לדוגמא‪ ,‬הביטויים האלה תקינים‪:‬‬
‫‪‬‬
‫‪‬‬
‫‪‬‬
‫))‪((a‬‬
‫]‪[b+a–2*7‬‬
‫‪{ + 32 * ( 37 * ) / [ 5 + 1 ] } – 4‬‬
‫נשים לב כי הביטוי האחרון תקין מבחינת הסוגריים‪ ,‬על אף שכביטוי חשבוני הוא אינו תקין‪.‬‬
‫‪‬‬
‫ואילו הביטויים האלה אינם תקינים‪:‬‬
‫‪‬‬
‫‪‬‬
‫‪‬‬
‫‪a+((c‬‬
‫]‪([3+a)+4‬‬
‫]‪[ ) ( 5 – 3 ] * [ 2 – 3‬‬
‫• האם לדעתכם ניתן יהיה להשתמש‬
‫באותו הרעיון האלגוריתמי כמקודם?‬
‫• ואם נגדיר כמה מונים?‬
‫בדיקת תקינות סוגריים‬
‫‪‬‬
‫‪‬‬
‫‪‬‬
‫‪‬‬
‫‪‬‬
‫‪‬‬
‫כדי לפתור את הבעיה נשתמש במחסנית‪ ,‬ונסרוק את הביטוי משמאל לימין‪.‬‬
‫כיוון שאנו בודקים את תקינות הביטוי רק מבחינת הסוגריים‪ ,‬נתעלם מכל התווים‬
‫שאינם סוגריים‪.‬‬
‫בזמן סריקת הביטוי‪ ,‬נדחוף למחסנית כל פותח שניתקל בו‪.‬‬
‫כשניתקל בסוגר‪ ,‬אז נשלוף מהמחסנית את הפותח הנמצא בראשה‪ ,‬ונשווה‬
‫ביניהם‪ .‬אם הם לא מאותו סוג – אז הביטוי אינו תקין‪.‬‬
‫ניסיון לשלוף פותח ממחסנית ריקה מציין גם הוא שהביטוי שאינו תקין‪ ,‬שכן אז‬
‫קיימים יותר סוגרים מפותחים‪.‬‬
‫אם הגענו לסוף הקלט והמחסנית טרם התרוקנה‪ ,‬סימן שהיו פותחים שאין להם‬
‫סוגרים‪ ,‬ואז הביטוי אינו תקין‪ .‬אם לעומת זאת המחסנית ריקה בסוף הקלט‪ ,‬אז‬
‫הביטוי תקין‪.‬‬
‫בדיקת תקינות סוגריים‬
‫‪‬‬
‫נכתוב את האלגוריתם‪:‬‬
‫אתחל‪-‬מחסנית ‪S ‬‬
‫כל עוד יש עדיין תווים בקלט‪ ,‬בצע‪:‬‬
‫קרא תו ‪ ch‬מהקלט‬
‫אם ‪ ch‬הוא פותח‪ ,‬אזי‪:‬‬
‫דחוף‪-‬למחסנית (‪)S,ch‬‬
‫אם ‪ ch‬הוא סוגר‪ ,‬בצע‪:‬‬
‫אם מחסנית‪-‬ריקה? (‪ , )S‬אזי‪:‬‬
‫החזר 'שקר'‬
‫אחרת‪:‬‬
‫שלוף‪-‬ממחסנית (‪old_ch  )S‬‬
‫אם ‪ ch‬ו‪ old_ch-‬אינם מתאימים‪ ,‬אזי‪:‬‬
‫החזר 'שקר'‬
‫אם לא מחסנית‪-‬ריקה? (‪ , )S‬אזי‪:‬‬
‫החזר 'שקר'‬
‫אחרת‪:‬‬
‫החזר 'אמת'‬
‫בדיקת תקינות סוגריים‬
‫)}‪5+([3+2]/{4-1‬‬
‫בדיקת תקינות סוגריים‬
‫)}‪5+([3+2]/{4-1‬‬
‫(‬
‫בדיקת תקינות סוגריים‬
‫)}‪5+([3+2]/{4-1‬‬
‫[‬
‫(‬
‫בדיקת תקינות סוגריים‬
‫)}‪5+([3+2]/{4-1‬‬
‫[‬
‫(‬
‫בדיקת תקינות סוגריים‬
‫)}‪5+([3+2]/{4-1‬‬
‫{‬
‫(‬
‫בדיקת תקינות סוגריים‬
‫)}‪5+([3+2]/{4-1‬‬
‫{‬
‫(‬
‫בדיקת תקינות סוגריים‬
‫)}‪5+([3+2]/{4-1‬‬
‫(‬
‫בדיקת תקינות סוגריים‬
‫)}‪5+([3+2]/{4-1‬‬
‫הביטוי תקין‬
‫בדיקת תקינות סוגריים‬
‫)‪(2+{5*2))+(3/5‬‬
‫בדיקת תקינות סוגריים‬
‫)‪(2+{5*2))+(3/5‬‬
‫(‬
‫בדיקת תקינות סוגריים‬
‫)‪(2+{5*2))+(3/5‬‬
‫{‬
‫(‬
‫בדיקת תקינות סוגריים‬
‫)‪(2+{5*2))+(3/5‬‬
‫{‬
‫(‬
‫אין התאמה בין הסוגר הנוכחי‬
‫לבין הפותח שבראש המחסנית‬
‫הביטוי אינו תקין!‬
‫בדיקת תקינות סוגריים‬
‫‪( ( 2 + 5) + 3‬‬
‫בדיקת תקינות סוגריים‬
‫‪( ( 2 + 5) + 3‬‬
‫(‬
‫בדיקת תקינות סוגריים‬
‫‪( ( 2 + 5) + 3‬‬
‫(‬
‫(‬
‫בדיקת תקינות סוגריים‬
‫‪( ( 2 + 5) + 3‬‬
‫(‬
‫(‬
‫בדיקת תקינות סוגריים‬
‫‪( ( 2 + 5) + 3‬‬
‫(‬
‫המחסנית אינה ריקה בסיום המעבר על הביטוי‬
‫הביטוי אינו תקין!‬
‫מחסנית‬
‫‪‬‬
‫‪‬‬
‫במהלך לימודינו הכרנו כל מיני טיפוסי נתונים (‪ :)data types‬מספר שלם (‪,)int‬‬
‫מספר ממשי (‪ ,)float‬תו (‪ ,)char‬וכו'‪.‬‬
‫גם מחסנית (‪ ,)stack‬כפי שהגדרנו אותה‪ ,‬היא סוג של טיפוס נתונים‪ ,‬אם כי‬
‫להבדיל מטיפוסי נתונים מוחשיים‪ ,‬הקיימים בשפת ‪ C‬ואנו יודעים כיצד הם‬
‫ממומשים בזיכרון המחשב‪ ,‬כגון ‪ ,int, double, long‬וכו'‪ ,‬הרי שהמחסנית היא‬
‫‪‬‬
‫‪‬‬
‫טיפוס נתונים מופשט (טנ"מ)‪ ,‬או באנגלית ‪.Abstract Data Type (ADT) -‬‬
‫מה הכוונה?‬
‫‪‬‬
‫‪‬‬
‫‪‬‬
‫מחסנית היא טיפוס נתונים לכל דבר‪ :‬אפשר להגדיר משתנה מטיפוס מחסנית‪ ,‬לכתוב‬
‫פונקציה שמקבלת מחסנית כפרמטר‪ ,‬להגדיר מערך של מחסניות‪ ,‬לכתוב פונקציה‬
‫שמחזירה מחסנית‪ ,‬להגדיר משתנה שהוא מצביע למחסנית‪ ,‬וכו'‪.‬‬
‫מצד שני‪ ,‬מחסנית היא טיפוס נתונים מופשט‪ ,‬במובן זה שאנחנו יכולים לעשות את כל‬
‫הדברים המפורטים מעלה‪ ,‬מבלי לדעת כיצד היא ממומשת בזיכרון המחשב‪.‬‬
‫כל העבודה שלנו עם משתנה מטיפוס מחסנית נעשתה דרך פעולות ממשק‬
‫(‪ ,)interface‬מבלי שהתעניינו כלל בשאלה כיצד נעשה בפועל המימוש‬
‫(‪ .)implementation‬הפרדה זו בין ממשק למימוש‪ ,‬וההסתרה של פרטי המימוש‬
‫מהמתכנת‪ ,‬היא מהמאפיינים של טנ"מ (‪.)ADT‬‬
‫מחסנית‬
‫‪‬‬
‫‪‬‬
‫‪‬‬
‫‪‬‬
‫‪‬‬
‫איך תיעשה ההפרדה בין ממשק למימוש בסביבת העבודה של ‪?C‬‬
‫נבנה בעצמנו יחידת ספרייה ‪ ,stack.h‬שתכלול את הכותרות של כל הפונקציות‬
‫הפועלות על מחסנית (דחיפה‪ ,‬שליפה‪ ,‬בדיקה האם ריק‪ ,‬איתחול‪ ,‬הצצה)‪.‬‬
‫נכתוב בקובץ ‪ stack.c‬מימוש לכל הפעולות השונות על מחסנית‪.‬‬
‫כשמתכנת אחר ירצה להשתמש במחסנית‪ ,‬הוא יצרף את יחידת הספרייה שלנו‬
‫באמצעות ההוראה ”‪ ,#include “stack.h‬ויזמן את הפונקציות הכלולות בה‪ ,‬מבלי‬
‫לדעת כיצד הן מומשו‪.‬‬
‫במהלך הקורס‪ ,‬נממש את טיפוס הנתונים המופשט 'מחסנית' בצורות שונות‪:‬‬
‫‪‬‬
‫‪‬‬
‫‪‬‬
‫‪‬‬
‫‪‬‬
‫באמצעות מערך סטטי‬
‫באמצעות מערך דינאמי‬
‫באמצעות רשימה מקושרת‬
‫וכו'‬
‫האם המתכנת שישתמש ביחידת הספרייה ‪ stack.h‬יהיה מודע למבנה הנתונים‬
‫בו אנחנו משתמשים מאחורי הקלעים?‬
‫‪‬‬
‫לא‪ ,‬ובכך תישמר ההפרדה בין ממשק למימוש‪.‬‬
‫מחסנית‬
‫‪‬‬
‫פתחו אלגוריתם אשר מקבל כקלט מחסנית ‪ S‬לא ריקה‪ ,‬ומחזיר את הערך‬
‫הגדול ביותר הנמצא בה‪ .‬הניחו ש‪ S-‬מועבר כפרמטר לפי ערך‪.‬‬
‫מצא‪-‬מקסימום (‪)S‬‬
‫אתחל‪-‬מחסנית ‪S1 ‬‬
‫שלוף‪-‬ממחסנית (‪max  )S‬‬
‫דחוף‪-‬למחסנית (‪)S1,max‬‬
‫כל עוד לא מחסנית‪-‬ריקה? (‪ ,)S‬בצע‪:‬‬
‫שלוף‪-‬ממחסנית (‪x  )S‬‬
‫דחוף‪-‬למחסנית (‪)S1,x‬‬
‫אם ‪ ,x > max‬אזי‪:‬‬
‫‪max  x‬‬
‫החזר ‪max‬‬
‫‪‬‬
‫האם הכרחי היה להשתמש במחסנית עזר?‬
‫מחסנית‬
‫‪‬‬
‫פתחו אלגוריתם אשר מקבל כקלט מחסנית ‪ S‬לא ריקה‪,‬‬
‫ומחזיר מחסנית המכילה את אותם האיברים‪ ,‬אך בסדר הפוך‪.‬‬
‫הפוך‪-‬מחסנית (‪)S‬‬
‫אתחל‪-‬מחסנית ‪S1 ‬‬
‫כל עוד לא מחסנית‪-‬ריקה? (‪ ,)S‬בצע‪:‬‬
‫שלוף‪-‬ממחסנית (‪x  )S‬‬
‫דחוף‪-‬למחסנית (‪)S1,x‬‬
‫החזר ‪S1‬‬
‫במקום שתי הוראות אלו‪ ,‬אפשר היה לרשום‪:‬‬
‫דחוף‪-‬למחסנית (שלוף‪-‬ממחסנית (‪)S1,)S‬‬
‫מחסנית‬
‫‪‬‬
‫פתחו אלגוריתם אשר מקבל כקלט מחסנית ‪ S‬לא ריקה‪ ,‬אשר אין בה‬
‫איבר המופיע יותר מפעם אחת‪ .‬האלגוריתם יחזיר מחסנית חדשה‪ ,‬שזהה‬
‫למחסנית ‪ ,S‬פרט לכך שהערך הגדול ביותר מבין איברי המחסנית ‪S‬‬
‫נמצא בראש המחסנית החדשה‪.‬‬
‫הגדול‪-‬בראש (‪)S‬‬
‫אתחל‪-‬מחסנית ‪S1 ‬‬
‫מצא‪-‬מקסימום (‪max  )S‬‬
‫כל עוד לא מחסנית‪-‬ריקה? (‪ ,)S‬בצע‪:‬‬
‫שלוף‪-‬ממחסנית (‪x  )S‬‬
‫אם ‪ ,x < max‬אזי‪:‬‬
‫דחוף‪-‬למחסנית (‪)S1,x‬‬
‫הפוך‪-‬מחסנית (‪S1  )S1‬‬
‫דחוף‪-‬למחסנית (‪)S1,max‬‬
‫החזר ‪S1‬‬
‫מחסנית‬
‫‪‬‬
‫פתחו אלגוריתם אשר מקבל כקלט מחסנית ‪ S‬לא ריקה‪ .‬האלגוריתם יחזיר מחסנית‬
‫חדשה‪ ,‬שזהה למחסנית ‪ ,S‬פרט לכך שהערך הגדול ביותר מבין איברי המחסנית ‪ S‬נמצא‬
‫בראש המחסנית החדשה‪ .‬במידה והוא מופיע יותר מפעם אחת‪ ,‬יש להעביר לראש‬
‫המחסנית את כל הערכים האלה‪.‬‬
‫הגדולים‪-‬בראש (‪)S‬‬
‫אתחל‪-‬מחסנית ‪S1 ‬‬
‫אתחל‪-‬מחסנית ‪S2 ‬‬
‫מצא‪-‬מקסימום (‪max  )S‬‬
‫כל עוד לא מחסנית‪-‬ריקה? (‪ ,)S‬בצע‪:‬‬
‫שלוף‪-‬ממחסנית (‪x  )S‬‬
‫אם ‪ ,x < max‬אזי‪:‬‬
‫דחוף‪-‬למחסנית (‪)S1,x‬‬
‫אחרת‪:‬‬
‫דחוף‪-‬למחסנית (‪)S2,x‬‬
‫הפוך‪-‬מחסנית (‪S1  )S1‬‬
‫כל עוד לא מחסנית‪-‬ריקה? (‪ ,)S2‬בצע‪:‬‬
‫דחוף‪-‬למחסנית (שלוף‪-‬ממחסנית (‪)S1,)S2‬‬
‫החזר ‪S1‬‬
‫האם היה הכרחי‬
‫להשתמש בשתי‬
‫מחסניות לצורך‬
‫פתרון הבעיה?‬
‫מחסנית‬
‫‪‬‬
‫פתחו אלגוריתם אשר מקבל כקלט מחסנית ‪ S‬לא ריקה‪ .‬האלגוריתם יחזיר מחסנית‬
‫חדשה‪ ,‬שזהה למחסנית ‪ ,S‬פרט לכך שהערך הגדול ביותר מבין איברי המחסנית ‪ S‬נמצא‬
‫בראש המחסנית החדשה‪ .‬במידה והוא מופיע יותר מפעם אחת‪ ,‬יש להעביר לראש‬
‫המחסנית את כל הערכים האלה‪.‬‬
‫הגדולים‪-‬בראש (‪)S‬‬
‫אתחל‪-‬מחסנית ‪S1 ‬‬
‫‪count_max  0‬‬
‫מצא‪-‬מקסימום (‪max  )S‬‬
‫כל עוד לא מחסנית‪-‬ריקה? (‪ ,)S‬בצע‪:‬‬
‫שלוף‪-‬ממחסנית (‪x  )S‬‬
‫אם ‪ ,x < max‬אזי‪:‬‬
‫דחוף‪-‬למחסנית (‪)S1,x‬‬
‫אחרת‪:‬‬
‫הגדל ב‪ 1-‬את ‪count_max‬‬
‫הפוך‪-‬מחסנית (‪S1  )S1‬‬
‫בצע ‪ count_max‬פעמים‪:‬‬
‫דחוף‪-‬למחסנית (‪)S1,max‬‬
‫החזר ‪S1‬‬
‫כתיב תוכי (‪)infix‬‬
‫‪‬‬
‫כאשר אנחנו כותבים ביטויים חשבוניים‪ ,‬אנחנו בדרך כלל נוהגים לכתוב את‬
‫האופרטור (‪ )operator‬בין שני האופרנדים (‪ .)operands‬לדוגמא‪:‬‬
‫‪‬‬
‫‪‬‬
‫‪‬‬
‫‪‬‬
‫‪‬‬
‫‪‬‬
‫בביטוי ‪ ,2 + 3‬האופרטור '‪ '+‬נמצא בין שני האופרנדים '‪ '2‬ו‪.'3'-‬‬
‫בביטוי )‪ ,A + (B * C‬האופרטור '*' נמצא בין שני האופרנדים '‪ 'B‬ו‪ ,'C'-‬והאופרטור '‪'+‬‬
‫נמצא בין שני האופרנדים ’‪ ‘A‬ו‪.')B * C('-‬‬
‫בביטוי ‪ ,(A + B) * C‬האופרטור '‪ '+‬נמצא בין שני האופרנדים '‪ 'A‬ו‪ ,'B'-‬והאופרטור '*'‬
‫נמצא בין שני האופרנדים '‪ 'C‬ו‪.')A + B('-‬‬
‫צורת כתיבה כזו נקראת כתיב תוכי (‪ ,)infix notation‬כיוון שהאופרטור נמצא‬
‫"בתוך" הביטוי‪ ,‬בין שני האופרנדים‪ .‬לביטויים הכתובים בייצוג תוכי קוראים‬
‫ביטויים תוכיים (‪.)infix experssions‬‬
‫אנחנו רגילים להשתמש בכתיב תוכי כשאנחנו כותבים ביטויים מתמטיים‪ ,‬אבל יש‬
‫לצורת כתיבה זו חולשה בולטת – היא דורשת שימוש בסוגריים כדי לכפות סדר‬
‫של ביצוע פעולות בתוך הביטוי‪.‬‬
‫נכיר כעת שתי צורות כתיב אחרות ‪ -‬כתיב תחילי (‪ )prefix notation‬וכתיב סופי‬
‫(‪ - )postfix notation‬שאינן מחייבות שימוש בסוגריים‪.‬‬
‫כתיב תחילי (‪)prefix‬‬
‫‪‬‬
‫‪‬‬
‫ב‪ 1920-‬המציא המתמטיקאי הפולני ‪ Jan Łukasiewicz‬צורת כתיב של ביטויים‬
‫אלגבריים ולוגיים‪ ,‬שבה האופרטור מופיע לפני האופרנדים‪ ,‬במקום בין שני‬
‫האופרנדים כמקובל‪.‬‬
‫צורת כתיבה זו נקראת כתיב תחילי (‪ ,)prefix notation‬וביטויים המופיעים בייצוג‬
‫זה נקראים ביטויים תחיליים (‪ .)prefix expressions‬לדוגמא‪:‬‬
‫‪‬‬
‫‪‬‬
‫‪‬‬
‫‪‬‬
‫‪‬‬
‫נשים לב שבדוגמא השלישית‪ ,‬בכתיב תוכי היה צורך להשתמש בסוגריים על מנת‬
‫לדעת מהי הפעולה המתבצעת קודם (לא ניתן היה להשמיט את הסוגריים‪ ,‬שכן‬
‫לכפל קדימות יותר גבוהה מאשר לחיבור)‪ ,‬ואילו בכתיב התחילי לא היה צורך‬
‫בסוגריים כלל‪ .‬זהו אחד המאפיינים של צורת ייצוג זו‪.‬‬
‫בשפת התכנות אסמבלי‪ ,‬הביטויים כתובים בכתיב תחילי‪:‬‬
‫‪‬‬
‫‪‬‬
‫‪‬‬
‫‪‬‬
‫הביטוי התוכי ‪ 2 + 3‬ייכתב בכתיב תחילי בתור ‪. + 2 3‬‬
‫הביטוי התוכי )‪ A + (B * C‬ייכתב בכתיב תחילי בתור ‪. + A * B C‬‬
‫הביטוי התוכי ‪ (A + B) * C‬ייכתב בכתיב תחילי בתור ‪.* + A B C‬‬
‫‪ADD AX,BX‬‬
‫‪SUB AX, BX‬‬
‫וכו'‬
‫יש המכנים צורת כתיבה זו בשם כתיב פולני (‪ ,)Polish notation‬לזכר‬
‫‪ Łukasiewicz‬הפולני‪ ,‬שהכניס לראשונה את הייצוג הזה לשימוש‪.‬‬
‫כתיב סופי (‪)postfix‬‬
‫‪‬‬
‫צורת כתיבה אחרת היא זו שבה האופרטור מופיע אחרי שני האופרנדים‪ .‬צורת‬
‫כתיבה זו נקראת כתיב סופי (‪ ,)postfix notation‬וביטויים המופיעים בייצוג זה‬
‫נקראים ביטויים סופיים (‪ .)postfix expressions‬לדוגמא‪:‬‬
‫‪‬‬
‫‪‬‬
‫‪‬‬
‫‪‬‬
‫‪‬‬
‫‪‬‬
‫הביטוי התוכי ‪ 2 + 3‬ייכתב בכתיב סופי בתור ‪. 2 3 +‬‬
‫הביטוי התוכי )‪ A + (B * C‬ייכתב בכתיב סופי בתור ‪. A B C * +‬‬
‫הביטוי התוכי ‪ (A + B) * C‬ייכתב בכתיב סופי בתור * ‪. A B + C‬‬
‫גם בצורת כתיבה זו אין צורך להשתמש בסוגריים כלל‪.‬‬
‫יש המכנים צורת זו בשם כתיב פולני הפוך (‪.)Reversed Polish notation‬‬
‫בשנות ה‪ '70-‬וה‪ '80-‬היו מחשבוני כיס מסוימים שעבדו בשיטה זו (כלומר‪ ,‬קודם‬
‫היה צריך להקליד את האופרנדים‪ ,‬ורק אח"כ את האופרטור)‪.‬‬
‫כתיב תחילי‪ ,‬תוכי‪ ,‬סופי‬
‫‪‬‬
‫המירו את הביטויים הבאים מייצוג תוכי לייצוג סופי ולייצוג תחילי‪:‬‬
‫ייצוג תוכי‬
‫‪7–2‬‬
‫‪A+B–C‬‬
‫)‪(A + B) * (C – D‬‬
‫‪(A – B) * C + D‬‬
‫‪‬‬
‫ייצוג סופי‬
‫–‪72‬‬
‫–‪AB+C‬‬
‫*–‪AB+CD‬‬
‫‪AB–C*D+‬‬
‫ניתן לכתוב אלגוריתמים המקבלים ביטוי‬
‫באחד משולשת הייצוגים‪ ,‬וממירים אותו‬
‫לשני הייצוגים האחרים‪.‬‬
‫ייצוג תחילי‬
‫‪–72‬‬
‫‪– +ABC‬‬
‫‪*+AB–CD‬‬
‫‪+*–ABCD‬‬
‫חישוב ביטוי בייצוג סופי‬
‫‪‬‬
‫כעת נכיר אלגוריתם המקבלים ביטוי בייצוג סופי‪ ,‬המכיל אופרטורים ומספרים‪,‬‬
‫ומחשב את ערכו‪ ,‬תוך שימוש במחסנית‪:‬‬
‫‪‬‬
‫‪‬‬
‫‪‬‬
‫‪‬‬
‫בכל פעם שאנו קוראים אופרנד‪ ,‬נדחוף אותו למחסנית‪.‬‬
‫לכן כאשר נגיע לאופרטור‪ ,‬נדע כי האופרנדים שלו (שעליהם הוא פועל) הינם שני‬
‫האיברים העליונים במחסנית‪.‬‬
‫אנו יכולים לשלוף את שני האיברים האלה ולבצע עליהם את הפעולה המצוינת על‪-‬ידי‬
‫האופרטור‪.‬‬
‫את התוצאה נדחוף למחסנית‪ ,‬כדי שתהיה שם כאופרנד בשביל האופרטור הבא‪.‬‬
‫חישוב ביטוי בייצוג סופי‬
‫‪‬‬
‫נכתוב את האלגוריתם‪:‬‬
‫אתחל‪-‬מחסנית ‪S ‬‬
‫כל עוד יש תווים בקלט‪ ,‬בצע‪:‬‬
‫קרא את תו הקלט הבא והשם אותו ב‪symbol-‬‬
‫אם ‪ symbol‬הוא אופרנד‪ ,‬אזי‪:‬‬
‫דחוף‪-‬למחסנית )‪(S,symbol‬‬
‫אחרת‪:‬‬
‫שלוף‪-‬ממחסנית )‪second_operand  (S‬‬
‫שלוף‪-‬ממחסנית )‪first_operand  (S‬‬
‫הפעל את האופרטור ‪ symbol‬על ‪ first_operand‬ו‪-‬‬
‫‪ ,second_operand‬והשם את התוצאה ב‪value-‬‬
‫דחוף‪-‬למחסנית )‪(S,value‬‬
‫החזר את שלוף‪-‬ממחסנית )‪(S‬‬
‫‪‬‬
‫‪‬‬
‫אם נסמן ב‪ n-‬את אורך מחרוזת הקלט‪ ,‬מהו סדר הגודל (חסם אסימפטוטי) של‬
‫סיבוכיות זמן הריצה‪ ,‬כפונקציה של ‪?n‬‬
‫עקבו אחר ריצת האלגוריתם עבור הביטוי הבא‪ ,‬הנתון בייצוג סופי‪:‬‬
‫‪623+–382/+*2*3+‬‬
‫נוסחאות נסיגה‬
‫‪‬‬
‫‪‬‬
‫‪‬‬
‫ראינו שאפשר לתאר את זמן הריצה של אלגוריתמים רקורסיביים‬
‫באמצעות נוסחת נסיגה‪ ,‬אשר מביעה את זמן הריצה של הבעיה‬
‫בעבור קלט בגודל ‪.n‬‬
‫נוכחנו בכך כשפיתחנו את באלגוריתם הרקורסיבי לפתרון בעיית‬
‫מגדלי האנוי‪ ,‬וכן כשפיתחנו את האלגוריתם למיון‪-‬מיזוג‪.‬‬
‫בשני המקרים ראינו כיצד לבנות נוסחה רקורסיבית המתארת את‬
‫מספר הצעדים המתבצע‪ ,‬ועברנו מכלל הנסיגה אל ביטוי סגור‪,‬‬
‫בדרכים שונות‪:‬‬
‫‪‬‬
‫‪‬‬
‫‪‬‬
‫שיטת ההצבה‪ :‬ניחוש הפתרון והוכחת נכונותו על‪-‬ידי אינדוקציה מתמטית‪.‬‬
‫שיטת האיטרציה‪ :‬פיתוח חוזר‪-‬ונשנה של הנוסחה הרקורסיבית‪ ,‬עד שמגיעים‬
‫לתנאי העצירה‪ ,‬ואז פישוט הביטוי המתקבל‪.‬‬
‫נכיר כעת מספר משפטים אשר יסייעו לנו לפתור משפחות של‬
‫נוסחאות נסיגה‪.‬‬
‫נוסחאות נסיגה‬
‫‪‬‬
‫משפט ‪ :1‬נתונים ‪ a,b,c‬קבועים לא‪-‬שליליים‪ ,‬המקיימים‬
‫‪ a>=1‬ו‪ .b>1-‬הפתרון לנוסחת הנסיגה‪:‬‬
‫‪T(n) = a · T(n/b) + cn‬‬
‫‪‬‬
‫הוא‪:‬‬
‫אם ‪a < b‬‬
‫אם ‪a = b‬‬
‫אם ‪a > b‬‬
‫)‪T(n) = Θ(n‬‬
‫)‪T(n) = Θ(nlogn‬‬
‫)‪T(n) = Θ(nlogba‬‬
‫נוסחאות נסיגה‬
‫‪‬‬
‫למשל‪ ,‬נוסחת הנסיגה של מיון‪-‬מיזוג היא‪:‬‬
‫)‪T(n) = 2T(n/2) + Θ(n‬‬
‫‪‬‬
‫נוסחה זו מתאימה למשפט ‪ ,1‬עבור הקבועים ‪b = 2 ,a = 2‬‬
‫ו‪ c-‬כלשהו‪.‬‬
‫הפתרון‪ ,‬מכיוון ש‪ a = b-‬הוא )‪ ,Θ(nlogn‬וזו אכן סיבוכיות זמן‬
‫הריצה של אלגוריתם למיון‪-‬מיזוג‪.‬‬
‫‪‬‬
‫תרגיל‬
‫‪‬‬
‫מהו סדר הגודל של נוסחת הנסיגה‪:‬‬
‫‪T(n) = 3T(n/2) + n‬‬
‫‪‬‬
‫נוסחה זו מתאימה למשפט ‪ ,1‬עבור הקבועים ‪, b = 2 ,a = 3‬‬
‫‪ .c = 1‬הפתרון‪ ,‬מכיוון ש‪ ,a > b-‬הוא )‪ ,Θ(nlog23‬כלומר‪,‬‬
‫בערך )‪.Θ(n1.58‬‬
‫תרגיל‬
‫‪‬‬
‫חשבו את סדר הגודל של נוסחת הנסיגה הבאה‪:‬‬
‫‪T(n) = 5T(n/5) + 15n‬‬
‫‪‬‬
‫נוסחה זו מתאימה למשפט ‪ ,1‬עבור הקבועים ‪b = 5 ,a = 5‬‬
‫ו‪ .c=15-‬הפתרון‪ ,‬מכיוון ש‪ ,a = b-‬הוא )‪.T(n)= Θ(nlogn‬‬
‫תרגיל‬
‫‪‬‬
‫‪‬‬
‫‪‬‬
‫‪‬‬
‫חשבו את סדר הגודל של נוסחת הנסיגה הבאה‪:‬‬
‫‪T(n) = T(n/2) + n‬‬
‫נוסחה זו מתאימה למשפט ‪,1‬‬
‫עבור הקבועים ‪b = 2 ,a = 1‬‬
‫ו‪.c = 1-‬‬
‫הפתרון‪ ,‬מכיוון ש‪,a < b-‬‬
‫הוא )‪.T(n) = Θ(n‬‬
‫נסו להגיע לפתרון זה על‪-‬ידי‬
‫שימוש בשיטת האיטרציה‪.‬‬