בחרו מבנה נתונים ופעולה וצפו בהם עובדים שלב אחר שלב
מבני נתונים הם אבני הבניין של כל תוכנה — הבחירה הנכונה משפיעה ישירות על ביצועים. מחסנית (Stack) פועלת לפי LIFO ומשמשת לרקורסיה וביטויים. תור (Queue) פועל לפי FIFO ומשמש ב-BFS. רשימה מקושרת מאפשרת הכנסה ומחיקה ב-O(1). עץ חיפוש בינארי (BST) שומר על סדר ומאפשר חיפוש ב-O(log n). טבלת גיבוב (Hash Table) מספקת גישה ממוצעת של O(1) לחיפוש, הכנסה ומחיקה.
לחצו על הפעל או שלב כדי להתחיל.
1 / 0
הוספת איבר לראש המחסנית - פעולה קבועה ללא תלות בגודל.
תמיד מוסיפים לראש - top pointer עולה ב-1.
O(1)
O(1)
כאשר מאחסנים פעולות לבצע/לבטל, או בחיפוש DFS.
מה משמעות LIFO?
מבנה נתונים הוא דרך לארגן ערכים בזיכרון יחד עם קבוצת הפעולות שניתן לבצע עליהם. הבחירה במבנה אינה עניין של סגנון: היא קובעת ישירות את סיבוכיות הזמן של כל פעולה בתוכנית. חיפוש ערך במערך לא ממוין הוא O(n); באותה כמות נתונים בטבלת גיבוב הוא O(1) בממוצע. זהו ההבדל בין תוכנית שמגיבה מיידית לכזו שנתקעת.
אין מבנה נתונים אחד שמנצח בכל הפעולות, וזו הנקודה המרכזית. מערך נותן גישה מיידית לפי אינדקס אך הכנסה יקרה באמצע; רשימה מקושרת הופכת את היחס; טבלת גיבוב מהירה בחיפוש לפי מפתח אך אינה שומרת סדר; עץ חיפוש בינארי שומר סדר במחיר של O(log n) במקום O(1). כל בחירה היא פשרה, ולכן צריך להתחיל מהשאלה אילו פעולות התוכנית מבצעת הכי הרבה.
בוויזואליזציה שלמעלה אפשר להריץ כל פעולה שלב-אחר-שלב ולראות בדיוק מה קורה בזיכרון: אילו תאים נבדקים, אילו מצביעים משתנים, ואיך העץ או הטבלה מתארגנים מחדש. זו הדרך היעילה ביותר להבין למה סיבוכיות מסוימת נובעת מהמבנה עצמו.
מערך הוא בלוק זיכרון רציף שבו כל איבר נמצא במרחק קבוע מקודמו, ולכן חישוב הכתובת של האיבר ה-i הוא פעולה אריתמטית אחת — O(1). הרציפות הזו גם מנצלת היטב את מטמון המעבד, מה שהופך מערכים למהירים בפועל אף יותר ממה שהתיאוריה מבטיחה. החיסרון הוא שהכנסה או מחיקה באמצע מחייבת הזזה של כל האיברים שאחריה, כלומר O(n).
מחסנית (Stack) מגבילה את הגישה לקצה אחד בלבד לפי עקרון LIFO — הנכנס אחרון יוצא ראשון. שלוש הפעולות שלה, דחיפה, שליפה והצצה, כולן O(1). המחסנית היא מבנה הנתונים שמאחורי מחסנית הקריאות של כל תוכנית רקורסיבית, מנגנוני ביטול פעולה, ובדיקת איזון סוגריים.
תור (Queue) מגביל את הגישה לשני קצוות לפי עקרון FIFO — הנכנס ראשון יוצא ראשון. הוא הבסיס לכל מערכת שמעבדת בקשות לפי סדר הגעה, לתזמון משימות במערכת הפעלה, ולאלגוריתם חיפוש לרוחב. מימוש נאיבי מעל מערך גורם להוצאה בעלות O(n), ולכן משתמשים בתור מעגלי או ברשימה מקושרת דו-כיוונית כדי לשמור על O(1) בשני הקצוות.
רשימה מקושרת מוותרת על הרציפות בזיכרון: כל צומת מחזיק ערך ומצביע לצומת הבא. התוצאה היא הכנסה ומחיקה ב-O(1) כשכבר מחזיקים מצביע למקום הנכון, אך גישה לפי אינדקס הופכת ל-O(n) ולוקליות הזיכרון נפגעת.
עץ חיפוש בינארי שומר על תכונה אחת בכל צומת: כל הערכים בתת-העץ השמאלי קטנים ממנו וכל הערכים בתת-העץ הימני גדולים ממנו. התכונה הזו מאפשרת לחפש ערך על ידי ירידה בענף אחד בלבד, ולפסול חצי מהאפשרויות בכל צעד — O(log n) בעץ מאוזן. יתרון נוסף הוא שמעבר In-Order מחזיר את כל הערכים ממוינים ב-O(n).
החולשה של עץ חיפוש בינארי היא שהמבנה תלוי בסדר ההכנסה. הכנסת ערכים בסדר עולה יוצרת עץ שהוא למעשה רשימה מקושרת, וכל הפעולות מתדרדרות ל-O(n). כדי למנוע זאת משתמשים בעצים מאוזנים כמו AVL או עץ אדום-שחור, שמבצעים רוטציות בעת ההכנסה ומבטיחים גובה O(log n) בכל מצב.
טבלת גיבוב נוקטת גישה שונה לגמרי: פונקציית גיבוב ממפה כל מפתח למיקום במערך, כך שהגישה אינה דורשת חיפוש כלל אלא חישוב — O(1) בממוצע. המחיר הוא התנגשויות, כששני מפתחות ממופים לאותו מיקום, ואותן פותרים בשרשור (רשימה בכל תא) או בכתובת פתוחה (חיפוש התא הפנוי הבא).
ההבדל המעשי בין השניים הוא הסדר. טבלת גיבוב מהירה יותר לחיפוש נקודתי לפי מפתח, אבל אינה שומרת שום סדר ולכן אינה יכולה לענות על שאילתות טווח או להחזיר את המינימום. עץ חיפוש בינארי איטי יותר בקבוע, אך תומך בשאילתות טווח, במינימום ומקסימום, ובמעבר ממוין — ולכן מסדי נתונים משתמשים בעצים לאינדקסים.
מחסנית פועלת לפי עקרון LIFO — האיבר שנכנס אחרון יוצא ראשון, כמו ערימת צלחות. תור פועל לפי FIFO — האיבר שנכנס ראשון יוצא ראשון, כמו תור בקופה. שניהם מבצעים את פעולות ההכנסה וההוצאה ב-O(1). מחסנית משמשת לרקורסיה, לביטול פעולות ולבדיקת סוגריים; תור משמש לתזמון משימות, לעיבוד בקשות ולאלגוריתם חיפוש לרוחב.
רשימה מקושרת עדיפה כשמבצעים הרבה הכנסות ומחיקות בהתחלה או באמצע ויש כבר מצביע למיקום — פעולות אלו O(1) לעומת O(n) במערך. היא גם מתאימה כשגודל האוסף משתנה מאוד ולא ידוע מראש. מערך עדיף כשנדרשת גישה אקראית לפי אינדקס, כשחשובה יעילות זיכרון, או כשסורקים את כל האיברים — הרציפות בזיכרון מנצלת את מטמון המעבד ומהירה משמעותית בפועל.
הכנסה, חיפוש ומחיקה בטבלת גיבוב הן O(1) בממוצע, כיוון שפונקציית הגיבוב מחשבת ישירות את המיקום במקום לחפש. במקרה הגרוע, כשכל המפתחות מתנגשים לאותו תא, הן מתדרדרות ל-O(n). בפועל, עם פונקציית גיבוב טובה ושמירה על גורם עומס נמוך (בדרך כלל מתחת ל-0.75) עם הגדלה אוטומטית, המקרה הגרוע כמעט אף פעם לא מתרחש.
עץ חיפוש בינארי הוא מבנה שבו לכל צומת יש לכל היותר שני ילדים, וכל הערכים בתת-העץ השמאלי קטנים מהצומת וכל הערכים בתת-העץ הימני גדולים ממנו. חיפוש מתחיל בשורש ויורד שמאלה או ימינה לפי ההשוואה, ובכך פוסל חצי מהאפשרויות בכל צעד — O(log n) בעץ מאוזן. מעבר In-Order על העץ מחזיר את כל הערכים בסדר ממוין.
כשנדרש סדר. טבלת גיבוב מהירה יותר לחיפוש נקודתי לפי מפתח, אך אינה שומרת סדר כלשהו. עץ חיפוש בינארי תומך בשאילתות טווח כמו כל הערכים בין 10 ל-50, במציאת מינימום ומקסימום, במעבר ממוין ובמציאת האיבר העוקב — כל אלה בלתי אפשריים ביעילות בטבלת גיבוב. זו הסיבה שמסדי נתונים משתמשים בעצים לאינדקסים.
המבנה של עץ חיפוש בינארי תלוי לחלוטין בסדר שבו מכניסים אליו ערכים. אם מכניסים ערכים בסדר עולה או יורד, כל צומת חדש נוסף תמיד לאותו צד, והעץ הופך לשרשרת שהיא למעשה רשימה מקושרת בגובה n. כדי למנוע זאת משתמשים בעצים מאוזנים כמו AVL או עץ אדום-שחור, שמבצעים רוטציות אוטומטיות ומבטיחים גובה O(log n).