
目录摘要一传递参数的必要性二单链表参数三双链表参数四双链表参数为哨兵位五单/双链表的实现差异摘要在链表的学习和实现中我们会发现链表相关函数都必须进行传参而单链表双链表的传参方式却又不同。为什么单链表虽然结构简单但实现复杂而双链表虽然结构复杂但实现简单此博客就这些问题进行一定的讲解.......注单链表单向不带头不循环链表博客单链表的实现讲解双链表双向带头循环链表博客双链表的实现讲解头结点在正确概念中指的是哨兵位节点而我们在不带头链表中也会将第一个节点成为头结点这是一种形象的说法会让我们更好理解但是要知道这种说法是不合规的一传递参数的必要性首先我们不从传参方式不同的角度来看而是从为什么都需要传参的角度来理解不管是单链表还是双链表 我们在插入删除相关的函数都是需要用到头指针的原因大致可分为两种类型1函数需要遍历本质是因为函数需要头指针才能进行遍历比如尾插则需要从头指针指向的节点向后变量找到尾节点....头指针或头节点的地址是访问整个链表的入口。由于链表的节点在内存中不是连续存储的而是通过指针链接在一起因此要访问、查找、插入或删除节点通常都需要从链表的起始位置开始沿着next指针对于双向链表还有prev指针一步步移动直到找到目标位置。2头指针可能会被改变在某些函数中不需要遍历链表但函数内部仍需要头指针比如头删尾删本质是因为删除之后可能导致头结点改变了从而头指针也要指向更新后的头结点二单链表参数而单链表是不带头的没有哨兵位的则代表头结点是可能更改的则头指针plist也是可能被修改的而头指针本身就是一级指针你调用的函数内部可能会修改头指针本身则你当然要传递二级指针也就是plist作为函数的参数啦~所以现在我们再回过头来看单链表的SList.c文件#pragma once #includestdio.h #includestdlib.h #includeassert.h typedef int SLTDataType; //定义链表结点的结构 typedef struct SListNode { SLTDataType data; struct SListNode* next; }SLTNode; //创建新节点 SLTNode* SLTBuyNode(SLTDataType x); //尾插 void SLTPushBack(SLTNode** pphead, SLTDataType x); //头插 void SLTPushFront(SLTNode** pphead, SLTDataType x); //尾删 void SLTPopBack(SLTNode** pphead); //头删 void SLTPopFront(SLTNode** pphead); //查找 SLTNode* SLTFind(SLTNode* phead, SLTDataType x); //在指定位置之前插⼊数据 void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x); //在指定位置之后插⼊数据 void SLTInsertAfter(SLTNode* pos, SLTDataType x); //删除pos结点 void SLTErase(SLTNode** pphead, SLTNode* pos); //删除pos之后的结点 void SLTEraseAfter(SLTNode* pos); //打印函数 void SLTPrint(SLTNode* phead); //销毁链表 void SListDestroy(SLTNode** pphead);解释我们会发现但凡参数是二级指针的则代表该函数可能会影响到plist本身①头删尾删头插尾插头删不必多说肯定会影响到plist本身而尾删时若为空链表则也等同于头删会影响到plist本身头插会有新节点作为头结点也会影响到plist本身而尾插时若为空链表则也相当于头插了会影响到plist本身②SLTInsert在pos位置前插入若pos为头结点则等同于头插所以会影响到plist本身SLTErase删除pos节点若pos节点为头结点则等同于头删吗所以会影响到plist本身SListDestory销毁链表则一定会销毁头结点所以会影响到plist本身③而SLTFindSLTEraseAfterSLTPrint则传递一级指针也就是实参为plist即可因为SLTFind查找函数只负责查找不可能改变头结点SLTEraseAfter在pos位置之后插入函数pos极限就是头结点所以最多也是在头结点之后插入函数不可能改变头结点SLTPrint同理三双链表参数而双链表是带头的是有哨兵位的则代表头结点是不可能被更改的则头指针plist值是固定的一定是哨兵位节点的地址所以在所有函数中我们只需要传递plist本身即可所以现在我们再回过头来看双链表的List.c文件#pragma once #includestdio.h #includestdlib.h #includeassert.h //单个节点的声明 typedef int LTDataType; typedef struct ListNode { struct ListNode* next; // 指向下一个节点的指针 struct ListNode* prev; // 指向前一个节点的指针 LTDataType data; // 节点存储的数据 }LTNode; // 创建新节点 LTNode* BuyLTNode(LTDataType x); // 初始化 LTNode* LTInit(); ////初始化的另一种写法 //void LTInit(LTNode** pphead) // 打印 void LTPrint(LTNode* phead); // 尾插 void LTPushBack(LTNode* phead, LTDataType x); // 尾删 void LTPopBack(LTNode* phead); // 头插 void LTPushFront(LTNode* phead, LTDataType x); // 头删 void LTPopFront(LTNode* phead); // 获取节点数量 int LTSize(LTNode* phead); // 查找 LTNode* LTFind(LTNode* phead, LTDataType x); // 在pos位置前插入 void LTInsert(LTNode* pos, LTDataType x); // 删除pos位置 void LTErase(LTNode* pos); //销毁 void ListDestroy(LTNode* phead); ////销毁 的另一种写法 //void ListDestroy(LTNode** pphead)解释我们会发现所有函数参数都是一级指针实参为plist本质是因为所有的函数都不会影响到plist的值其永远指向哨兵位但是仅有ListDestroy销毁函数也会影响到plist本身Q销毁函数是肯定也会把哨兵位进行销毁的所以plist最后值为NULL那为什么这里还能传递一级指针呢A①首先传递一级指针代表phead的值和plist的值一模一样此时若是更改phead的值则无法影响到plist本身但是销毁函数不更改phead的值而是直接free(phead)因为phead的值和plist的值是一致的所以free(phead)就等同于在main函数中free(plist)而我们唯一需要善后的地方吗在于我们在main函数中ListDestroy函数之后还需手动的将plist置为NULL因为你在销毁函数中将phead置为NULL是影响不到plist的②其次我们如果不想保持所有接口参数的一致性不保持都为一级指针。则我们可以将销毁函数的参数设置为二级指针这样我们就可以在ListDestroy内部对(*pphead)置NULL了四双链表参数为哨兵位❓️那既然双链表的函数是不会影响到头结点的则不会影响到头指针的值的我们也知道传递头指针是为了遍历链表那为什么不传递头结点本身呢函数接收到头结点本身不也可以通过头结点的成员变量next和prev变量链表吗为什么还非要传递指针呢传递哨兵位节点本身是一定不对的缺陷1形参的开销如果你直接传递一个结构体变量函数形参则完整地拷贝一份整个结构体到函数的栈空间里一个节点就是data和两个指针节点一多开销太大缺陷2无法更改哨兵位的指向传递结构体本身函数得到的是哨兵节点的副本。这个副本虽然和哨兵位的datanextprev到一模一样你也能通过prev和next前后遍历但是你再怎么你也是一个副本节点你虽然什么都和哨兵位一样但是你的地址和哨兵位不一样这就代表你如果修改副本的next和prev虽然可以修改但是影响不到哨兵位节点的next和prev的一丝一毫这意味着头插头删等操作都无法进行如图缺陷3如图可得如果你从副本节点开始遍历则你再也回不到你的副本节点了副本通过next就会进入到双链表中而因为没有节点的指针指向这个副本所以无法回到副本这意味这如果你采取传递结构体本身的方式则你的函数一般会使用到判断副本结构体的指针来进行循环则一定死循环五单/双链表的实现差异为什么单链表虽然结构简单但实现复杂而双链表虽然结构复杂但实现简单其实本质就是空间换时间在设计上双链表更复杂占用的空间更多但是在实现和效率是双链表更优秀写单链表最头疼的就是处理“头节点”和“尾节点”的空指针问题。单链表如果你要删除的是最后一个节点或者在第一个节点前插入你需要写大量的if (prev NULL)之类的判断否则程序就会因为空指针崩溃。双链表由于有prev指针有哨兵头节点整个链表变成了一个完美的闭环。头节点的prev指向尾节点尾节点的next指向头节点。这时候所有的节点都是“中间节点”没有任何特殊的边界情况需要处理。你的代码只需要写一套通用的逻辑就能处理所有情况。总结单链表结构简单每个节点只存一个next省内存结构图看起来清爽。单链表实现复杂因为信息量少不知道前驱是谁导致代码必须通过复杂的遍历和大量的if-else来弥补信息的缺失。双链表结构复杂多了哨兵位每个节点多了一个prev指针内存开销变大结构图看起来乱糟糟。双链表实现简单因为信息量足左右邻居都知道代码逻辑变得非常直接、暴力且统一大大降低了写出 Bug 的概率。但只要理解了双链表的结构实现起来更简单了效率也飙升了所以在实际的工程开发中大家往往更偏爱双链表因为开发和维护的便利性远比省那几个字节的内存重要的多 [ 作者 ] shylyly [ 首次发布 ] 2026.5.26❌ [ 最新修改 ] 无 [ 声明 ] 由于笔者水平有限文中难免有疏漏或不妥之处还望读者不吝赐教