单链表:数据结构中的灵活“链条”

news/2025/2/24 5:34:33

目录

  • 🚀前言
  • 🤔单链表是什么?
    • 💯单链表的结构特点
    • 💯单链表的用途
  • ✍️单链表的实现与接口解释
    • 💯打印链表
    • 💯尾插操作
    • 💯头插操作
    • 💯头删操作
    • 💯尾删操作
    • 💯查找操作
    • 💯插入操作
    • 💯删除操作
    • 💯示例代码
  • 🌟单链表的优缺点
    • 💯优点
    • 💯缺点
  • 💻总结

🚀前言

  • 在计算机科学中,数据结构是组织和存储数据的基础工具,它直接影响程序的效率和可扩展性。单链表作为一种经典的线性数据结构,以其简单、灵活且高效的特性被广泛应用于各种编程场景中。从动态数据集合的管理到内存分配,从队列和栈的实现到文件系统的目录结构,单链表都扮演着重要的角色。
  • 单链表的核心思想是通过节点的链接来组织数据。每个节点包含两部分:数据域和指针域。数据域用于存储实际的数据,而指针域则指向下一个节点。这种结构使得单链表在插入和删除操作中表现出色,尤其是当数据集合的大小动态变化时,单链表能够高效地分配和释放内存。
  • 本文将详细介绍单链表的基本概念、用途以及实现代码,并对代码中的各个接口进行详细解释。通过本文,你将不仅了解单链表的实现原理,还会掌握如何在实际编程中灵活运用单链表来解决实际问题。

🤔单链表是什么?

单链表是一种线性数据结构,由一系列节点组成,每个节点包含两部分:数据域指针域。数据域用于存储实际的数据,而指针域则指向下一个节点。单链表的这种结构使得它在内存中是“非连续”的,节点通过指针相互连接,形成一条“链”。这种设计使得单链表在插入和删除操作中非常高效,因为这些操作只需要调整指针,而不需要移动大量数据。
在这里插入图片描述

💯单链表的结构特点

  1. 动态内存分配:单链表的节点是动态分配的,可以根据需要随时扩展或收缩。这使得单链表非常适合处理不确定数量的数据。

  2. 非连续存储:单链表的节点在内存中是非连续的,每个节点通过指针连接到下一个节点。这种设计使得单链表在插入和删除操作中非常高效。

  3. 单向性:单链表的节点只能通过指针访问下一个节点,因此它是单向的。如果需要双向访问,可以使用双向链表。

💯单链表的用途

  1. 动态数据集合:单链表可以方便地插入和删除节点,适用于处理动态变化的数据集合。例如,任务队列、事件监听器等。

  2. 内存管理:在内存分配和回收中,单链表可以用来管理空闲内存块。操作系统中的内存管理器经常使用链表来跟踪空闲内存区域。

  3. 队列和栈的实现:单链表可以用来实现先进先出(FIFO)的队列或后进先出(LIFO)的栈。通过简单的指针操作,可以高效地实现入队、出队、入栈和出栈操作。

  4. 文件系统:在文件系统中,单链表可以用来管理文件的目录结构。例如,文件的链接、目录的嵌套等都可以通过链表实现。

  5. 链式存储结构:在某些算法中,单链表可以作为链式存储结构,例如哈希表的链地址法。通过链表解决哈希冲突是一种常见的方法。


✍️单链表的实现与接口解释

以下是一个基于C语言的单链表实现代码,我们将逐一解释每个接口的功能和实现细节。代码中包含注释,帮助你更好地理解每个操作的实现逻辑。

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

// 定义单链表节点的数据类型
typedef int SLTDataType;

// 定义单链表节点结构
typedef struct SListNode {
    SLTDataType data;          // 数据域
    struct SListNode* next;    // 指针域,指向下一个节点
} SLTNode;

💯打印链表

// 打印链表,不会改变链表的头指针,传一级指针
void SListPrint(SLTNode* phead) {
    SLTNode* cur = phead;      // 当前节点指针
    while (cur != NULL) {      // 遍历链表
        printf("%d ", cur->data); // 打印当前节点的数据
        cur = cur->next;       // 移动到下一个节点
    }
    printf("\n");              // 输出换行符
}

功能:打印链表中的所有数据。

实现细节:通过一个临时指针cur遍历链表,逐个访问每个节点的数据并打印。由于打印操作不会改变链表的结构,因此只需要传递一级指针即可。

