הגדרות בסיסיות
אוסף של פעולות על קבוצת נתונים יקרא טיפוס נתונים מופשט.
ניתן לממש באופן קונקרטי טיפוס נתונים מופשט ע"י מבנה נתונים.
אוסף של פרוצדורות, אחת לכל פעולה, הממשות את הפעולות של טיפוס הנתונים המופשט.
נממש מחסנית באמצעות מערך(אינסופי) ומצביע לראש המחסנית:
- אתחול מבצע .
- הוספת איבר תתבצע
- נוכל לקרוא לראש המחסנית ע"י
- נוכל להוציא איבר כך: .
- נוכל לבדוק האם המחסנית ריקה ע"י .
העקרון המנחה שלנו בבניית המחסנית הוא העקרון אשר הוא Last in - first out. בעברית: אחרון שנכנס הוא הראשון לצאת.
הפעולות שניצור יהיו:
-
יצירת מחסנית חדשה ריקה: .
-
הוספת איבר לאוסף האיברים במחסנית תתבצע: .
-
החזרת האיבר האחרון שהוכנס ל- ועדיין לא הוצא: .
-
הוצאת האיבר האחרון שהוכנס ל- ועדיין לא הוצא: .
-
בדיקה האם המחסנית ריקה: (יחזיר אם ריקה ואחרת )
- אם נבצע יותר -ים מ--ים על מחסנית ריקה, האם המחסנית תהיה ריקה?
- אם נבצע יותר -ים מ--ים על מחסנית ריקה, האם היא תהיה ריקה?
בואו, ברוח הדוגמה האחרונה נסתכל על עוד כמה מימושים עבור מחסנית:
נגדיר את הצומת:
typedef struct node {
DATA_TYPE info;
struct node *next;
} NODE;
typedef NODE* STACK;
ואז הפעולות שלנו יראו כך:
void push(STACK *S, DATA_TYPE x)
{
NODE *p;
p = malloc(sizeof(NODE));
p->info = x;
p->next = *S;
*S = p;
}
void pop(STACK *S)
{
STACK t;
t = (*S)->next;
free(*S);
*S = t;
}
על מנת לבחור מימוש נרצה להסתכל על כמה שיקולים:
- שיקולי מהירות: מערך מהיר יותר מרשימה. אין צורך בהקצאת אובייקטים וההתנהגות המקומית(הרציפות בזכרון) מאפשרת ניצול מיברי של זכרון .
- שיקולי זכרון: מערך מחייב לדעת חסם על גודל המחסנית בעת יצירתה. הקצאת ייתר תיצור בזבוז בזכרון בעוד שחוסר במקום יגרום גם כן לבעיות. ברשימה מקושרת אין חסם עליון אך נדרשת הקצאת מקום נוסף לכל מצביע.
בפועל, נבחר מודל היברידי כמו המודל הבא:
נבנה את המחסנית שלנו באמצעות מערך בגודל קבוע וכאשר הוא מתמלא לחלוטין נקצה מחדש מערך נוסף ונקשר אותו ברשימה מקושרת.
כעת נבנה תור, כאשר העקרון המנחה הוא . כלומר, הראשון שנכנס הוא הראשון לצאת.
תכונות מבנה הנתונים שלנו יהיו:
- אתחול() יוצרת ומחזירה תור חדש שאוסף האיברים שלו ריק.
- הוספת איבר() מוסיפה את האיבר לסוף אוסף האיברים הנמצאים בתור .
- הוצאת איבר() מוציאה ומחזירה את האיבר הישן ביותר בתור.
- הצצה לראש התור() מחזירה את האיבר הישן ביותר בתור ללא הוצאתו.
- בדיקת ריקנות() מחזירה אם אוסף האיברים של ריק ו- אחרת.
נממש כעת את מבנה הנתונים שלנו בכמה דרכים:
נגדיר את המערך בן איברים ושני מיקומים: המציין את מיקום האיבר שלאחר סוף התור ו- אשר ממציין את מקום האיבר שבראש התור.
create(Q):
f = r = 0;
head(Q):
return Q[f];
enqueue(Q, x):
if ((r + 1) % n != f):
Q[r] = x;
r = (r + 1) % n;
dequeue(Q):
if (f != r):
x = Q[f];
f = (f + 1) % n;
return x;
is_empty(Q):
return (f == r);
נשתמש בשני מצביעים: מצביע לראש התור ומצביע לסוף התור .
בכתיבה ושימוש במבני נתונים יש לנו כמה עקרונות:
- המשתמש במבנה:
- הפשטה וכימוס: המשתמש מכיר רק את ממשק הפעולות ואת השפעתן החיצונית.
- הסתרת המידע: המשתמש אינו נדרש להכיר את פרטי המימוש הפנימיים או להסתמך עליהם.
- מתכנת המבנה:
- גמישות ושדרוג: ניתן להחליף את מימוש המבנה בכל עת מבלי לפגוע בתוכניות המשתמשות בו.
- פיתוח הדרגתי: אפשר להתחיל במימוש פשוט ומהיר ובהמשך לשפר למימוש מתקדם ויעיל יותר.
- תמיכה בשפות עיליות: ב- ובשפות מונחות עצמים ניתן לכתוב מספר מחלקות המממשות ממשק מופשט משותף ולבחור את המימוש הרצוי.
- איכות המימוש נקבעת ע"י:
- יעילות זמן ריצה.
- יעילות זיכרון (מקום).
- פשטות התכנות: קוד קריא, נקי, מובן ובר תחזוקה.