הרצאה 06 רשימות מקושרות קרן כליף
ביחידה זו נלמד: מהי רשימה מקושרת פונקציות של רשימות מקושרות מימושים רקורסיביים לפונקציות של רשימות מקושרות ההבדל בין מערך לרשימה וריאציות של רשימות מקושרות © Keren Kalif
מבנה נתונים מבנה נתונים הוא אוסף המאפשר לנו להחזיק יותר מאיבר אחד למשל: מערך קיימים מבני נתונים נוספים שלכל אחד יש את היתרונות והחסרונות שלו, ואלו ישפיעו על בחירת שימוש המבנה נתונים אחד או אחר היתרונות של מערך: גישה ישירה לאיבר הוספה בקלות לסוף המערך החסרונות של מערך: הסרה/הוספה לאמצע מורכבת גודל מוגבל © Keren Kalif
רשימה מקושרת זהו מבנה נתונים המאפשר הוספה דינאמית של איברים לכל מקום באוסף בקלות אין צורך להחליט מראש כמה איברים יהיו ברשימה הרעיון הוא יצירת איבר וקישורו למקום המתאים ברשימה בכל רגע נתון כל איבר ברשימה מקושרת הוא מבנה המכיל נתון והצבעה לאיבר הבא האיבר האחרון ברשימה יצביע ל- NULL בניגוד למערך, האיברים אינם ברצף בזיכרון typedef int type; typedef struct LNode } type data; struct LNode* next; } LNode; 4 9 6 typedef struct List } struct LNode* head; } List; © Keren Kalif
דוגמאת הוספת ערכים לרשימה lst head 1 ??? 2 2 void main() { List lst; lst.head = (LNode*)calloc(1, sizeof(LNode)); lst.head->data = 1; lst.head->next = (LNode*)calloc(1, sizeof(LNode)); lst.head->next->data = 2; } © Keren Kalif
פונקציה המחשבת את כמות הצמתים ברשימה int getListLength(const List* lst) { int count = 0; LNode* temp = lst->head; while (temp != NULL) count++; temp = temp->next; } return count; count = 0 count = 1 count = 2 temp lst head 1 ??? 2 2 © Keren Kalif
פונקציה המדפיסה את איברי הרשימה void printList(const List* lst) { LNode* temp = lst->head; while (temp != NULL) printf("%d ", temp->data); temp = temp->next; } 1 2 1 temp lst head 1 ??? 2 2 © Keren Kalif
פונקציה המשחררת את איברי הרשימה void freeList(List* lst) { LNode* current = lst->head; LNode* next; while (current) next = current->next; free(current); current = next; } lst->head = NULL; current next lst head 1 ??? 2 2 © Keren Kalif
שדרוג הרשימה עד כה החזקנו בתוך המבנה List מצביע לראש הרשימה, והוספת איבריו הייתה באמצעותו: void main() { List lst; lst.head = (LNode*)calloc(1, sizeof(LNode)); lst.head->data = 1; lst.head->next = (LNode*)calloc(1, sizeof(LNode)); lst.head->next->data = 2; lst.head->next->next = (LNode*)calloc(1, sizeof(LNode)); lst.head->next->next->data = 3; freeList(&lst); } © Keren Kalif
שדרוג הרשימה כדי להקל על הוספת איברים לסוף הרשימה נשדרג את המבנה List כך שיכיל גם מצביע לאיבר האחרון: void main() { List lst; lst.head = (LNode*)calloc(1, sizeof(LNode)); lst.head->data = 1; lst.tail = lst.head; lst.tail->next = (LNode*)calloc(1, sizeof(LNode)); lst.tail->next->data = 2; lst.tail = lst.tail->next; lst.tail->next->data = 3; freeList(&lst); } typedef struct List } struct LNode *head, *tail; } List; ניתן לראות שכעת הקוד של הוספת איבר לסוף הרשימה זהה, ובפרט חוסך לולאה המטיילת לאיבר האחרון, ולכן יעילות הפעולה היא (1)O lst 3 1 head 2 tail © Keren Kalif
פונקציה המאתחלת רשימה ריקה פונקציה זו מאתחלת בשדות ה- head וה- tail את הערך NULL List makeEmptyList() { List lst; lst.head = lst.tail = NULL; return lst; } © Keren Kalif
פונקציה הבודקת האם רשימה ריקה int isEmpty(const List* lst) { return lst->head == NULL; } © Keren Kalif
פונקציה שיוצרת איבר להכנסה לרשימה הפונקציה תקבל את הערך שיהיה באיבר, ומצביע לאיבר הבא: פונקציה זו תשמש אותנו בפונקציות הבאות LNode* createNewNode(type newData, LNode* next) { LNode* newNode = (Node*)calloc(1], sizeof(LNode)); newNode->data = newData; newNode->next = next; return newNode; } © Keren Kalif
הוספת ערך לראש הרשימה void insertValueToHead(List* lst, type newData) { LNode* newNode = createNewNode(newData, lst->head); if (isEmpty(lst)) lst->head = lst->tail = newNode; else lst->head = newNode; } newData = 4 3 2 lst head tail 4 newNode © Keren Kalif
הוספת ערך לראש הרשימה (שהפעם ריקה) void insertValueToHead(List* lst, type newData) { LNode* newNode = createNewNode(newData, lst->head); if (isEmpty(lst)) lst->head = lst->tail = newNode; else lst->head = newNode; } newData = 4 lst head tail 4 newNode © Keren Kalif
הוספת ערך לסוף הרשימה void insertValueToTail(List* lst, type newData) { LNode* newNode = createNewNode(newData, NULL); if (isEmpty(lst)) lst->head = lst->tail = newNode; else lst->tail->next = newNode; lst->tail = newNode; } 3 2 lst newData = 4 head tail 4 newNode © Keren Kalif
הוספת ערך לסוף הרשימה (שהפעם ריקה) void insertValueToTail(List* lst, type newData) { LNode* newNode = createNewNode(newData, NULL); if (isEmpty(lst)) lst->head = lst->tail = newNode; else lst->tail->next = newNode; lst->tail = newNode; } lst newData = 4 head tail 4 newNode © Keren Kalif
הוספת איבר לסוף הרשימה void insertNodeToTail(List* lst, LNode* newNode) { if (isEmpty(lst)) lst->head = lst->tail = newNode; else lst->tail->next = newNode; lst->tail = newNode; } © Keren Kalif
דוגמאת החברים © Keren Kalif
דוגמא: פיצול רשימה לזוגיים ואי-זוגיים void main() { List lst = makeEmptyList(); List lstEven, lstOdd; int i; for (i=1 ; i <= 10 ; i++) insertValueToTail(&lst, i); splitListToEvenAndOdd(&lst, &lstEven, &lstOdd); printf("There are %d even nodes: ", getListLength(&lstEven)); printList(&lstEven); printf("\n"); printf("There are %d odd nodes: ", getListLength(&lstOdd)); printList(&lstOdd); freeList(&lstEven); freeList(&lstOdd); } © Keren Kalif
דוגמא: פיצול רשימה לזוגיים ואי-זוגיים (2) דוגמא: פיצול רשימה לזוגיים ואי-זוגיים (2) 3 void splitListToEvenAndOdd(List* src, List* even, List* odd) { LNode* current = src->head; LNode* next; *even = makeEmptyList(); *odd = makeEmptyList(); while (current) next = current->next; if (current->data%2 == 0) insertNodeToTail(even, current); else insertNodeToTail(odd, current); current->next = NULL; current = next; } 1 2 src head tail even head tail current next odd head tail פונקציה זו הורסת את הרשימה המקורית. כדי לשמור עליה היה מריך להעתיק קודם את רשימת המקור. © Keren Kalif
העתקת רשימה List copyList(const List* lst) { List res = makeEmptyList(); LNode* temp = lst->head; while (temp) insertValueToTail(&res, temp->data); temp = temp->next; } return res; © Keren Kalif
הרעיון מאחורי הוספת ערך לאמצע רשימה דוגמא: הוספת הערך 8 אחרי הערך 9 ראשית נייצר איבר חדש נחבר את האיבר החדש לאיבר שאמור להיות אחריו נחבר לאיבר החדש את האיבר שלפניו 4 9 6 8 © Keren Kalif
הוספת איבר לאמצע הרשימה (אחרי ערך מסוים) void insertAfterValue(List* lst, type insertAfter, type newValue) { LNode* temp = lst->head; while (temp && temp->data != insertAfter) temp = temp->next; if (temp) LNode* newNode = (LNode*)malloc(sizeof(LNode)); newNode->data = newValue; newNode->next = temp->next; temp->next = newNode; הוספה לאחר איבר שלא קיים - לא תבוצע 2 9 6 insertAfter = 9 newValue = 8 head temp temp 8 newNode © Keren Kalif
5 2 9 מחיקת איבר void removeFirstValueOf(List* lst, type toRemove) { LNode* prev = lst->head; LNode* temp = lst->head; while (temp && temp->data != toRemove) prev = temp; temp = temp->next; prev->next = temp->next; free(temp); 5 toRemove = 9 prev 2 9 head temp temp © Keren Kalif
2 9 6 חיפוש ערך LNode* findValue(List* lst, type lookFor) { LNode* temp = lst->head; while (temp) if (temp->data == lookFor) return temp; temp = temp->next; return NULL; } lookFor = 9 2 9 6 head temp temp © Keren Kalif
2 4 1 7 2 דוגמא true false true void main() { List list = makeEmptyList(); LNode* found; printf("Is Empty? %s\n", isEmpty(&list) ? "true" : "false"); insertValueToHead(&list, 4); insertValueToHead(&list, 7); insertValueToTail(&list, 1); insertAfterValue(&list, 4, 2); insertAfterValue(&list, 8, 9); // doesn't add.. insertAfterValue(&list, 1, 2); removeFirstValueOf(&list, 2); found = findValue(&list, 4); found = findValue(&list, 10); printList(&list); printf(“\nIs Empty? %s\n", isEmpty(&list) ? "true" : "false"); freeList(&list); דוגמא 2 true 4 head 1 7 2 false true © Keren Kalif
מימושים רקורסיביים – שחרור רשימה void freeListRec(List* lst) { freeListHelper(lst->head); lst->head = NULL; lst->tail = NULL; } void freeListHelper(LNode* head) { if (head == NULL) return; freeListHelper(head->next); free(head); } © Keren Kalif
מימושים רקורסיביים – הדפסת רשימה void printListRec(List* lst) { printListHelper(lst->head); } void printListHelper(LNode* head) { if (head == NULL) printf("\n"); return; } printf("%d ", head->data); printListHelper(head->next); © Keren Kalif
מימושים רקורסיביים – חיפוש איבר LNode* findValueRec(List* lst, type lookFor) { return findValueRec(lst->head, lookFor); } LNode* findValueHelper(LNode* head, type lookFor) { if (head == NULL) return NULL; if (head->data == lookFor) return head; return findValueRec(head->next, lookFor); } © Keren Kalif
מימושים רקורסיביים – אורך רשימה int getListLengthRec(List* lst) { return getListLengthHelper(lst->head); } int getListLengthHelper(LNode* head) { if (head == NULL) return 0; return 1 + getListLengthHelper(head->next); } © Keren Kalif
דוגמאת ניהול הזכרון © Keren Kalif
וריאציות של רשימה מקושרת עבודה עם dummy head: בעת הוספה/הסרה וכד' חוסך בדיקה האם האיבר הוא הראשון רשימה דו-כיוונית: יעיל עבור טיול דו כיווני, מאפשר לא לשמור את המשתנה prev בכל מקום בו צריך לשמור הפניה לאיבר הקודם (למשל במחיקה) מאפשר גישה מיידית לאיבר קודם מקל על פעולת החלפה של 2 איברים רשימה מעגלית האיבר האחרון מצביע לראשון © Keren Kalif
יתרונות/חסרונות לשימוש ברשימה מקושרת יתרונות: ניתן להוסיף בקלות איברים לכל מקום באוסף ניתן להסיר איברים בקלות חסרונות: אין גישה ישירה לאיבר שימוש בהקצאה דינאמית עבור כל איבר חדש נשתמש כאשר הפעולות העיקריות על האוסף יהיו הוספה להתחלה, לאמצע, הסרה © Keren Kalif
ביחידה זו למדנו: מהי רשימה מקושרת פונקציות של רשימות מקושרות מימושים רקורסיביים לפונקציות של רשימות מקושרות ההבדל בין מערך לרשימה וריאציות של רשימות מקושרות © Keren Kalif