Doubly Linked List (רשימה מקושרת דו כיוונית)
עודכן לאחרונה
רשימה מקושרת דו כיוונית היא רשימה מקושרת שבה כל צומת מחזיק שני מצביעים: next (לצומת הבא) ו-prev (לצומת הקודם). הקישור הנוסף לאחור מאפשר לעבור על הרשימה בשני הכיוונים ולמחוק צומת ב-O(1) כשכבר יש לכם הפניה אליו, כי אפשר להגיע ישירות לצומת שלפניו במקום ללכת מהראש. לחצו על הפעלה למעלה כדי לראות צמתים מתחברים ומתחברים מחדש בשני הכיוונים.
המחיר הוא מצביע נוסף לכל צומת ויותר ניהול: כל הכנסה ומחיקה חייבות לעדכן גם את next וגם את prev אצל השכנים. זה המבנה שמאחורי תורים דו צדדיים (deque) ומטמוני LRU רבים, שבהם הסרה מהירה משני הקצוות חשובה.
סיבוכיות זמן
| פעולה | סיבוכיות | הערות |
|---|---|---|
| הכנסה בראש או בזנב | O(1) | עם מצביעי ראש וזנב |
| מחיקת צומת ידוע | O(1) | מגיעים ל-prev ישירות, בלי הליכה |
| חיפוש | O(n) | עדיין סריקה ליניארית |
| גישה לפי אינדקס | O(n) | אין גישה אקראית |
רשימה מקושרת חד כיוונית מול דו כיוונית
| היבט | חד כיוונית | דו כיוונית |
|---|---|---|
| מצביעים לכל צומת | 1 (next) | 2 (next + prev) |
| מעבר לאחור | לא | כן |
| מחיקת צומת ידוע | O(n) (מציאת prev) | O(1) |
| תקורת זיכרון | נמוכה יותר | גבוהה יותר |
דוגמה מפורטת
בניית [10, 20, 30] על ידי הוספה לסוף, ואז מחיקת הצומת 20:
| צעד | מבנה | פעולה |
|---|---|---|
| התחלה | null | רשימה ריקה: head ו-tail שניהם null |
| הוספת 10 | 10 | הצומת הראשון; head ו-tail מצביעים שניהם אליו, prev = next = null |
| הוספת 20 | 10 <-> 20 | קובעים 10.next = 20 ו-20.prev = 10; tail = 20 |
| הוספת 30 | 10 <-> 20 <-> 30 | קובעים 20.next = 30 ו-30.prev = 20; tail = 30 |
| מחיקת 20 | 10 <-> 30 | בעזרת 20.prev ו-20.next: קובעים 10.next = 30 ו-30.prev = 10 ב-O(1) |
מתי להשתמש ברשימה מקושרת דו כיוונית
| השתמשו בה כאשר | הימנעו ממנה כאשר |
|---|---|
| אתם צריכים לעבור על הרשימה או לחבר בה קטעים בשני הכיוונים | אתם תמיד זזים רק קדימה: רשימה מקושרת חד כיוונית קלה יותר |
אתם מוחקים צמתים כלשהם שכבר יש לכם הפניה אליהם (O(1)) | אתם ניגשים בעיקר לפי מיקום: השתמשו במערך לגישה של O(1) |
| אתם מממשים deque, מטמון LRU או היסטוריית ביטול פעולות | הזיכרון מוגבל: מצביע ה-prev הנוסף מכפיל את תקורת הקישורים |
| הכנסות והסרות קורות לעתים קרובות בשני הקצוות | הנתונים קטנים ומשתנים לעתים רחוקות: העתקות של מערך זולות יותר |
קוד Doubly Linked List
מימוש נקי של Doubly Linked List שאפשר להריץ, ב-Python, JavaScript, Java, C++, C. בחרו שפה, העתיקו את הקוד, או פתחו אותו טעון מראש בעורך האונליין של Coddy.
קוד Doubly Linked List ב-Python
1class Node:2 def __init__(self, value):3 self.value = value4 self.prev = None5 self.next = None6
7
8class DoublyLinkedList:9 def __init__(self):10 self.head = None11 self.tail = None12
13 def append(self, value):14 node = Node(value)15 if self.tail is None:16 self.head = self.tail = node17 return18 # Wire both directions: old tail <-> new node19 node.prev = self.tail20 self.tail.next = node21 self.tail = node22
23 def prepend(self, value):24 node = Node(value)25 if self.head is None:26 self.head = self.tail = node27 return28 node.next = self.head29 self.head.prev = node30 self.head = node31
32 def forward(self):33 values, current = [], self.head34 while current:35 values.append(current.value)36 current = current.next37 return values38
39 def backward(self):40 values, current = [], self.tail41 while current:42 values.append(current.value)43 current = current.prev44 return values45
46
47lst = DoublyLinkedList()48for value in [2, 3, 4]:49 lst.append(value)50lst.prepend(1)51
52print("Forward: ", lst.forward())53print("Backward:", lst.backward())קוד Doubly Linked List ב-JavaScript
1class Node {2 constructor(value) {3 this.value = value;4 this.prev = null;5 this.next = null;6 }7}8
9class DoublyLinkedList {10 constructor() {11 this.head = null;12 this.tail = null;13 }14
15 // Tail pointer makes append O(1)16 append(value) {17 const node = new Node(value);18 if (!this.tail) {19 this.head = node;20 this.tail = node;21 return;22 }23 node.prev = this.tail;24 this.tail.next = node;25 this.tail = node;26 }27
28 forward() {29 const out = [];30 for (let n = this.head; n; n = n.next) out.push(n.value);31 return out;32 }33
34 backward() {35 const out = [];36 for (let n = this.tail; n; n = n.prev) out.push(n.value);37 return out;38 }39}40
41const list = new DoublyLinkedList();42for (const value of [10, 20, 30, 40]) list.append(value);43console.log("Forward: ", list.forward().join(" <-> "));44console.log("Backward:", list.backward().join(" <-> "));קוד Doubly Linked List ב-Java
1public class Main {2 static class Node {3 int value;4 Node prev, next;5 Node(int value) { this.value = value; }6 }7
8 static Node head, tail;9
10 static void append(int value) {11 Node node = new Node(value);12 if (head == null) { head = tail = node; return; }13 node.prev = tail;14 tail.next = node;15 tail = node;16 }17
18 // Unlink in O(1) once found: fix both neighbor pointers19 static void delete(int value) {20 for (Node cur = head; cur != null; cur = cur.next) {21 if (cur.value != value) continue;22 if (cur.prev != null) cur.prev.next = cur.next; else head = cur.next;23 if (cur.next != null) cur.next.prev = cur.prev; else tail = cur.prev;24 return;25 }26 }27
28 public static void main(String[] args) {29 append(1);30 append(2);31 append(3);32 append(4);33
34 StringBuilder forward = new StringBuilder("Forward:");35 for (Node cur = head; cur != null; cur = cur.next) forward.append(" ").append(cur.value);36 System.out.println(forward);37
38 StringBuilder backward = new StringBuilder("Backward:");39 for (Node cur = tail; cur != null; cur = cur.prev) backward.append(" ").append(cur.value);40 System.out.println(backward);41
42 delete(3);43 StringBuilder after = new StringBuilder("After delete 3:");44 for (Node cur = head; cur != null; cur = cur.next) after.append(" ").append(cur.value);45 System.out.println(after);46 }47}קוד Doubly Linked List ב-C++
1#include <iostream>2
3struct Node {4 int value;5 Node* prev = nullptr;6 Node* next = nullptr;7 explicit Node(int v) : value(v) {}8};9
10struct DoublyLinkedList {11 Node* head = nullptr;12 Node* tail = nullptr;13
14 void append(int value) {15 Node* node = new Node(value);16 if (tail == nullptr) {17 head = tail = node;18 return;19 }20 node->prev = tail; // link both directions21 tail->next = node;22 tail = node;23 }24
25 void prepend(int value) {26 Node* node = new Node(value);27 if (head == nullptr) {28 head = tail = node;29 return;30 }31 node->next = head;32 head->prev = node;33 head = node;34 }35
36 void printForward() const {37 for (Node* cur = head; cur != nullptr; cur = cur->next) {38 std::cout << cur->value << " <-> ";39 }40 std::cout << "null\n";41 }42
43 void printBackward() const {44 for (Node* cur = tail; cur != nullptr; cur = cur->prev) {45 std::cout << cur->value << " <-> ";46 }47 std::cout << "null\n";48 }49};50
51int main() {52 DoublyLinkedList list;53 for (int value : {10, 20, 30}) list.append(value);54 list.prepend(5);55 std::cout << "Forward: ";56 list.printForward();57 std::cout << "Backward: ";58 list.printBackward();59 return 0;60}קוד Doubly Linked List ב-C
1#include <stdio.h>2#include <stdlib.h>3
4typedef struct Node {5 int value;6 struct Node* prev;7 struct Node* next;8} Node;9
10Node* head = NULL;11Node* tail = NULL;12
13Node* newNode(int value) {14 Node* n = calloc(1, sizeof(Node));15 n->value = value;16 return n;17}18
19void append(int value) {20 Node* node = newNode(value);21 if (tail == NULL) {22 head = tail = node;23 return;24 }25 node->prev = tail; // link both directions26 tail->next = node;27 tail = node;28}29
30void prepend(int value) {31 Node* node = newNode(value);32 if (head == NULL) {33 head = tail = node;34 return;35 }36 node->next = head;37 head->prev = node;38 head = node;39}40
41void printForward(void) {42 for (Node* cur = head; cur != NULL; cur = cur->next) {43 printf("%d <-> ", cur->value);44 }45 printf("NULL\n");46}47
48void printBackward(void) {49 for (Node* cur = tail; cur != NULL; cur = cur->prev) {50 printf("%d <-> ", cur->value);51 }52 printf("NULL\n");53}54
55int main(void) {56 int values[] = {10, 20, 30};57 for (int i = 0; i < 3; i++) append(values[i]);58 prepend(5);59 printf("Forward: ");60 printForward();61 printf("Backward: ");62 printBackward();63 return 0;64}שאלות נפוצות על רשימה מקושרת דו כיוונית
מה ההבדל בין רשימה מקושרת חד כיוונית לדו כיוונית?
next), ולכן אפשר לזוז רק קדימה. רשימה מקושרת דו כיוונית מוסיפה מצביע prev, שמאפשר לעבור אחורה ולמחוק צומת שיש לכם הפניה אליו ב-O(1), במחיר של מצביע נוסף לכל צומת ועדכון של שני קישורים בכל שינוי.מתי רשימה מקושרת דו כיוונית שווה את הזיכרון הנוסף?
O(1) של צמתים כלשהם שכבר יש אליהם הפניה. המקרים הקלאסיים הם deque (תורים דו צדדיים) ומטמוני LRU, שבהם רשומות מוזזות ומוצאות משני הקצוות לעתים קרובות.למה רשימה מקושרת דו כיוונית יכולה למחוק צומת ב-O(1)?
O(n)); ברשימה מקושרת דו כיוונית מצביע ה-prev של הצומת נותן לכם את הקודם ישירות, ולכן החיבור מחדש הוא O(1).רשימה מקושרת דו כיוונית או מערך: במה להשתמש?
O(1) לפי אינדקס ומעבר ידידותי למטמון, וכשההכנסות קורות בעיקר בסוף. השתמשו ברשימה מקושרת דו כיוונית כשמכניסים או מסירים לעתים קרובות באמצע או בשני הקצוות בהינתן הפניה לצומת, כי זה O(1) לעומת הזזה של O(n) במערך. מערכים מנצחים בזיכרון ובמקומיות; הרשימה מנצחת בשינויים מבניים.מהו צומת זקיף ברשימה מקושרת דו כיוונית?
null עבור head, tail והכנסות ומחיקות בגבולות, ומאפשר לכל פעולה ללכת באותו מסלול קוד של חיבור מחדש. מימושים רבים של מטמון LRU בסביבת ייצור משתמשים בשני זקיפים כדי לא לטפל בקצוות כמקרה מיוחד.מהו הבאג הנפוץ ביותר במחיקה מרשימה מקושרת דו כיוונית?
head או את tail כשמוחקים את הצומת הראשון או האחרון, מה שמשאיר מצביע תלוי לצומת ששוחרר. בדקו תמיד אם node.prev הוא null (עדכנו את head) או אם node.next הוא null (עדכנו את tail) לפני שמחברים מחדש את השכנים.