学海无涯,苦作舟啊!
老铁们加油
源代码放在总结处,需要的同志可以直接跳转到最后
在学习完顺序表之后,我们就来学习一种新的数据结构–单链表,关于链表,其实有很多种,单链表,双链表,循环链表 …等等。
在学习完顺序表之后,我们仔细比对它与数组之间的区别,其实并不是很大,所以在实现顺序表的时候,相对的容易很多,但是上一篇文章的结尾我们也说了,顺序表在内存中是连续存储的,不仅仅很容易造成空间碎片,而且有时在插入的时候,需要移动大量的元素,这无疑造成了效率降低。
那么此时,计算机的前辈们就想,能不能把内存中那些零散的空间利用起来,这时,我们的链表就来了。
回想一下我们在实现顺序表的时候,是通过“下标(size)”来找到要存储和删除元素的,那我们在链表中也是用一个类似下标(其实就是指针,后面解释)的东西来记录下一个元素的地址,这样就不必需要各个元素是连续的了,我们通过指针就可以找到下一个元素的位置,进行访问了。
示意图:
(注意这里我画的箭头都是单向的,后面讲到双向链表的时候,会对其修改,我们先从最简单的单链表开始)
所以我们现在了解到,单链表需要一个data的数据域和一个指针域,最后一个指针域的指针指向NULL,表示链表的结束。一个数据域和指针域组成的结构称为结点
那么遍历一遍链表,即可知道链表的长度。
通过修改指针的指向即可完成插入和删除(后面在实现各种函数的时候,会讲解),时间复杂度为O(1)!是不是比顺序表的O(n)好多了。
单链表对内存空间的要求很低,即使你的链表很长,链表也能创建成功,那么就有人问了,难道链表不会创建失败吗?
当然不是,但是想象一下,当我们的链表创建失败是什么场景,内存中一点零散的空间都没有了!!这是很危险的,这时候的电脑很容易造成崩溃,还谈什么链表呢。
**说到这,相比大家也就明白了一点,和顺序表不同的是,链表是不需要静态这种结构的,链表的结点时按需创建的,也就是你用多少我就给你多少。**这也是相比于顺序表的优势之处。
好了,了解完链表的基础结构之后,我们就来手撕单链表把!
前面说到需要数据域和指针域两个变量,所以我们还是使用结构体来创建一个结点(这一点在后面的数据结构的讲解中就不再说了)。
首先假设创建了一个长度为5的单链表,那此时就需要有一个函数(数据结构种这种函数也成为接口),来实现创建单链表的操作。
单链表的创建分为两步
1.生成结点
2.将结点链接起来(串联起来)
所以我们封装两个函数,一个负责实现生成(BuySLnode),一个负责实现链接(CreatetSL)
因为考虑到后面的插入时,还需要BuySLnode函数来生成结点,所以我们将创建链表封装成两个函数,以此来减少代码量。
打印链表的实现没什么好讲的,只需要遍历链表,当结点的指针域为NULL 时就停止遍历即可。
OK,我们现在来测试一下这两个函数是否能达到我们的预期
运行截图:
OK那么我们接着往下走
接下来要实现的是尾插链表,这个函数的实现我们会解释的相对清晰一点,因为很多初学者不明白这个要使用二级指针。之后的函数讲解会相对的快一些。
想来讲解一下思路,要想在链表的尾部插入一个新的结点,首先我们要先遍历整个链表,找到表尾,之后将表尾的指针域指向新结点的地址,然后将新结点的指针域指向NULL
思路很简单,但是我们要想明白一件事,就是我们要对原来的链表进行修改,那这时候我们就要传原来链表的地址过去,而不能将原来链表的地址作为值进行传参(因为我这里实现的函数都是void类型的,所以要使用二级指针进行传参),这样链表才能被修改。
然后在插入时,我们同样需要思考一件事情,如果当前链表为空的话该怎么进行插入,这时就有同志说,简单啊!按照上面的方式直接插入不就行了吗。
天真了!老铁
错误代码:
如果链表为空的时候,我们还这样写,那么就是对空指针的使用了!
正确的做法应该是在前面加上判断是否为空链表的判断
正确代码:
这才是完整的尾插,大家结合我的注释应该能看的懂这一过程,如果对你来说有点吃力,那建议反复查看,特别是为什么使用二级指针这一部分,后面的函数的讲解就会加快一些节奏了。
大家先看一下代码,然后我来解释,有疑惑没关系,接着往下看就得了。
首先删除的链表肯定不能为空,所以进行断言一下,保证代码的安全性。
然后我们先来看正常的删除的情况
第一步先找到倒数第二个结点的位置
然后将该结点指向NULL
释放最后的结点
但是如果链表的长度为1呢?
这时根本不存在倒数第二个结点,这时就要在前面加一个判断的条件了,如果当前链表的长度为1的话,就直接释放结点,并将头指针(指向第一个结点的指针称为头指针)改为NULL。
对于链表长度为1的情况,还是有一种解决的办法,用另外一个指针来记录cur的前一个结点的地址,这时候只需要对前一个结点进行修改即可,不需要再进行链表长度的判断了。
因为头插链表和头删链表的操作相对的来收,算是比较简单的,所以我们放在一起来讲,首先我们还是先来了解一下思路。
我们先来看头插的实现思路。
要想在链表的第一个结点位置插入一个新的结点,那么这个新的结点将取代旧的结点,那么就需要将新结点的next指向原来的结点,然后再改变结点的地址,见下图:
代码:
因为条件的原因,后半部分的代码都以这种图片给出,但是源代码还是不变,链接放在最后了。
接下来我们再来看一下头删的实现。
其实也很简单,只需要将头结点赋值给第二个结点,然后释放掉第一个结点就好了,然后用assert断言一下空表的情况。
这里我们实现的查询链表的功能是,如果找到了这个结点,那么就把这个结点的地址返回,如果查找失败的话就返回NULL。
思路也很简单,遍历链表即可,没什么难度。
同样的,大家要注意点空表的情况。
接下来的两个功能的实现,稍微有一丢丢的复杂,但是肯定还是能理解的。
这里我们在特定位置的后面进行插入新结点,之所以不在前面的是因为,如果想要在特定位置的前面进行插入,就需要先遍历整个链表找到前一个结点的位置,是比较浪费时间的。
功能要求是,输入一个结点,然后在这个结点之后的位置进行插入新的结点。
需要考虑的意外情况就是,输入的结点不能为空,所以我们需要断言一下。
先用一个指针cur记录下pos之后的那个结点的位置,然后插入新结点,然后让新结点指向cur。
同样的顺序不能乱!!
特定位置的删除也比较容易,只需要将该位置的前一个的结点指向该位置的下一个结点,然后再释放掉该结点就OK了。
当我们要删除的结点是第一个结点时,上面所说的思路就不行了,因为第一个结点前面并没有其他的结点了,这时候我们只需要调用头删的函数就好了,尾结点的删出,上面的方法依然有效,所以只需要看情况条用头删即可。
最后便是销毁链表,依次释放所有的结点,然后指针指向空即可。
先给出源代码的地址链接:代码地址
我们来回归一下从一开始的创建单链表和各个功能函数的实现,其实无非就是通过控制next指针来完成主要的插入和删除,其中有些功能的实现的顺序并不能乱,当大家自己写一遍单链表之后,也就能记住了,关键是如何从无到有的这个思路是怎么来的,如果大家对上面的思路和代码有自己的见解和优化的话,之后一定要尝试修改一下。
单链表只是链表结构中最简单的一种结构,只有真正的掌握了单链表的实现,才能继续向下开展新的学习!