应用场景:在调试链表操作时,打印链表可以帮助我们快速检查链表的状态。


💯尾插操作

// 尾插操作,会改变链表的头指针,传二级指针
void SListPushBack(SLTNode** pphead, SLTDataType x) {
    SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode)); // 创建新节点
    newnode->data = x;                                    // 设置新节点的数据
    newnode->next = NULL;                                 // 新节点的下一个节点为空

    if (*pphead == NULL) {                                // 如果链表为空
        *pphead = newnode;                                // 新节点成为头节点
    } else {
        SLTNode* tail = *pphead;                          // 找到尾节点
        while (tail->next != NULL) {
            tail = tail->next;
        }
        tail->next = newnode;                             // 将新节点连接到尾节点
    }
}

功能:在链表尾部插入一个新节点。

实现细节:如果链表为空,新节点直接成为头节点;否则,找到尾节点并将其next指向新节点。由于尾插操作可能会改变链表的头指针(当链表为空时),因此需要传递二级指针。

应用场景:尾插操作常用于构建链表,例如从数组中读取数据并依次插入链表尾部。


💯头插操作

// 头插操作
void SListPushFront(SLTNode** pphead, SLTDataType x) {
    SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode)); // 创建新节点
    newnode->data = x;                                    // 设置新节点的数据
    newnode->next = *pphead;                              // 新节点的下一个节点指向当前头节点
    *pphead = newnode;                                    // 新节点成为头节点
}

功能:在链表头部插入一个新节点。

实现细节:新节点的next指向当前头节点,然后将新节点设置为新的头节点。头插操作不会遍历链表,因此时间复杂度为O(1)。

应用场景:头插操作常用于需要频繁在链表头部插入数据的场景,例如实现栈的入栈操作。


💯头删操作

// 头删操作
void SListPopFront(SLTNode** pphead) {
    if (*pphead == NULL) return;                          // 如果链表为空,直接返回
    SLTNode* next = (*pphead)->next;                      // 保存下一个节点
    free(*pphead);                                        // 释放当前头节点
    *pphead = next;                                       // 更新头节点
}

功能:删除链表头部的节点。

实现细节:保存当前头节点的下一个节点,释放当前头节点,然后更新头节点为下一个节点。头删操作同样不会遍历链表,时间复杂度为O(1)。

应用场景:头删操作常用于实现栈的出栈操作或队列的出队操作。


💯尾删操作

// 尾删操作
void SListPopBack(SLTNode** pphead) {
    if (*pphead == NULL) return;                         // 如果链表为空,直接返回
    else if ((*pphead)->next == NULL) {                  // 如果链表只有一个节点
        free(*pphead);                                   // 释放该节点
        *pphead = NULL;                                  // 设置头节点为空
    } else {
        SLTNode* prev = NULL;                            // 前驱节点
        SLTNode* tail = *pphead;                         // 尾节点
        while (tail->next != NULL) {                     // 找到尾节点
            prev = tail;
            tail = tail->next;
        }
        free(tail);                                      // 释放尾节点
        prev->next = NULL;                               // 前驱节点的next置为空
    }
}

=功能:删除链表尾部的节点。

实现细节:如果链表为空,直接返回;如果链表只有一个节点,释放该节点并设置头节点为空;否则,找到尾节点的前驱节点,释放尾节点,并将前驱节点的next置为NULL。尾删操作需要遍历链表以找到尾节点,因此时间复杂度为O(n)。

应用场景:尾删操作常用于需要从链表末尾移除数据的场景,例如实现队列的出队操作,或者在处理动态数据集合时移除最后一个元素。


💯查找操作

// 查找操作
SLTNode* SListFind(SLTNode* phead, SLTDataType x) {
    SLTNode* cur = phead;                                // 当前节点指针
    while (cur) {                                        // 遍历链表
        if (cur->data == x) {                            // 如果找到目标数据
            return cur;                                  // 返回该节点
        }
        cur = cur->next;                                 // 移动到下一个节点
    }
    return NULL;                                         // 如果未找到,返回NULL
}

功能:查找链表中是否存在某个数据的节点。

实现细节:通过遍历链表,逐个比较节点的数据,如果找到目标数据,返回该节点;否则返回NULL。查找操作的时间复杂度为O(n),因为最坏情况下需要遍历整个链表。

