链式存储结构如何实现C语言线性表?

99ANYc3cd6
预计阅读时长 23 分钟
位置: 首页 C语言 正文

与顺序存储结构(如数组)不同,链式存储结构不要求逻辑上相邻的元素在物理位置上也相邻,它通过指针将一组零散的内存块链接起来,形成一个数据序列。

c语言线性表的链式存储结构
(图片来源网络,侵删)

链表的基本概念

链表由一系列节点组成,每个节点包含两部分:

  1. 数据域:存储数据元素本身的信息。
  2. 指针域:存储指向下一个节点的指针。

对于单向链表,最后一个节点的指针域指向 NULL,表示链表的结束。

示意图:

  Head Node 1       Node 2       Node 3       ...      Node N
  +--------+      +--------+      +--------+            +--------+
  |  Data  |   -> |  Data  |   -> |  Data  |    ... ->  |  Data  |
  +--------+      +--------+      +--------+            +--------+
  |  Next  |----->|  Next  |----->|  Next  |--------->  |  NULL  |
  +--------+      +--------+      +--------+            +--------+

单向链表的C语言实现

下面是一个完整的、带有注释的单向链表C语言实现,包含了创建、插入、删除、查找、遍历和销毁等基本操作。

c语言线性表的链式存储结构
(图片来源网络,侵删)

1 定义节点结构体

我们需要定义一个结构体来表示链表的节点。

#include <stdio.h>
#include <stdlib.h> // 用于 malloc 和 free
// 定义链表节点结构体
typedef struct Node {
    int data;           // 数据域,这里以 int 类型为例
    struct Node *next;  // 指针域,指向下一个节点
} Node;

2 初始化链表

链表的头指针 head 是一个指向第一个节点的指针,如果链表为空,head 就为 NULL

// 初始化一个空链表
Node* initList() {
    return NULL; // 空链表的头指针为 NULL
}

3 创建新节点

这是一个辅助函数,用于动态分配内存并创建一个新节点。

// 创建一个新节点
Node* createNode(int data) {
    Node *newNode = (Node*)malloc(sizeof(Node));
    if (newNode == NULL) {
        printf("内存分配失败!\n");
        exit(1); // 内存分配失败,退出程序
    }
    newNode->data = data;
    newNode->next = NULL;
    return newNode;
}

4 插入节点

插入操作是链表的精髓,有多种插入方式,我们介绍最常用的两种:

a) 在链表头部插入(头插法)

  • 时间复杂度: O(1)
  • 将新节点作为新的头节点。
// 在链表头部插入节点
Node* insertAtHead(Node *head, int data) {
    Node *newNode = createNode(data);
    newNode->next = head; // 新节点的next指向原来的头节点
    head = newNode;       // 更新头节点指针
    return head;          // 返回新的头节点
}

b) 在链表尾部插入(尾插法)

  • 时间复杂度: O(n),因为需要遍历到链表末尾。
  • 将新节点添加到链表的最后。
// 在链表尾部插入节点
Node* insertAtTail(Node *head, int data) {
    Node *newNode = createNode(data);
    // 如果链表为空,新节点就是头节点
    if (head == NULL) {
        return newNode;
    }
    // 遍历到链表的最后一个节点
    Node *current = head;
    while (current->next != NULL) {
        current = current->next;
    }
    // 将最后一个节点的next指向新节点
    current->next = newNode;
    return head; // 头节点不变
}

5 删除节点

删除节点需要找到待删除节点的前一个节点,然后修改其 next 指针。

// 删除值为 data 的第一个节点
Node* deleteNode(Node *head, int data) {
    if (head == NULL) {
        printf("链表为空,无法删除,\n");
        return NULL;
    }
    // 如果要删除的是头节点
    if (head->data == data) {
        Node *temp = head;
        head = head->next; // 头指针后移
        free(temp);        // 释放旧头节点的内存
        return head;
    }
    // 寻找要删除的节点及其前一个节点
    Node *current = head;
    while (current->next != NULL && current->next->data != data) {
        current = current->next;
    }
    // 如果找到了要删除的节点
    if (current->next != NULL) {
        Node *temp = current->next; // temp 是要删除的节点
        current->next = temp->next; // 前一个节点的next指向要删除节点的下一个
        free(temp);                 // 释放内存
        printf("删除了值为 %d 的节点,\n", data);
    } else {
        printf("未找到值为 %d 的节点,\n", data);
    }
    return head;
}

6 查找节点

遍历链表,查找是否存在指定值的节点。

// 查找值为 data 的节点,如果找到返回节点指针,否则返回 NULL
Node* findNode(Node *head, int data) {
    Node *current = head;
    while (current != NULL) {
        if (current->data == data) {
            return current; // 找到,返回节点指针
        }
        current = current->next;
    }
    return NULL; // 未找到
}

