1. Giới thiệu
Ngôn ngữ C là một ngôn ngữ lập trình được sử dụng rộng rãi trong lập trình hệ thống và phát triển hệ thống nhúng. Trong đó, “cấu trúc danh sách” là một công cụ rất hữu ích để quản lý và thao tác dữ liệu. Bài viết này sẽ giải thích chi tiết về cấu trúc danh sách trong C, từ khái niệm cơ bản đến các ví dụ triển khai cụ thể.
Tầm quan trọng của cấu trúc danh sách
Cấu trúc danh sách là một cấu trúc dữ liệu giúp quản lý dữ liệu theo thứ tự. Đặc biệt hữu ích trong các tình huống sau:
- Quản lý dữ liệu có thứ tự
- Khi cần thêm hoặc xóa dữ liệu động
- Sử dụng bộ nhớ hiệu quả
Ví dụ, trong ứng dụng quản lý công việc, việc thêm hoặc xóa tác vụ diễn ra thường xuyên, vì vậy cấu trúc danh sách linh hoạt sẽ phù hợp hơn mảng.
Mục tiêu của bài viết
Bài viết này sẽ hướng dẫn cách triển khai cấu trúc danh sách trong C, kèm theo các ví dụ mã nguồn cụ thể và cách thao tác với nó. Cuối cùng, người đọc sẽ hiểu được khái niệm cơ bản và cách áp dụng thực tế của cấu trúc danh sách để sử dụng trong chương trình của mình.
Mục tiêu học tập
- Hiểu các kiến thức cơ bản về cấu trúc danh sách.
- Học cách triển khai danh sách liên kết trong C.
- Nâng cao khả năng áp dụng thông qua các ví dụ mã nguồn thực tiễn.