应用场景:查找操作是链表的基本功能之一,常用于判断某个数据是否存在于链表中,或者获取某个数据对应的节点地址,以便进行进一步的操作。


💯插入操作

// 在pos前插入x
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x) {
    if (pos == *pphead) {                                // 如果插入位置是头节点
        SListPushFront(pphead, x);                       // 调用头插操作
    } else {
        SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode)); // 创建新节点
        newnode->data = x;                               // 设置新节点的数据
        newnode->next = NULL;

        SLTNode* prev = *pphead;                         // 前驱节点
        while (prev->next != pos) {                      // 找到pos的前驱节点
            prev = prev->next;
        }
        prev->next = newnode;                            // 将新节点插入到pos前
        newnode->next = pos;
    }
}

功能:在指定节点pos之前插入一个新节点。

实现细节:如果pos是头节点,调用头插操作;否则,找到pos的前驱节点,将新节点插入到pos之前。插入操作的时间复杂度为O(n),因为需要遍历链表以找到插入位置。

应用场景:插入操作常用于需要在链表的某个特定位置插入数据的场景,例如在排序后的链表中插入新元素,或者在实现某些算法时动态调整链表结构。


💯删除操作

// 删除pos位置的值(如果有相同的,删除第一个)
void SListErase(SLTNode** pphead, SLTNode* pos) {
    if (pos == *pphead) {                                // 如果删除的是头节点
        SListPopFront(pphead);                           // 调用头删操作
    } else {
        SLTNode* prev = *pphead;                         // 前驱节点
        while (prev->next != pos) {                      // 找到pos的前驱节点
            prev = prev->next;
        }
        prev->next = pos->next;                          // 将前驱节点的next指向pos的下一个节点
        free(pos);                                       // 释放pos节点
    }
}

功能:删除链表中指定位置pos的节点。

实现细节:如果pos是头节点,调用头删操作;否则,找到pos的前驱节点,调整指针,使其跳过pos节点,然后释放pos节点。删除操作的时间复杂度为O(n),因为需要遍历链表以找到删除位置。

应用场景:删除操作常用于需要从链表中移除某个特定节点的场景,例如在实现队列的出队操作时移除队首元素,或者在处理动态数据集合时移除某个特定元素。


💯示例代码

以下是完整的单链表操作示例代码,展示了如何使用上述接口完成链表的创建、插入、删除和打印等操作。

int main() {
    SLTNode* plist = NULL; // 初始化链表为空

    // 尾插操作
    SListPushBack(&plist, 1); // 尾部插入1
    SListPushBack(&plist, 2); // 尾部插入2
    SListPushBack(&plist, 3); // 尾部插入3
    SListPushBack(&plist, 4); // 尾部插入4

    // 头插操作
    SListPushFront(&plist, 2); // 头部插入2

    // 打印链表
    SListPrint(plist);
    puts(""); // 输出换行

    // 头删操作
    SListPopFront(&plist);

    // 尾删操作
    SListPopBack(&plist);

    // 在指定位置插入
    SLTNode* pos = SListFind(plist, 3); // 查找值为3的节点
    if (pos) {
        SListInsert(&plist, pos, 30); // 在3之前插入30
    }

    // 打印链表
    SListPrint(plist);
    puts("");

    // 在指定位置插入
    pos = SListFind(plist, 1); // 查找值为1的节点
    if (pos) {
        SListInsert(&plist, pos, 30); // 在1之前插入30
    }

    // 打印链表
    SListPrint(plist);
    puts("");

    // 删除指定位置的节点
    pos = SListFind(plist, 30); // 查找值为30的节点
    if (pos) {
        SListErase(&plist, pos); // 删除值为30的节点
    }

    // 打印链表
    SListPrint(plist);
    puts("");

    return 0;
}

输出示例
假设链表的初始操作顺序为:

  1. 尾插1、2、3、4

  2. 头插2

  3. 头删

  4. 尾删

  5. 在值为3的节点前插入30

  6. 在值为1的节点前插入30

  7. 删除值为30的节点

最终输出为:

2 1 2 3 4
2 1 30 3 4
2 1 3 4

🌟单链表的优缺点

💯优点

  1. 动态内存分配:单链表可以根据需要动态分配内存,适合处理不确定数量的数据集合。

  2. 高效插入和删除:插入和删除操作只需要调整指针,不需要移动大量数据,因此时间复杂度较低。

  3. 节省内存:单链表不需要预先分配固定大小的内存空间,因此可以更高效地利用内存。

