Stack (מחסנית)
עודכן לאחרונה
מחסנית היא אוסף עם קצה פתוח אחד בלבד. מוסיפים ערך על ידי דחיפה (push) לראש המחסנית, ומסירים ערך על ידי שליפה (pop) של הראש, כך שהערך האחרון שנכנס הוא תמיד הראשון שיוצא. זו המשמעות של LIFO, וזה כל הכלל: אי אפשר להגיע לאמצע בלי להסיר קודם את מה שמעליו. לחצו על הפעלה למעלה וצפו בעמודה שגדלה עם כל push ומתקצרת מאותו קצה עם כל pop.
ההגבלה היא בדיוק העניין. מכיוון ששתי הפעולות נוגעות רק בראש, כל אחת מהן היא O(1) לא משנה כמה גבוהה המחסנית, והצפיוּת הזו היא הסיבה שמחסניות נמצאות מתחת לחלק גדול כל כך של עולם המחשוב: מחסנית הקריאות שמריצה רקורסיה, היסטוריית הביטולים בעורך, התאמת סוגריים במנתח תחבירי, והמחסנית המפורשת שהופכת חיפוש לעומק רקורסיבי ללולאה. אם מחליפים את הקצה שממנו מסירים, מקבלים תור במקום.
סיבוכיות זמן וזיכרון
למחסנית הסטנדרטית שמבוססת על מערך או על רשימה מקושרת:
| פעולה | סיבוכיות | הערות |
|---|---|---|
| Push | O(1) | O(1) בממוצע לשיעורין במערך דינמי, שמדי פעם משנה את גודלו. |
| Pop | O(1) | תמיד האיבר העליון, ולכן אין צורך להזיז איברים. |
| Peek (ראש) | O(1) | קריאת הראש בלי להסיר אותו. |
| חיפוש | O(n) | לא לשם כך נועדה מחסנית: צריך לשלוף איבר אחרי איבר עד למטה. |
| זיכרון | O(n) | משבצת אחת לכל ערך שמאוחסן. |
צעד אחר צעד
| צעד | מה קורה |
|---|---|
| 1 | המחסנית מתחילה ריקה, והראש לא מצביע על שום דבר. |
| 2 | Push כותב את הערך במיקום של הראש ומעלה את הראש במקום אחד. |
| 3 | כל push נוסף נוחת ישירות מעל הערך הקודם. |
| 4 | Pop קורא את הערך שבראש, ואז מוריד את הראש במקום אחד. |
| 5 | הערך שחוזר הוא תמיד זה שנדחף לאחרונה. |
| 6 | שליפה ממחסנית ריקה היא שגיאה שנקראת stack underflow, ולכן קוד אמיתי בודק קודם is_empty(). |
דוגמה מפורטת
דחיפה של 3, 7, 5 ואז ריקון המחסנית:
| פעולה | המחסנית (מלמטה למעלה) | מחזירה |
|---|---|---|
push(3) | [3] | כלום |
push(7) | [3, 7] | כלום |
push(5) | [3, 7, 5] | כלום |
pop() | [3, 7] | 5, הערך החדש ביותר |
pop() | [3] | 7 |
pop() | [] | 3, הערך הוותיק ביותר, אחרון |
מתי להשתמש במחסנית
| כדאי כאשר | עדיף להימנע כאשר |
|---|---|
| צריך לקבל קודם את הפריט האחרון: ביטול פעולות, כפתורי חזרה, התאמת סוגריים | צריך קודם את הפריט הוותיק ביותר, ולשם כך יש תור |
| הופכים אלגוריתם רקורסיבי לאיטרטיבי | צריך לחפש או לגשת לפי אינדקס לאמצע הנתונים |
| מנתחים מבנה מקונן כמו ביטויים, JSON או HTML | קוראים רבים צריכים גישה שרירותית, ושם מערך או מפה מתאימים יותר |
רוצים הכנסה והסרה מובטחות ב-O(1) בלי איזון מחדש | צריך לשמור את הנתונים ממוינים, וזה מה שערימה או עץ נותנים |
קוד Stack
מימוש נקי של Stack שאפשר להריץ, ב-Python, JavaScript, Java, C++, C. בחרו שפה, העתיקו את הקוד, או פתחו אותו טעון מראש בעורך האונליין של Coddy.
קוד Stack ב-Python
1stack = []2
3# Push three values onto the top4for value in [3, 7, 5]:5 stack.append(value)6 print(f"push {value} -> {stack}")7
8# Pop them back off: last in, first out9while stack:10 value = stack.pop()11 print(f"pop {value} -> {stack}")12
13print("empty:", len(stack) == 0)קוד Stack ב-JavaScript
1const stack = [];2
3// Push three values onto the top4for (const value of [3, 7, 5]) {5 stack.push(value);6 console.log(`push ${value} ->`, stack);7}8
9// Pop them back off: last in, first out10while (stack.length > 0) {11 const value = stack.pop();12 console.log(`pop ${value} ->`, stack);13}14
15console.log('empty:', stack.length === 0);קוד Stack ב-Java
1import java.util.ArrayDeque;2import java.util.Deque;3
4public class Main {5 public static void main(String[] args) {6 Deque<Integer> stack = new ArrayDeque<>();7
8 // Push three values onto the top9 for (int value : new int[] {3, 7, 5}) {10 stack.push(value);11 System.out.println("push " + value + " -> " + stack);12 }13
14 // Pop them back off: last in, first out15 while (!stack.isEmpty()) {16 int value = stack.pop();17 System.out.println("pop " + value + " -> " + stack);18 }19
20 System.out.println("empty: " + stack.isEmpty());21 }22}קוד Stack ב-C++
1#include <iostream>2#include <stack>3
4int main() {5 std::stack<int> stack;6
7 // Push three values onto the top8 for (int value : {3, 7, 5}) {9 stack.push(value);10 std::cout << "push " << value << " -> size " << stack.size() << "\n";11 }12
13 // Pop them back off: last in, first out14 while (!stack.empty()) {15 int value = stack.top();16 stack.pop();17 std::cout << "pop " << value << " -> size " << stack.size() << "\n";18 }19
20 std::cout << "empty: " << std::boolalpha << stack.empty() << "\n";21 return 0;22}קוד Stack ב-C
1#include <stdio.h>2
3#define CAP 164
5int stack[CAP];6int top = 0; /* index of the next free slot */7
8int main(void) {9 int values[3] = {3, 7, 5};10
11 /* Push three values onto the top */12 for (int i = 0; i < 3; i++) {13 stack[top++] = values[i];14 printf("push %d -> size %d\n", values[i], top);15 }16
17 /* Pop them back off: last in, first out */18 while (top > 0) {19 int value = stack[--top];20 printf("pop %d -> size %d\n", value, top);21 }22
23 printf("empty: %d\n", top == 0);24 return 0;25}שאלות נפוצות על מחסנית
מה המשמעות של LIFO?
מה ההבדל בין מחסנית לתור?
O(1); מחסנית מסירה מאותו קצה (LIFO), ותור מסיר מהקצה השני (FIFO). כל השאר, כולל טבלת הסיבוכיות שלמעלה, זהה.מהן הפעולות העיקריות על מחסנית?
push מוסיפה ערך לראש, pop מסירה ומחזירה את הערך שבראש, peek (לפעמים top) קוראת את הראש בלי להסיר אותו, ו-is_empty מדווחת אם נשאר משהו. כל ארבע הפעולות הן O(1).מה זה stack overflow?
איך מממשים מחסנית?
O(1) בממוצע לשיעורין וידידותי למטמון: ה-list של Python וה-ArrayDeque של Java עובדים כך. רשימה מקושרת דוחפת ושולפת בראש הרשימה, וזה O(1) גם במקרה הגרוע בלי שינויי גודל, אבל עולה מצביע לכל איבר. ה-std::stack של C++ הוא מתאם שרץ כברירת מחדל על std::deque, מערך מחולק למקטעים, ומקבל מכל אחר אם מעבירים לו אחד.