【数据结构】:链表的核心实现与运行解析

news/2025/11/11 22:55:16/文章来源:https://www.cnblogs.com/gccbuaa/p/19211850

在这里插入图片描述

博主名称:月夜的风吹雨

个人专栏: 《C语言》《基础数据结构》

⛺️任何一个伟大的思想,都有一个微不足道的开始!

引言

单链表是一种物理存储非连续、逻辑存储连续的数据结构,其数据元素通过节点间的指针链接维持逻辑顺序。相较于顺序表,单链表无需预先分配固定内存,插入、删除操作更灵活,是哈希桶、图的邻接表等复杂数据结构的基础组件。本文基于 C 语言,系统解析单链表的初始化、节点申请、插入、删除、查找及销毁等核心操作,重点阐述每个函数的设计思路与实现逻辑。



一、单链表的基础定义与核心概念

单链表的基本构成单元是 “节点”,每个节点包含 “数据域”(存储数据)与 “指针域”(存储下一个节点的地址)。为适配不同数据类型,先通过typedef定义通用数据类型SLTDataType,再定义节点结构体:

在这里插入图片描述

typedef int SLTDataType; // 可根据需求修改为char、float等
typedef struct SListNode
{
SLTDataType data;       // 节点数据域
struct SListNode* next; // 指针域:指向 next 节点
} SLTNode;

后续所有操作均围绕该节点结构展开,核心设计原则是 “避免野指针”“不破坏链表逻辑连续性”,且根据是否修改头指针,决定使用一级指针或二级指针传参(修改头指针需传二级指针,否则传一级指针)。


二、单链表的核心操作实现

2.1 链表初始化(SLTInit)

初始化的目标是将链表的头指针置为NULL,表示空链表。

注意:头指针是指向结构体的指针,它并非首个结构体本身,而是指向第一个结构体节点的指针。

void SLTInit(SLTNode** pphead)
{
assert(pphead); // 确保传入的指针有效
*pphead = NULL; // 头指针置空,链表为空
}

2.2 节点申请(SLTBuyNode)

单链表插入操作需先申请新节点,该函数统一封装节点申请逻辑,避免重复代码。

  • 节点需从堆区申请(malloc),申请可能失败(如内存不足),需检查返回值;申请成功后,需初始化数据域(赋值x)与指针域(置NULL,避免随机指向),最后返回新节点地址。
  • 调用malloc申请节点大小的内存,perror处理申请失败场景并退出程序;成功则赋值datanext,返回新节点指针。
SLTNode* SLTBuyNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
// 检查内存申请结果
perror("malloc fail:");
exit(1); // 异常退出,避免后续野指针操作
}
newnode->data = x;  // 初始化数据域
newnode->next = NULL; // 初始化指针域,新节点默认无后续节点
return newnode;
}

2.3 尾部插入(SLTPushBack)

尾部插入是在链表末尾添加新节点,需区分 “空链表”“非空链表” 两种场景。

  • 空链表时,头指针为NULL,直接让头指针指向新节点即可;非空链表时,需先遍历找到最后一个节点(nextNULL的节点),再将其next指向新节点。由于空链表场景需修改头指针,需传二级指针pphead
  • 先申请新节点,断言pphead有效;若链表为空(*pphead == NULL),头指针指向新节点;否则用临时变量ptail遍历找到尾节点,将ptail->next指向新节点。
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newnode = SLTBuyNode(x); // 申请新节点
if (*pphead == NULL)
{
// 空链表:头指针直接指向新节点
*pphead = newnode;
}
else
{
// 非空链表:找尾节点
SLTNode* ptail = *pphead; // 临时指针遍历,不修改头指针
while (ptail->next)
{
// 尾节点的next为NULL,循环终止时ptail是尾节点
ptail = ptail->next;
}
ptail->next = newnode; // 尾节点连接新节点
}
}

2.4 头部插入(SLTPushFront)

头部插入是在链表开头添加新节点,新节点成为新的头节点。

  • 头部插入需让新节点的next指向原头节点,再让头指针指向新节点 —— 若先修改头指针,会丢失原链表地址。由于需修改头指针,需传二级指针pphead
  • 申请新节点,新节点的next指向原头节点(*pphead),再将头指针更新为新节点,完成头部插入。
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newnode = SLTBuyNode(x);
newnode->next = *pphead; // 新节点连接原头节点
*pphead = newnode;       // 新节点成为新头节点
}

