当前位置: 首页 > news >正文

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


在这里插入图片描述

🎬 博主名称:月夜的风吹雨

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

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

引言

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


文章目录

  • 引言
  • 一、单链表的基础定义与核心概念
  • 二、单链表的核心操作实现
    • 2.1 链表初始化(SLTInit)
    • 2.2 节点申请(SLTBuyNode)
    • 2.3 尾部插入(SLTPushBack)
    • 2.4 头部插入(SLTPushFront)
    • 2.5 尾部删除(SLTPopBack)
    • 2.6 头部删除(SLTPopFront)
    • 2.7 指定位置前插入(SLTInsert)
    • 2.8 指定位置后插入(SLTInsertAfter)
    • 2.9 删除指定节点(SLTErase)
    • 2.10 删除指定节点后的节点(SLTEraseAfter)
    • 2.11 链表打印(SLTPrint)
    • 2.12 链表销毁(SListDesTroy)
  • 三、总结


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

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

在这里插入图片描述

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

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


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

2.1 链表初始化(SLTInit)

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

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

  • 链表未初始化时,头指针可能是随机值,直接使用会导致野指针问题。由于需修改头指针本身(从随机值改为NULL),需通过二级指针pphead间接操作头指针(一级指针传参会导致形参副本修改,不影响实参)。
  • 断言pphead不为空(防止传入空指针),将*pphead(即头指针)赋值为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; // 保存原头节点的nextfree(*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) { // 循环找prevprev = prev->next;}newnode->next = pos;  // 新节点连接posprev->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的原nextpos->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非头节点:找prevSLTNode* prev = *pphead;while (prev->next != pos) {prev = prev->next;}prev->next = pos->next; // prev连接pos的nextfree(pos);              // 释放pospos = 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连接待删除节点的nextfree(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.dtcms.com/a/481581.html

相关文章:

  • 【Verilog】系统任务和编译指令
  • 辅助分类器GAN(ACGAN)
  • 交流网站建设心得体会wordpress首页固定页面
  • 专门做有机食品的网站dedecms怎么部署网站
  • 大学生个体创业的网站建设百度搭建wordpress
  • 自己公司网站维护上海动易 网站
  • 摄影网站设计企业官网用什么系统
  • 网站开发工具最适合网站建设和网络优化
  • 东莞东坑网站设计中牟网站建设
  • 官方模板关键字生成的代码添加在网站的什么地方?郴州网站建设服务
  • 查看一个网站开发语言wap网站分享到微信
  • 网站备案流程教程今天的热点新闻
  • 网站站内链接福田欧曼前四后八
  • 网站建设具体方案免费企业邮箱排名
  • 温州建设小学的网站企业网站建设与实现的论文
  • 网站机房建设目的wordpress导航设置
  • 怎么构建网站wordpress 关闭伪静态
  • 做app推广上哪些网站做金融的看哪些网站
  • 机械设备做公司网站下载好了网站模板怎么开始做网站
  • 珠宝网站模板网络营销的概念与含义谷歌
  • 沧州网站建设联系电话做学徒哪个网站好
  • 著名的网站有哪些网页设计工资一般多少
  • 网站建设能挣钱免费的宣传平台有哪些
  • 外贸网站经典营销案例网站空间商是什么意思
  • 做教案比较好的网站国外友链买卖平台
  • 广东网站建设人员网址在线生成二维码
  • 东莞seo整站优化怎么做网站下载链接
  • 用路由器做简单的网站宁波正规seo推广
  • 有关商业网站的风格特征杭州seo公司
  • 做网站帮外国人淘宝深圳市龙岗区建设工程交易中心