7 遍历(打印)链表

从头节点开始,依次访问每个节点并打印其数据。

// 打印链表中的所有节点
void printList(Node *head) {
    if (head == NULL) {
        printf("链表为空,\n");
        return;
    }
    Node *current = head;
    printf("链表内容: ");
    while (current != NULL) {
        printf("%d -> ", current->data);
        current = current->next;
    }
    printf("NULL\n");
}

8 销毁链表(释放内存)

为了避免内存泄漏,当不再需要链表时,必须释放所有节点占用的内存。

// 释放整个链表的内存
void destroyList(Node *head) {
    Node *current = head;
    Node *nextNode;
    while (current != NULL) {
        nextNode = current->next; // 先保存下一个节点的地址
        free(current);            // 释放当前节点
        current = nextNode;       // 移动到下一个节点
    }
    printf("链表已销毁,\n");
}

完整示例代码

下面是一个 main 函数,演示如何使用上述所有函数。

int main() {
    // 1. 初始化一个空链表
    Node *head = initList();
    printList(head);
    // 2. 使用头插法插入数据
    head = insertAtHead(head, 10);
    head = insertAtHead(head, 20);
    head = insertAtHead(head, 30);
    printf("头插法插入 30, 20, 10 后:\n");
    printList(head);
    // 3. 使用尾插法插入数据
    head = insertAtTail(head, 40);
    head = insertAtTail(head, 50);
    printf("尾插法插入 40, 50 后:\n");
    printList(head);
    // 4. 查找节点
    int searchValue = 20;
    Node *foundNode = findNode(head, searchValue);
    if (foundNode != NULL) {
        printf("找到了值为 %d 的节点,\n", searchValue);
    } else {
        printf("未找到值为 %d 的节点,\n", searchValue);
    }
    // 5. 删除节点
    head = deleteNode(head, 30); // 删除头节点
    head = deleteNode(head, 50); // 删除中间节点
    head = deleteNode(head, 99); // 删除不存在的节点
    printf("删除 30 和 50 后:\n");
    printList(head);
    // 6. 销毁链表
    destroyList(head);
    head = NULL; // 防止悬空指针
    return 0;
}

程序运行结果:

链表为空。
头插法插入 30, 20, 10 后: 30 -> 20 -> 10 -> NULL
尾插法插入 40, 50 后: 30 -> 20 -> 10 -> 40 -> 50 -> NULL
找到了值为 20 的节点。
删除了值为 30 的节点。
删除了值为 50 的节点。
未找到值为 99 的节点。
删除 30 和 50 后: 20 -> 10 -> 40 -> NULL
链表已销毁。

链表的优缺点

优点

  1. 动态大小:链表的大小可以在运行时动态地增加或缩小,无需预先分配固定大小的内存。
  2. 高效的插入和删除:在已知节点位置的情况下,插入和删除操作的时间复杂度为 O(1),相比之下,数组在中间或头部插入/删除需要移动大量元素,时间复杂度为 O(n)。
  3. 内存利用灵活:链表的节点可以分布在内存的任何位置,不存在内存浪费(数组可能因预留空间而浪费)。

缺点

  1. 额外的存储空间:每个节点都需要一个额外的指针域来存储下一个节点的地址,这比数组要多占用一些内存。
  2. 访问效率低:不能像数组那样通过下标直接访问任意位置的元素,访问第 i 个节点必须从头节点开始遍历,时间复杂度为 O(n)。
  3. 实现相对复杂:指针操作容易出错,如内存泄漏、悬空指针等问题,需要仔细管理。

其他类型的链表

除了上述的单向链表,还有其他变种:

  • 双向链表:每个节点包含两个指针,一个指向前一个节点,一个指向后一个节点,这使得双向遍历和删除节点更容易(因为不需要找到前一个节点)。
  • 循环链表:链表的最后一个节点的 next 指针指向头节点,形成一个环,常用于实现队列等数据结构。
  • 静态链表:借助数组来描述链表,指针域存储的是数组下标,这在某些没有指针的语言或特定场景下会用到。

链式存储结构是C语言中实现线性表的一种非常重要且灵活的方式,它通过牺牲一些随机访问的效率,换取了动态内存管理和高效插入删除的优势,理解并掌握链表的实现是学习数据结构与算法的基石。

-- 展开阅读全文 --
头像
织梦数据库链接文件配置在哪?
« 上一篇 03-05
织梦CSS链接如何修改?
下一篇 » 03-05

相关文章

取消
微信二维码
支付宝二维码

目录[+]