Η παρουσίαση φορτώνεται. Παρακαλείστε να περιμένετε

Η παρουσίαση φορτώνεται. Παρακαλείστε να περιμένετε

מבני נתונים ויעילות אלגוריתמים

Παρόμοιες παρουσιάσεις


Παρουσίαση με θέμα: "מבני נתונים ויעילות אלגוריתמים"— Μεταγράφημα παρουσίασης:

1 מבני נתונים ויעילות אלגוריתמים
מכללת אורט כפר-סבא מבני נתונים ויעילות אלגוריתמים רשימה מקושרת מימוש מחסנית באמצעות רשימה מקושרת אורי וולטמן

2 חידה לחימום במשחק השחמט, מלך, כידוע, יכול לזוז למרחק של משבצת אחת במאונך, במאוזן או באלכסון. סטודנט הצליח להעביר את המלך על כל משבצת של לוח שח רגיל (8×8), כך שהמלך ביקר בכל משבצת פעם אחת בדיוק, והמסלול שלו הסתיים במשבצת ממנה התחיל. האם יתכן שמספר המהלכים האלכסוניים שהוא ביצע שווה ל-17?

3 רשימה מקושרת מבנה הנתונים העיקרי בו השתמשנו עד כה על מנת לייצג אוסף נתונים בזיכרון המחשב, היה מערך (array). מערך מאוחסן בצורה רציפה בזיכרון, ויש לכך יתרונות וחסרונות. מהם? נניח ונרצה למצוא מבנה נתונים שיאפשר לנו לייצג אוסף נתונים בזיכרון, אבל שיתגבר על חלק מהחולשות של מערך. אין חובה שיאוחסן באופן רציף בזיכרון, וכך נוכל להתגבר על בעיות שנוצרות בעת שמנסים לאחסן אוסף נתונים גדול מאוד. אם ננסה להכניס נתון חדש באמצע האוסף, לא נאלץ לבזבז זמן רב בהזזת האיברים שאחריו. דוגמא למבנה נתונים כזה הוא מבנה נתונים הנקרא רשימה מקושרת (linked list).

4 רשימה מקושרת רשימה מקושרת היא מבנה נתונים המורכב מאוסף של חוליות (nodes) המשורשרות ביניהן באמצעות מצביעים. כל חוליה מכילה, בדרך כלל, שדה של מידע (ולפעמים, כמה שדות כאלה), ושדה נוסף המכיל מצביע לחוליה הבאה. שדה המצביע של החוליה האחרונה ברשימה המקושרת מכיל את הערך NULL, המציין את סוף הרשימה. נחזיק מצביע לראש הרשימה (לחוליה הראשונה בשרשרת). הבדל בולט ומיידי בין רשימה מקושרת לבין מערך, הוא שברשימה, כדי להגיע לאיבר מסוים, צריך לסרוק את הרשימה החל מתחילתה, בניגוד למערך, שם ניתן לגשת ישירות לאיבר לפי אינדקס. head 144 5- 201 12 NULL

5 רשימה מקושרת כדי להגדיר חוליה ברשימה מקושרת, נשתמש במבנה (struct) המכיל שדה אינפורמציה ושדה של מצביע לחוליה הבאה. typedef int list_info; struct node_type { list_info info; struct node_type *next; }; נשים לב שהגדרת struct node_type היא מעט מעגלית, שכן אחד השדות במבנה הוא מצביע למשתנה מסוג מבנה. נוכל לתת למבנה שם מקוצר: typedef struct node_type node; head 144 5- 201 12 NULL

6 רשימה מקושרת הנה דוגמא לקטע קוד, היוצר את המבנה המתואר מטה: pos pos
node *head, *pos; head = malloc(sizeof(node)); head->info = 144; pos = malloc(sizeof(node)); head->next = pos; pos->info = -5; pos->next = malloc(sizeof(node)); pos->next->info = 201; pos = pos->next; pos->next->info = 12; pos->next->next = NULL; pos pos head 144 5- 201 12 NULL

