客官先看看笔者的实现
这篇文章来讲述线性表中的单向循环链表
,完整的实现请自行导航到gitee托管仓库。
传送门:https://gitee.com/prexer/data-structure-and-algorithms
所有的样例,都可以通过如下方式进行编译和检测:
$ git clone https://gitee.com/prexer/data-structure-and-algorithms.git
$ cd data-structure-and-algorithms/liner_list/single_circular_linked_list
$ make run
循环列表是否需要重新抽象?
答案看起来是否定的
,因为循环列表和非循环列表的数据抽象是一样的,所以我们不需要重新来抽象数据类型的属性,依然如下:
typedef int ItemType;
typedef struct NODE {
struct Node * next;
ItemType value;
} Node;
没错,数据类型的属性并没有变化,但循环列表的特殊,就在于操作它的方式,确切的说一切都始于插入操作,它让链表的结尾不再是NULL,而是表结构的开始元素。
填在给我们的实现添加约束:
- 表结构是有序的,从小到大排列
- 表结构
没有表头节点
,仅仅有一个记录进入表的指针,任何一个
指向表节点的指针,都可以作为头指针
- 表中的元素允许重复
看起来是不是很简单,其实实现起来还是需要点功底,不信你自己试试?
实现单向循环列表的插入操作
同样的,我们并不给出完成的程序结构,而是将插入操作的实现放在文章中分析,套路依旧是:
- 用自己的语言描述出来你要实现的内容
- 看看笔者的笨拙实现
- 小小的分析和总结
如何描述单向循环列表的插入操作?
脑袋里有东西后,需要让它能够用自己的语言说出来,这样学习才会高效。
对于插入操作,我们知道,由于程序会在插入操作的时候修改头指针,指针的指针必不可少。
那么函数原型应该描述为:
插入操作接收一个指向头指针的指针和一个待插入的新值,并返回表示操作是否成功的布尔值。
在实现代码之前,笔者总是会在纸上或者脑袋里进行分析和演算,这样做有很大的帮助,其实计算机行业真正具有价值的部分是计算思维
,而不是你动手敲代码的快感,如果你能保证自己写的代码经过很少的调试就能完全符合需求,那你就是天才。而像我这样的普通人,一定要有腹稿才行,想好了再写才是快速的方法。
顺便说一句,写代码,敲击键盘只是体力劳动
,而程序员的工作应该是脑力劳动
才对。
插入操作最难的地方就是你要知道插入的位置是否特殊,所以我们分为:
- 插入的位置在头部(这里说头部并不准确,应该说是头指针指向的那个节点,毕竟我们说了,头指针可以换)
- 插入的位置在非头部,也就是其他的位置,哪怕表中只有一个节点,依旧符合这个规律
在实现之前,你要知道为什么这么区分?
因为我们是循环链表,一旦头部节点变化的时候,指向它的节点都要变化,而指向它的节点有两个:
- 一个是头指针
- 另一个是尾巴节点的next指针
掌握了这个,赶快取实现吧!
实现的代码
static Node ** get_tail(Node ** linkp){
Node * current = *linkp;
Node * head = *linkp;
int counter = 0;
if (current != NULL){
while (! (current == head && counter != 0)){
linkp = &(current->next);
current = *linkp;
counter++;
}
return linkp;
}else{
return NULL;
}
}
int scll_insert(Node ** linkp, ItemType new_value){
Node * current = *linkp;
Node * head = *linkp;
Node ** orig = linkp;
int counter = 0;
if (current != NULL){
while (! (current == head && counter != 0) ){
if (current->value >= new_value){
break;
}
else {
linkp = &(current->next);
current = *linkp;
counter++;
}
}
}
Node * new = malloc(sizeof(Node));
if (new == NULL)
return ERROR;
new->value = new_value;
if (current == NULL){
new->next = new;
*linkp = new;
}else{
if (current == head && counter == 0){
Node ** tail_of_next = get_tail(orig);
new->next = current;
*orig = new;
*tail_of_next = new;
}else{
new->next = current;
*linkp = new;
}
}
return OK;
}
让你的脑袋动起来
写了这么多代码,其实真正能剩下的,基本都是你总结过的,并且面向一定问题的记忆,这种东西人们称为经验;而往往这种东西是没有办法快速获得的,都要经过思考和沉淀才可以获得这些记忆印记。
那我们就开始思考这个实现,然后想想有什么可以总结和归纳的。
首先,就像笔者之前提及的那样,C语言中的函数调用
都是值传递
,所以,函数内部的形参变量你随便该它本身,并没有影响;同样的套路也适合于在函数中定义的临时变量
,通常他们被申请在栈空间
上,你就算修改了他们也不会对外界有影响,这也是函数式编程常用的技巧。
由于我们需要一个可以随着迭代前进
的变量,所以定义了一个current
,你可以随便修改它。
由于我们没有头节点,只有一个头指针,所以我们定义了定义一个记录头部的位置指针,这样绕一圈,回来的时候,你知道在哪里可以停止,这就是head
的用法。
orig
用来在第一个节点替换或者修改的时候,来修改头指针的,这也是为什么它是指针的指针。
还有一个重要的变量是counter
,这是一个技巧,用来判断遍历是否进行的标志,如果没有进行它就是0
。
定义好变量后,就是要注意的细节
- 指针一定要在解引用之前判断是否是NULL,这是常识,这也是判断表是否为空的标志
while (! (current == head && counter != 0) )
这个语句值得考究,首先笔者是这样思考的- 首先我们判断什么时候应该结束,也是正常思考的方向,停止条件其实有两个,一个是绕一圈回来了,另一个是找到了匹配的位置
- 而这里要注意,这量个条件是有先后顺序的,先判断什么,后判断什么是很重要的,所以
if (current->value >= new_value)
在while内部
- 一定要记得让自己的程序趋近于终止条件,如果没有就回去检查,你可以看看我们程序中是如何趋近终止的
处理完这部分之后,重要的部分就是待插入节点应该插入的位置,其实他有三种情况:
- 空表插入
- 在表的第一个节点之前插入
- 在表的非第一个节点之前插入
这里推荐一个好的思考方法:
- 用笔在纸上画出简单的链表链接图,一定要注意标注指针和箭头
- 对于插入操作来说,有两个步骤,一个是插入之前的图示,一个是插入之后的图示,一定要画出来
- 对于插入后的图中,你要修改的规则是
- 把new指向的部分先在程序中实现(这里先后顺序很重要)
- 把指向new的节点按照不会被覆盖的顺序修改
- 再数一遍箭头,看是否有漏掉
完成这些,基本上你的程序就不会错。
再次提醒,这里有一个函数是get_tail
获得指向尾节点next指针的指针,函数中都是用的临时变量和实参拷贝,所以可以随便修改;而且第一个节点切换的时候,尾巴一定要跟着修改
!
其他相关的操作实现细节解释
scll_locate
依旧是需要:记录第一个节点的head和counter计数器,然后定义一个推进的变量就完成了。
scll_delete
是需要注意一点,它分为三种情况:(这里已经判断过表为空的情况,下面不考虑这个)
- 如果表中只有一个节点,并删除这个唯一的节点,这个节点被删除后,需要把
*rootp
设置为NULL
,一定要注意,环型链表,只有一个节点的时候,自己的next会指向自己,这样*rootp = record->next
就无法工作了,所以不能和下一个条件写在一起 - 如果删除的是一个并非只有一个节点的列表中第一个节点
- 如果删除的是一个并非只有一个节点的列表中非第一个节点
比较投机取巧的实现是下面两个函数
- scll_modify: 它利用了scll_delete和scll_insert来完成处理
- scll_destroy: 则是利用了scll_delete来逐个删除
好了,就是这些看似简单的东西,需要注意的还是蛮多的,最后多说一句,一定要画图哦。