Trong lập trình C, Danh Sách Liên Kết Đơn (Singly Linked List) là một cấu trúc dữ liệu cơ bản và quan trọng, cho phép lưu trữ dữ liệu linh hoạt với số lượng phần tử không cố định. Bài viết này sẽ giúp bạn hiểu rõ hơn về danh sách liên kết đơn, cách cài đặt, và các ứng dụng thực tế. Đây là một chủ đề rất phù hợp cho những người muốn nắm vững các kỹ thuật quản lý dữ liệu hiệu quả và tối ưu hóa mã nguồn.

1. Giới thiệu về Danh Sách Liên Kết Đơn trong C

Danh sách liên kết đơn là một cấu trúc dữ liệu dạng tuyến tính, bao gồm các phần tử được gọi là node. Mỗi node trong danh sách chứa hai phần chính:

  1. Dữ liệu (Data): Lưu trữ giá trị của node.
  2. Con trỏ (Pointer): Trỏ đến node kế tiếp trong danh sách.

Danh sách liên kết đơn rất hữu ích khi bạn cần quản lý bộ nhớ một cách linh hoạt, vì kích thước của danh sách không cố định như mảng, và bạn có thể dễ dàng thêm hoặc xóa các phần tử trong danh sách.

2. Cấu trúc của Danh Sách Liên Kết Đơn

Trong C, để tạo một danh sách liên kết đơn, bạn sẽ cần khai báo một cấu trúc Node đại diện cho mỗi node trong danh sách. Cấu trúc này bao gồm hai thành phần:

  • Dữ liệu: Là giá trị của node.
  • Con trỏ: Trỏ đến node tiếp theo.

Khai báo cấu trúc Node:

#include <stdio.h>
#include <stdlib.h>

// Cấu trúc của một node trong danh sách liên kết đơn
struct Node {
    int data;              // Giá trị của node
    struct Node* next;     // Con trỏ trỏ đến node tiếp theo
};

Ở đây, data là giá trị mà node lưu trữ, và next là con trỏ trỏ đến node tiếp theo trong danh sách. Nếu next bằng NULL, điều đó có nghĩa là node này là node cuối cùng trong danh sách.

3. Các thao tác cơ bản với Danh Sách Liên Kết Đơn

1. Khởi tạo danh sách

Để bắt đầu làm việc với danh sách liên kết, đầu tiên bạn cần khởi tạo một danh sách trống. Ban đầu, con trỏ head sẽ được gán giá trị NULL vì danh sách chưa có node nào.

struct Node* head = NULL; // Khởi tạo danh sách trống

2. Thêm một node vào đầu danh sách

Để thêm một node vào đầu danh sách, bạn cần tạo một node mới, đặt giá trị và con trỏ của node này trỏ đến node hiện tại của head, sau đó cập nhật head để trỏ đến node mới này.

void insertAtBeginning(int value) {
    struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
    newNode->data = value;
    newNode->next = head;
    head = newNode;
}

3. Thêm một node vào cuối danh sách

Để thêm một node vào cuối danh sách, bạn cần duyệt qua danh sách để tìm node cuối cùng, sau đó thêm node mới vào sau node cuối cùng này.

void insertAtEnd(int value) {
    struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
    newNode->data = value;
    newNode->next = NULL;

    if (head == NULL) {
        head = newNode;
    } else {
        struct Node* temp = head;
        while (temp->next != NULL) {
            temp = temp->next;
        }
        temp->next = newNode;
    }
}

4. Xóa một node

Để xóa một node trong danh sách, bạn cần tìm node muốn xóa và điều chỉnh các con trỏ sao cho không còn tham chiếu đến node đó. Ví dụ, dưới đây là hàm xóa một node với giá trị key:

void deleteNode(int key) {
    struct Node* temp = head;
    struct Node* prev = NULL;

    if (temp != NULL && temp->data == key) {
        head = temp->next;
        free(temp);
        return;
    }

    while (temp != NULL && temp->data != key) {
        prev = temp;
        temp = temp->next;
    }

    if (temp == NULL) return;

    prev->next = temp->next;
    free(temp);
}

5. Duyệt danh sách và in các phần tử

Để duyệt danh sách, bạn bắt đầu từ head và lần lượt truy cập từng node cho đến khi gặp NULL (cuối danh sách):

void printList() {
    struct Node* temp = head;
    while (temp != NULL) {
        printf("%d -> ", temp->data);
        temp = temp->next;
    }
    printf("NULL\n");
}

4. Ưu và Nhược Điểm của Danh Sách Liên Kết Đơn

Ưu điểm

  • Kích thước động: Khác với mảng, danh sách liên kết không có kích thước cố định. Bạn có thể thêm hoặc xóa phần tử dễ dàng mà không cần cấp phát lại bộ nhớ.
  • Dễ dàng thao tác: Việc thêm hoặc xóa một phần tử không đòi hỏi dịch chuyển các phần tử khác như trong mảng.

Nhược điểm

  • Truy cập tuần tự: Để truy cập một phần tử, bạn phải duyệt từ đầu danh sách, khiến thời gian truy cập dài hơn so với mảng.
  • Sử dụng nhiều bộ nhớ: Mỗi node cần thêm một con trỏ để trỏ đến node tiếp theo, tăng sử dụng bộ nhớ.

5. Các Ứng Dụng Thực Tế của Danh Sách Liên Kết Đơn

1. Quản lý bộ nhớ động

Danh sách liên kết đơn rất phù hợp cho các ứng dụng cần quản lý bộ nhớ động hoặc khi cần lưu trữ lượng dữ liệu biến động. Ví dụ: quản lý các bộ đệm hoặc các tác vụ trong lập trình hệ thống.

2. Tổ chức dữ liệu dạng hàng đợi (Queue) hoặc ngăn xếp (Stack)

Danh sách liên kết đơn có thể được sử dụng để cài đặt các cấu trúc dữ liệu phổ biến như hàng đợi và ngăn xếp, giúp tối ưu hóa thao tác thêm và xóa dữ liệu.

3. Sử dụng trong các hệ điều hành và trình biên dịch

Trong các hệ điều hành và trình biên dịch, danh sách liên kết đơn thường được sử dụng để quản lý các bảng tiến trình, bảng trang, hoặc quản lý bộ nhớ.

6. So sánh Danh Sách Liên Kết Đơn với Danh Sách Liên Kết Đôi

Danh sách liên kết đơn chỉ có con trỏ trỏ đến node tiếp theo, trong khi danh sách liên kết đôi có hai con trỏ, một trỏ đến node tiếp theo và một trỏ đến node trước. Điều này giúp dễ dàng duyệt ngược và thao tác linh hoạt hơn, nhưng tiêu tốn nhiều bộ nhớ hơn so với danh sách liên kết đơn.

Tiêu chí Danh sách liên kết đơn Danh sách liên kết đôi
Bộ nhớ Ít hơn Nhiều hơn
Duyệt ngược Không
Thao tác chèn/xóa ở giữa Chậm hơn Nhanh hơn do có con trỏ ngược

7. Kết luận

Danh sách liên kết đơn là một cấu trúc dữ liệu quan trọng trong lập trình C, giúp bạn quản lý dữ liệu động một cách linh hoạt và hiệu quả. Dù có những nhược điểm như khó truy cập ngẫu nhiên, danh sách liên kết đơn vẫn là giải pháp lý tưởng cho các ứng dụng cần tối ưu hóa thao tác thêm và xóa phần tử, đặc biệt trong các bài toán quản lý bộ nhớ và hệ thống.