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

目录

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

🚀前言

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

🤔单链表是什么?

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

💯单链表的结构特点

  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); // 尾部插入1SListPushBack(&plist, 2); // 尾部插入2SListPushBack(&plist, 3); // 尾部插入3SListPushBack(&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.rhkb.cn/news/22845.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Redis面试宝典【刷题系列】

文章目录 一、什么是Redis&#xff1f;二、Redis相比Memcached有哪些优势&#xff1f;三、Redis支持的数据类型有哪些&#xff1f;四、Redis的主要消耗的物理资源是什么&#xff1f;五、Redis的全称是什么&#xff1f;六、Redis有哪些数据淘汰策略&#xff1f;七、为什么Redis需…

uni-app集成sqlite

Sqlite SQLite 是一种轻量级的关系型数据库管理系统&#xff08;RDBMS&#xff09;&#xff0c;广泛应用于各种应用程序中&#xff0c;特别是那些需要嵌入式数据库解决方案的场景。它不需要单独的服务器进程或系统配置&#xff0c;所有数据都存储在一个单一的普通磁盘文件中&am…

pytest-html

首先安装pytest-html库 #执行命令 pytest --htmlreport.html ./pytest-html.pyimport pytest import logging def test_pass():"""用例通过"""assert Truedef test_fail():"""用例失败"""assert Falsedef test_e…

kafka为什么这么快?

前言 Kafka的高效有几个关键点&#xff0c;首先是顺序读写。磁盘的顺序访问速度其实很快&#xff0c;甚至比内存的随机访问还要快。Kafka在设计上利用了这一点&#xff0c;将消息顺序写入日志文件&#xff0c;这样减少了磁盘寻道的时间&#xff0c;提高了吞吐量。与传统数据库的…

从DeepSeek的爆火来看大模型微调技术的发展方向

“深度人工智能”是成都深度智谷科技旗下的人工智能教育机构订阅号&#xff0c;主要分享人工智能的基础知识、技术发展、学习经验等。此外&#xff0c;订阅号还为大家提供了人工智能的培训学习服务和人工智能证书的报考服务&#xff0c;欢迎大家前来咨询&#xff0c;实现自己的…

Dify使用教程(创建应用)

Dify的安装部署我已经写过了&#xff0c;简单的模型配置我也在前面进行了讲解&#xff0c;今天我们主要来讲讲如何使用Dify。 一、创建应用 我们可以通过三种方式在Dify的工作室内创建应用 01 基于应用模板创建&#xff08;新手推荐&#xff09;02 创建一个空白应用03 通过D…

system verilog的流操作符

流操作符&#xff0c;有分为操作对象是一整个数组和单独的数据两种&#xff0c;例如bit [7:0] a[4]和bit [31:0] b&#xff0c;前者操作对象是数组&#xff0c;后者是单独一个较大位宽的数。 流操作符有<<和>>&#xff0c;代表从右向左打包和从左向右打包。 打包的…

项目实战--网页五子棋(匹配模块)(4)

上期我们完成了游戏大厅的前端部分内容&#xff0c;今天我们实现后端部分内容 1. 维护在线用户 在用户登录成功后&#xff0c;我们可以维护好用户的websocket会话&#xff0c;把用户表示为在线状态&#xff0c;方便获取到用户的websocket会话 package org.ting.j20250110_g…

hot100_108. 将有序数组转换为二叉搜索树

hot100_108. 将有序数组转换为二叉搜索树 思路 给你一个整数数组 nums &#xff0c;其中元素已经按 升序 排列&#xff0c;请你将其转换为一棵 平衡 二叉搜索树。 示例 1&#xff1a; 输入&#xff1a;nums [-10,-3,0,5,9] 输出&#xff1a;[0,-3,9,-10,null,5] 解释&#…

Win11更新系统c盘爆满处理

1.打开磁盘管理 2.右击c盘选择属性&#xff0c;进行磁盘管理&#xff0c;选择详细信息。 3.选择以前安装的文件删除即可释放c盘空间。

深入理解 JSP 与 Servlet:原理、交互及实战应用

