先吐槽一下:前段时间在好兄弟的帮助下,敲完了学生成绩管理系统,当时感觉链表真滴好难啊。
不过敲完这个管理系统,也对链表入了个门。近几天前开始学数据结构,有了前面的基础感觉链表理解起来也不是很难。
我总结了下我目前接触到的关于链表的基本操作,主要的操作其实也就“增删查改”。以后肯定还会再更新的,感觉链表挺灵活的,操作起来会挺多变的。
建议看的每个函数时时候和总体的代码合起来看,这样就不会对那些参数感到混乱,总体的代码放在文末
为了小伙伴们能快速get到每个函数的功能,下面的那些函数名特意起得那么长
OK,那就先上一张自制的思维导图(用Word做的,有点难看,将就着看吧)
(看到上面的思维导图你应该有一眼就能看出如何去处理的操作吧,我猜你肯定也有一时半会想不出的,可以点击目录直达不会的地方哦。毕竟这是长长长长文,慢慢看过去肯定会看烦了。)
OK,少说废话,直接开始。
思维导图目录
一、 建立链表
首先我们先定义下结点(数据域就整这两个简单的吧)
//结点的定义
typedef struct node
{
char name[10];//姓名
char num[10];//序号
struct Node *next;
}Node;
Node *Head= NULL;//在main函数外,全局变量,代表头指针,不是头结点哈
Node *Rear=NULL;//在main函数外,全局变量,代表尾指针,不是尾节点哈
就我个人而言,我喜欢利用全局变量来建立一个头指针,然后在给链表增加信息时在头结点上慢慢延伸出去。这样就建立起了链表。不过是在增加信息的时候建立的而已,这里先做个铺垫。
题外话:本来这里我想写关于链表的初始化的,但我感觉会让很多小伙伴看懵了(其实我也有点懵),因为那要用到二级指针了。其实就我个人而言,我很少对链表进行那样的初始化。所以也就没写(主要是怕写错了,误导了大家),等我真正理解了二级指针初始化链表在更新一下,总感觉少了二级指针初始化链表是不完美的,大家等我更新吧
二、 插入
插入操作这里就是增加链表的结点嘛,情况也比较多,我先把我遇到的情况按下面例子一一阐述。
(1)头插法
用头插法建成的链表,输出时刚好和输入顺序相反,下面用图例形象的说明我所遇到头插法的不同情况。你们看的时候可能会疑惑,那就是为何空链表时我这里为何没有头结点,其实啊,这与我上面定义的全局变量有关,再翻上去看看吧,哈哈哈。
要注意的是头插法的两种情况:空链表和非空链表
OK,话不多说,先来张自制图
因为我没在图上画上操作代码,所以看图的时候建议和下面的代码一起看。
下面由上图来编写头插法代码
//头插法函数
void InsertFromHead()
{
//先赋值
char name[10];
char num[10];
printf("Please input one name:\n");
scanf("%s",name);
printf("Please input one num:\n");
scanf("%s",num);
Node *p;
p = (Node*)malloc(sizeof(Node));
strcpy(p->name,name);
strcpy(p->num,num);
p->next = NULL;//防止指针乱指
//头插法的两种情况,对应了上图的两种情况
if (NULL == Head)//空链表
{
Head = p;//在这里头指针成了头结点
Head->next = NULL;
}
else
{
//新节点的下一个指向头
p->next = Head;
//头向前移动一个
Head = p;
}
}
我写的代码里面也有注释,别忘了看呀,这里确实有点难理解,不过看我画的图也不会那么难理解的。
(2)尾插法
其实尾插法我觉得是最简单的增加链表的方法了,这里我觉得大家都能顾名思义了,不过呢,为了让大家更好理解(嘿嘿,其实是方便我以后复习),我决定还是画图来说明下。和头插法一样有两种情况。
OK,话不多说,先来张自制图
上面这张图是空表的情况下,进行尾插入。其实可以类比头插法的空表情况。结合代码看会更加容易理解,这画图的不太好操作,望见谅没把操作代码画上去。
再来一张不是空表情况下的尾插法图示
上面这张图试着标了下操作序号,以及关键性的代码。结合下面的代码看这张图更容易理解。
//尾插法函数
void InsertFromRear()
{
char name[10];
char num[10];
printf("Please input one name:\n");
scanf("%s",name);
printf("Please input one num:\n");
scanf("%s",num);
Node *p;
p = (Node*)malloc(sizeof(Node));
strcpy(p->data,data);
strcpy(p->num,num);
//尾插法
if(NULL == Head)//空表情况下
{
Head = p;
Rear = p;
}
else//不是空表时尾插
{
Rear->next = p;
Rear = p;
}
Rear->next = NULL;
}
(3)中间插入
这里中间插入,我们要做到就是先找到我们要插入的位置。所以可以先去看看下面的“查找”再来看这里。
(3.1)指定结点前插入
看这个前建议先理解“在指定结点后插入”,因为我接下来要操作的就是,先将新结点接到指定结点后面,然后将他们的数据域的值互换即可达到在“指定节点前插入”的效果。有些小伙伴可能想不太通这里,欢迎私信我,我教你啊,哈哈哈。
OK,话不多说,上代码
//在指定结点前插入
void InsertBefore(Node *p)//这里的参数是从查找函数中得来的,具体看合并后的代码
{
//先赋值
char name[10];
char num[10];
printf("Please input one name:\n");
scanf("%s",name);
printf("Please input one num:\n");
scanf("%s",num);
//创建新结点信息
Node *pNew,*pt;
pNew = (Node*)malloc(sizeof(Node));
strcpy(pNew->name,name);
strcpy(pNew->num,num);
pNew->next = NULL;
//先插入到指定节点后面
if(Rear == p)//当指定结点是尾结点的时候
{
Rear->next = pNew;
Rear = pNew;
}
else
{
pNew->next = p->next;//先连后断
p->next = pNew;
}
//再交换数据域中的数据
// pt = (Node*)malloc(sizeof(Node));
//下面这里特意这样写,直观地感受交换数据
strcpy( pt->name , p->name); strcpy( pt->num , p->num);
strcpy(p->name , pNew->name); strcpy(p->num , pNew->num);
strcpy(pNew->name , pt->name); strcpy(pNew->num , pt->num);
}
其实和在指定位置后插入是差不多的,就是在后面多了个数据交换。理解了在指定结点后入,理解这个那肯定是水到渠成。
图解和在指定结点后插入是一样的,我就不画图了。
(3.2)指定结点后插入
首先我们得先找到我们所需操作的结点位置,所以先查找一下,返回一个指定结点的地址。再将这个地址传进插入函数中。OK,先上代码看看
这是在main函数中的操作
//main函数中的大致操作如下
Node *pmain;//用来保存指定节点的地址
pmain = FindListByNum();//当然也可以使用Node *FindListByName()和Node *FindListByRank(),
//在合并后的代码那里我会处理下这里的选择何种查找方法,小伙伴们也可以去看看
InsertAfter(pmain);
这是插入函数
//在指定位置后插入
void InsertAfter(Node *p)
{
//先赋值
char name[10];
char num[10];
printf("Please input one name:\n");
scanf("%s",name);
printf("Please input one num:\n");
scanf("%s",num);
//创建新结点信息
Node *pNew;
pNew = (Node*)malloc(sizeof(Node));
strcpy(pNew->name,name);
strcpy(pNew->num,num);
pNew->next = NULL;
//分析不同情况
if(Rear == p)//当指定结点是尾结点的时候
{
Rear->next = pNew;
Rear = pNew;
}
else
{
pNew->next = p->next;//先连后断
p->next = pNew;
}
//这里可能有小伙伴会有疑惑了,为什么不用判断空表的情况,其实这就要回到我们的查找了,其实在那里就已经将空表的情况给分析到了
}
再来张图帮助你们理解下
三、 输出链表
这个真没啥可说的,就是遍历链表,然后一个一个输出就OK啦。
上代码
//输出链表
void PrintList()
{
Node *p = Head;//从头结点开始
if (NULL == p)//先判断是不是空链表
printf("The list is empty!\n"); //空链表时的提示信息
while (p != NULL)
{
printf("name is %s,num is %s\n",p->name,p->num);
p = p->next;//结点下移一个,遍历链表
}
}
四、修改
这个又要用到查找函数了,我们先用查找函数找到要修改的结点,然后将他的地址传给修改函数就可以修改了。
OK,话不多说,上代码
//修改指定结点
void ModifyNode(Node *p)//这里的参数是从查找函数中得来的,具体看合并后的代码
{
//输入想要改成的数据,重新赋给指定结点
char name[10];
char num[10];
printf("Please input one name:\n");
scanf("%s",name);
printf("Please input one num:\n");
scanf("%s",num);
strcpy(p->name,name);
strcpy(p->num,num);
}
五、查找
我认为查找是个辅助函数,单独用没什么灵魂,一般和中间插入函数、修改函数、删除函数等一起使用才更能凸显这个函数的用处。我们可以按值查找,也可以按序号查找。
(1)按序号查找
下面这里我就先以按序号查找为例给出代码。
注意哈:我这里说的按序号查找不是按我前面定义的num来查找哈,而是第几个的意思。
直接上代码
//按序号查找某个节点
Node *FindListByRank()//用Node *是因为这里要返回一个结构体指针 ,所以是用Node
{
int i=0;//作为计数器
int n;//需要查找的序号,就第n个结点信息
Node *p;
printf("Please input the rank of the node you want to find:\n");
scanf("%d",&n);
p = Head;
while(p!=NULL)
{
i++;
if(i==n)
{
return p;
}
else
{
p = p->next;
}
}
printf("No information about the node!\n");
return NULL; //如果没查到,就返回空值,作为main函数中判断结束的条件
}
(2)按数据查找
再来按num值查找的和按name值查找的代码
//按num值查找某个节点
Node *FindListByNum()//用Node *是因为这里要返回一个结构体指针 ,所以是用Node
{
Node *p;
char num[10];
printf("Please input the num of the node you want to find!\n");
scanf("%s",num);
p = Head;
while (p!=NULL)
{
if(0 == strcmp(p->num,num))
{
return p;
}
p = p->next;
}
printf("No information about the node!\n");
return NULL; //如果没查到,就返回空值,作为main函数中判断结束的条件
}
//按name值查找某个节点
Node *FindListByName()
{
Node *p;
char name[10];
printf("Please input the name of the node you want to find!\n");
scanf("%s",name);
p = Head;
while (p!=NULL)
{
if(0 == strcmp(p->name,name))
{
return p;
}
p = p->next;
}
printf("No information about the node!\n");
return NULL; //如果没查到,就返回空值,作为main函数中判断结束的条件
}
关于查找就这些东西了,这应该是链表操作里面最简单的了。无非就是设置好判断条件,找到了就返回值,没找到就往下走直到走到尽头。
六、删除
思维导图里关于删除的情况是不是贼多,怂了没?是不是不想学了,哈哈哈,这里刚开始确实会有点难理解。
我看网上好多讲解就是给段代码然后就不管了,那真的不行,因为情况太多了,太难理解了。为了让小伙伴们理解这里我还是画下图来说明。
(1)从头开始删
其实啊,不管是从头开始删,还是从尾开始删,其目的都是将链表清空,释放内存。所以在程序中两个随便用一个就行,看个人喜好吧。
OK,先上张图理解理解
就是将头结点一步一步往下移,然后释放掉之前的就OK
话不多说,来段代码体会一下
//从头开始删除
void DeletHead()
{
//先考虑是不是空表
if(NULL == Head)
{
printf("This list is empty!\n");
return;
}
Node *p;//用来做中转站
while(NULL != Head)
{
p = Head;//让头结点的内容先给他
Head = Head->next;//头结点下移
free(p);
}
printf("This list has been clean!\n");
}
耐心看我写的代码,我都给了敲详细的注释
(2)从尾开始删
我不喜欢从尾部开始删除,因为比从头开始删要麻烦,这个我就当是拓展思路了。这个呢其实和从头开始是差不多的,只不过是将尾结点慢慢往前移。
但是我们的操作就和从头开始删的不一样了。因为我们直接把尾结点移到他前面那个节点上去,貌似很难做到。
这里真的有点难,我想了好久
由于情况复杂所以这里我们要迂回一下,通过另一种方法来达到和尾结点前移一样的效果,
那就是先看图,再看代码
看懂我图里所表达的处理方法后,再看我写的代码,注释超多,绝对看得懂
话不多说,上代码
//从尾开始删除
void DeletRear()
{
Node *p;//用来做中转站
p = Head;//从头开始遍历
//先考虑是不是空表
if(NULL == Head)
{
printf("This list is empty!\n");
return;
}
//下面这个循坏是 链表不是只有单个结点的情况 ,也就是说走完下面这个循坏就还剩一个结点
while (NULL != Head && NULL!=p->next)
{
while (p->next->next != NULL)//这里判断的其实是:p是不是尾结点的前一个结点
{
p = p->next;//p不是尾结点的前一个结点,那就往下走
}
free(p->next);//p->next就是尾结点,这里释放的就是尾结点
p->next = NULL;//因为释放了原先的尾结点后,那么现在在p就成了尾结点,所以给p->next赋个NULL
p = Head;//再从头开始遍历
}
//走完上面的循坏后,还剩一个结点
// 下面是只有单个节点的情况
if (Head->next == NULL)
{
free(Head);
}
printf("This list has been clean!\n");
}
题外话:我感觉这是基本操作里最难的,而且最不常用的了。纯当开拓思维了
(3)删除指定节点
(3.1)删除指定节点:前驱结点位置已知
这种情况是常规情况,也是我们在操作时所会采取的的方法。要删除一个指定节点,我们都是先找到指定结点前的结点。假设指定结点的前驱结点为P,那么就可以直接令P->next指向指定结点的后继结点,语句就是 p->next = p->next->next,再释放指定结点就完成删除啦。
但是真滴就这么简单吗?其实这里的情况也有几种。我在这里调试了好久,情况远不是各位想的那么简单,这里真的有点难搞,耐心点, 待我细细道来。
回想下“从尾开始删除”中是不是也出现了p->next->next,所以我们就可以拿来类比下。当只有一个或者两个结点时,p->next->next这个玩意就不太好使了,所以我们要分情况考虑。
看图更容易理解
上图这两种简单的情况,再去看下代码就更容易理解。
继续看图,下面的图适用于链表有三个及三个以上结点的情况
上面这张图就是最常规的情况,那就是待删除的结点是中间结点时
下面是待删结点是头结点和尾结点时候的情况
以上就是我所想到的所有情况了,看图貌似很简单的样子,对吧?但是代码有点绕哦
ok,话不多说,上代码
//删除指定结点
void DeletList(Node *p)
{
//这里不用判断是不是空表,根据查找函数可知,能进到删除函数里的肯定不是空表
//只有一个结点时
if (NULL == Head->next)
{
free(Head);
Head = NULL;
return;
}
//只有两个结点时
else if ( Head->next == Rear)
{
if (Head == p) //指定结点是头结点时
{
free(Head);
Head = Rear;
}
else //指定结点是尾结点时
{
free(Rear);
Rear = Head;
Rear->next = NULL;
}
}
//三个及三个以上的结点
else
{
Node *p1 = Head;
//判断指定结点是不是头节点
if (Head == p)
{
Head = Head->next;
free(p1);
p1 = NULL;//free只是将p1指向的内容释放了,但p1这个指针仍然存在,为了不让他变成野指针,所以使他等于NULL
return ;
}
while (p1 != NULL)
{
if (p1->next == p) //找到前驱结点后,就可以开始删除操作了
{
if( p == Rear)//指定结点为尾结点的情况
{
free(p);
p = NULL;//free只是将p指向的内容释放了,但p这个指针仍然存在,为了不让他变成野指针,所以使他等于NULL
Rear = p1;//使前驱结点变成尾结点
Rear->next = NULL;
return ;
}
else //结点为中间的结点时
{
//先记住要删除的节点
Node *p2 = p1->next;
p1->next = p1->next->next;
free(p2);
p2 = NULL;
return ;
}
}
p1 = p1->next;//当没找到指定结点的前驱结点时就往下继续找
}
}
}
(3.2)删除指定节点:只知指定结点位置
其实这种情况是我在刚接触链表删除时,脑子没想到利用前一结点去删除。真正在链表删除的时候也不会不知道前驱结点的位置,所以这个大家就当是拓展下思维吧。
这种情况的处理方法和在指定节点前插入新结点有点类似,要来点迂回战术。
那就是既然我知道指定结点的位置,那我就可以把指定结点的后继结点给删掉,只不过先将后继结点的数据先赋给指定结点,不就成了吗。
OK,看图说话
这里,我就不写代码了。因为在实际操作中根本不会这样去做,纯当拓展思维咯。
(4)按值删除
这个按值删除其实就是删除指定节点的一种变形,理解了删除指定节点,看懂这个按值删除那肯定是轻轻松松。
(4.1)按值删除:只删遇到的第一个
这里我们可以调用下之前的删除函数,就函数里面调用函数,超级便捷,我把详细的解释都放到代码注释里去了,这里就不啰嗦了。
话不多说,上代码
//删除指定值,只遇到的第一个结点
void DeletDataNode()
{
char num[10];//这里设num为指定值,也可以换成数据域其他数据
printf("Please input the num you want to delet:\n");
scanf("%s",num);
Node *p = Head;//建立新节点,让他从头开始去遍历
while (p!=NULL)
{
if(0 == strcmp(p->num,num))
{
DeletList(p);//直接调用我们上面的删除函数
return;
}
p = p->next;//没找到就继续往下遍历
}
printf("The num is not exist!\n");//循坏结束了,还没找到的就输出一个提示信息
}
(4.2)按值删除:是这个值的全删掉
这个就是在上面的代码上把循坏里的return给去掉了,目的就是让他走完整个循坏,将所有的指定值都找出来,并且删除。代码我就不上了哈,但是在合并代码的那里会有。
写在最后的吐槽:为了使这篇文章更加严谨,看了很多文章,恶补了好多细节知识。我觉得我的逻辑思维在写文章时会慢慢变严谨。比如之前我老是忘了考虑空表的情况,现在第一反应就是先考虑他。 其实刚开始我画的思维导图并没有这么多,但是在写的时候,脑子里会多想一下,就像分类讨论一样,慢慢滴,情况就变多了。
然后特感谢我的好兄弟每次都那么耐心地给我解答疑惑,学习上有一挚友,真的是件很幸运的事。
由于是初学者,尽管写的时候严谨再严谨,查阅再查阅,但是文中肯定还是会存在不严谨的知识漏洞,望小伙伴们多多包涵,欢迎私信和我交流呀
为了不使这篇文章看上去太冗长,所以合并后的代码我放到了另一篇文章。点合并代码查看。
再来几个疑惑解答
Q1:为什么我的函数几乎没有参数传递?
A1:把参数放函数内,便于小伙伴们读懂代码
***(转载请注明出处,谢谢。更新ing…***)