מבני נתונים פתרונות לסט שאלות דומה לשאלות מתרגיל 3 השאלות נתונה רשימה משורשרת L המכילה n מספרים שלמים חיוביים מתחום לא חסום כאשר 1 k n = 2 עבור > 0 k כלשהו. נניח שהמספרים ברשימה מקיימים את התכונה הבאה: המספר המקסימלי ב- L מופיע בדיוק פעם אחת, המספר השני הכי גדול מופיע פעמיים, המספר השלישי הכי גדול מופיע 4 פעמים, המספר הרביעי הכי גדול מופיע 8 פעמים וכך הלאה..1 max min א. מה יעילותו של האלגוריתם הבא למיון L? הוכיחו תשובתכם. כל עוד יש מספרים ב- L: 1. נמצא את המקסימום הנוכחי max ב- L. L הנוכחית מתחילתה, נסרוק את הרשימה 2. ונוריד אותו מ- L. ב. מה יעילותו של האלגוריתם הבא למיון L? הוכיחו תשובתכם. כל עוד יש מספרים ב- L: 1. נמצא את המינימום הנוכחי min ב- L. L הנוכחית מתחילתה, נסרוק את הרשימה 2. ונוריד אותו מ- L. נדפיס כל מספר ששווה ל- נדפיס כל מספר ששווה ל- נגדיר סדרה בנויה היטב של סוגריים באופן רקורסיבי: 2. הסדרות ),(,{ ] [ בנויות היטב. אם a ו- b שתי סדרות בנויות היטב אז גם הסדרות הבאות בנויות היטב: [a],{a,(a) b). ו- a כאן לשרשור הסדרות (הכוונה ab למשל {] ו- ] [ { אינן בנויות היטב, ואילו הסדרה ) ( ] { [ בנויה היטב. כתבו אלגוריתם המקבל קלט הבנוי מסוגריים ומכריע האם הוא סדרה בנויה היטב. על האלגוריתם להשתמש רק במחסנית כמבנה נתונים, כאשר השימוש במחסנית יהיה כקופסה שחורה, כלומר רק ע"י הפעולות,Push(x),IsEmpty( ),Top( ),Pop( ) ו- ),MakeEmpty( מבלי להיכנס למימוש הפנימי של המחסנית. לצורך מעבר על הקלט על האלגוריתם להשתמש בפעולה ) GetNextParen( המחזיר את סימן הסוגריים הבא בקלט, או END_OF_INPUT בסוף הקלט. 3. נניח שמוגדר טיפוס הנתונים המופשט מחסנית Stack היכול להכיל מספרים שלמים, ולבצע את הפעולות הבאות:,Push(x),IsEmpty( ),Top( ),Pop( ) ו- ).MakeEmpty( כמו כן מוגדר טיפוס הנתונים המופשט תור דו-כווני DQueue היכול להכיל מספרים שלמים ולבצע את הפעולות הבאות: ) MakeEmpty( מרוקנת את התור.
) IsEmpty( מחזירה true אם"ם התור ריק. HeadEnqueue(x) מוסיפה נתון חדש לראש התור. ) HeadDequeue( - מורידה ומחזירה את הנתון הנצמא בראש התור. TailEnqueue(x) מוסיפה נתון חדש בסוף התור. ) TailDequeue( מורידה ומחזירה את הנתון הנמצא בסוף התור. תור דו-כווני דומה מאוד לתור, אולם הוא מאפשר להוסיף ולהוריד נתונים משני צדי התור. 2 הראו כיצד ניתן לממש בעזרת מחסנית אחת S, תור דו-כווני אחד,DQ וזיכרון נוסף בגודל קבוע (ללא שימוש ברקורסיה) את טיפוס הנתונים המופשט מחסנית אמצע MStack היכולה להכיל מספרים שלמים, ולבצע את הפעולות הבאות, כל אחת ב- (1)Θ : MPush(x) מוסיפה נתון חדש לראש המחסנית (כמו מחסנית רגילה) ) MPop( מורידה את הנתון שנמצא בראש המחסנית ומחזירה אותו כערך(כמו מחסנית רגילה) MPushMid(x) מכניסה את x לאמצע המחסנית. אם במבנה כרגע n נתונים, ו- n זוגי אז x יכנס בדיוק לאמצע, אך אם n אי-זוגי x יכנס n 1 אחרי הנתון ה-. 4. ברצוננו לממש טיפוס נתונים מופשט מחסנית מינימום MinStack שתומך בפעולות,Push(x) ו- Pop() הרגילות המוגדרות על מחסנית, וכן מאפשר לבצע את הפעולה ) Min( שמחזירה את האיבר הקטן ביותר מבין הנתונים שנמצאים כרגע במחסנית (מבלי להוציא אותו מהמחסנית!). א. תארו מבנה נתונים שמאפשר לבצע כל אחת מהפעולות האלה בזמן (1)Θ. ב. כעת נרצה להוסיף לטיפוס הנתונים המופשט מחסנית את מינימום את הפעולה ) DeleteMin( המוציאה את הנתון המינימלי מהמחסנית. הוכיחו כי לפחות אחת מבין הפעולות Push(x), ) DeleteMin( Pop( ), Min( ), תדרוש Ω(log(n)) בכל מימוש שהוא. 5. נרצה לממש תור בעזרת 2 מחסניות. הסבירו כיצד תתבצע כל אחת מהפעולות הבסיסיות המוגדרות על תור, תוך שימוש בפעולות הבסיסיות של מחסנית כקופסאות שחורות, וכן זיכרון נוסף בגודל קבוע (ללא שימוש ברקורסיה). א. הציעו מימוש בו היעילות הנדרשת עבור פעולות התור:.O(1) תתבצע ב- EnQueue(x) ) DeQueue( תתבצע ב-,O(k) כאשר k הוא מספר הנתונים כרגע בתור. בנוסף נדרש שכל סדרה של n פעולות EnQueue ו- DeQueue (בסדר כלשהו) המתחילה מתור.O( n ריק תיקח ) 2 ב. הציעו מימוש בו היעילות הנדרשת עבור פעולות התור:.O(1) תתבצע ב- EnQueue(x) ) DeQueue( תתבצע ב-,O(k) כאשר k הוא מספר הנתונים כרגע בתור. בנוסף נדרש שכל סדרה של n פעולות EnQueue ו- DeQueue (בסדר כלשהו) המתחילה מתור ריק תיקח.O(n)
ע( *6. ברצוננו לממש טיפוס נתונים מופשט תור מינימום MinQueue שתומך בפעולות,EnQueue(x) ו- DeQueue() הרגילות המוגדרות על תור, וכן מאפשר לבצע את הפעולה ) Min( שמחזירה את האיבר הקטן ביותר מבין הנתונים שנמצאים כרגע בתור (מבלי להוציא אותו מהתור!). הציעו מימוש בו היעילות המושגת עבור הפעולות: EnQueue(x) תתבצע ב-,O(k) כאשר k הוא מספר הנתונים כרגע בתור..O(1) תתבצע ב- DeQueue( ).O(1) תתבצע ב- Min( ) בנוסף נדרש שכל סדרה של n פעולות DeQueue,EnQueue ו- Min (בסדר כלשהו) המתחילה מתור ריק תיקח.O(n) הערה: למעשה אפשר (אך קשה יותר) לבצע את פעולת EnQueue ב- (n O(log במקרה הגרוע, המעוניינים מוזמנים לנסות זאת. פתרונות נבחרים שאלה 1: הסבר כללי: נסמן ב- L את מספר הנתונים שנמצא ברגע נתון ברשימה L. שלב א': אפשר למצוא את המכסימום (המינימום) ברשימה ב- Θ( L ) פעולות "י אלגוריתם פשוט שמשווה בין המספרים). שלב ב': הדפסת כל המופעים של המכסימום והורדתו מהרשימה ייעשו בעוד Θ( L ) פעולות. לכן, כל איטרציה של הלולאה תיקח Θ( L ) פעולות, אולם כמובן אורך הרשימה הולך וקטן מאיטרציה לאיטרציה, ולכן עלינו לחבר טור מתאים. א) נשים לב מנתוני השאלה שלאחר i איטרציות הורדנו סה"כ מהרשימה: 1 i + 2 i 1 = 2 + 8 1 + 2 + 4 + נתונים. לכן אורך הרשימה לאחר i איטרציות יהיה 1+ i L. = 2 n הרשימה תתרוקן כאשר נוריד + 2 k 1 = 2 k 1 = n + 8 1 + 2 + 4 + נתונים. לכן יהיו k איטרציות, ואילו log(n+1) k. = מספר הפעולות הכולל שיתבצעו יהיה: k 1 k 1 k 1 i i i k Θ(n 2 + 1) =Θ n 2 + 1 =Θ kn + k 2 =Θ ( kn + k 2 + 1 ) =Θ(n log n) i= 0 i= 0 i= 0 ב) מנתוני השאלה סה"כ יש ברשימה 1+2+4+8+ +2 k 1 = 2 k 1 = n נתונים. לכן המינימום מופיע Θ(n/2) 2 1 k = 2 k 2/ = פעמים, ואחרי האיטרציה הראשונה אורך הרשימה יהיה Θ(n/2). L = Θ(n n/2) = האיבר השני הכי קטן מופיע Θ(n/4) 2 2 k = 2 k 4/ = פעמים. ולכן אחרי שתי איטרציות אורך הרשימה יהיה Θ(n/4). L = Θ(n/2 n/4) = כאשר באופן כללי, האיבר ה- i הכי קטן מופיע ) i 2 k i = 2 k /2 i = Θ(n/2 פעמים. לכן, אחרי i איטרציות נוריד מהרשימה סה"כ ) i Θ(n/2 + n/4 + + n/2 נתונים. ולכן אורך הרשימה אחרי i איטרציות יהיה: ) i. L = Θ( n (n/2 + n/4 + + n/2 i ) = Θ(n/2 בדומה לסעיף א', מספר השלבים יהיה k. לכן מספר הפעולות הכולל שיתבצעו יהיה:
. k 1 i= 0 n 2 i. כמו-כן, ברור גם ש- Ω(n) = k 1 i= 0 n 2 i k 1 1 1 = n n i i i= 0 2 i= 0 2 = 2n = Ο(n) ולכן סה"כ היעילות היא Θ(n) במקרה זה. שאלה 2: הרעיון: שומרים במחסנית סימני פתיחת סוגריים. כאשר מגיע סימן סוגר סוגריים, משווים אותו לסימן הנמצא בראש המחסנית, בכדי לוודא שהוא סוגר את הסימן הפותח האחרון שנראה. פסאודו-קוד: 1. אתחל מחסנית ריקה. כל עוד לא הסתיים הקלט: 2. נקרא את התו הבא. 2.1 אם הוא סוגר שמאלי 2.2 נכניס אותו למחסנית. 2.2.1 //התו הוא סוגר ימני אחרת //המחרוזת אינה חוקית כי חסר סוגר שמאלי מתאים. אם המחסנית ריקה 2.2.1 נעצור ונכריז על טעות. 2.2.1.1 //המחסנית אינה ריקה אחרת נוציא את התו שנמצא בראש המחסנית. 2.2.1.1 אם הוא לא הסוגר השמאלי שמתאים לסוגר הימני שקראנו, 2.2.1.2 נעצור ונכריז על טעות. 2.2.1.2.1 //יש יותר סוגריים שמאליים מימניים. 3. אם בסיום הקלט המחסנית אינה ריקה נעצור ונכריז על טעות. 3.1 אחרת נכריז שהמחרוזת חוקית. 3.1 bool IsLeagal ( ){ Stack S; bool EndOfInput = false; bool Leagal = true; char ch, top; מימוש בשפת תכנות: while (!EndOfInput && Leagal){ ch = GetNextParen ( ); if (ch == END_OF_INPUT ) EndOfInput = true; else if ( (ch == ( ) (ch == [ ) (ch == { ) ) S.Push ( ch ); else // ch == ) ch == ] ch == if ( S.IsEmpty ( ) ) Leagal = false; else{ top = S.Pop ( ); if (!( (ch == ) && top == ( ) (ch == ] && top == [ ) (ch == && top == { ) ) ) Leagal = false;
if ( S.IsEmpty ( ) ) return Leagal; else return false; שאלה 3: הערה: הפתרון מניח שכאשר יש להכניס נתון לאמצע המחסנית, ומספר הנתונים במחסנית הוא אי-זוגי, אז n 1 הנתון ייכנס אחרי הנתון ה-. 2 הרעיון הכללי במימוש הוא שהתור הדו-כווני (DQ) (S) תהווה את החצי התחתון של מחסנית האמצע. יהווה את החצי העליון של מחסנית האמצע והמחסנית נשמור חצי מהאיברים בתור הדו כיווני וחצי במחסנית, כאשר נרשה מצב בו יש איבר אחד יותר בתור מאשר במחסנית. נגדיר שלאורך כל זמן השימוש במחסנית תישמר האינווריאנטה (החוקיות): 0 Size_ DQ Size_ S 1 החוקיות קובעת כי אחד מהשניים תמיד יתקיים: או ש- ב- 1 מ-. Size _ S Size _ DQ או ש-, Size _ DQ = Size _ S גדול ראש התור הנתון בראש המחסנית אמצע DQ DQ זנב התור DQ ראש המחסנית S נקודת האמצע של המחסנית אמצע הנתון הוותיק ביותר במחסנית אמצע S לצורך מימוש טיפוס הנתונים המופשט מחסנית אמצע נשתמש במבני הנתונים מחסנית S, ותור דו כיווני.DQ בנוסף נחזיק שני משתנים : Size_S - מספר הנתונים במחסנית - Size_DQ מספר הנתונים בתור הדו כיווני
S.MakeEmpty () DQ.MakeEmpty () Size_S = Size_DQ = 0 אתחול : Void MPush (x) { DQ.HeadEnqueue (x) Size_DQ ++ במצב בו יש יותר מידי איברים בתור נעביר את זנב התור למחסנית. // If ( Size_DQ - Sizs_S > 1)then S.push ( DQ.TailDequeue()) Size_S ++ Size_DQ Type MPop () { If DQ.IsEmpty() then return NULL Else Ret = DQ.HeadDeqeue( ); Size_DQ -- במצב בו יש מעט מידי איברים בתור נעביר את האיבר בראש המחסנית לזנב התור // If (Size_DQ - Size_S < 0) then DQ.TailEnqueue ( S.pop()) Size_S -- Size_DQ++ Return Ret void MPushMid (x) { If (Size_S < Size_DQ ) then S.push (x) Size_S ++ Else DQ.TailEnqueue (x) Size_DQ ++
שאלה 4: א) נממש את המחסנית בדומה למימוש הרגיל של מחסנית כרשימה משורשרת עם מצביע top שמצביע לראש הרשימה. אולם בכל קודקוד x ברשימה יהיו הפעם שני מצביעים. מצביע next לאיבר הבא ברשימה המשורשרת (כלומר לאיבר שהוכנס לפני x למחסנית), ומצביע min למינימום שהיה במבנה הנתונים בזמן ש- x הוכנס למחסנית. כך אם המינימום הנוכחי יורד מהמחסנית, נוכל מיד לדעת מי המינימום החדש. פעולת :Push(x) נוסיף את x לראש המחסנית כלומר לראש הרשימה המשורשרת. זאת ניתן לעשות כמובן בזמן (1)Θ. המצביע next יצביע לאיבר שהיה קודם בראש. המצביע min יצביע להיכן שהצביע המצביע min של האיבר שהיה בראש המחסנית לפני ש- x הצטרף. אם x קטן מהמינימום הנוכחי במחסנית, אז minimum יצביע אליו. כלומר (בהנחה שהמחסנית אינה ריקה כרגע): temp = new Node(); temp->data = x; temp->next = top; temp->min = minimum; if (x < minimum->data ) minimum = temp; top = temp; פעולת :Pop() נוריד מהרשימה את הקודקוד שנמצא בראש הרשימה ונחזיר את הנתון שנשמר בו. בנוסף, אם הורדנו את הקודקוד שאליו הצביע,minimum אז נעדכן את minimum בעזרת השדה min של הקודקוד שירד (שהצביע כזכור למינימום שהיה במחסנית לפני שהוא הוכנס): if (minimum == top) minimum = top->min; היעילות היא שוב (1)Θ. min next פעולת :Min() נחזיר את.minimum->data היעילות היא (1)Θ. NULL to לאחר פעולת :Pop NUL to ב) נוכיח שבמקרה זה לפחות אחת הפעולות תיקח (n.ω(log נניח בשלילה שיש מבנה נתונים שתומך בכל הפעולות ביעילות שקטנה אסימפטוטית מ-.log n נראה שאז אפשר למיין n מספרים בפחות מ- nlogn פעולות. זו תהיה סתירה לחסם התחתון שראינו למיון. הנה האלגוריתם למיון: נכניס את n המספרים למחסנית בעזרת פעולת.Push נבצע n פעמים.DeleteMin
שאלה 5: נניח שמוגדר לנו טיפוס הנתונים.Stack נשתמש בשתי מחסניות מטיפוס זה כדי לממש את התור. מחסנית אחת תשמש לשמירת איברי התור. בתחתית המחסנית יהיה האיבר הראשון שהוכנס לתור ובראשה האיבר האחרון. לכן הכנסת איבר לסוף התור היא פשוטה - פשוט נכניס אותו לראש המחסנית. כדי להוציא את האיבר שבראש התור - נעביר את כל האיברים ממחסנית זו למחסנית השניה. בצורה זו נהפוך את סדר האיברים. כעת נוציא את האיבר שנמצא בראש המחסנית השנייה, ולסיום נחזיר חזרה את כל יתר האיברים למחסנית הראשונה. מימוש התור ב- ++C יהיה: Class Queue{ private: Stack S1,S2; public: Queue(); ~Queue(); int IsEmpty(); void MakeEmpty(); void Enqueue(Type item); Type Dequeue(void); ; Queue::Queue(){ S1.MakeEmpty(); S2.MakeEmpty(); void Queue::Enqueue(Type item){ S1.Push(item); Type Queue::Dequeue(void){ Type first; // first item in Queue. while (!S1.IsEmpty()) S2.Push(S1.Pop()); first = S2.Pop(); while (!S2.IsEmpty()) S1.Push(S2.Pop()); return first; כפי שניתן לראות, יעילות כל הפעולות היא (1)Θ, פרט לפעולה Dequeue שתיקח Θ(n) פעולות, כאשר n הוא מספר האיברים בתור. ב. הרעיון הפעם הוא ששתי המחסניות יכילו נתונים. הנה הרעיון של כל פעולה בקיצור: פעולת :Enqueue נוסיף נתון לראש המחסנית S. 1 פעולת :Dequeue אם S 2 אינה ריקה נוריד נתון שנמצא בראש S. 2 אחרת, נעביר אליה את כל הנתונים מ- S 1 ואז נוריד נתון שנמצא בראש S. 2 יעילות של n פעולות :Enqueue, Dequeue מכיוון שיש בסה"כ n פעולות, הרי הכנסנו/הורדנו מהתור בסה"כ n איברים. נספור לכל איבר שמוכנס לתור (ובסופו של דבר יורד מהתור) את מספר פעולות Push,Pop שביצענו עליו. כל איבר מוכנס פעם אחת ל- S, 1 מועבר פעם אחת ל- S 2 ויוצא פעם אחת מ- S. 2 לכן, בסה"כ מבצעים עליו 4 פעולות Push,Pop במהלך חייו בתור. מכאן היעילות הכוללת תהיה.Θ(n) שימו לב שעדיין יש פעולות Dequeue שייקחו זמן.Θ(n)
שאלה 6: יש לממש מבנה נתונים של תור - פעולות EnQueue ו- + DeQueue פעולת מינימום.Min הזמנים הנדרשים: Θ(n) :EnQueue זמן במקרה הגרוע (1)Θ :DeQueue זמן במקרה הגרוע (1)Θ :Min זמן במקרה הגרוע ובנוסף: n פעולות EnQueue צריכות לקחת Θ(n) זמן. הערה: אפשר לבצע את פעולת EnQueue ב- (n Θ(log במקרה הגרוע (במקום.(Θ(n) מבנה נתונים: בנוסף למבנה הרגיל של תור (למשל בעזרת רשימה משורשרת): 1: נחזיק עם כל איבר y, מצביע y.nextmin לאיבר x שהופך להיות מינימום עם יציאת y מהתור. המצביע y x ייקבע עם הכנסת x לתור (ולכן ההצבעה מ- y עשויה להשתנות מספר פעמים). נחזיק גם את המצביע ההפוך x. y נקרא למצביע זה x.nsl כאשר משמעות NSL היא Left"."Nearest Smaller to the לנוחות הדיון נסתכל על התור כהולך משמאל לימין כאשר ראש התור הוא משמאל וסוף התור מימין. בראיה כזו y הוא באמת האיבר הקרוב ביותר משמאלו של x שקטן ממנו. 2: נחזיק מצביע Min לאיבר המינימלי בתור. אלגוריתמים לביצוע הפעולות DeQueue,EnQueue ו- :Min.Min נחזיר את האיבר :Min 1..Min = y.nextmin נעדכן y = Min שבראש התור. אם y נוציא את האיבר :DeQueue.2 :EnQueue(x).3 3.1. נעדכן את שדה NSL של x כך: 3.1.1. נסמן את האיבר האחרון בתור (לפני ש- x נכנס) ב- z..3.1.2 כל עוד z > x נבצע.z = z.nsl x.nsl = z.3.1.3.3.2 נעדכן עתה את שדה NextMin של.z.NextMin = x :z ניתוח זמן של n פעולות :EnQueue הזמן הוא (1)Θ ועוד מספר הפעמים הכולל שהלכנו בעקבות שדה NSL בפעולה z = z.nsl (צעד 3.1.2). נראה שעבור כל איבר z בתור, נלך בעקבות שדה NSL שלו פעם אחת לכל היותר: אם כבר הלכנו בעקבות שדה NSL של z אז יש מימינו של z איבר 'x שקטן ממנו, אך אז לא ייתכן שנגיע ל- z כאשר נכנס איבר x מאוחר יותר מ- 'x (כי 'x < x וקרוב ל- x משמאלו יותר מאשר z).