7 חיפוש איבר ברשימה מקושרת
מעוניינים לכתוב פונקציה המקבלת בתור פרמטרים את המצביע head לראש רשימה מקושרת, ואת האיבר x. על הפונקציה להחזיר מצביע למופע הראשון של x הנמצא בה, או להחזיר NULL במידה ו-x לא מופיע ברשימה אפילו פעם אחת. node *list_search (node *head, list_info x) { node *pos = head; while ((pos != NULL) && (pos->info != x)) pos = pos->next; return pos; } pos pos pos pos pos head NULL

8 חיפוש איבר ברשימה מקושרת
סוג החיפוש שעשינו הוא חיפוש לינארי (linear search). כזכור, סיבוכיות זמן הריצה של חיפוש כזה היא Θ(n), כאשר n הוא מספר האיברים ברשימה המקושרת. במידה ואיברי הרשימה המקושרת היו ממוינים, האם היינו יכולים להשתמש בחיפוש בינארי (binary search), ובכך להשיג זמן ריצה טוב יותר, של Θ(logn)? לא, משום שבכדי להשתמש בחיפוש בינארי, עלינו להיות מסוגלים לגשת ישירות לכל איבר באוסף הנתונים, באמצעות אינדקס. זה אפשרי כאשר מדובר במערך, אך לא כאשר מדובר ברשימה מקושרת, שבה בכדי להגיע לאיבר מסוים, צריך להתחיל בראש הרשימה, ולסרוק עד שפוגשים אותו.

9 הכנסת איבר לרשימה מקושרת
כאשר רצינו להכניס איבר למקום באמצע מערך, היינו צריכים להזיז את כל האיברים שאחריו צעד אחד ימינה, כדי לפנות עבורו מקום. האם כך ננהג גם ברשימה? נניח שיש לנו את הערך x, ומצביע pos המצביע אל אחת החוליות ברשימה המקושרת. מעוניינים להוסיף חוליה חדשה לרשימה מקושרת, שיכיל את הערך x, ושיימצא אחרי המקום ש-pos מצביע עליו. נעשה זאת כך: node *pnew = malloc(sizeof(node)); pnew->info = x; pnew->next = pos->next; pos->next = pnew; pnew x pos head NULL

10 הכנסת איבר לראש רשימה מקושרת
נניח, כמו קודם, כי נתון לנו הערך x, וכי מעוניינים להוסיף חוליה חדשה לרשימה המקושרת, המכילה את הערך x. אולם הפעם, נניח כי מעוניינים שהחוליה החדשה תתווסף בראש הרשימה (כלומר, שהמצביע head יצביע עליה). אילו שינויים צריך לעשות בקטע הקוד הקודם? node *pnew = malloc(sizeof(node)); pnew->info = x; pnew->next = pos->next; pos->next = pnew; head; head pnew x head NULL

11 מחיקת האיבר שבראש רשימה מקושרת
נניח כי מעוניינים למחוק את החוליה הנמצאת בראש הרשימה המקושרת (בהנחה שהרשימה איננה ריקה, כלומר – יש בה לפחות חוליה אחת). כיצד נעשה זאת? pos = head; head = head->next; free(pos); נשים לב כי המצביע pos לא מצביע כעת אל אזור בזכרון אליו אמורה להיות לנו גישה, ולכן יש להקפיד לא להשתמש בו לאחר ביצוע הוראת ה-free. במקרים כאלו, לפעמים, נציב בו את הערך NULL, כדי למנוע מצב בו ניגש בטעות לכתובת שאליה הוא מצביע. /* head = pos->next; */ pos head head NULL

12 מחיקת איבר ברשימה מקושרת
כאשר רצינו למחוק איבר באמצע מערך, היינו צריכים להזיז את כל האיברים שאחריו צעד אחד שמאלה. האם כך ננהג גם כשנרצה למחוק איבר ברשימה מקושרת? נניח כי המצביע pos מצביע אל אחת החוליות ברשימה המקושרת, שאיננה החוליה הראשונה, ואנו מעוניינים למחוק אותה. כיצד נעשה זאת? temp = head; while (temp->next != pos) temp = temp->next; temp->next = pos->next; free(pos); pos = temp->next; /* temp->next = temp->next->next */ temp temp pos pos head NULL