2. Cấu trúc danh sách là gì
Cấu trúc danh sách là một trong những cấu trúc dữ liệu giúp quản lý dữ liệu theo thứ tự. Trong phần này, chúng ta sẽ tìm hiểu về khái niệm cơ bản và các loại của cấu trúc danh sách.
Khái niệm cơ bản về cấu trúc danh sách
Cấu trúc danh sách bao gồm các phần tử dữ liệu (nút) được kết nối với nhau theo dạng chuỗi. Mỗi nút có hai thành phần chính:
- Phần dữ liệu: Lưu trữ giá trị thực tế.
- Phần liên kết: Lưu con trỏ trỏ tới nút tiếp theo.
Nhờ cơ chế này, chúng ta có thể thêm hoặc xóa dữ liệu một cách động, rất tiện lợi khi làm việc với dữ liệu có độ dài thay đổi.
Sự khác nhau giữa mảng và danh sách
Mảng và cấu trúc danh sách khác nhau ở các điểm sau:
Hạng mục | Mảng | Cấu trúc danh sách |
---|---|---|
Kích thước | Cố định (xác định khi khai báo) | Thay đổi linh hoạt (có thể thay đổi kích thước động) |
Truy cập dữ liệu | Truy cập trực tiếp qua chỉ số | Phải duyệt tuần tự từ đầu danh sách |
Quản lý bộ nhớ | Sử dụng vùng nhớ liên tục | Sử dụng vùng nhớ rời rạc |
Thêm / Xóa dữ liệu | Tốn chi phí cao (cần dịch chuyển các phần tử) | Chi phí thấp (chỉ thay đổi con trỏ) |
Vì vậy, cấu trúc danh sách phù hợp khi cần tính linh hoạt, trong khi mảng phù hợp khi cần truy cập dữ liệu nhanh.
Tổng quan và đặc điểm của danh sách liên kết
Ví dụ điển hình của cấu trúc danh sách là “danh sách liên kết”. Danh sách liên kết có các đặc điểm sau:
- Kích thước thay đổi linh hoạt
Có thể thêm hoặc xóa nút khi cần, giúp quản lý bộ nhớ hiệu quả. - Dễ dàng chèn / xóa nút
Chỉ cần thay đổi con trỏ, có thể chèn hoặc xóa ở giữa hay cuối danh sách. - Tìm kiếm chậm hơn
Phải duyệt tuần tự từ đầu danh sách, mất nhiều thời gian hơn so với mảng.
Các loại danh sách liên kết
Danh sách liên kết được chia thành 3 loại chính:
- Danh sách liên kết đơn
Mỗi nút chỉ trỏ tới nút kế tiếp.
- Sử dụng ít bộ nhớ và cấu trúc đơn giản.
- Chỉ có thể duyệt theo một hướng (không thể quay lại nút trước).
- Danh sách liên kết đôi
Mỗi nút trỏ tới cả nút trước và nút sau.
- Có thể duyệt cả hai chiều, linh hoạt hơn.
- Tốn nhiều bộ nhớ hơn danh sách liên kết đơn.
- Danh sách vòng
Nút cuối trỏ ngược về nút đầu.
- Có thể lặp lại liên tục, phù hợp cho xử lý vòng lặp.
- Tiện lợi cho các ứng dụng đặc biệt.
Phần tiếp theo sẽ giải thích chi tiết “Cách triển khai danh sách liên kết trong C” với các ví dụ mã nguồn cụ thể.
3. Cách triển khai danh sách liên kết trong C
Phần này sẽ hướng dẫn chi tiết cách triển khai danh sách liên kết bằng ngôn ngữ C, kèm theo ví dụ mã nguồn cụ thể.
Cấu trúc cơ bản của danh sách liên kết
Một danh sách liên kết bao gồm các thành phần sau:
- Định nghĩa nút
- Mỗi nút chứa dữ liệu và con trỏ trỏ đến nút kế tiếp.
- Con trỏ đầu (Head Pointer)
- Trỏ đến phần tử đầu tiên của danh sách, dùng để quản lý toàn bộ danh sách.
Ví dụ mã: Triển khai cơ bản danh sách liên kết
Dưới đây là ví dụ mã nguồn cho các thao tác cơ bản trên danh sách liên kết:
#include <stdio.h>
#include <stdlib.h>
// Định nghĩa nút
typedef struct Node {
int data; // Phần dữ liệu
struct Node* next; // Con trỏ trỏ đến nút tiếp theo
} Node;
// Tạo nút mới
Node* createNode(int data) {
Node* newNode = (Node*)malloc(sizeof(Node)); // Cấp phát bộ nhớ
if (newNode == NULL) {
printf("Không thể cấp phát bộ nhớ.\n");
exit(1);
}
newNode->data = data;
newNode->next = NULL; // Ban đầu nút mới không trỏ tới đâu
return newNode;
}
// Thêm nút vào đầu danh sách
void insertAtHead(Node** head, int data) {
Node* newNode = createNode(data); // Tạo nút mới
newNode->next = *head; // Nút mới trỏ đến nút hiện tại ở đầu
*head = newNode; // Cập nhật con trỏ đầu
}
// Hiển thị danh sách
void printList(Node* head) {
Node* temp = head;
while (temp != NULL) {
printf("%d -> ", temp->data);
temp = temp->next;
}
printf("NULL\n");
}
// Giải phóng bộ nhớ
void freeList(Node* head) {
Node* temp;
while (head != NULL) {
temp = head;
head = head->next;
free(temp); // Giải phóng bộ nhớ
}
}
int main() {
Node* head = NULL; // Khởi tạo danh sách rỗng
// Thêm dữ liệu vào danh sách
insertAtHead(&head, 3);
insertAtHead(&head, 2);
insertAtHead(&head, 1);
// Hiển thị danh sách
printList(head);
// Giải phóng bộ nhớ
freeList(head);
return 0;
}
Giải thích mã nguồn
- Tạo nút
- Sử dụng cấp phát bộ nhớ động để tạo nút mới.
- Chèn nút
- Chèn dữ liệu vào đầu hoặc cuối danh sách để mở rộng.
- Xóa nút
- Xóa nút đầu tiên hoặc nút có giá trị cụ thể.
- Quản lý bộ nhớ
- Giải phóng bộ nhớ đã sử dụng để tránh rò rỉ bộ nhớ.

