Linked List (רשימה מקושרת)
עודכן לאחרונה
רשימה מקושרת שומרת רצף כשרשרת של צמתים, שבה כל צומת מחזיק ערך ומצביע לצומת הבא. בניגוד למערך, הצמתים לא רציפים בזיכרון: הולכים לאורך הרשימה בעזרת מצביעי ה-next. לחצו על הפעלה למעלה כדי לראות צמתים מקושרים בראש ובזנב, ערך שמחפשים על ידי הליכה לאורך השרשרת, וצומת שמוסר על ידי חיבור מחדש של מצביע.
הכנסה או מחיקה בראש הן O(1), כי רק מפנים את הראש מחדש. הגעה למיקום באמצע או לזנב היא O(n), כי צריך ללכת לשם קודם. הפשרה הזו, קצוות זולים בלי גישה אקראית, היא מה שמבדיל רשימה מקושרת ממערך.
סיבוכיות זמן
| פעולה | סיבוכיות | הערות |
|---|---|---|
| הכנסה בראש | O(1) | מפנים את הראש מחדש |
| הכנסה בזנב | O(n) | קודם הולכים עד הסוף (O(1) עם מצביע זנב) |
| חיפוש | O(n) | הולכים לפי מצביעי next |
| מחיקת הראש | O(1) | מפנים את הראש אל מעבר לו |
| גישה לפי אינדקס | O(n) | אין גישה אקראית |
רשימה מקושרת מול מערך
| היבט | רשימה מקושרת | מערך |
|---|---|---|
| זיכרון | צמתים מפוזרים ומצביעים | בלוק רציף |
| גישה אקראית | O(n) | O(1) |
| הכנסה או מחיקה בהתחלה | O(1) | O(n) (הזזה) |
| ידידותיות למטמון | חלשה | טובה |
דוגמה מפורטת
בניית הרשימה [10, 20], הוספת 5 בהתחלה, ואז מחיקת 20:
| צעד | מבנה | פעולה |
|---|---|---|
| התחלה | head -> null | רשימה ריקה |
| הכנסה לראש 10 | head -> 10 -> null | מפנים את הראש לצומת חדש שה-next שלו הוא הראש הישן (null) |
| הכנסה לזנב 20 | head -> 10 -> 20 -> null | הולכים לצומת 10 וקובעים את ה-next שלו לצומת חדש 20 |
| הכנסה לראש 5 | head -> 5 -> 10 -> 20 -> null | הצומת החדש 5 מצביע לראש הישן 10; הראש מצביע עכשיו ל-5 |
| מחיקת 20 | head -> 5 -> 10 -> null | הולכים ל-10 ומשנים את ה-next שלו מ-20 ל-null; הצומת 20 מנותק |
מתי להשתמש ברשימה מקושרת
| השתמשו בה כאשר | הימנעו ממנה כאשר |
|---|---|
| אתם מכניסים או מוחקים בקצוות (או בצומת שכבר בידיכם) הרבה יותר משאתם ניגשים לפי אינדקס | אתם צריכים גישה אקראית מהירה לפי מיקום (אינדקס של O(1)) |
| הגודל משתנה הרבה ואתם לא רוצים עלות של שינוי גודל או העתקה | אתם מריצים לולאות צפופות שבהן מקומיות המטמון קובעת את הביצועים |
| אתם בונים תור, מחסנית או רשימת סמיכות | הזיכרון מוגבל: כל צומת משלם מצביע נוסף לכל איבר |
| אתם צריכים הפניות יציבות לצמתים ששורדות הכנסות במקומות אחרים | אתם בעיקר קוראים ולעתים רחוקות משנים, ולכן מערך פשוט ומהיר יותר |
קוד Linked List
מימוש נקי של Linked List שאפשר להריץ, ב-Python, JavaScript, Java, C++, C. בחרו שפה, העתיקו את הקוד, או פתחו אותו טעון מראש בעורך האונליין של Coddy.
קוד Linked List ב-Python
1class Node:2 def __init__(self, value):3 self.value = value4 self.next = None5
6
7class LinkedList:8 def __init__(self):9 self.head = None10
11 def append(self, value):12 node = Node(value)13 if self.head is None:14 self.head = node15 return16 current = self.head17 while current.next:18 current = current.next19 current.next = node20
21 def find(self, value):22 current = self.head23 while current:24 if current.value == value:25 return True26 current = current.next27 return False28
29 def delete(self, value):30 # Re-link the previous node around the match31 current, prev = self.head, None32 while current:33 if current.value == value:34 if prev is None:35 self.head = current.next36 else:37 prev.next = current.next38 return True39 prev, current = current, current.next40 return False41
42 def __str__(self):43 values, current = [], self.head44 while current:45 values.append(str(current.value))46 current = current.next47 return " -> ".join(values) + " -> None"48
49
50lst = LinkedList()51for value in [3, 7, 1, 9]:52 lst.append(value)53
54print("List: ", lst)55print("find(7): ", lst.find(7))56lst.delete(1)57print("After delete(1):", lst)קוד Linked List ב-JavaScript
1class Node {2 constructor(value) {3 this.value = value;4 this.next = null;5 }6}7
8class LinkedList {9 constructor() {10 this.head = null;11 }12
13 append(value) {14 const node = new Node(value);15 if (!this.head) {16 this.head = node;17 return;18 }19 let current = this.head;20 while (current.next) current = current.next;21 current.next = node;22 }23
24 find(value) {25 for (let n = this.head; n; n = n.next) {26 if (n.value === value) return n;27 }28 return null;29 }30
31 delete(value) {32 if (!this.head) return false;33 if (this.head.value === value) {34 this.head = this.head.next;35 return true;36 }37 // Walk to the node just before the one to remove38 for (let n = this.head; n.next; n = n.next) {39 if (n.next.value === value) {40 n.next = n.next.next;41 return true;42 }43 }44 return false;45 }46
47 toArray() {48 const out = [];49 for (let n = this.head; n; n = n.next) out.push(n.value);50 return out;51 }52}53
54const list = new LinkedList();55for (const value of [10, 20, 30, 40]) list.append(value);56console.log("List:", list.toArray().join(" -> "));57console.log("find(30):", list.find(30) !== null);58list.delete(20);59console.log("After delete(20):", list.toArray().join(" -> "));קוד Linked List ב-Java
1public class Main {2 static class Node {3 int value;4 Node next;5 Node(int value) { this.value = value; }6 }7
8 static Node head;9
10 static void append(int value) {11 Node node = new Node(value);12 if (head == null) { head = node; return; }13 Node cur = head;14 while (cur.next != null) cur = cur.next;15 cur.next = node;16 }17
18 static boolean find(int value) {19 for (Node cur = head; cur != null; cur = cur.next) {20 if (cur.value == value) return true;21 }22 return false;23 }24
25 // Unlink the first node holding value26 static void delete(int value) {27 if (head == null) return;28 if (head.value == value) { head = head.next; return; }29 Node cur = head;30 while (cur.next != null && cur.next.value != value) cur = cur.next;31 if (cur.next != null) cur.next = cur.next.next;32 }33
34 static void print() {35 StringBuilder sb = new StringBuilder();36 for (Node cur = head; cur != null; cur = cur.next) {37 sb.append(cur.value).append(" -> ");38 }39 System.out.println(sb.append("null"));40 }41
42 public static void main(String[] args) {43 append(3); append(7); append(1); append(9);44 print();45 System.out.println("find 7: " + find(7));46 delete(7);47 print();48 System.out.println("find 7: " + find(7));49 }50}קוד Linked List ב-C++
1#include <iostream>2
3struct Node {4 int value;5 Node* next = nullptr;6 explicit Node(int v) : value(v) {}7};8
9struct LinkedList {10 Node* head = nullptr;11
12 void append(int value) {13 Node* node = new Node(value);14 if (head == nullptr) {15 head = node;16 return;17 }18 Node* cur = head;19 while (cur->next != nullptr) cur = cur->next;20 cur->next = node;21 }22
23 bool find(int value) const {24 for (Node* cur = head; cur != nullptr; cur = cur->next) {25 if (cur->value == value) return true;26 }27 return false;28 }29
30 void remove(int value) {31 if (head == nullptr) return;32 if (head->value == value) { // removing the head is a special case33 Node* old = head;34 head = head->next;35 delete old;36 return;37 }38 for (Node* cur = head; cur->next != nullptr; cur = cur->next) {39 if (cur->next->value == value) {40 Node* old = cur->next;41 cur->next = old->next;42 delete old;43 return;44 }45 }46 }47
48 void print() const {49 for (Node* cur = head; cur != nullptr; cur = cur->next) {50 std::cout << cur->value << " -> ";51 }52 std::cout << "null\n";53 }54};55
56int main() {57 LinkedList list;58 for (int value : {10, 20, 30, 40}) list.append(value);59 list.print();60 std::cout << std::boolalpha << "find(30): " << list.find(30) << "\n";61 list.remove(20);62 list.remove(10);63 list.print();64 return 0;65}קוד Linked List ב-C
1#include <stdbool.h>2#include <stdio.h>3#include <stdlib.h>4
5typedef struct Node {6 int value;7 struct Node* next;8} Node;9
10Node* head = NULL;11
12void append(int value) {13 Node* node = malloc(sizeof(Node));14 node->value = value;15 node->next = NULL;16 if (head == NULL) {17 head = node;18 return;19 }20 Node* cur = head;21 while (cur->next != NULL) cur = cur->next;22 cur->next = node;23}24
25bool find(int value) {26 for (Node* cur = head; cur != NULL; cur = cur->next) {27 if (cur->value == value) return true;28 }29 return false;30}31
32void deleteValue(int value) {33 if (head == NULL) return;34 if (head->value == value) { // removing the head is a special case35 Node* old = head;36 head = head->next;37 free(old);38 return;39 }40 for (Node* cur = head; cur->next != NULL; cur = cur->next) {41 if (cur->next->value == value) {42 Node* old = cur->next;43 cur->next = old->next;44 free(old);45 return;46 }47 }48}49
50void printList(void) {51 for (Node* cur = head; cur != NULL; cur = cur->next) {52 printf("%d -> ", cur->value);53 }54 printf("NULL\n");55}56
57int main(void) {58 int values[] = {10, 20, 30, 40};59 for (int i = 0; i < 4; i++) append(values[i]);60 printList();61 printf("find(30): %s\n", find(30) ? "true" : "false");62 deleteValue(20);63 deleteValue(10);64 printList();65 return 0;66}שאלות נפוצות על רשימה מקושרת
מה ההבדל בין רשימה מקושרת למערך?
O(1) אבל הכנסות ומחיקות של O(n) שמזיזות איברים. רשימה מקושרת שומרת צמתים בכל מקום בזיכרון, מחוברים במצביעים, מה שנותן הכנסות ומחיקות של O(1) במיקום ידוע אבל גישה של O(n), כי צריך ללכת לאורך השרשרת.מתי כדאי להשתמש ברשימה מקושרת?
מהי סיבוכיות הזמן של רשימה מקושרת?
O(1). חיפוש, או הגעה לזנב בלי מצביע זנב, הם O(n), כי הולכים לפי מצביעי next אחד אחד. אין גישה אקראית של O(1): גישה לפי אינדקס ברשימה מקושרת היא O(n).מה ההבדל בין רשימה מקושרת חד כיוונית לדו כיוונית?
next לכל צומת, כך שאפשר ללכת בכיוון אחד, ומחיקת צומת דורשת הפניה לצומת שלפניו. רשימה מקושרת דו כיוונית מוסיפה מצביע prev, שמאפשר מעבר לאחור ומחיקה של O(1) של צומת שבידיכם, במחיר של מצביע נוסף לכל צומת ויותר ניהול בכל הכנסה ומחיקה.למה הכנסה בזנב היא O(n) אם הכנסה בראש היא O(1)?
head, וזו עבודה קבועה. הגעה לזנב פירושה הליכה לפי מצביעי next מהראש עד הסוף, וזה O(n). שמירת מצביע tail נפרד הופכת גם את ההכנסה בזנב ל-O(1), ולכן רשימות בעולם האמיתי עוקבות לעתים קרובות אחרי שני הקצוות.האם לרשימות מקושרות יש הכנסה של O(1) בכל מקום?
O(1) רק כשכבר יש בידיכם מצביע לצומת שאחריו מכניסים. מציאת המיקום הזה לפי ערך או אינדקס עדיין עולה O(n), כי צריך ללכת לאורך השרשרת כדי להגיע אליו.