חזור לתיקייה: 02340218 - מבני נתונים 1

0 - הקדמה (פשוט חזרה על מתמ)

חיפוש בתיקיה
Ctrl K
לחץ כאן או הקש Ctrl K כדי לחפש בכל התיקיה
  • הגדרות בסיסיות

0 - הקדמה (פשוט חזרה על מתמ)

02340218

מבני נתונים 1

רשימות הרצאה לקריאה בשבת

הקדמה (פשוט חזרה על מתמ)

הופק מתוך תקיית התואר של שקד

uni-folder.vercel.app

הגדרות בסיסיות

הגדרה 1 (טיפוס נתונים מופשט)

אוסף של פעולות על קבוצת נתונים יקרא טיפוס נתונים מופשט.

ניתן לממש באופן קונקרטי טיפוס נתונים מופשט ע"י מבנה נתונים.

הגדרה 2 (מבנה נתונים)

אוסף של פרוצדורות, אחת לכל פעולה, הממשות את הפעולות של טיפוס הנתונים המופשט.

דוגמה (מימוש למחסנית)

נממש מחסנית באמצעות מערך(אינסופי) ומצביע toptoptop לראש המחסנית:

  • אתחול מבצע top=0;top=0;top=0;.
  • הוספת איבר תתבצע top=top+1;A[top]=x;top=top+1;A[top]=x;top=top+1;A[top]=x;
  • נוכל לקרוא לראש המחסנית ע"י if(top>0)  returnA[top];if (top>0)\;return A[top];if(top>0)returnA[top];
  • נוכל להוציא איבר כך: if(top>0)  top=top−1if(top>0)\;top=top-1if(top>0)top=top−1.
  • נוכל לבדוק האם המחסנית ריקה ע"י top==0top== 0top==0.

העקרון המנחה שלנו בבניית המחסנית הוא העקרון LIFOLIFOLIFO אשר הוא Last in - first out. בעברית: אחרון שנכנס הוא הראשון לצאת.

הפעולות שניצור יהיו:

  • יצירת מחסנית חדשה ריקה: create()create()create().

  • הוספת איבר xxx לאוסף האיברים במחסנית SSS תתבצע: push(S,  x)push(S,\;x)push(S,x).

  • החזרת האיבר האחרון שהוכנס ל-SSS ועדיין לא הוצא: top(S)top(S)top(S).

  • הוצאת האיבר האחרון שהוכנס ל-SSS ועדיין לא הוצא: pop(S)pop(S)pop(S).

  • בדיקה האם המחסנית ריקה: is_empty(S)is\_empty(S)is_empty(S) (יחזיר truetruetrue אם SSS ריקה ואחרת falsefalsefalse)

הערה (שאלות למחשבה)

  1. אם נבצע יותר pushpushpush-ים מ-poppoppop-ים על מחסנית ריקה, האם המחסנית תהיה ריקה?
  2. אם נבצע יותר poppoppop-ים מ-pushpushpush-ים על מחסנית ריקה, האם היא תהיה ריקה?

בואו, ברוח הדוגמה האחרונה נסתכל על עוד כמה מימושים עבור מחסנית:

Proposition (מימוש מחסנית בעזרת רשימה מקושרת)

נגדיר את הצומת:

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;
}

על מנת לבחור מימוש נרצה להסתכל על כמה שיקולים:

  1. שיקולי מהירות: מערך מהיר יותר מרשימה. אין צורך בהקצאת אובייקטים וההתנהגות המקומית(הרציפות בזכרון) מאפשרת ניצול מיברי של זכרון CacheCacheCache.
  2. שיקולי זכרון: מערך מחייב לדעת חסם על גודל המחסנית בעת יצירתה. הקצאת ייתר תיצור בזבוז בזכרון בעוד שחוסר במקום יגרום גם כן לבעיות. ברשימה מקושרת אין חסם עליון אך נדרשת הקצאת מקום נוסף לכל מצביע.

בפועל, נבחר מודל היברידי כמו המודל הבא:

Proposition

נבנה את המחסנית שלנו באמצעות מערך בגודל קבוע וכאשר הוא מתמלא לחלוטין נקצה מחדש מערך נוסף ונקשר אותו ברשימה מקושרת.

דוגמה (תור)

כעת נבנה תור, כאשר העקרון המנחה הוא FIFOFIFOFIFO. כלומר, הראשון שנכנס הוא הראשון לצאת.
תכונות מבנה הנתונים שלנו יהיו:

  • אתחול(create()create()create()) יוצרת ומחזירה תור חדש שאוסף האיברים שלו ריק.
  • הוספת איבר(enqueue(Q,  x)enqueue(Q,\;x)enqueue(Q,x)) מוסיפה את האיבר xxx לסוף אוסף האיברים הנמצאים בתור QQQ.
  • הוצאת איבר(dequeue(Q)dequeue(Q)dequeue(Q)) מוציאה ומחזירה את האיבר הישן ביותר בתור.
  • הצצה לראש התור(head(Q)head(Q)head(Q)) מחזירה את האיבר הישן ביותר בתור ללא הוצאתו.
  • בדיקת ריקנות(is_empty(Q)is\_empty(Q)is_empty(Q)) מחזירה truetruetrue אם אוסף האיברים של QQQ ריק ו-falsefalsefalse אחרת.

נממש כעת את מבנה הנתונים שלנו בכמה דרכים:

Proposition (מימוש באמצעות מערך)

נגדיר את המערך QQQ בן nnn איברים ושני מיקומים: rrr המציין את מיקום האיבר שלאחר סוף התור ו-fff אשר ממציין את מקום האיבר שבראש התור.

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);
Proposition (מימוש באמצעות רשימה מקושרת)

נשתמש בשני מצביעים: מצביע לראש התור firstfirstfirst ומצביע לסוף התור lastlastlast.

בכתיבה ושימוש במבני נתונים יש לנו כמה עקרונות:

  1. המשתמש במבנה:
    1. הפשטה וכימוס: המשתמש מכיר רק את ממשק הפעולות ואת השפעתן החיצונית.
    2. הסתרת המידע: המשתמש אינו נדרש להכיר את פרטי המימוש הפנימיים או להסתמך עליהם.
  2. מתכנת המבנה:
    1. גמישות ושדרוג: ניתן להחליף את מימוש המבנה בכל עת מבלי לפגוע בתוכניות המשתמשות בו.
    2. פיתוח הדרגתי: אפשר להתחיל במימוש פשוט ומהיר ובהמשך לשפר למימוש מתקדם ויעיל יותר.
    3. תמיכה בשפות עיליות: ב-C++C++C++ ובשפות מונחות עצמים ניתן לכתוב מספר מחלקות המממשות ממשק מופשט משותף ולבחור את המימוש הרצוי.
  3. איכות המימוש נקבעת ע"י:
    1. יעילות זמן ריצה.
    2. יעילות זיכרון (מקום).
    3. פשטות התכנות: קוד קריא, נקי, מובן ובר תחזוקה.