4. Các loại danh sách liên kết
Phần này sẽ giới thiệu các loại danh sách liên kết phổ biến cùng đặc điểm, ưu điểm và nhược điểm của từng loại. Qua đó, bạn sẽ biết cách lựa chọn loại phù hợp tùy nhu cầu.
1. Danh sách liên kết đơn
Danh sách liên kết đơn là cấu trúc trong đó mỗi nút chỉ trỏ tới nút tiếp theo.
Đặc điểm:
- Các phần tử chỉ di chuyển được theo một hướng.
- Tốn ít bộ nhớ, dễ triển khai.
Ưu điểm:
- Kích thước linh hoạt nhờ cấp phát bộ nhớ động.
- Thêm hoặc xóa phần tử hiệu quả.
Nhược điểm:
- Không thể quay lại nút trước, việc tìm kiếm ngược không hiệu quả.
2. Danh sách liên kết đôi
Danh sách liên kết đôi là cấu trúc trong đó mỗi nút trỏ tới cả nút trước và nút sau.
Đặc điểm:
- Mỗi nút có hai con trỏ: “liên kết tới trước” và “liên kết tới sau”.
- Di chuyển được theo cả hai hướng.
Ưu điểm:
- Dễ dàng tìm kiếm hoặc xóa dữ liệu theo cả hai hướng.
- Thuận tiện cho các thao tác đảo ngược danh sách.
Nhược điểm:
- Tốn nhiều bộ nhớ hơn do cần lưu thêm một con trỏ.
- Các thao tác với nút phức tạp hơn.
3. Danh sách vòng
Danh sách vòng là cấu trúc trong đó nút cuối cùng trỏ ngược về nút đầu tiên.
Đặc điểm:
- Không có nút kết thúc, di chuyển liên tục theo vòng tròn.
- Có thể triển khai dưới dạng liên kết đơn hoặc liên kết đôi.
Ưu điểm:
- Thuận tiện cho các thao tác lặp, dễ di chuyển giữa nút đầu và nút cuối.
- Thích hợp cho hàng đợi vòng hoặc xử lý bộ đệm.
Nhược điểm:
- Không có điểm kết thúc nên cần chú ý khi duyệt hoặc xóa dữ liệu.
Bảng so sánh các loại danh sách
Loại | Đặc điểm | Ưu điểm | Nhược điểm |
---|---|---|---|
Danh sách liên kết đơn | Chỉ tiến được một hướng | Dễ triển khai, tốn ít bộ nhớ | Không thể di chuyển ngược |
Danh sách liên kết đôi | Di chuyển được hai hướng | Linh hoạt, tìm kiếm/xóa dễ dàng | Tốn bộ nhớ, thao tác phức tạp |
Danh sách vòng | Nút cuối trỏ về nút đầu | Thích hợp cho xử lý vòng lặp | Khó quản lý điểm kết thúc |
Tiếp theo, chúng ta sẽ tìm hiểu về “Các thao tác trên danh sách liên kết” với các ví dụ mã nguồn chi tiết.
5. Các thao tác trên danh sách liên kết
Phần này sẽ giải thích chi tiết các thao tác cơ bản trên danh sách liên kết, bao gồm chèn phần tử, xóa phần tử và tìm kiếm. Các ví dụ mã nguồn sẽ giúp bạn dễ dàng áp dụng vào thực tế.
1. Chèn phần tử
Có ba cách phổ biến để chèn một phần tử vào danh sách liên kết:
(1) Chèn vào đầu danh sách
Thêm một nút mới vào vị trí đầu tiên.
void insertAtHead(Node** head, int data) {
Node* newNode = (Node*)malloc(sizeof(Node));
newNode->data = data;
newNode->next = *head; // Nút mới trỏ tới nút đầu hiện tại
*head = newNode; // Cập nhật nút đầu
}
(2) Chèn vào cuối danh sách
Thêm một nút mới vào vị trí cuối cùng.
void insertAtTail(Node** head, int data) {
Node* newNode = (Node*)malloc(sizeof(Node));
newNode->data = data;
newNode->next = NULL;
if (*head == NULL) { // Danh sách rỗng
*head = newNode;
return;
}
Node* temp = *head;
while (temp->next != NULL) { // Di chuyển đến nút cuối
temp = temp->next;
}
temp->next = newNode; // Gắn nút mới vào cuối
}
2. Xóa phần tử
Có thể xóa phần tử ở đầu hoặc phần tử có giá trị cụ thể.
(1) Xóa phần tử đầu tiên
void deleteAtHead(Node** head) {
if (*head == NULL) {
printf("Danh sách rỗng.\n");
return;
}
Node* temp = *head;
*head = (*head)->next; // Cập nhật nút đầu
free(temp); // Giải phóng bộ nhớ
}
(2) Xóa phần tử theo giá trị
void deleteNode(Node** head, int key) {
Node* temp = *head;
Node* prev = NULL;
// Nếu nút đầu chứa giá trị cần xóa
if (temp != NULL && temp->data == key) {
*head = temp->next;
free(temp);
return;
}
// Tìm nút chứa giá trị cần xóa
while (temp != NULL && temp->data != key) {
prev = temp;
temp = temp->next;
}
if (temp == NULL) {
printf("Không tìm thấy giá trị.\n");
return;
}
prev->next = temp->next;
free(temp);
}
3. Tìm kiếm phần tử
Duyệt qua danh sách để tìm phần tử có giá trị cụ thể.
int search(Node* head, int key) {
Node* temp = head;
int position = 0;
while (temp != NULL) {
if (temp->data == key) {
return position; // Trả về vị trí tìm thấy
}
temp = temp->next;
position++;
}
return -1; // Không tìm thấy
}
4. Hiển thị danh sách
In ra tất cả các phần tử trong danh sách.
void printList(Node* head) {
Node* temp = head;
while (temp != NULL) {
printf("%d -> ", temp->data);
temp = temp->next;
}
printf("NULL\n");
}
Tóm tắt
Trong phần này, chúng ta đã triển khai các thao tác cơ bản trên danh sách liên kết:
- Chèn phần tử vào đầu, cuối hoặc vị trí bất kỳ.
- Xóa phần tử ở đầu hoặc theo giá trị.
- Tìm kiếm phần tử trong danh sách và trả về vị trí.