13 הכנסת איבר לרשימה מקושרת ממוינת
אנחנו כבר יודעים איך מכניסים חוליה במקום כלשהו לרשימה מקושרת. כעת נכתוב קטע קוד המקבל ערך x ומצביע head לראש רשימה מקושרת לא ריקה הממוינת בסדר עולה. צריך להכניס חוליה חדשה עם הערך x במקום המתאים ברשימה, כדי לא "לקלקל" את היותה ממוינת. נדגים כיצד נכניס ערך (למשל, x = 8) לרשימה ממוינת: node *pos = head, *pnew; while ((pos->next != NULL) && (pos->next->info < x)) pos = pos->next; pnew = malloc(sizeof(node)); pnew->info = x; pnew->next = pos->next; pos->next = pnew; האם קטע קוד זה יעבוד כשורה אם הרשימה הממוינת ריקה? ואם היא מכילה חוליה אחד בלבד? ואם האיבר החדש קטן מכל האיברים שכעת ברשימה? או גדול מכל האיברים שכעת ברשימה? pnew 8 pos pos pos head 3 6 7 9 NULL

14 היפוך רשימה מקושרת האם קטע קוד זה יעבוד כשורה
נניח שמקבלים מצביע head לראש רשימה מקושרת, ומעוניינים להפוך את סדר האיברים ברשימה, כך שהאיבר שקודם היה בראש הרשימה יהיה כעת בזנב הרשימה, והאיבר שקודם היה בזנב הרשימה יהיה כעת בראש הרשימה: node *prev = head, *curr = prev->next, *nxt = curr->next; prev->next = NULL; while (nxt != NULL) { curr->next = prev; prev = curr; curr = nxt; nxt = nxt->next; } head = curr; האם קטע קוד זה יעבוד כשורה אם הרשימה ריקה? ואם הרשימה מכילה חוליה אחת או שתיים? head prev prev curr nxt nxt curr prev curr nxt NULL head NULL