💯缺点

  1. 访问效率低:单链表只能通过线性遍历来访问节点,无法像数组那样通过索引快速访问,因此访问效率较低。

  2. 额外内存开销:每个节点都需要存储一个指针,这会增加一定的内存开销。

  3. 单向访问:单链表只能从头节点向后遍历,无法反向访问,这在某些场景下可能会带来不便。


💻总结

  • 单链表作为一种简单而灵活的数据结构,在动态数据处理中具有独特的优势。通过本文的介绍,我们详细解释了单链表的基本概念、用途以及实现代码中的各个接口功能。这些接口涵盖了链表的创建、插入、删除、查找和打印等操作,能够满足大多数链表操作的需求。
  • 单链表的实现虽然简单,但在实际应用中却非常强大。它不仅可以用于存储和管理动态数据,还可以作为其他复杂数据结构(如队列、栈、哈希表等)的基础实现。通过理解和掌握单链表的实现和操作,我们可以更好地应对各种数据结构相关的问题。
  • 希望本文能帮助你更好地理解单链表的实现和应用。如果你对单链表或其他数据结构有更多问题,欢迎继续探索和学习

http://www.niftyadmin.cn/n/5863986.html

相关文章

Spring Boot 概要(官网文档解读)

Spring Boot 概述 Spring Boot 是一个高效构建 Spring 生产级应用的脚手架工具&#xff0c;它简化了基于 Spring 框架的开发过程。 Spring Boot 也是一个“构件组装门户”&#xff0c;何为构件组装门户呢&#xff1f;所谓的“构件组装门户”指的是一个对外提供的Web平台&#x…

MongoDB#数据删除优化

分批删除 const batchSize 10000; // 每批删除 10,000 条数据 let deletedCount 0; do {const result db.xxx_collection.deleteMany({createTime: { $lt: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000) }}, { limit: batchSize });deletedCount result.deletedCount;p…

智能测试执行 利用算法 利用图像识别、自然语言处理等技术实现自动化测试执行

以下将从Web应用和移动应用两个方面,给出利用图像识别、自然语言处理等技术实现自动化测试执行的实例,并附上部分代码示例。 Web应用自动化测试实例:模拟用户登录操作测试 需求理解 对于一个Web应用的登录功能进行自动化测试,我们可以结合自然语言处理理解测试用例描述,…

C++ 标准库——函数对象和函数适配器

文章目录 函数对象函数适配器bindbind的重载问题bind的引用问题 mem_fnfunction C标准库提供了函数对象和函数适配器功能 函数对象 许多标准库算法都接受函数对象&#xff08;或函数&#xff09;参数&#xff0c;用来控制其工作方式。常见的函数对象包括——比较标准、谓词&am…

C#实现Modbus TCP 通讯测试软件

C#实现Modbus TCP 通讯测试软件&#xff0c;源码&#xff0c;包括读写功能。 文件列表 WindowsFormsApplication6/WindowsFormsApplication6.sln , 1041 WindowsFormsApplication6/WindowsFormsApplication6.v12.suo , 39936 WindowsFormsApplication6/WindowsFormsApplicati…

python使用httpx_sse调用sse流式接口对响应格式为application/json的错误信息的处理

目录 问题描述方案 问题描述 调用sse流式接口使用httpx_sse的方式 import httpxfrom httpx_sse import connect_sse# 省略无关代码try:with httpx.Client() as client:with connect_sse(client, "GET", url, paramsparam) as event_source:clear_textbox(response_t…

Pytorch使用手册-音频数据增强(专题二十)

音频数据增强 torchaudio 提供了多种方式来增强音频数据。 在本教程中,我们将介绍一种应用效果、滤波器、RIR(房间脉冲响应)和编解码器的方法。 最后,我们将从干净的语音合成带噪声的电话语音。 import torch import torchaudio import torchaudio.functional as Fprin…

创建第一个 Maven 项目(二)

六、添加依赖 在 Maven 项目开发过程中&#xff0c;添加依赖是一项常见且关键的操作。通过添加依赖&#xff0c;我们可以引入项目所需的各种库和框架&#xff0c;极大地扩展项目的功能。接下来&#xff0c;我们将以 JUnit 依赖为例&#xff0c;详细介绍如何在 Maven 项目中添加…