6. Quản lý bộ nhớ trong danh sách liên kết
Phần này sẽ trình bày các lưu ý quan trọng về quản lý bộ nhớ khi triển khai danh sách liên kết. Cụ thể, chúng ta sẽ tìm hiểu về cấp phát bộ nhớ động và phòng tránh rò rỉ bộ nhớ để viết chương trình an toàn và hiệu quả.
1. Cấp phát bộ nhớ động là gì?
Trong C, khi tạo một nút mới cho danh sách liên kết, chúng ta sử dụng cấp phát bộ nhớ động. Điều này cho phép cấp phát vùng nhớ khi chương trình đang chạy, giúp quản lý dữ liệu linh hoạt.
Ví dụ mã: Cấp phát bộ nhớ động cho nút
Node* createNode(int data) {
Node* newNode = (Node*)malloc(sizeof(Node)); // Cấp phát bộ nhớ
if (newNode == NULL) { // Kiểm tra nếu cấp phát thất bại
printf("Không thể cấp phát bộ nhớ.\n");
exit(1); // Thoát chương trình
}
newNode->data = data;
newNode->next = NULL;
return newNode;
}
2. Tầm quan trọng của việc giải phóng bộ nhớ
Khi sử dụng danh sách liên kết, nếu không giải phóng vùng nhớ đã cấp phát cho các nút sau khi không còn sử dụng, sẽ dẫn đến rò rỉ bộ nhớ.
Ví dụ mã: Giải phóng bộ nhớ
void freeList(Node* head) {
Node* temp;
while (head != NULL) {
temp = head; // Lưu nút hiện tại
head = head->next; // Chuyển sang nút tiếp theo
free(temp); // Giải phóng bộ nhớ
}
}
3. Thực hành tốt để tránh rò rỉ bộ nhớ
- Luôn giải phóng bộ nhớ sau khi dùng xong
- Giải phóng vùng nhớ đã cấp phát ngay khi không còn sử dụng.
- Sử dụng công cụ hỗ trợ kiểm tra
- Công cụ như Valgrind hoặc AddressSanitizer giúp phát hiện vị trí rò rỉ bộ nhớ.
Ví dụ chạy Valgrind (Linux)
valgrind --leak-check=full ./a.out
- Quản lý con trỏ rõ ràng
- Xác định rõ quyền sở hữu vùng nhớ của từng con trỏ.
- Sau khi giải phóng, đặt con trỏ về
NULL
để tránh truy cập nhầm.
Tóm tắt
- Cấp phát bộ nhớ động giúp quản lý dữ liệu linh hoạt.
- Luôn giải phóng bộ nhớ sau khi sử dụng để tránh rò rỉ.
- Sử dụng công cụ kiểm tra để đảm bảo chương trình an toàn.
7. Ví dụ ứng dụng và triển khai thực tế
Phần này sẽ giới thiệu các ví dụ về cấu trúc dữ liệu được triển khai dựa trên danh sách liên kết. Đặc biệt, chúng ta sẽ tìm hiểu cách triển khai Stack (Ngăn xếp) và Queue (Hàng đợi) để thấy được tính linh hoạt và ứng dụng thực tiễn của danh sách liên kết.
1. Ví dụ triển khai Stack
Stack là cấu trúc dữ liệu hoạt động theo nguyên tắc “Vào sau ra trước” (LIFO). Phần tử được thêm vào sau cùng sẽ được lấy ra đầu tiên.
Mã ví dụ: Triển khai Stack bằng danh sách liên kết
#include <stdio.h>
#include <stdlib.h>
typedef struct Node {
int data;
struct Node* next;
} Node;
Node* createNode(int data) {
Node* newNode = (Node*)malloc(sizeof(Node));
newNode->data = data;
newNode->next = NULL;
return newNode;
}
void push(Node** top, int data) {
Node* newNode = createNode(data);
newNode->next = *top;
*top = newNode;
}
int pop(Node** top) {
if (*top == NULL) {
printf("Stack rỗng.\n");
exit(1);
}
Node* temp = *top;
int poppedData = temp->data;
*top = (*top)->next;
free(temp);
return poppedData;
}
void printStack(Node* top) {
while (top != NULL) {
printf("%d -> ", top->data);
top = top->next;
}
printf("NULL\n");
}
int main() {
Node* stack = NULL;
push(&stack, 10);
push(&stack, 20);
push(&stack, 30);
printf("Nội dung Stack: ");
printStack(stack);
printf("Giá trị lấy ra: %d\n", pop(&stack));
printf("Giá trị lấy ra: %d\n", pop(&stack));
printf("Nội dung Stack: ");
printStack(stack);
return 0;
}
2. Ví dụ triển khai Queue
Queue là cấu trúc dữ liệu hoạt động theo nguyên tắc “Vào trước ra trước” (FIFO). Phần tử được thêm vào trước sẽ được lấy ra trước.
Mã ví dụ: Triển khai Queue bằng danh sách liên kết
#include <stdio.h>
#include <stdlib.h>
typedef struct Node {
int data;
struct Node* next;
} Node;
typedef struct Queue {
Node* front;
Node* rear;
} Queue;
Queue* createQueue() {
Queue* q = (Queue*)malloc(sizeof(Queue));
q->front = q->rear = NULL;
return q;
}
void enqueue(Queue* q, int data) {
Node* newNode = (Node*)malloc(sizeof(Node));
newNode->data = data;
newNode->next = NULL;
if (q->rear == NULL) {
q->front = q->rear = newNode;
return;
}
q->rear->next = newNode;
q->rear = newNode;
}
int dequeue(Queue* q) {
if (q->front == NULL) {
printf("Queue rỗng.\n");
exit(1);
}
Node* temp = q->front;
int data = temp->data;
q->front = q->front->next;
if (q->front == NULL) {
q->rear = NULL;
}
free(temp);
return data;
}
void printQueue(Queue* q) {
Node* temp = q->front;
while (temp != NULL) {
printf("%d -> ", temp->data);
temp = temp->next;
}
printf("NULL\n");
}
int main() {
Queue* q = createQueue();
enqueue(q, 1);
enqueue(q, 2);
enqueue(q, 3);
printf("Nội dung Queue: ");
printQueue(q);
printf("Giá trị lấy ra: %d\n", dequeue(q));
printf("Giá trị lấy ra: %d\n", dequeue(q));
printf("Nội dung Queue: ");
printQueue(q);
return 0;
}
Tóm tắt
- Stack sử dụng nguyên tắc LIFO, phù hợp cho xử lý đệ quy hoặc đánh giá biểu thức.
- Queue sử dụng nguyên tắc FIFO, thích hợp cho xử lý hàng đợi, bộ đệm.

