Danh sách liên kết
Cập nhật lần cuối
Danh sách liên kết (linked list) lưu một dãy giá trị dưới dạng chuỗi các nút, trong đó mỗi nút chứa một giá trị và một con trỏ trỏ đến nút tiếp theo. Khác với mảng, các nút không nằm liền nhau trong bộ nhớ: bạn đi theo các con trỏ next để duyệt danh sách. Nhấn Phát ở trên để xem các nút được nối vào đầu và cuối, một giá trị được tìm bằng cách đi dọc chuỗi, và một nút bị gỡ ra bằng cách nối lại một con trỏ.
Chèn hoặc xóa ở đầu danh sách tốn O(1) vì bạn chỉ cần trỏ lại head. Đi đến một vị trí ở giữa hoặc ở cuối tốn O(n) vì bạn phải đi đến đó trước. Sự đánh đổi này, hai đầu rẻ nhưng không truy cập ngẫu nhiên được, chính là điểm phân biệt danh sách liên kết với mảng.
Độ phức tạp thời gian
| Thao tác | Độ phức tạp | Ghi chú |
|---|---|---|
| Chèn vào đầu | O(1) | Trỏ lại head |
| Chèn vào cuối | O(n) | Phải đi đến cuối trước (O(1) nếu có con trỏ tail) |
| Tìm kiếm | O(n) | Đi theo các con trỏ next |
| Xóa đầu | O(1) | Trỏ head qua nút đó |
| Truy cập theo chỉ số | O(n) | Không truy cập ngẫu nhiên được |
Danh sách liên kết và mảng
| Khía cạnh | Danh sách liên kết | Mảng |
|---|---|---|
| Bộ nhớ | Các nút rải rác cùng con trỏ | Một khối liền nhau |
| Truy cập ngẫu nhiên | O(n) | O(1) |
| Chèn/xóa ở đầu | O(1) | O(n) (phải dời) |
| Thân thiện với cache | Kém | Tốt |
Ví dụ minh họa
Xây dựng danh sách [10, 20], thêm 5 vào đầu, rồi xóa 20:
| Bước | Cấu trúc | Thao tác |
|---|---|---|
| Bắt đầu | head -> null | Danh sách rỗng |
| Chèn 10 vào đầu | head -> 10 -> null | Trỏ head đến một nút mới có next là head cũ (null) |
| Chèn 20 vào cuối | head -> 10 -> 20 -> null | Đi đến nút 10, đặt next của nó là một nút mới 20 |
| Chèn 5 vào đầu | head -> 5 -> 10 -> 20 -> null | Nút mới 5 trỏ đến head cũ 10; head giờ trỏ đến 5 |
| Xóa 20 | head -> 5 -> 10 -> null | Đi đến 10, đổi next của nó từ 20 thành null; nút 20 bị tách khỏi danh sách |
Khi nào nên dùng danh sách liên kết
| Nên dùng khi | Tránh dùng khi |
|---|---|
| Bạn chèn hoặc xóa ở hai đầu (hoặc tại một nút đang giữ) nhiều hơn hẳn việc truy cập theo chỉ số | Bạn cần truy cập ngẫu nhiên nhanh theo vị trí (truy cập chỉ số O(1)) |
| Kích thước thay đổi nhiều và bạn không muốn tốn chi phí đổi kích thước hay sao chép | Bạn chạy các vòng lặp chặt, nơi tính cục bộ của cache quyết định hiệu năng |
| Bạn đang xây dựng hàng đợi, ngăn xếp hoặc danh sách kề | Bộ nhớ hạn hẹp: mỗi phần tử tốn thêm một con trỏ |
| Bạn cần tham chiếu ổn định đến các nút, không bị ảnh hưởng khi chèn ở chỗ khác | Bạn chủ yếu đọc và hiếm khi thay đổi, nên mảng đơn giản và nhanh hơn |
Code Linked List
Một cách cài đặt Linked List gọn gàng, chạy được ngay bằng Python, JavaScript, Java, C++, C. Chọn một ngôn ngữ, sao chép code hoặc mở sẵn trong Coddy Playground.
Code Linked List bằng 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)Code Linked List bằng 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(" -> "));Code Linked List bằng 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}Code Linked List bằng 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}Code Linked List bằng 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}Câu hỏi thường gặp về danh sách liên kết
Danh sách liên kết khác mảng như thế nào?
O(1) nhưng chèn/xóa tốn O(n) vì phải dời phần tử. Danh sách liên kết lưu các nút ở bất kỳ đâu trong bộ nhớ, nối với nhau bằng con trỏ, cho phép chèn/xóa O(1) tại vị trí đã biết nhưng truy cập tốn O(n) vì phải đi dọc chuỗi.Khi nào nên dùng danh sách liên kết?
Danh sách liên kết phát huy thế mạnh khi bạn thường xuyên chèn hoặc xóa ở hai đầu (hoặc tại một nút đã có tham chiếu) và hiếm khi cần truy cập ngẫu nhiên, ví dụ hàng đợi, ngăn xếp, danh sách kề và LRU cache. Nếu cần truy cập nhanh theo chỉ số, mảng hoặc mảng động thường tốt hơn.
Độ phức tạp thời gian của danh sách liên kết là bao nhiêu?
O(1). Tìm kiếm, hoặc đi đến cuối khi không có con trỏ tail, tốn O(n) vì bạn đi theo từng con trỏ next một. Không có truy cập ngẫu nhiên O(1): truy cập theo chỉ số trong danh sách liên kết tốn O(n).Danh sách liên kết đơn và danh sách liên kết đôi khác nhau như thế nào?
next cho mỗi nút, nên bạn chỉ đi được một chiều và muốn xóa một nút thì cần tham chiếu đến nút đứng trước nó. Danh sách liên kết đôi thêm con trỏ prev, cho phép duyệt ngược và xóa một nút đang giữ trong O(1), đổi lại mỗi nút tốn thêm một con trỏ và mỗi lần chèn, xóa phải cập nhật nhiều hơn.Tại sao chèn vào cuối tốn O(n) trong khi chèn vào đầu chỉ tốn O(1)?
head, một công việc hằng số. Đi đến cuối nghĩa là phải đi theo các con trỏ next từ head đến tận cuối, tốn O(n). Giữ thêm một con trỏ tail riêng giúp chèn vào cuối cũng chỉ tốn O(1), đó là lý do các danh sách trong thực tế thường theo dõi cả hai đầu.Danh sách liên kết có chèn O(1) ở mọi vị trí không?
O(1) khi bạn đã có sẵn con trỏ đến nút đứng trước vị trí chèn. Tìm vị trí đó theo giá trị hoặc chỉ số vẫn tốn O(n) vì bạn phải đi dọc chuỗi để đến đó.