2024年冰冰学习笔记:一步一步带你实现《单链表》_单链表的连接,非科班程序员金三银四求职经历

img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

return newnode;

}


#### 2.3.2数据尾插函数


        尾增就是将数据连接到已有的链表后面,让原本指向空指针的最后一个结点指向新开辟的结点。


        我们需要考虑两种情况:


        (1)原有的链表plist存有数据,将新插入的数据newnode连接在后面。


        (2)原有的链表没有数据,plist是一个空指针,需要将新开辟的结点视为第一个数据插入,所以需要将plist原本指向的空改为指向newnode。


![](https://img-blog.csdnimg.cn/d430a5fac2474518a056f4f28b8f8a9a.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_20,color_FFFFFF,t_70,g_se,x_16)  


        第一种情况比较好实现,我们只需要找到原链表的最后一个结点即可,最后一个结点的next必定指向NULL,将其作为判定条件,tail开始指向plist,判断tail->next是否为空;不为空,tail赋值为当前结点的next,直到找到tail->next为空停下来。停下来之后让tail->next=newnode。


![](https://img-blog.csdnimg.cn/ca269199b1d24c159e52a51ad40eb60f.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_20,color_FFFFFF,t_70,g_se,x_16)        newnode的next不用再次赋值给NULL,因为在开辟newnode的时候已经将其改为NULL了。


        当原有链表plist为空的时候,我们就得小心了。


        有人会说,那有啥注意的,不就是加个判断语句吗?


![](https://img-blog.csdnimg.cn/4adb67e79918410ab5723c1d6a3fe815.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_11,color_FFFFFF,t_70,g_se,x_16)


        那我们运行一下


![](https://img-blog.csdnimg.cn/367dc4e6627448bd97f506a3375f0dbc.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_15,color_FFFFFF,t_70,g_se,x_16)


        什么情况,怎么只有一个NULL,我明明插入了 1。


        调试看看


![](https://img-blog.csdnimg.cn/7d18a17387c64089a0ede66499dedf9c.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_20,color_FFFFFF,t_70,g_se,x_16)        这就奇怪了,难不成还是形参和实参的问题?


        但是我传过去的就是指针呀!


        没错,这还是传值调用,传过去的plist只是调用处的一份临时拷贝,并不是想要改变的plist的本身。不对呀,我传的是地址呀?是地址没错,但是我们将指向NULL的plist更改,plist本身就是指针;就好比以前我们传递的参数是int类型的变量a,如果采用int类型的变量传递,我们无法改变a本身,所以我们需要传递a的地址,就需要用int\*类型的指针来接收,然后才能改变int类型的a。此时我们要改变的是SLTNode\*类型的plist本身,就需要传递plist的地址,也就是指针的地址,那就是二级指针,因此我们接收得用SLTNode\*\*类型的二级指针接收。


![](https://img-blog.csdnimg.cn/9bcc91761fa844709a4b8b3b639a87e6.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_20,color_FFFFFF,t_70,g_se,x_16)


  有了以上的分析,那我们的代码就可以实现为下面这样:        



void SLTPushBack(SLTNode** pplist, SLTDateType x)
{
assert(pplist);
SLTNode* newnode=BuySLTNode(x);//创建新结点
SLTNode* tail = *pplist;
//空链表情况
if ( *pplist == NULL )
{
*pplist = newnode;
}
else//链表不为空
{
while ( tail->next != NULL )
{
tail = tail->next;
}
tail->next = newnode;
}
}


        此时我们进行尾插就不会存在问题了。


![](https://img-blog.csdnimg.cn/cdf3eef549c7458c86672fd2b5f32aa0.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_19,color_FFFFFF,t_70,g_se,x_16)



#### 2.3.3数据尾删函数


        尾删就是将链表中存储最后一个数据的结点释放掉,需要注意的是,我们需要将前一个结点的next更新为NULL,要不然就会出现空指针的问题。


        尾删要分为三种情况:


        (1)链表为空,一个结点也没有,此时应该报错,没有元素怎么删除,直接用assert报错。


        (2)链表只有一个元素,释放后链表为空,那plist就应该更改为指向NULL,需要更改plist本身,因此我们需要使用二级指针。


        (3)链表有多个元素,找到最后一个元素所在的结点,该结点的next指向NULL,释放掉该结点的内存,将前一个结点的next指向NULL。


        前两个比较好解决,关键是在有多个元素的时候如何找到最后结点的前一个结点呢?


        其实我们只需要创建两个临时变量就好了,一个用来找需要释放的结点,一个寻找前一个结点。


![](https://img-blog.csdnimg.cn/b36122b3175c4d69aa657e14bb84a590.gif)


  有了以上的分析,我们的代码如下:



void SLTPopBack(SLTNode** pplist)
{
assert(pplist&&*pplist);//*pplist==NULL表明为空链表,不需要删除
//只有一个元素
if ( (*pplist)->next == NULL )
{
free(*pplist);
pplist = NULL;
}
else//多个元素
{
SLTNode
tail = pplist;
SLTNode
pretail = NULL;
while ( tail->next != NULL )
{
pretail = tail;
tail = tail->next;
}
free(tail);
pretail->next = NULL;
}
}


        当然我们还有一种写法,原理和上面双指针的一样,只不过没有创建pretail。


![](https://img-blog.csdnimg.cn/888610765b3b4cfabc5aba28a82480b6.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_20,color_FFFFFF,t_70,g_se,x_16)


### 2.4数据的头部增加和删除


        只有尾增尾删远远达不到我们想要的目的,和顺序表一样,我们也需要头增头删。


#### 2.4.1数据头增函数


        数据的头增函数相比于尾增函数来说,实现方式比较简单。我们只需要创建新的结点将其放入即可,无论原链表是否存有结点,我们只需要将newnode的next指向原链表的plist,plist在重新指向newnode即可。


实现代码:



void SLTPushFront(SLTNode**pplist,SLTDateType x)
{
assert(pplist);
SLTNode* newnode = BuySLTNode(x);//创建新结点
newnode->next = *pplist;
*pplist = newnode;
}


#### 2.4.2数据头删函数


        数据的头删函数相对来说也比较简单, 和尾删一样,需要检查链表是否为空链表,空链表的情况下不需要删除。


        需要注意的是,我们需要先将plist指向plist的结点中next的地址保存到临时变量tmp中,然后才能将其释放掉,再将tmp中存放的地址赋值给plist。


![](https://img-blog.csdnimg.cn/57728fe5e6434b70a3dc9f869846bfe7.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_20,color_FFFFFF,t_70,g_se,x_16)


代码:



void SLTPopFront(SLTNode** pplist)
{
assert(pplist&&*pplist);//pplist==NULL表明为空链表,不需要删除
SLTNode
tmp = (*pplist)->next;
free(*pplist);
*pplist = tmp;
}


### 2.5查找和更改


        链表中的元素也需要查找和修改,查找函数还是和顺序表中类似的实现逻辑,找到了返回结点地址,找不到返回NULL指针。指针遍历方式与打印函数相同。



SLTNode* SLTFind(const SLTNode* const plist, SLTDateType x)
{
SLTNode* cur = plist;
while ( cur!= NULL )
{
if ( cur->date == x )
{
return cur;
}
cur = cur->next;
}
return NULL;
}


        修改函数不需要特定的编写,我们通过查找可以得到需要修改元素的存储位置的指针,直接通过指针就可以修改数据。


![](https://img-blog.csdnimg.cn/86c9be8d69d84ff48c39c525c85b590b.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_20,color_FFFFFF,t_70,g_se,x_16)


### 2.6任意位置之前 / 之后的插入


        任意位置的插入分为两种,在提供的位置pos的前面插入一个结点,或者在pos之后插入一个结点。两种插入方式看似差不多,实则不然,两种方式的实现方式以及效率都截然不同。


#### 2.6.1任意位置之前插入函数


        既然要将一个数据放到pos的前面,那我们就得从头遍历,找到pos的位置,然后将newnode的next指向pos,将原本pos之前的结点的next变更为newnode。


        还有一种情况,那就是pos指向的位置就是链表开头,此时实际上就是头插,我们只需要调用即可。当pos传过来是NULL但是plist不为NULL时,此时视为在最后进行数据插入(也可视为传参错误,进行报错)。


![](https://img-blog.csdnimg.cn/8eea929e7de749c0a81af2c71f480e5e.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_20,color_FFFFFF,t_70,g_se,x_16)


最终我们实现的代码如下所示:



void SLTInsertBefor(SLTNode** pplist, SLTNode* pos, SLTDateType x)
{
assert(pplist);
//assert(pos);//pos为NULL视为报错
SLTNode* newnode = BuySLTNode(x);//创建新结点
if ( pos == pplist )//头插
{
SLTPushFront(pplist, x);
}
else
{
SLTNode
cur = *pplist;
while ( cur->next != pos )
{
cur = cur->next;
}
cur->next = newnode;
newnode->next = pos;
}
}


#### 2.6.2任意位置之后插入函数


        任意位置之后的插入函数比起任意位置之前插入的函数来说简直容易多了,而且不需要进行遍历,也不需要更改原链表的指针plist。


        如果原链表指针为NULL,我们进行报错,原链表一个元素都没有怎么在元素之后进行插入呢?


        至于其他情况,我们只需要将newnode->next改为pos->next,再将pos->next改为newnode。


![](https://img-blog.csdnimg.cn/b45a9c668a884eecaae19405e66fd598.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_20,color_FFFFFF,t_70,g_se,x_16)


        我们发现整个过程不需要改变plist的指向,也不需要遍历链表。即便是在最后一个结点后面增加,也毫无影响,只不过是pos->next为NULL,插入后newnode->next指向NULL ,pos->next指向newnode。所以我们函数的参数只需要pos和数据x即可,不需要接收plist地址的二级指针。


        我们会清楚的发现,任意位置之后的代码时间复杂度相比于任意位置之前的代码简化的太多,任意位置之前的删除时间复杂度为O(N),任意位置之后的删除时间复杂度为O(1)。


代码:



void SLTInsertAfter(SLTNode* pos, SLTDateType x)
{
assert(pos);
SLTNode* newnode = BuySLTNode(x);//创建新结点
newnode->next = pos->next;
pos->next = newnode;
}


### 2.7任意位置的删除和任意位置之后的删除


        有了任意位置的插入我们还需要任意位置的删除,但有些人肯定会疑惑,我都可以任意位置的删除了,干嘛要写个任意位置之后的删除函数呢?我们通过两个函数的对比来回答这个问题。


#### 2.7.1任意位置的删除


        任意位置的删除,分为四种情况:


        (1)pos==plist,即删除plist指向的链表的第一个元素,实际上就是头删,调用头删即可。


        (2)plist==NULL,链表没有元素,不需要删除,直接报错(也可不单独检测,因为如果链表为空,则pos必为空,可以在检测pos时一同检测)。


        (3)pos==NULL,删除NULL指向的结点?不存在,报错。


        (4)pos为正常值,将pos指向的next保存到tmp,然后释放掉pos结点,最后将pos之前的结点的next赋值为tmp。


![](https://img-blog.csdnimg.cn/b97e72902eec4a7e9bc7664539744b95.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_20,color_FFFFFF,t_70,g_se,x_16)


 代码如下:



void SLTErase(SLTNode** pplist, SLTNode* pos)
{
assert(pplist&&*pplist&&pos); //pplist == NULL表明为空链表,不需要删除
//pos==NULL无法删除指定节点
SLTNode
cur = pplist;
if ( cur == pos )//删除第一个–>头删
{
SLTPopFront(pplist);
}
else//多结点情况
{
while ( cur->next != pos )
{
cur = cur->next;
}
SLTNode
tmp = cur->next->next;
free(cur->next);
cur->next = tmp;
}
}


#### 2.7.2任意位置之后的删除


        任意位置之后的删除和任意位置之后的插入极其相似,我们不需要二级指针pplist来更改plist,也不需要遍历链表找删除的元素,我们只需要保证pos不为NULL以及pos后面有元素可以删除即可。


![](https://img-blog.csdnimg.cn/e7d00e4c658c4511bb00e3c6b473b27a.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_20,color_FFFFFF,t_70,g_se,x_16)


实现代码:



void SLTEraseAfter(SLTNode* pos)
{
assert(pos&&pos->next);//pos为NULL或者pos后面没有元素可删除,报错
SLTNode* tmp = pos->next->next;
free(pos->next);
pos->next = tmp;
}


        我们发现任意位置之后的删除的时间复杂度依然为O(1)任意位置的删除则为O(N)。所以通常使用任意位置之后的插入和删除。


### 2.8链表销毁


        链表的销毁函数就是将链表中的每个结点释放,避免照成内存泄漏。我们只需要创建一个指针cur对链表进行遍历,然后将cur的next预先保存到next指针变量中,释放掉cur所指向的结点,然后cur在走到下一个结点。当cur->NULL时,说明链表结点全部释放完毕,然后将调用方的链表指针置为NULL即可。


代码:



void SLTDestory(SLTNode* plist)
{
assert(plist);
SLTNode* cur = plist;
while ( cur != NULL )
{
SLTNode* next = cur->next;
free(cur);
cur = next;
}
}



## 三、主逻辑函数框架和测试结果


        主逻辑函数就是为了调用各功能所搭建的主体框架,该框架的搭建仅凭个人喜好进行搭建,下面展示的是博主自己搭建的主逻辑代码的运行视频,具体链表代码已上传gitee仓库,如有需要可以自行下载。



> 
> 单链表运行视频:[运行视频](https://bbs.csdn.net/topics/618668825)
> 
> 
> 代码仓库地址:[单链表实现代码](https://bbs.csdn.net/topics/618668825)
> 
> 
> 


获取数据函数与数据更改函数:



SLTDateType Get()
{
printf(“请输入你要插入的数字:\n”);
SLTDateType x = 0;
scanf(“%d”, &x);
return x;
}

SLTNode* find(SLTNode* plist, SLTDateType* y)
{
SLTDateType x;
printf(“请输入参考位置数据和在此插入或更改的数据:\n”);
scanf(“%d %d”, &x,y);
SLTNode* ret = SLTFind(plist, x);
return ret;
}

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

SLTDateType* y)
{
SLTDateType x;
printf(“请输入参考位置数据和在此插入或更改的数据:\n”);
scanf(“%d %d”, &x,y);
SLTNode* ret = SLTFind(plist, x);
return ret;
}

[外链图片转存中…(img-0l4X168z-1715593710441)]
[外链图片转存中…(img-vq64PYp7-1715593710442)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 7
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值