Chương 3: NGĂN XẾP, HÀNG ĐỢI VÀ DANH SÁCH MÓC NỐI (STACK, QUEUE,
3.3. Danh sách liên kết đơn
3.3.1. Giới thiệu và định nghĩa
Một danh sách móc nối, hoặc ngắn gọn hơn, một danh sách, là một dãy có thứ tự các phần tử được gọi là đỉnh. Danh sách có điểm bắt đầu, gọi là tiêu đề hay đỉnh đầu, một điểm cuối cùng gọi là đỉnh cuối. Mọi đỉnh trong danh sách đều có cùng kiểu ngay cả khi kiểu này có nhiều dạng khác nhau.
Bản chất động là một trong những tính chất chính của danh sách móc nối. Có thể thêm hoặc bớt đỉnh trong danh sách vào mọi lúc, mọi vị trí. Vì số đỉnh của danh sách không thể dự kiến trước được, nên khi thực hiện, chúng ta phải dùng con trỏ mà không dùng được mảng để bảo đảm việc thực hiện hiệu quả và tin cậy.
Mỗi đỉnh trong danh sách đều gồm hai phần. Phần thứ nhất chứa dữ liệu. Dữ liệu có thể chỉ là một biến đơn hoặc là một cấu trúc (hoặc con trỏ cấu trúc) có kiểu nào đó. Phần thứ hai của đỉnh là một con trỏ chỉ vào địa chỉ của đỉnh tiếp theo trong danh sách. Vì vậy có thể dễ dàng sử dụng các đỉnh của danh sách qua một cấu trúc tự trỏ hoặc đệ qui.
Danh sách móc nối đơn giản dưới đây xây dựng mỗi đỉnh của danh sách chỉ lưu giữ một biến nguyên.
/*đỉnh của danh sách đơn chỉ chứa một số nguyên*/
struct don { int phantu;
struct don *tiep;
};
typedef struct don don_t;
Trong trường hợp này, biến nguyên phantu của từng đỉnh chứa dữ liệu còn biến con trỏ tiep chứa địa chỉ của đỉnh tiếp theo. Sơ đồ biểu diễn danh sách móc nối đơn được biểu diễn như hình dưới đây:
Phần_tử Phần_tử Phần_tử ....
Hình 3.4. Danh sách móc nối đơn
Tổng quát hơn, mỗi đỉnh của danh sách có thể chứa nhiều phần tử dữ liệu. Trong trường hợp này, hợp lý hơn cả là định nghĩa một kiểu cấu trúc tương ứng với dữ liệu cần lưu giữ tại mỗi đỉnh. Phương pháp này được sử dụng trong định nghĩa kiểu sau đây:
/*đỉnh của danh sách tổng quát */
struct tq {
thtin_t phantu;
struc tq*tiep;
};
typedef struct tq tq_t;
Kiểu cấu trúc thtin_t phải được định nghĩa trước đó để tương ứng với các dữ liệu sẽ được lưu trữ tại từng đỉnh. Danh sách được tạo nên từ kiểu đỉnh này giống như ở sơ đồ trong Hình 3.4, ngoại trừ việc mỗi phantu là một biến nguyên.
3.3.2. Các thao tác trên danh sách móc nối
Các thao tác trên danh sách móc nối bao gồm việc cấp phát bộ nhớ cho các đỉnh và gán dữ liệu cho con trỏ. Để danh sách được tạo nên đúng đắn, ta biểu diễn phần tử cuối danh sách là một con trỏ NULL. Con trỏ NULL là tín hiệu thông báo không còn phần tử nào tiếp theo trong danh sách nữa.
Tiện hơn cả là chúng ta định nghĩa một con trỏ tới danh sách như sau:
struct node { int infor;
struct node *next;
};
typedef struct node *NODEPTR; // Con trỏ tới node Cấp phát bộ nhớ cho một node:
NODEPTR Getnode(void) { NODEPTR p;
P = (NODEPTR) malloc(sizeof( struct node));
Return(p);
}
Giải phóng bộ nhớ của một node”
NODEPTR Freenode( NODEPTR p){
free(p);
}
Chèn một phần tử mới vào đầu danh sách:
Các bước để chèn một phần tử mới vào đầu danh sách cần thực hiện là:
9 Cấp không gian bộ nhớ đủ lưu giữ một đỉnh mới;
9 Gán các giá trị con trỏ thích hợp cho đỉnh mới;
9 Thiết lập liên kết với đỉnh mới.
Sơ đồ biểu diễn phép thêm một đỉnh mới vào đầu danh sách được thể hiện như trên hình 3.5.
Node cần chèn vào đầu danh sách móc nối.
Hình 3.5. Thêm đỉnh mới vào đầu danh sách móc nối đơn void Push_Top( NODEPTR *plist, int x) {
NODEPTR p;
p= Getnode(); // cấp không gian nhớ cho đỉnh mới p -> infor = x; // gán giá trị thích hợp cho đỉnh mới p ->next = *plist;
*plist = p; // thiết lập liên kết }
Thêm một phần tử mới vào cuối danh sách:
Để thêm một node vào cuối danh sách, ta cần thực hiện qua các bước sau:
9 Cấp phát bộ nhớ cho node mới;
9 Gán giá trị thích hợp cho node mới;
9 Di chuyển con trỏ tới phần tử cuối danh sách;
9 Thiết lập liên kết cho node mới.
Sơ đồ thể hiên phép thêm một phần tử mới vào cuối danh sách được thể hiện như trong hình 3.6
infor next infor next
infor next
infor next
infor next infor next
infor next
infor next NULL
void Push_Bottom( NODEPTR *plist, int x) { NODEPTR p, q;
p= Getnode(); // cấp phát bộ nhớ cho node mới p->infor = x; // gán giá trị thông tin thích hợp q = *plist; // chuyển con trỏ tới cuối danh sách while (q-> next != NULL)
q = q -> next;
// q là node cuối cùng của danh sách liên kết q -> next = p; //node cuối bây giờ là node p;
p ->next = NULL; // liên kết mới của p }
Thêm node mới q vào giữa danh sách trước node p:
Để thêm node q vào trước node p, chúng ta cần lưu ý node p phải có thực trong danh sách. Giả sử node p là có thực, khi đó xảy ra hai tình huống: hoặc node p là node cuối cùng của danh sách liên kết tức p->next =NULL, hoặc node p chưa phải là cuối cùng hay p->next
!= NULL. Trường hợp thứ nhất, chúng ta chỉ cần gọi tới thao tác Push_Bottom(). Trường hợp thứ 2, chúng ta thực hiện theo các bước như sau:
9 Cấp phát bộ nhớ cho node mới;
9 Gán giá trị thích hợp cho node;
9 Thiết lập liên kết node q với node kế tiếp p;
9 Thiết lập liên kết node node p với node q;
void Push_Before( NODEPTR p, int x ){
NODEPTR q;
if (p->next==NULL) Push_Bottom(p, x);
else {
q= Getnode(); // cấp phát bộ nhớ cho node mới q -> infor = x; // gán giá trị thông tin thích hợp
q-> next = p-> next; // thiết lập liên kết node q với node kế tiếp p;
p->next = q; // thiết lập liên kết node p với node kế tiếp q;
} }
Sơ đồ thêm node vào giữa danh sách được thể hiện như sau:
p
q
Hình 3.7. Phép thêm phần tử vào giữa danh sách liên kết đơn.
Xoá một node ra khỏi đầu danh sách:
Khi loại bỏ node khỏi đầu danh sách liên kết, chúng ta cần chú ý rằng nếu danh sách đang rỗng thì không thể thực hiện việc loại bỏ. Trong trường hợp còn lại, ta thực hiện như sau:
9 Dùng node p trỏ tới đầu danh sách;
9 Dịch chuyển vị trí đầu danh sách tới node tiếp theo;
9 Loại bỏ liên kết với p;
9 Giải phóng node p;
void Del_Top( NODEPTR *plist) { NODEPTR p;
p = *plist; // node p trỏ tới đầu danh sách;
if (p==NULL) return; // danh sách rỗng
(*plist) = (*plist) -> next; // dịch chuyển node gốc lên node kế tiếp p-> next = NULL; //loại bỏ liên kết với p
Freenode(p); // giải phóng p;
}
Loại bỏ node ở cuối danh sách:
Một node ở cuối danh sách có thể xảy ra ba tình huống sau:
9 Danh sách rỗng: ta không cần thực hiện loại bỏ;
9 Danh sách chỉ có đúng một node: ứng với trường hợp loại bỏ node gốc;
Trường hợp còn lại danh sách có nhiều hơn một node, khi đó ta phải dịch chuyển tới node gần node cuối cùng nhất để thực hiện loại bỏ.
void Del_Bottom(NODEPTR *plist) { NODEPTR p, q;
if (*plist==NULL) return; //không làm gì
else if ( (*plist)->next==NULL)) // danh sách có một node Del_Top(plist);
infor next
infor next infor next
infor next
NULL
p = *plist;
while (p->next!=NULL){
q = p;
p = p->next; // q là node sau node p;
}
// p là node cuối danh sách;
q->next =NULL; //node cuối cùng là q Freenode(p); //giải phóng p;
} }
Loại bỏ node ở giữa danh sách (trước node p):
Cần để ý rằng, nếu trước node p là NULL (p->next==NULL) thì ta không thực hiện loại bỏ được. Trường hợp còn lại chúng ta thực hiện như sau:
9 Dùng node q trỏ tới node trước node p;
9 Loại bỏ liên kết của q;
9 Giải phóng q.
void Del_before(NODEPTR p){
NODEPTR q;
if (p->next==NULL) return; // không làm gì q = p ->next;
p->next = q->next;
Freenode(q);
}
Bạn đọc có thể tìm thấy những cài đặt cụ thể của danh sách liên kết đơn trong các tài liệu [1], [2].