2.5 尾部删除(SLTPopBack)

尾部删除是移除链表最后一个节点,需区分 “仅一个节点” 与 “多个节点” 两种场景。

  • 删除前需确保链表非空(断言*pphead != NULL);仅一个节点时,删除后需将头指针置NULL(否则头指针指向已释放的节点,成为野指针);多个节点时,需找到倒数第二个节点,将其nextNULL,再释放最后一个节点。因需修改头指针(仅一个节点场景),传二级指针pphead
  • 断言链表非空,若仅一个节点,直接释放头节点并置NULL;否则用prevptail两个指针遍历,prev跟随ptail,找到倒数第二个节点后,释放ptail(尾节点),prev->nextNULL
void SLTPopBack(SLTNode** pphead)
{
assert(pphead && *pphead); // 断言链表非空
if ((*pphead)->next == NULL)
{
// 仅一个节点:删除后链表为空
free(*pphead);
*pphead = NULL;
}
else
{
// 多个节点:找倒数第二个节点
SLTNode* ptail = *pphead;
SLTNode* prev = ptail;
while (ptail->next)
{
// 循环终止时ptail是尾节点,prev是倒数第二个
prev = ptail;
ptail = ptail->next;
}
free(ptail);        // 释放尾节点
ptail = NULL;       // 避免野指针
prev->next = NULL;  // 倒数第二个节点成为新尾节点
}
}

2.6 头部删除(SLTPopFront)

头部删除是移除链表第一个节点,让头指针指向原第二个节点。

  • 删除前需确保链表非空,先保存原头节点的next(避免释放后丢失后续链表),再释放原头节点,最后让头指针指向保存的next。因需修改头指针,传二级指针pphead
  • 断言链表非空,保存原头节点的next,释放原头节点,头指针更新为保存的next
void SLTPopFront(SLTNode** pphead)
{
assert(pphead && *pphead); // 断言链表非空
SLTNode* next = (*pphead)->next; // 保存原头节点的next
free(*pphead);                  // 释放原头节点
*pphead = next;                 // 新头节点为原第二个节点
}

2.7 指定位置前插入(SLTInsert)

在给定节点pos前插入新节点,需区分pos是否为头节点。

  • pos是头节点,插入逻辑与头部插入一致,可直接调用SLTPushFront(头插);若pos非头节点,需找到pos的前一个节点prev,让prev->next指向新节点,新节点的next指向pos。因可能修改头指针(pos为头节点场景),传二级指针pphead
  • 断言pos有效,判断pos是否为头节点,是则头插;否则遍历找prevprev->next == pos),连接新节点与posprev

在插入之前,我们需要先定位目标位置指针pos。由于pos存储的是地址,而我们无法直接输入地址进行查找,因此在实际插入操作前,还需要一个专门的查找步骤来获取目标位置的地址。

//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)  //仅查找不改第一节点
{
assert(phead);
SLTNode* pur = phead;
while (pur)
{
if (pur->data == x) return pur;
pur = pur->next;
}
return NULL;
}
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pphead && *pphead && pos); // 断言参数有效
if (pos == *pphead)
{
// pos是头节点:调用头插
SLTPushFront(pphead, x);
}
else
{
// pos非头节点:找pos的前一个节点
SLTNode* newnode = SLTBuyNode(x);
SLTNode* prev = *pphead;
while (prev->next != pos)
{
// 循环找prev
prev = prev->next;
}
newnode->next = pos;  // 新节点连接pos
prev->next = newnode;  // prev连接新节点
}
}

2.8 指定位置后插入(SLTInsertAfter)

在给定节点pos后插入新节点,无需修改头指针,仅需操作posnext

  • pos后插入无需头节点参与,也不修改头指针,故传一级指针pos即可。需先让新节点的next指向posnext(避免丢失pos后的节点),再让posnext指向新节点,确保顺序正确。
  • 断言pos有效,申请新节点,新节点next指向pos->nextpos->next指向新节点。

这个与上面的在指定位置之前插入一样,要在插入之前找到节点位置

void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos); // 断言pos有效
SLTNode* newnode = SLTBuyNode(x);
newnode->next = pos->next; // 新节点连接pos的原next
pos->next = newnode;       // pos连接新节点
}

2.9 删除指定节点(SLTErase)

