Stack<T> הוא ערימה: שמים איברים בראש עם Push ולוקחים אותם מהראש עם Pop, כך שהאיבר האחרון שנכנס הוא הראשון שיוצא (LIFO). רק הראש נגיש, וכל פעולה עליו לוקחת זמן קבוע.
Push, Pop ו-Peek
פלט:
On top: green
Count: 3
Took green
On top: red
Took red
Took blue
Count: 0
green נדחף אחרון, ולכן הוא יוצא ראשון. Peek מחזיר את הראש בלי לשנות את המחסנית, וכך בודקים מה Pop ייתן לפני שמחליטים לקחת אותו.
החריגה של מחסנית ריקה ו-TryPop
Pop או Peek על מחסנית ריקה זורקים InvalidOperationException. זה קורה הכי הרבה ב-parsers ובאלגוריתמים שמקבלים קלט עם יותר פריטים סוגרים מפותחים.
פלט:
Caught InvalidOperationException
True 10
False 0
TryPop ו-TryPeek (.NET Core 2.0 ואילך) מחזירים false על מחסנית ריקה ומציבים במשתנה ה-out את ערך ברירת המחדל, כאן 0. ב-.NET Framework, בדקו קודם Count > 0.
סדר המעבר: הראש קודם
מעבר על מחסנית לא מסיר כלום, והוא הולך מלמעלה למטה, בסדר ש-Pop היה מחזיר את האיברים:
פלט:
checkout products home
checkout > products > home
True
home
checkout
העותק ההפוך מפיל אנשים בפח: הבנאי מקבל כל IEnumerable<T> ודוחף את האיברים שלו לפי הסדר, ומחסנית עוברת על האיברים מהראש, כך שהראש הישן מגיע לתחתית העותק. היפוך הרצף קודם (Reverse() של LINQ מחזיר את האיברים מהתחתית) נותן עותק עם אותו ראש.
גם דחיפה של רשימת איברים למחסנית חדשה הופכת אותם, וזו דרך מהירה להפוך רצף: new Stack<char>("hello") מוציא בחזרה o, l, l, e, h.
דוגמה: היסטוריית ביטול
עורכי טקסט שומרים כל שינוי במחסנית. ביטול (undo) מוציא את השינוי האחרון ומחזיר אותו לאחור; ביצוע מחדש (redo) שומר מחסנית שנייה של שינויים שבוטלו.
פלט:
Hello, world!
Hello, world
Hello
Hello, world
שמירה של תמונות מצב שלמות היא הגרסה הפשוטה ביותר. עורכים אמיתיים דוחפים במקום זאת אובייקטי פקודה קטנים (מה הוכנס, ואיפה), שלכל אחד מהם יש מתודה לבטל את עצמו, אבל שתי המחסניות עובדות באותה צורה.
דוגמה: איזון סוגריים
בדיקה ש-(, [ ו-{ נסגרים בסדר הנכון היא התרגיל הקלאסי על מחסניות, ואותה לוגיקה יושבת בתוך כל קומפיילר ו-parser של JSON.
פלט:
"f(a[i], {x: 1})" -> True
"(]" -> False
"((a)" -> False
"a)b(" -> False
"" -> True
שלוש בדיקות הכישלון מתאימות לשלוש הדרכים שבהן סוגריים משתבשים: סוגר בלי שום דבר פתוח (a)b(, שנתפס על ידי Count == 0 במקום חריגה מ-Pop), סוגר מהסוג הלא נכון ((]), ופותחים שאף פעם לא נסגרו (((a), שנתפסים בבדיקה הסופית).
שימושים נוספים
- חיפוש לעומק. החליפו את התור בחיפוש לרוחב במחסנית והמעבר ילך לעומק לפני שהוא הולך לרוחב. מחסנית מפורשת גם מחליפה רקורסיה כשהקלט עמוק מספיק כדי להסתכן ב-
StackOverflowException, שאי אפשר לתפוס. - חישוב ביטויים. כתיב postfix (
3 4 + 2 *) מחושב על ידי דחיפת מספרים והוצאת שניים לכל אופרטור. - חזרה לאחור (backtracking). היסטוריית ניווט, פתרון מבוכים ומצבי parser דוחפים מיקום וחוזרים אליו במבוי סתום.
ראו את Queue למקבילה מסוג "ראשון נכנס, ראשון יוצא".
Stack מול Queue מול List
Stack<T> | Queue<T> | List<T> | |
|---|---|---|---|
| סדר היציאה | החדש ביותר קודם | הוותיק ביותר קודם | כל סדר, לפי אינדקס |
| הוספה | Push | Enqueue | Add, Insert |
| הסרה | Pop (ראש) | Dequeue (חזית) | Remove, RemoveAt |
| הצצה | Peek | Peek | list[i] |
| גרסאות בטוחות | TryPop, TryPeek | TryDequeue, TryPeek | לא נחוצות |
לכמה תהליכונים, ConcurrentStack<T> ב-System.Collections.Concurrent מציע Push, TryPop ו-TryPeek בלי נעילות.
טעויות נפוצות
- הוצאה בלי בדיקה. מחסנית ריקה זורקת
InvalidOperationException; בדקו אתCountאו השתמשו ב-TryPop. - ציפייה ש-
foreachיתחיל מהאיבר הראשון שנדחף. הוא מתחיל מהראש. - העתקה עם
new Stack<T>(stack). העותק הפוך. - דחיפה בתוך
foreachעל אותה מחסנית. זורק חריגה; השתמשו בלולאתwhile (stack.Count > 0).
שאלות נפוצות
מה זה Stack ב-C#?
Stack<T> ב-System.Collections.Generic הוא אוסף מסוג "אחרון נכנס, ראשון יוצא" (LIFO). Push שם איבר בראש, Pop מסיר ומחזיר את האיבר שבראש, ו-Peek מחזיר את האיבר שבראש בלי להסיר אותו. שלושתם רצים בזמן קבוע.
מה קורה כשמבצעים Pop על מחסנית ריקה ב-C#?
Pop ו-Peek זורקים InvalidOperationException כשהמחסנית ריקה. בדקו קודם stack.Count > 0, או השתמשו ב-TryPop(out var item) וב-TryPeek(out var item), שמחזירים false במקום לזרוק חריגה (.NET Core 2.0 ואילך).
באיזה סדר foreach עובר על Stack?
מלמעלה למטה: האיבר שנדחף אחרון בא ראשון, באותו סדר ש-Pop היה מחזיר אותם. ToArray() משתמש באותו סדר. אחת התוצאות היא ש-new Stack<T>(otherStack) יוצר עותק הפוך, כי הבנאי דוחף את האיברים בסדר שבו הוא עובר עליהם.
מה ההבדל בין Stack ל-Queue ב-C#?
Stack<T> מחזיר קודם את האיבר החדש ביותר (אחרון נכנס, ראשון יוצא), ואילו Queue<T> מחזיר קודם את הוותיק ביותר (ראשון נכנס, ראשון יוצא). השתמשו במחסנית להיסטוריית ביטול, למבנים מקוננים ולחיפוש לעומק; השתמשו בתור לעיבוד עבודה לפי סדר ההגעה ולחיפוש לרוחב.
איך בודקים איזון סוגריים ב-C#?
סרקו את המחרוזת פעם אחת. דחפו כל סוגר פותח ל-Stack<char>. בכל סוגר סוגר, המחסנית חייבת להיות לא ריקה והאיבר שבראשה חייב להיות הסוגר הפותח המתאים, ואז מוציאים אותו. המחרוזת מאוזנת אם הסריקה מסתיימת כשהמחסנית ריקה.