Hiểu về con trỏ trong C/C++ phần 2
2018-08-18Content:
Trích đoạn câu trả lời của Linus Torvalds trên Slashdot Interview:
I’ve seen too many people who delete a singly-linked list entry by keeping track of the “prev” entry, and then to delete the entry, doing something like:
if (prev) prev->next = entry->next; else head = entry->next;
and whenever I see code like that, I just go “This person doesn’t understand pointers”. And it’s sadly quite common. People who understand pointers just use a “pointer to the entry pointer”, and initialize that with the address of the list_head. And then as they traverse the list, they can remove the entry without using any conditionals, by just doing:
*pp = entry->next;
Trong bài này chúng ta sẽ tìm hiểu solution mà Linus đã đề xuất. Trước tiên hãy cùng code hàm xoá một node trong một danh sách liên kết (dslk) đơn mà đa số mọi người thường viết như sau:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Delete `target` node from a singly linked list whose head is `head`.
void deleteNode(ListNode*& head, ListNode* target) {
ListNode* entry = head;
ListNode* prev = NULL;
while(entry) {
if (entry == target) {
if (prev) prev->next = entry->next;
else head = entry->next;
break;
}
prev = entry;
entry = entry->next;
}
delete target;
}
Giải thích một chút về việc truyền head
bằng tham chiếu: nếu tham số head
không được truyền bằng tham chiếu, thì chúng ta sẽ chỉ xoá được những nodes không phải là head
(vì câu lệnh entry = entry->next
lần đầu tiên - tức là entry = head->next
giúp entry duyệt trên chính các con trỏ được cấp phát trên HEAP), chứ không xử lý được trường hợp nodes được xoá là head
, vì lúc này con trỏ head
ở trong hàm và ở ngoài hàm là hoàn toàn khác nhau, trường hợp target = head
, câu lệnh head = target->next
sẽ được chạy. Câu lệnh này thứ nhất, nó chỉ làm thay đổi con trỏ head
ở trong hàm mà thôi (con trỏ head
ngoài hàm không ảnh hưởng gì); thứ 2, nó chỉ thay đổi hướng trỏ của con trỏ head
này sang bộ nhớ tại target->next
, chứ không thực sự thay đổi ở con trỏ nằm trên bộ nhớ HEAP mà con trỏ head
ban đầu trỏ tới. Nói tóm lại, ta truyền tham số head
bằng tham chiếu để nếu head
bên trong hàm thay đổi thì head
ngoài hàm cũng thay đổi.
Cách code trên không tối ưu ở chỗ nếu muốn xoá một node, chúng ta cần biết node trước đó của nó, riêng head
thì không có node trước đó nên trường hợp đặc biệt này phải luôn được xét. Linus Torvalds muốn tránh viết thêm code cho trường hợp đặc biệt này: dùng con trỏ cấp 2 để xác định node liền trước của mọi node, kể cả head
. Để hiểu được cách làm này, trước hết cần phải hiểu rằng giả sử dslk có n node, thì n node này sẽ nằm ở HEAP (tại n vùng nhớ); mỗi node này đếu chứa con trỏ next
trỏ đến vùng nhớ của node tiếp theo. Vậy con trỏ nào trỏ đến node đầu tiên? Đó là head
! Và nó nằm trên STACK:
Đầu tiên, khởi tạo con trỏ entry
trỏ đến node đầu tiên của danh sách:
1
ListNode* entry = head;
Tương tự như thuật toán đầu tiên, entry
sẽ duyệt qua toàn bộ node của dslk, và ta cần biết node liền trước của node mà entry
đang trỏ đến tại mọi thời điểm. Khi entry
trỏ đến node đầu tiên, ta có thể xem con trỏ head
đang nằm trên STACK cũng là một node của dslk và lúc này nó chính là node liền trước của node mà entry
đang trỏ đến. Một cách tự nhiên, ta cần một con trỏ trỏ đến con trỏ head
này - một con trỏ cấp 2 (pointer to pointer):
1
ListNode** pp = &head;
Minh hoạ bộ nhớ tại thời điểm hiện tại:
Lúc này, chúng ta sẽ không còn lo đến trường hợp node cần xoá là head
nữa, vì đã luôn luôn xác định được node liền trước cuả mọi node, phần code còn lại hoàn toàn tương tự như thuật toán đầu tiên, code đầy đủ của thuật toán mới:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Delete `target` node from a singly linked list whose head is `head`.
void deleteNode(ListNode*& head, ListNode* target) {
ListNode** pp = &head;
ListNode* entry = head;
while(entry) {
if (entry == target) {
*pp = entry->next;
break;
}
pp = &(entry->next);
entry = entry->next;
}
delete target;
}
Và thậm chí có thể không sử dụng entry
:
1
2
3
4
5
6
7
8
9
// Delete `target` node from a singly linked list whose head is `head`.
void deleteNode(ListNode*& head, ListNode* target) {
ListNode** pp = &head;
while(*pp != target) {
pp = &((*pp)->next);
}
*pp = target->next;
delete target;
}
Happy coding,