删除给定节点pos,需区分pos是否为头节点。

  • pos是头节点,删除逻辑与头部删除一致,调用SLTPopFront;若pos非头节点,需找到pos的前一个节点prev,让prev->next指向pos->next,再释放pos。因可能修改头指针,传二级指针pphead
  • 断言参数有效,判断pos是否为头节点,是则头删;否则找prev,修改prev->next,释放pos并置NULL
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead && *pphead && pos); // 断言参数有效
if (pos == *pphead)
{
// pos是头节点:调用头删
SLTPopFront(pphead);
}
else
{
// pos非头节点:找prev
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next; // prev连接pos的next
free(pos);              // 释放pos
pos = NULL;             // 避免野指针
}
}

2.10 删除指定节点后的节点(SLTEraseAfter)

删除pos后的节点,无需修改头指针,仅需操作posnext

  • 需先确保pos后有节点(断言pos->next != NULL),保存pos->next(待删除节点),让pos->next指向pos->next->next,再释放保存的节点,避免丢失后续链表。
  • 断言pospos->next有效,保存待删除节点,修改pos->next,释放待删除节点并置NULL
void SLTEraseAfter(SLTNode* pos) {
assert(pos && pos->next); // 断言pos有效且后有节点
SLTNode* next = pos->next; // 保存待删除节点
pos->next = pos->next->next; // pos连接待删除节点的next
free(next);                  // 释放待删除节点
next = NULL;                 // 避免野指针
}

2.11 链表打印(SLTPrint)

遍历链表并打印所有节点数据,验证链表逻辑正确性,不修改链表内容。

  • 打印无需修改头指针,故传一级指针phead;用临时变量pcur遍历(避免修改头指针),循环打印pcur->data,直至pcurNULL(尾节点后),最后打印NULL标识链表结束。
  • pcurphead开始遍历,循环打印数据,pcur更新为pcur->next,结束时打印NULL
void SLTPrint(SLTNode* phead)
{
SLTNode* pcur = phead; // 临时指针遍历
while (pcur)
{
printf("%d -> ", pcur->data);
pcur = pcur->next;
}
printf("NULL\n"); // 标识链表结束
}

2.12 链表销毁(SListDesTroy)

释放链表所有节点的内存,避免内存泄漏,最后将头指针置NULL

  • 销毁需释放每个节点,可复用头部删除逻辑(每次删头节点,直至链表为空),因需修改头指针,传二级指针pphead
  • 循环调用SLTPopFront删除头节点,直至*ppheadNULL,所有节点释放完毕。
void SListDesTroy(SLTNode** pphead)
{
assert(pphead && *pphead); // 断言链表非空
while (*pphead)
{
// 循环删头节点,直至链表为空
SLTPopFront(pphead);
}
}

三、总结

单链表的核心操作围绕 “指针管理” 与 “边界场景处理” 展开,关键结论如下:

  1. 传参原则:需修改头指针(初始化、头插、头删、尾删、指定位置前插入、销毁)传二级指针,否则传一级指针;
  2. 边界场景:空链表、仅一个节点、指定位置为头 / 尾节点,需单独处理,避免野指针与链表断裂;
  3. 内存管理:节点从堆区申请后,必须在删除、销毁时free,且free后需置NULL,避免野指针。

掌握这些操作后,可基于单链表实现通讯录、栈、队列等更复杂的功能,也为后续学习双向链表、循环链表奠定基础。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/962808.shtml

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

相关文章

11.11每日总结

今天主要的课程有人机交互技术和软件构造。软考结束了 唉 已经做好下学期再努力试一次的准备了。今天还完善了心理健康平台的设计,继续加油

Meta AI 推出全语种语音识别系统,支持 1600+语言;谢赛宁、李飞飞、LeCun 联手发布「空间超感知」AI 框架丨日报

开发者朋友们大家好:这里是 「RTE 开发者日报」,每天和大家一起看新闻、聊八卦。我们的社区编辑团队会整理分享 RTE(Real-Time Engagement) 领域内「有话题的技术」、「有亮点的产品」、「有思考的文章」、「有态度…

Python Socket网络编程

1. Socket参数介绍 服务器端server = socket.socket(AF.INET,sock.SOCK_STREAM)Socket Typessocket.SOCK_STREAM #for tcpsocket.SOCK_DGRAM #for udp socket.SOCK_RAW #原始套接字,普通的套接字无法处理ICMP、…

拥护向量机(SVM)(二)---优化算法

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