15 מחסנית בעבר ראינו כיצד אפשר לייצג את טיפוס הנתונים המופשט (טנ"מ) 'מחסנית', הן על-ידי מערך סטטי והן על-ידי מערך דינאמי. כשיישמנו את הטנ"מ בסביבת העבודה, הפרדנו בין הכותרות של הפונקציות (שנשמרו בקובץ stack.h) לבין גוף הפונקציות (שנשמרו בקובץ stack.c). כך השגנו הפרדה בין ממשק (interface) למימוש (implementation), שאיפשרה לנו לשנות את הייצוג של הטנ"מ בזיכרון, מבלי שנצטרך לשנות תוכניות שכבר נכתבו, שעושות שימוש במחסנית. מה הייתה הסיבה בגללה העדפנו לייצג מחסנית באמצעות מערך דינאמי ולא באמצעות מערך סטטי? גודלו של מערך סטטי נקבע מראש, ואי אפשר לאחסן בו יותר איברים, לעומת מערך דינאמי, שגודלו משתנה לפי הצורך. במערך סטטי עלול להיות בזבוז זכרון (אם מגדירים מערך גדול, ומשתמשים רק בחלק קטן ממנו), לעומת מערך דינאמי, שגודלו תמיד כפי הדרוש. `

16 מחסנית האם הייצוג באמצעות מערך דינאמי הוא נטול חולשות? אילו קשיים עשויים להתעורר במידה ונרצה לאחסן מספר גדול מאוד של איברים במחסנית? העובדה שמערך מאוחסן באופן רציף בזיכרון, פירושה שאם נרצה לאחסן מספר רב של איברים במחסנית, אז ייתכן ונתקשה למצוא בלוק זיכרון פנוי שהוא ארוך מספיק. האם הכרנו מבנה נתונים שיכול לעזור לנו להתמודד עם בעיה זו? אם נייצג את טיפוס הנתונים המופשט 'מחסנית' באמצעות רשימה מקושרת (linked list), אז גם אם המחסנית תכיל מספר רב של איברים, קרוב לוודאי שנוכל לאחסן אותה בזיכרון, שכן ברשימה מקושרת, החוליות לא חייבות להיות מאוחסנות אחת אחרי השנייה בזיכרון.

17 מחסנית נכלול בקובץ stack.h הגדרה של חוליה (node) ברשימה מקושרת, ונגדיר מחסנית בתור מבנה, המכיל בתוכו מצביע לחוליה הראשונה (ראש המחסנית): typedef int stack_item; struct node_type { stack_item info; struct node_type *next; }; typedef struct { struct node_type *top; } stack;

18 מחסנית הכותרות של הפונקציות ב-stack.h נותרות ללא שינוי:
void stack_init(stack *s); int stack_empty (stack s); int stack_full (stack s); void stack_push (stack *s, stack_item x); stack_item stack_pop (stack *s); stack_item stack_top (stack s); השינויים שערכנו במימוש של המחסנית לא מחייבים לערוך שינויים בחתימות של הפונקציות שבממשק. מדוע? הפונקציות עדיין מקבלות את אותם הפרמטרים, מחזירות את אותם הערכים, ומקיימות את אותן טענות כניסה ויציאה. מבחינת מתכנת שמשתמש ביחידת הספרייה stack.h שאנחנו מספקים, השינוי שעשינו במימוש, לא משפיע בכלל על הממשק. מבחינת אותו מתכנת, העובדה ששינינו את הייצוג של טנ"מ מחסנית ממערך דינאמי לרשימה מקושרת, זה משהו שקרה "מאחורי הקלעים", ולא צריך להשפיע בכלל על האופן שבו הוא עובד עם יחידת הספרייה. כמו כן, הוא לא צריך לשכתב את התכניות שעושות שימוש ביחידה.

19 מחסנית בקובץ המימוש stack.c, נצטרך לערוך שינויים:
void stack_init (stack *s) { s->top = NULL; } int stack_empty (stack s) return (s.top == NULL); int stack_full (stack s) return 0; למה במימוש באמצעות רשימה מקושרת, הפונקציה stack_full תחזיר תמיד 'שקר'? מתכנת מציע למחוק את הפונקציה stack_full מיחידת הספרייה (גם מהממשק וגם מהמימוש), שכן אין בה יותר צורך. מה אתם חושבים על הצעתו?

20 מחסנית כך נממש דחיפה למחסנית: כך נממש שליפה ממחסנית:
void stack_push (stack *s, stack_item x) { struct node_type *temp = malloc(sizeof(struct node_type)); temp->info = x; temp->next = s->top; s->top = temp; } כך נממש שליפה ממחסנית: stack_item stack_pop (stack *s) if (!stack_empty(*s)) { struct node_type *temp = s->top; stack_item data = s->top->info; s->top = temp->next; free(temp); return data; איך ניתן היה להיעזר במצביע temp כדי לכתוב בצורה אחרת את השורה השנייה בבלוק שאחרי ה-if? האם הפונקציה stack_push כשורה גם אם המחסנית ריקה? האם שתי הפונקציות תפעלנה כשורה גם אם המחסנית מכילה איבר יחיד?

21 מחסנית הצצה למחסנית נכתבה כך, גם במימוש באמצעות מערך סטטי וגם במימוש באמצעות מערך דינאמי: stack_item stack_top (stack s) { if (!stack_empty(s)) return s.data[s.top]; } כיצד תראה אותה הפונקציה במימוש באמצעות רשימה מקושרת? return (s.top)->info;

22 מחסנית מהי סיבוכיות זמן הריצה של כל אחת מפעולות הממשק של טנ"מ מחסנית, אם הוא ממומש באמצעות רשימה מקושרת? במימוש מחסנית באמצעות רשימה מקושרת, בדומה למימוש באמצעות מערך סטטי או למימוש באמצעות מערך דינאמי, כל אחת מפעולות הממשק (איתחול, שליפה, דחיפה, בדיקה האם ריק, בדיקה האם מלא, הצצה) מבצעת כמות קבועה של עבודה, ללא קשר לשאלה כמה איברים נמצאים כרגע במחסנית. מכיוון שהפעולות הללו אינן תלויות בגודל n, הן מתבצעות בזמן קבוע Θ(1).

23 תרגיל סטודנט, הכותב תכנית המשתמשת ביחידת הספריה stack.h, מעוניין לכתוב פונקציה המקבלת כפרמטר מחסנית לא ריקה, ומחזירה 1 אם יש במחסנית יותר מאיבר יחיד, ו-0 אם יש במחסנית רק איבר אחד. הוא כתב את הפונקציה הבאה: int check_size (stack s) { return ((s.top)->next != NULL); } חברו, הפותר בעיה דומה, כתב את הפונקציה הזו: return (s.top > 0); מי מבין שתי הפונקציות נכונה יותר?

24 תרגיל שתי הפונקציות שגויות, משום ששתיהן ניגשות ישירות למימוש של טנ"מ מחסנית, במקום להשתמש בפעולות הממשק! כאשר משתמשים ביחידת הספרייה stack.h, אנחנו לא יודעים באיזה צורה בחרו לממש את המחסנית (במערך סטטי? במערך דינאמי? ברשימה מקושרת? בדרך אחרת?), ולכן, אין אנו יכולים להניח הנחות לגבי האופן שבו המחסנית מיוצגת בזיכרון. מבחינת נכונות, ומתוך שיקולים של הנדסת תוכנה, נסכים שכדי לגשת למחסנית, מחוץ לקובץ המימוש stack.c, מותר לנו להשתמש רק בפעולות הממשק: int check_size (stack s) { stack_pop(&s); return (!stack_empty(s)); } האם הפונקציה הזו תלויה במימוש שבו המתכנת שבנה את יחידת הספרייה stack.h בחר להשתמש? האם המתכנת שכתב את הפונקציה check_size הזו יודע כיצד מומשה יחידת הספרייה? האם זה משנה מבחינתו? אם נחליט לשנות את האופן שבו ממומשת המחסנית, האם נצטרך לשכתב פונקציה זו?

25 עוד תרגיל כזכור, בייצוג מחסנית באמצעות רשימה מקושרת, הגדרנו מחסנית באופן הבא: typedef struct { struct node_type *top; } stack; איזה יתרונות יש להגדרת מחסנית בתור מבנה המכיל מצביע לחוליה, במקום, למשל, להגדיר פשוט מחסנית בתור מצביע לחוליה הראשונה? typedef struct node_type *stack; כאשר מגדירים מחסנית בתור רשומה, אפשר לאחסן ברשומה שדות נוספים, מלבד שדה מצביע לחוליה הראשונה. לדוגמא, אפשר לאחסן שדה בשם num_items המונה את מספר איברי המחסנית. כשמאתחלים מחסנית חדשה באמצעות stack_init, ערכו של השדה num_items ייקבע לאפס. כשדוחפים או שולפים איבר מראש המחסנית, השדה num_items יגדל או ייקטן ב-1. כאשר רוצים לבדוק האם המחסנית ריקה, ניתן לבדוק האם ערכו של top הוא NULL (כפי שכתבנו), או לבדוק האם ערכו של השדה num_items הוא אפס. כעת אם נרצה, נוכל להוסיף לממשק של טנ"מ מחסנית, את פעולת הממשק stack_size המחזירה את מספר האיברים המאוחסן במחסנית. מה סיבוכיותה? ומה הייתה סיבוכיותה אלמלא היינו שומרים את השדה num_items? סיבוכיות הפעולה היא קבועה Θ(1), לעומת סיבוכיות לינארית Θ(n) ללא שדה זה.


Κατέβασμα ppt "מבני נתונים ויעילות אלגוריתמים"

Παρόμοιες παρουσιάσεις


Διαφημίσεις Google