Linked list đơn, đôi và so sánh với mảng
Hiểu nút, con trỏ next và prev, phân biệt linked list đơn với đôi, và biết khi nào danh sách liên kết thắng mảng.
DễCấu trúc dữ liệu ~45 phút
Ôn lại trước khi học
Giới thiệu
Bài này học nút, con trỏ và hai loại linked list. Bạn biết khi nào dùng list thay mảng.
Kiến thức cần nhớ
Nút và list đơn
- Nút gồm dữ liệu và con trỏ next.
- Next trỏ tới nút kế tiếp trong chuỗi.
- Head là nút đầu, cửa vào duy nhất.
- Ví dụ đời thường: đoàn tàu, mỗi toa nối toa sau.
- Đọc phần tử thứ k phải duyệt từ head.
List đôi và list vòng
- List đôi thêm con trỏ prev trỏ ngược.
- Prev giúp duyệt hai chiều, xóa nhanh hơn.
- List vòng cho nút cuối trỏ về head.
- Ví dụ đời thường: lịch sử trình duyệt đi tới lui.
- Giá phải trả là tốn thêm bộ nhớ.
So sánh với mảng
- Mảng truy xuất thứ k trong .
- List chèn xóa đầu trong .
- Mảng thân thiện cache hơn list.
- Ví dụ đời thường: mảng như kệ sách đánh số.
| Thao tác | Mảng | List đơn |
|---|---|---|
| Đọc thứ k | ||
| Chèn xóa đầu | ||
| Chèn cuối | nếu thiếu tail | |
| Bộ nhớ thêm | Thấp | 1 con trỏ mỗi nút |
Ví dụ
- Input: A -> B -> C, chèn X sau A. Output: A -> X -> B -> C.
- Giải thích: chỉ nối lại 2 con trỏ nên không mất nút nào.
# Chèn sau nút đã biết
new_node.next = cur.next # nút mới trỏ tới nút kế
cur.next = new_node # nút hiện tại trỏ sang nút mới
💻 Bài tập
Ngôn ngữ: C (hoặc C++ không dùng std::list). Nộp vào day-002/.
Bài 1: Singly linked list
typedef struct Node { int val; struct Node *next; } Node;
typedef struct { Node *head; Node *tail; size_t size; } SList;
| Hàm | Việc phải làm | Độ phức tạp |
|---|---|---|
void sl_init(SList *l) | List rỗng: head = tail = NULL, size = 0 | |
bool sl_push_front(SList *l, int x) | Thêm đầu | |
bool sl_push_back(SList *l, int x) | Thêm cuối nhờ tail | |
bool sl_pop_front(SList *l, int *out) | Gỡ đầu. Rỗng trả false | |
bool sl_insert_after(SList *l, Node *n, int x) | Chèn sau nút n | |
bool sl_remove(SList *l, int x) | Xóa nút đầu tiên có giá trị x | |
Node *sl_find(const SList *l, int x) | Trả nút đầu tiên có giá trị x, không thấy trả NULL | |
bool sl_get(const SList *l, size_t k, int *out) | Đọc phần tử thứ k | |
void sl_reverse(SList *l) | Đảo ngược tại chỗ, không cấp nút mới | |
void sl_clear(SList *l) | Giải phóng mọi nút |
Ràng buộc
tailluôn đúng sau mọi thao tác, kể cả khi list còn 0 hoặc 1 nút.- Viết thêm hàm
void sl_print(const SList *l)in dạng[1 -> 2 -> 3]để debug.
Test mẫu
push_back 1, 2, 3 -> [1 -> 2 -> 3], size=3
insert_after(find(1), 9) -> [1 -> 9 -> 2 -> 3]
remove(3) -> [1 -> 9 -> 2], tail->val = 2
reverse -> [2 -> 9 -> 1], tail->val = 1
pop_front x3, pop_front lần 4 -> lần 4 trả false, head = tail = NULL
push_back 5 sau khi rỗng -> [5], head == tail
Bài 2: Doubly linked list và lịch sử trình duyệt
Viết DList với nút có prev và next. Cài thêm dl_remove_node(DList *l, DNode *n) chạy .
Dùng DList làm lịch sử trình duyệt:
| Hàm | Hành vi |
|---|---|
visit(url) | Xóa toàn bộ trang phía sau trang hiện tại, thêm url, chuyển tới url |
back(steps) | Lùi tối đa steps bước, trả URL hiện tại |
forward(steps) | Tiến tối đa steps bước, trả URL hiện tại |
visit a, b, c -> hiện tại c
back(2) -> a
forward(1) -> b
visit d -> d (trang c bị xóa)
forward(1) -> d (không còn trang sau)
back(10) -> a
Bài 3: Đo mảng và list (nâng cao)
Dùng Dynamic Array ngày 1 và SList Bài 1. Đo thời gian với :
push_backphần tử.- Chèn đầu phần tử.
- Duyệt tính tổng phần tử.
In bảng thời gian (ms). Viết 3 câu giải thích vì sao duyệt mảng nhanh hơn duyệt list dù cùng .
Dùng
clock_gettime(CLOCK_MONOTONIC, ...) để đo. Chạy mỗi phép đo 5 lần, lấy trung vị.✅ Tiêu chí hoàn thành
- Mọi test mẫu pass bằng
assert. - AddressSanitizer và valgrind sạch: không leak, không use-after-free.
- Bảng đo Bài 3 có số thật kèm giải thích về cache.
- Vẽ tay được các con trỏ đổi thế nào khi
insert_aftervàreverse.
❓ Câu hỏi ôn tập
- Nút gồm những gì, head là gì?
- List đơn khác list đôi ở điểm nào?
- Chèn đầu list tốn bao nhiêu bước?
- Khi nào nên dùng mảng thay list?
🌕 Bạn đã hoàn thành ngày 2. Làm hết bài tập phía trên rồi sang ngày tiếp theo.