8. Tổng kết
Bài viết này đã trình bày chi tiết về danh sách liên kết trong ngôn ngữ C, từ khái niệm cơ bản đến các ví dụ triển khai cụ thể, cũng như những ứng dụng thực tế. Dưới đây là phần tóm tắt những nội dung quan trọng và gợi ý cho bước học tiếp theo.
1. Tóm tắt nội dung chính
Kiến thức cơ bản về danh sách liên kết
- Danh sách liên kết là cấu trúc dữ liệu có thể thay đổi kích thước động, rất linh hoạt.
- Khác với mảng, danh sách liên kết dễ dàng thêm hoặc xóa phần tử, nhưng việc tìm kiếm phải duyệt tuần tự.
Triển khai và thao tác cơ bản
- Giải thích cách tạo nút, chèn, xóa và tìm kiếm phần tử trong danh sách liên kết với ví dụ mã nguồn.
- Nhấn mạnh tầm quan trọng của quản lý bộ nhớ để viết chương trình an toàn và tối ưu.
Các loại và ứng dụng
- Danh sách liên kết đơn: đơn giản, tiết kiệm bộ nhớ, phù hợp cho thao tác một chiều.
- Danh sách liên kết đôi: linh hoạt hơn với khả năng di chuyển hai chiều.
- Danh sách vòng: thích hợp cho các tác vụ lặp liên tục.
Ví dụ ứng dụng
- Triển khai Stack (LIFO) và Queue (FIFO) bằng danh sách liên kết.
- Ứng dụng trong xử lý đệ quy, hàng đợi tác vụ, bộ đệm dữ liệu.
2. Kỹ năng đạt được
Thông qua bài viết, bạn đã nắm được:
- Hiểu biết về cấu trúc dữ liệu
- Kỹ năng lập trình C nâng cao
- Khả năng giải quyết vấn đề
3. Chủ đề nên học tiếp
- Ứng dụng cấu trúc dữ liệu
- Cấu trúc cây (Cây nhị phân, AVL Tree)
- Thuật toán đồ thị (Tìm kiếm theo chiều rộng BFS, tìm kiếm theo chiều sâu DFS)
- Quản lý dữ liệu nâng cao
- Bảng băm (Hash Table), Map
- Kết hợp mảng động và danh sách liên kết
- Tối ưu thuật toán
- Cải tiến thuật toán sắp xếp và tìm kiếm
- Phân tích độ phức tạp thời gian và bộ nhớ
4. Gợi ý dự án thực hành
- Ứng dụng quản lý công việc
- Hệ thống mô phỏng
- Công cụ phân tích dữ liệu
5. Lời kết
Danh sách liên kết là một cấu trúc dữ liệu cơ bản nhưng có phạm vi ứng dụng rất rộng, từ các bài tập thuật toán đến phát triển hệ thống phức tạp. Hy vọng qua bài viết này, bạn đã nắm vững kiến thức từ cơ bản đến nâng cao và có thể áp dụng vào các dự án thực tế.