英语翻译题做法

🔹快速浏览全文和完形填空一样,做翻译题前,我先快速浏览一遍全文,大概知道文章讲什么、关于什么。如果时间充裕,可以这样做;但如果考试时间紧张,直接读划线句的翻译部分。重点看首尾两段,也能大致把握主旨。�…

Python show memory

py -m pip install psutil import os import psutil import asyncio import time from datetime import datetimeclass Book:def __init__(self,id,name,author,isbn,title,topic):self.id=idself.name=nameself.autho…

LeetCode - 1171. - 教程

LeetCode - 1171. - 教程pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", &q…

研发度量DORA+SPACE+OST 影响模型

目录总结DORA 指标Space指标传统指标参考资料 总结 作为研发总监,建议您建立一个三层指标仪表盘:顶层(DORA + 业务结果): 关注结果和对业务的贡献。这是您向 CEO 或董事会汇报的指标。核心 DORA 4 指标。 功能发布…

断句

文言文断句,传统上称为“句读”,明辨“句读”是阅读文言文最基本的方法;文言文断句,在通读全文、了解大意的基础上,可借助以下几种方法断句:一、借助虚词标志性词语断句 虚词是明辨句读的重要标志,尤其是代词、…

GBT - What is gradient-boosted tree regression?

GBT - What is gradient-boosted tree regression?Gradient-Boosted Tree Regression (GBT Regression) — also called Gradient Boosted Regression Trees (GBRT) or Gradient Boosting Regression — is a powerfu…

保姆级教程 十分钟带你配置好vscode xdebug

本文详细介绍了在VSCode中配置Xdebug进行PHP远程调试的完整流程。首先通过PHP版本确认和路径查找确定环境配置基础,然后通过手动或自动方法下载匹配的Xdebug扩展并进行正确安装。重点讲解了php.ini文件的关键配置项设…

比特币的简单技术原理

比特币技术核心详解:从密码学原理到区块链架构 比特币不仅仅是一种数字货币,更是一个建立在密码学和分布式网络之上的革命性系统。理解其技术内核,是理解其价值主张的关键。 一、 终极目标:数字黄金与无需信任的价…

详细介绍:HUD-汽车图标内容

详细介绍:HUD-汽车图标内容pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco",…

后端八股之mysql - 指南

后端八股之mysql - 指南2025-11-11 22:38 tlnshuju 阅读(0) 评论(0) 收藏 举报pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; fon…

nginx拦截ip

nginx拦截iphttp中加# 提取客户端真实 IPmap $http_x_forwarded_for $client_ip { "~^(?P<ip>\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})" "$ip"; default …

2025年包装机厂家推荐排行榜,全自动包装机,全自动包装机生产线,非标定制生产线,非标定制机器公司精选指南

2025年包装机厂家推荐排行榜:全自动包装机生产线与非标定制设备选购指南 行业背景与发展趋势 随着制造业智能化转型的深入推进,包装机械行业正迎来技术革新的关键时期。全自动包装机生产线作为现代工业生产的重要装备…

【CI130x 离在线】FIFO的学习及实例

FIFO(先进先出)命名管道,用于无关系进程间的通信,因为Linux中所有事物都是文件,它在文件系统中以文件名的形式存在。创建管道用函数mkfifo(const char * pathname,mode_t mode); 函数说明:pathname参数为创建实名…

2025年包装机厂家权威推荐榜:全自动包装机、半自动包装机,高效智能包装解决方案精选

2025年包装机厂家权威推荐榜:全自动包装机、半自动包装机,高效智能包装解决方案精选随着制造业智能化转型的深入推进,包装机械行业正迎来技术革新的关键时期。全自动包装机和半自动包装机作为生产线上的关键设备,其…

CF1187F

有 \(n\) 个整数 \(a_1 \sim a_n\),每个数在 \([l_i, r_i]\) 随机选择,设 \(B = \sum\limits_{i = 1}^n [a_i \ne a_{i - 1}](a_0 = 0)\),求 \(E(B^2)\)。 \(n \le 2 \times 10^5, 1 \le l_i \le r_i \le 10^9\)。首…

刷题日记—数组—数组偏移

好久没好好刷基础题了,今天接着刷了数组类型,碰到了一个数组偏移问题,如下:题解: #include<iostream> using namespace std; bool fire[10000][10000]; int main(){ //要注意数组越界问题int n,m,k;cin&…