前言:
本篇文章的语言:C
建议:在对顺序表和单向的链表有比较好的熟练度之后,再看本篇文章会好很多,
当然如果你只了解了顺序表的实现,当然也可看这篇文章。每日一句:
”低头看路,敬事如仪“
文章中给出的是代码的截图,源代码的链接,放在最后总结处,需要的自取
什么是带头双向循环链表
我们在开始用代码去实现该链表之前,先来了解一下带头双向循环链表的含义是什么。
首先先来解释一下什么是带头,这里的头指的是头结点,头结点是指向链表第一个结点的结点,它的next指向的就是链表的第一个结点,但是注意,头结点不属于链表,在计算链表的长度时,也不纳入计算。(如下图)
如果你之前并没了解过链表,这时就有人疑惑了,什么好端端的链表要给它加一个”头“呢?
这是因为,有了头结点之后,我们在后续的某些位置进行插入的时候,可以方便很多,不需要为空指针的问题担忧
现在不理解每关系,我们后续在实现该链表的功能时,还有具体来说。
什么是双向?
双向的意思即是,一个结点有两个指针域,前驱指针和后驱指针,前驱指针指向的是该结点的前一个结点的位置,后驱指针指向的是该结点的后一个结点的位置。
如果你之前了解过链表的话,应该会知道单链表只有一个指针域,这个指针域存放的是下一个结点的位置,这也是单向链表的一大缺陷,而且在相关的函数实现上也增加了一些困难的情况,但是双向的链表就不会,特别是带有头结点的,操作可谓是简简单单呐!
循环的意思便是,链表像一个环一样,首尾相连接。
在大概了解带头双向循环链表是怎么一回事之后,接下来我们就来开始实现该链表。
初始化
对于带头双向循环链表的初始化还是很简单的,我们只需要创建一个头结点,之后的操作便是对头结点的插入和删除。
这里就直接给出.h文件的所有声明了。
下面的就是链表的初始化部分。
这里将BuySLNode封装成一个函数的目的是为了之后在插入结点的时候减少代码量,不理解就顺着向下看。
头插和头删
OK,老铁们我们来看第一个功能,在头部进行插入和删除。
在所有的位置进行插入和删除时,我们只需要改变相应的指针的指向即可,下面使用画图来解释
可以看到我们只需要将第一个结点和头结点之间的联系断掉,然后添加新的结点newnode链接起来即可,对应的代码实现也是比较简单的。
大家先不用管最后被注释掉的代码。
后面也会解释的。
删除也是差不多的原理,即先找到第一个结点,然后绕过第一个结点,将头结点和第二个结点链接起来,再释放掉第一个结点即可
代码:
尾插和尾删
在理解了头插和头删之后,尾插和尾删也是同样的道理,通过头结点就可以直接找到尾结点(因为该链表是双向的)。
尾插:
尾删:
特定位置的删除和插入
在特定的位置删除和插入也没什么稀奇的,原理和上面的一样,但是巧妙的是,上面所将的头插头删和尾插尾删都可以通过调用特定位置的删除和插入这两个函数来实现。
特定位置插入
特定位置删除
所以我们在尾插尾删和头插头删的时候,只需要在调用这两个函数时给好参数即可。
这里要提一下的是,我们实现的在特定位置之前的插入,所以在调用尾删的时候,并不是给的尾结点,而是尾结点的后一个结点也就是头结点
链表的打印
链表的打印就很简单了,是需要遍历链表打印数据即可,值得注意的是,循环的结束条件,因为头结点纳入真正的链表,所以我们要从头结点的next开始遍历,当遍历到最后一个结点之后,再遍历时,就是头结点了,所以循环的结束条件就是不等于头结点
代码:
查找结点
这里我实现的查找功能是,输入一个结点的位置,判断该结点是否存在,存在返回该结点, 不存在返回NULL;
所以这个函数就需要有返回类型
循环结束的条件和打印的是相同的道理
链表的判空
判断链表是否为空的条件就是,看头结点的next是否是它自己,如果是,说明链表中除了头结点,没有其他的,此时就是空表,反之则不是 空表
链表的长度
设置一个计数器,遍历链表,返回长度即可
销毁链表
销毁链表时,就需要一个一个结点进行释放,之后再释放掉头结点
循环还是一样的,从第一个结点开始,这里要注意一下,我们不能先释放掉cur,而是先记录cur->next再进行释放cur,因为先释放cur的话,就找不到cur->next了。
总结
源代码地址:带头双向循环链表
我们对比该链表和单链表,是不是在设计上要简单很多,这时候我们再来谈为什么带头的原因,试想一下,如果不带头结点的话,我们在插入时,是不是需要判断一下前一个是不是空指针,如果是的话,就要通过if…else来控制空指针的情况,带上头结点之后就不会出现这种情况,因为头结点一直不可能为空。
带头双向循环链表也是最贴近我们现实生活需求的一个数据结构,完美的解绝了单链表和顺序表的缺点,当然也不是说单链表和顺序表就没用了,比如顺序表的下标可以随机访问,单链表的尾插和尾删更快,这都是它们各自的优点,而且后面学了其他数据结构之后,大家对这一点理解会更加深刻!