Xóa phần tử
Sau bài này bạn sẽ làm được
- Cài xóa đầu trong O(1) và xóa cuối trong O(n)
- Dùng con trỏ tới con trỏ để bỏ hẳn trường hợp đặc biệt ở đầu danh sách
- Giữ tail đúng sau mọi phép xóa
- Tránh dùng con trỏ sau khi đã giải phóng
Xóa khó hơn thêm, vì bạn phải tìm được nút đứng trước nút cần xóa, và vì nút đầu danh sách không có ai đứng trước nó. Mẹo con trỏ tới con trỏ trong bài này xóa hẳn trường hợp đặc biệt đó.
#Xóa đầu, O(1)
/* Xóa nút đầu. Nếu out khác NULL thì chép giá trị ra đó.
Trả về 0 nếu xóa được, -1 nếu danh sách rỗng. */
int list_xoa_dau(List *l, int *out)
{
if (l->head == NULL) return -1;
Node *n = l->head;
if (out != NULL) *out = n->data;
l->head = n->next; /* đầu mới là nút thứ hai */
if (l->head == NULL) /* vừa xóa nút cuối cùng */
l->tail = NULL;
free(n);
--l->size;
return 0;
}#Xóa cuối, O(n) dù có tail
Đây là chỗ danh sách đơn lộ ra giới hạn của nó. Bạn cầm sẵn tail, nhưng để xóa nó thì phải cho nút áp chót trỏ vào NULL, mà từ tail không có cách nào đi ngược lại. Phải duyệt từ đầu.
/* Xóa nút cuối. O(n) vì phải tìm nút áp chót. */
int list_xoa_cuoi(List *l, int *out)
{
if (l->head == NULL) return -1;
if (out != NULL) *out = l->tail->data;
if (l->head == l->tail) { /* chỉ có một nút */
free(l->head);
l->head = NULL;
l->tail = NULL;
l->size = 0;
return 0;
}
Node *ap_chot = l->head;
while (ap_chot->next != l->tail)
ap_chot = ap_chot->next;
free(l->tail);
ap_chot->next = NULL;
l->tail = ap_chot;
--l->size;
return 0;
}| Thao tác | Danh sách đơn | Danh sách đôi (Bài 21.7) |
|---|---|---|
| Thêm đầu | O(1) | O(1) |
| Thêm cuối | O(1) nhờ tail | O(1) |
| Xóa đầu | O(1) | O(1) |
| Xóa cuối | O(n) | O(1) |
| Xóa nút đã có con trỏ | O(n) | O(1) |
Hai ô O(n) trong cột giữa chính là lý do danh sách đôi tồn tại. Nếu chương trình của bạn xóa ở cuối nhiều, hoặc xóa nút đã cầm sẵn con trỏ nhiều, thì tám byte thêm cho mỗi nút là cái giá rất đáng.
#Xóa theo giá trị, cách thông thường
/* Xóa nút ĐẦU TIÊN có data bằng gia_tri.
Bản thông thường: phải theo dõi cả nút hiện tại lẫn nút trước nó. */
int list_xoa_gia_tri_thuong(List *l, int gia_tri)
{
Node *truoc = NULL;
Node *hien = l->head;
while (hien != NULL) {
if (hien->data == gia_tri) {
if (truoc == NULL) l->head = hien->next; /* xóa nút đầu */
else truoc->next = hien->next; /* xóa nút giữa */
if (l->tail == hien) l->tail = truoc; /* xóa nút cuối */
free(hien);
--l->size;
return 0;
}
truoc = hien;
hien = hien->next;
}
return -1; /* không tìm thấy */
}#Mẹo con trỏ tới con trỏ
l->head với nút đầu tiên, và là truoc->next với mọi nút còn lại. Nhờ vậy hai trường hợp gộp thành một./* Cùng chức năng, không còn trường hợp đặc biệt cho nút đầu. */
int list_xoa_gia_tri(List *l, int gia_tri)
{
Node **pp = &l->head; /* địa chỉ của ô đang trỏ tới nút hiện tại */
while (*pp != NULL) {
Node *n = *pp;
if (n->data == gia_tri) {
*pp = n->next; /* ghi thẳng vào ô đó */
if (l->tail == n) /* vừa xóa nút cuối */
l->tail = (*pp == NULL)
? ((pp == &l->head) ? NULL : /* danh sách thành rỗng */
(Node *)((char *)pp - offsetof(Node, next)))
: l->tail;
free(n);
--l->size;
return 0;
}
pp = &n->next; /* tiến sang ô next của nút hiện tại */
}
return -1;
}/* Không có tail thì mẹo này thật sự đẹp: bảy dòng, không trường hợp riêng. */
int xoa_gia_tri(Node **head, int gia_tri)
{
for (Node **pp = head; *pp != NULL; pp = &(*pp)->next) {
Node *n = *pp;
if (n->data == gia_tri) {
*pp = n->next;
free(n);
return 0;
}
}
return -1;
}pp bắt đầu ở &head
Ô nhớ đầu tiên đang giữ địa chỉ một nút chính là biến
head. Nó không nằm trong nút nào cả, nhưng vai trò thì giống hệt trườngnext.Mỗi bước, pp nhảy sang &(*pp)->next
Tức là địa chỉ trường
nextcủa nút vừa xét. Đó lại là ô đang giữ địa chỉ nút kế tiếp.Xóa là ghi vào *pp
Không cần biết ô đó nằm ở đâu. Nếu nó là
headthìheadđược sửa, nếu nó làtruoc->nextthìtruoc->nextđược sửa. Một dòng lo cả hai.
| Bản thường | Bản dùng Node **pp | |
|---|---|---|
| Số dòng thân hàm | 18 | 10 |
| Số nhánh if trong vòng lặp | 3 | 1 |
| Xử lý riêng nút đầu | Có | Không |
| Dễ đọc với người mới | Có | Cần làm quen |
| Hợp khi List có tail | Có | Không, phải bù thêm |
#Xóa mọi phần tử khớp
Đây là chỗ mẹo trên tỏa sáng: xóa nhiều nút trong một lượt mà không rối. Điểm cần chú ý là không tiến pp khi vừa xóa, vì ô đó giờ đang giữ nút kế tiếp và phải xét lại.
for (Node **pp = &head; *pp; pp = &(*pp)->next) {
if ((*pp)->data == v) {
Node *n = *pp;
*pp = n->next;
free(n);
/* vòng for vẫn tiến pp, nên bỏ sót nút vừa nhảy vào vị trí này */
}
}size_t xoa_tat_ca(Node **head, int v)
{
size_t dem = 0;
Node **pp = head;
while (*pp != NULL) {
Node *n = *pp;
if (n->data == v) {
*pp = n->next; /* KHÔNG tiến pp, xét lại chính ô này */
free(n);
++dem;
} else {
pp = &n->next; /* chỉ tiến khi không xóa */
}
}
return dem;
}truoc: [1, 7, 7, 2, 7, 3, 7, 7] da xoa 5 phan tu sau : [1, 2, 3]
truoc: [1, 7, 7, 2, 7, 3, 7, 7] da xoa 3 phan tu sau : [1, 7, 2, 3, 7]
#Sau khi free thì con trỏ chết
Node *n = *pp;
free(n);
*pp = n->next; /* đọc vùng đã giải phóng */Node *n = *pp;
*pp = n->next; /* đọc xong rồi mới free */
free(n);Bản sai thường vẫn chạy đúng, vì bộ cấp phát chưa kịp ghi đè vùng đó. Bài 14.9 đã chỉ ra glibc ghi con trỏ liên kết vào 16 byte đầu của khối vừa giải phóng, mà next nằm ở byte thứ 8, nên nó sẽ bị phá. Chỉ là không phải lần nào cũng phá ngay.
Chương trình thử đầy đủ
#include <stdio.h>
#include "list.h"
int main(void)
{
List l;
int v;
list_khoi_tao(&l);
for (int i = 1; i <= 5; ++i)
if (list_them_cuoi(&l, i * 10) != 0) { list_huy(&l); return 1; }
list_in(&l);
list_xoa_dau(&l, &v); printf("xoa dau -> %d ", v); list_in(&l);
list_xoa_cuoi(&l, &v); printf("xoa cuoi -> %d ", v); list_in(&l);
printf("xoa 30 -> %d ", list_xoa_gia_tri(&l, 30)); list_in(&l);
printf("xoa 99 -> %d ", list_xoa_gia_tri(&l, 99)); list_in(&l);
while (list_xoa_dau(&l, NULL) == 0) { }
list_in(&l);
printf("xoa tren danh sach rong: %d\n", list_xoa_dau(&l, NULL));
list_huy(&l);
return 0;
}[10, 20, 30, 40, 50] (size = 5) xoa dau -> 10 [20, 30, 40, 50] (size = 4) xoa cuoi -> 50 [20, 30, 40] (size = 3) xoa 30 -> 0 [20, 40] (size = 2) xoa 99 -> -1 [20, 40] (size = 2) [] (size = 0) xoa tren danh sach rong: -1
All heap blocks were freed -- no leaks are possible ERROR SUMMARY: 0 errors from 0 contexts
Tự làm thử
- Cài đủ bốn hàm xóa, gọi
list_kiem_trasau mỗi lần và chạy dưới valgrind cho tới khi không còn rò rỉ. - Viết lại
list_xoa_gia_tritheo cả hai cách, rồi so số dòng và số nhánh của hai bản. - Bỏ dòng cập nhật
tailtronglist_xoa_dau, xóa hết phần tử rồi thêm cuối một phần tử. Chạy dưới-fsanitize=addressvà đọc báo cáo. - Cài
xoa_tat_catheo mẫuNode **pp, thử với danh sách toàn phần tử giống nhau và với danh sách rỗng. - Cài
list_loc(List *l, int (*giu)(int))giữ lại những phần tử mà hàmgiutrả về khác 0, dùng mẫuNode **pp.
Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.
Tóm tắt
- Xóa đầu là O(1), xóa cuối trong danh sách đơn là O(n) vì phải tìm nút áp chót.
- Mọi hàm xóa phải đặt
tailvề NULL khi danh sách trở thành rỗng, nếu khôngtailthành con trỏ treo. - Mẹo
Node **ppgiữ địa chỉ của liên kết chứ không phải của nút, nhờ vậy nút đầu không còn là trường hợp riêng. - Khi xóa nhiều nút trong một lượt, chỉ tiến
ppở nhánh không xóa. - Luôn đọc
n->nextxong rồi mớifree(n). Đảo lại thường vẫn chạy, nhưng đó là hành vi không xác định.