一、引言 在 Java Web 开发领域,JSP(JavaServer Pages)和 Servlet 是两个至关重要的技术,它们共同构成了动态网页开发的基础。Servlet 作为服务器端的 Java 程序,负责处理客户端请求并生成响应;而 JSP 则是一种简化的 Servlet 开发方式,允许开发者在 HTML 页面中嵌入 J…

[通俗易懂C++]:指针和const

之前的文章有说过,使用指针我们可以改变指针指向的内容(通过给指针赋一个新的地址)或者改变被保存地址的值(通过给解引用指针赋一个新值): int main() {int x { 5 }; // 创建一个整数变量 x&#xff0c;初始值为 5int* ptr { &x }; // 创建一个指针 ptr&#xff0c;指向 …

DL/CV领域常见指标术语(FLOPS/mIoU/混淆矩阵/F1-measure)------一篇入门

1. FLOPS、FLOPs和GFLOPs FLOPS: floating-point operations per second&#xff0c;每秒浮点运算次数&#xff0c;用来衡量硬件性能。 FLOPs&#xff1a;floating point of operations&#xff0c;是浮点运算次数&#xff0c;用来衡量算法、模型的复杂度。 GFLOPS&#xff…

被裁20240927 --- WSL-Ubuntu20.04安装cuda、cuDNN、tensorRT

cuda、cuDNN、tensorRT的使用场景 1. CUDA&#xff08;Compute Unified Device Architecture&#xff09; 作用&#xff1a; GPU 通用计算&#xff1a;CUDA 是 NVIDIA 的并行计算平台和编程模型&#xff0c;允许开发者直接利用 GPU 的并行计算能力&#xff0c;加速通用计算任…

DeepSeek vs ChatGPT:AI对决中的赢家是……人类吗?

DeepSeek vs ChatGPT&#xff1a;AI对决中的赢家是……人类吗&#xff1f; 文章目录 DeepSeek vs ChatGPT&#xff1a;AI对决中的赢家是……人类吗&#xff1f;一、引言1. 背景2. 问题 二、DeepSeek vs ChatGPT&#xff1a;谁更胜一筹&#xff1f;2.1 语言生成能力评测对比场景…

旧手机热点无法提供ipv6解决方法(emui 8 热点提供ipv6)

旧手机热点无法提供ipv6解决方法 手机&#xff1a;荣耀8x 系统版本: EMUI 8 网络&#xff1a;移动流量卡 解决方案 设置-》无线和网络-》移动网络-》接入点名称(APN)-》cmiot 修改 APN协议: IPv4/IPv6 修改 APN漫游协议: IPv4/IPv6

I2C学习笔记-软件模拟I2C

I2C学习笔记&#xff08;软件模拟&#xff09; 介绍GPIO的配置信号的展示起始信号 与 停止信号应答信号&#xff08;非应答信号&#xff09;检测应答信号发送一个字节数据接收一个字节数据 硬件配置实物测试 介绍 I2C的信号大概有 起始信号、应答信号、停止信号、写数据、读数…

VUE四:Vue-cli

什么是Vue-cli vue-cli是官方提供的一个脚手架,用于快速生成一个vue的项目模板; 预先定义好的目录结构及基础代码&#xff0c;就好比咱们在创建 Maven项目时可以选择创建一个骨架项目&#xff0c;这个骨架项目就是脚手架,我们的开发更加的快速; 什么是web pack 本质上&#…

基于 openEuler 构建 LVS-DR 群集

目录 对比 LVS 负载均衡群集的 NAT 模式和 DR 模式&#xff0c;比较其各自的优势 NAT 模式&#xff08;网络地址转换模式&#xff09; DR 模式&#xff08;直接路由模式&#xff09; 基于 openEuler 构建 LVS-DR 群集 实验准备环境 配置web服务器 web1 web2 首先下载ngi…

传统的自动化行业的触摸屏和上位机,PLC是否会被取代?

传统的自动化行业的触摸屏和上位机是否会被取代&#xff1f; 在工业自动化领域&#xff0c;触摸屏和上位机长期扮演着核心角色&#xff0c;尤其在污水处理、化工生产等场景中&#xff0c;它们通过实时数据采集、逻辑控制、报警联动等功能&#xff0c;保障了生产设备的稳定运行…