赞
踩
欢迎各位来到 Harper.Lee 的编程学习小世界!
博主主页传送门:Harper.Lee的博客
我将在这里分享我的学习过程等心得
创作不易,码字不易,兄弟们养成先赞后看的好习惯哦!
想一同进步的uu,可以来后来找我哦!
在前面的文章中,博主详细地介绍了顺序表的实现方法及其书写过程,大家可以跳转去学习:DS:顺序表的实现
目录
1. 在顺序表的中间/头部插入数据,会涉及到数据的多次移动,效率不高;
2. 增容需要申请空间、拷贝数据、释放旧空间,会有不小心的消耗;
3. 增容一般是呈1.5倍、2倍形式增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,我们再继续插入5个数据,那么就会浪费95个空间。
总的来说,顺序表存在以下几个问题:中间 /头部插入效率低下、增容降低运行效率、增容造成空间浪费。为了解决这几个问题,提出了一个新的数据结构---链表。
概念:链表也是顺序表的一种,在逻辑结构上是线性的,在物理结构上是非线性的,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。如,int a =10 ; float f = 0.1 ;变量 a 和 f 的物理空间不一定是连续的,这取决于OS操作系统分配空间的方法。
链表就类似于现实生活中的火车(如下图),火车是由一节一节的车厢和一个火车头组成的,车厢之间是通过可拆可卸的钩子挂在一起的,每节车厢都是独立存在的。当处于旅游旺季时,增加车厢;如果出现候补,处于淡季时,就可以通过减少车厢来减少空间的浪费。当车厢使用数量需要变化时,只需要将⽕⻋⾥的某节⻋厢去掉/加上,不会影响其他⻋厢,每节⻋厢都是独⽴存在的。
类似地,链表是由一个一个的节点(结点也一样,没有区别)组成。由之前的知识可知,动态申请的空间在堆上,链表在物理结构虽然不是线性的,但它可以通过节点联系起来,就像一条弯曲的链子,可以通过拉直让物理结构变成想象的线性结构(就好比弯曲的链子拉直)。由下图可知,链表中的节点存储的内容为两个部分:可以指向下一个节点的地址(pointer)以及本节点的存储数据(data)。此外, 链表的每个节点都是独立申请的空间,就像火车的每节车厢都是独立的一样。
⻋厢是独立存在的,且每节车厢都有车门。想象⼀下这样的场景,假设每节车厢的车门都是锁上的状态,需要不同的钥匙才能解锁,每次只能携带⼀把钥匙的情况下,如何从车头走到车尾?
最简单的做法:每节⻋厢⾥都放⼀把下⼀节⻋厢的钥匙。
图中指针变量plist保存的是第⼀个节点的地址,我们称plist此时“指向”第⼀个节点,如果我们希望plist“指向”第⼆个节点时,只需要修改plist保存的内容为0x0012FFA0。 链表中每个节点都是独⽴申请的(即需要插入数据时才去申请⼀块节点的空间,而顺序表是要动态申请一块连续的空间),我们需要通过指针变量来保存下⼀个节点位置,这样就能能从当前节点找到下⼀个节点,将整个链表串联起来。
单链表(single linked list)是由一个一个的节点组成,节点又由指针(指向下一个结点的指针)和数据组成。如果要定义节点,只需要定义链表节点的结构即可,因此需要用到结构体指针。
- //本项目相关可能用到的头文件
- #include <stdio.h>
- #include <stdlib.h>
- #include <assert.h>
-
- //定义节点的结构
- //data+指向下一个节点的指针
-
- typedef int SLTDataType;//类型用SLDataType来表示,便于后续操作对数据类型额更改
-
- typedef struct SListNode
- {
- SLTDataType data;//当前节点存储的数据
- struct SListNode* next;//next:(指向下一个结点的结构体指针变量)
- }SLTNode;//重命名一个简单的名字
当我们想要保存⼀个整型数据时,实际是向操作系统申请了⼀块内存,这个内存不仅要保存整型数据,也需要保存下⼀个节点的地址(当下⼀个节点为空时保存的地址为空)。
当我们想要从第⼀个节点⾛到最后⼀个节点时,只需要在前⼀个节点拿上下⼀个节点的地址(下⼀个节点的钥匙)就可以了。
- SLTNode* node1 = (SLTNode*)malloc(sizeof(SLTNode));//动态申请空间的函数,malloc、caoolc、realloc等函数
- //链表中不涉及增容,所以使用malloc? 返回值void*强制类型转换SLTNode*
- node1->data = 1;
-
- //第一个节点的指针指向那个下一个节点的地址,所以先创建第二个节点,便于前面的node1找到(存储)后面的节点的地址
- SLTNode* node2 = (SLTNode*)malloc(sizeof(SLTNode));
- node2->data = 2;
-
- SLTNode* node3 = (SLTNode*)malloc(sizeof(SLTNode));
- node3->data = 3;
-
- SLTNode* node4 = (SLTNode*)malloc(sizeof(SLTNode));
- node4->data = 4; //最后一个节点
现在虽然创建好了一个个的节点,但是这些节点之间相互独立,没有联系在一起。因此,使用next指针变量将两个节点之间进行连接。
链接节点:(指针)
- //使用指针将四个节点通过个next连接起来
- node1->next = node2;
- node2->next = node3;
- node3->next = node4;
- node4->next = NULL;//最后一个节点,不需要存储地址了,没有节点了
为什么说来链表节点的创建用malloc开辟动态内存空间?
之前在顺序表里,我们使用了realloc开辟动态内存空间,是因为顺序表需要进行增容操作,动态申请连续的空间。链表是由一个一个的节点组成,只需要按照节点进行动态申请空间即可,不涉及增容操作,因此节点的空间只需一次开辟一次内存内存就可以了,而且它也没有连续存放的要求,不需要在原有的空间基础上申请,因此使用malloc就可以了。
在对单链表进行操作时,我们会涉及到尾插、头插、指定位置插入等的操作,并且还要为新节点动态申请一块空间,所以对新节点申请这一功能进行函数封装,以便于直接使用,无需像4.1 那样一个一个的手动实现。链表初始时多少空链表,我们在链表里进行多次增加数据
- //当插入节点时,都在重复判断空间是否充足,然后再malloc申请空间,
- // 因此此功能单独提取出来
- SLTNode* SLTBuyNode(SLTDataType x)//SLTNode*为返回数据的类型
- {
- SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
- if (newnode == NULL)
- {
- //空间申请失败!
- perror("malloc fail!");
- exit(1);//异常退出,使用非零
- }
- newnode->data = x;
- newnode->next = NULL;
- }
代码如下:
- //链表的打印:
- void SLTPrint(SLTNode* phead)//链表的头节点
- {
- SLTNode* pcur = phead;
- while (pcur)//等价于pcur != NULL
- {
- printf("%d->", pcur->data);
- pcur = pcur->next;//两个节点通过next打印输出5
- }
- printf("NULL\n");//此时pcur为NULL,不需要置空了
- }
调用该链表打印函数:
分析代码运行过程:plist作为函数参数,传给函数中的行参phead,phead得到plist存储的数据即第一个节点的地址,也就是phead得到链表第一个节点的地址。pcur作为二级指针,接受了phead存储的值(一级指针),也得到第一个节点的地址。地址不为空,就开始进行打印操作,pcur->pcur.next; 进行,pcur的next存储的内容变成下一个节点地址。判断pcur是否为NULL,若pcur存储NULL,循环条件为假,循环结束,打印结束。(运行结果:1->2->3->4->NULL)
1、为什么不直接使用node1,而要多创建一个结构体指针变量plist呢?
调用打印函数时,多创建了一个结构体指针变量plist指向第一个节点,虽然有点多此一举,但是可以不用改变链表的数据。
2、 这里其实不需要传地址,因为打印函数只不过是展示,并没有对链表的数据进行处理,值传递也可以(SLTNode*phead),如果我们是为了保持接口一致性,可以使用二级指针接收参数。
接口定义:
- void SLTPrint(SLTNode* phead);//从链表的头phead开始打印
-
- //链表的接口定义,
- //尾插
- void SLTPushBack(SLTNode** pphead, SLTDataType x);//pphead头节点
- //头插
- void SLTPushFront(SLTNode** pphead, SLTDataType x);
-
- //尾删
- void SLTPopBack(SLTNode** pphead);
- //头删
- void SLTPopFront(SLTNode** pphead);
图解分析:
尾插需要找到尾节点,再将尾节点和新节点连接起来。在代码中,我们定义了一个新的指针ptail,指向第一个节点的地址,让它进行遍历,指导找到尾节点。然后让尾节点的next指针指向新的节点。但如果链表是一个空链表,就没有尾节点了,所以要考虑链表是否为空的情况。
代码如下:
- //尾插
- //要先抓到尾节点,再将尾节点和新节点链接起来
- void SLTPushBack(SLTNode** pphead, SLTDataType x)
- {
- assert(pphead);
- //*pphead可以为空
- //*pphead是指向第一个节点的指针
- SLTNode* newnode = SLTBuyNode(x);//建立一个新节点
- if (*pphead == NULL)//判断第一个节点是否为空
- {
- //空链表:申请一个新的节点,让phead指向这个新的节点,此时,链表就变成只有一个节点的链表
- *pphead = newnode;//
- }
- else
- {
- //非空链表:找尾节点,从头开始找
- SLTNode* ptail = *pphead;//定义ptail先指向头节点,然后从头往后(循环)开始找尾巴
- while (ptail->next)//->表示指针的解引用
- {
- ptail = ptail->next;//指向下一个节点,继续往下走
- }//此时ptail指向的就是尾节点
-
- //连接尾节点之前需要一个新节点,在进行新节点的创建
- ptail->next = newnode;
- }
注意:
1、为什么要创建一个结构体指针ptail去接收*pphead,而不直接使用*pphead?
用ptail记录头节点,用它通过遍历寻找尾节点 (先找尾节点,再连接尾节点和新节点)
*pphead是指向头节点的一级指针,我们是通过二级指针去接收该一级指针的地址,所以*pphead是会被改变的,如果我们在寻找尾结点的时候直接用*pphead,虽然也可以找到尾结点,但是头结点也会因此而丢失,所以我们在利用头结点去遍历链表时,一定要创建一个临时变量去接收头节点再去遍历。
2、 为什么传入的参数要使用SLTNode** pphead(二级指针)?
运行结果如下图:
调用函数时,plist是一个指向第一个节点的一级指针变量,将它的变量名直接作为参数传给行参,其实传过去的是它的值,而不是它的地址,(调试发现:行参的值没有改变,说明是传值调用)而这里需要的是地址即传址调用而非传值调用。此外,plist是一级指针,因此行参接收 &plist 二级指针,类型就为二级指针。
上图中,左边是实参三种形式,右边是行参三种形式。
3、为什么使用assert(pphead)?
如果调用尾插函数时,程序猿给实参传入了一个NULL,行参接收到后就会对它进行解引用,程序会出错。因此加入断言,断言错误就会报错。
4、为什么还要判断链表是否为空链表?
如果链表为空链表,则把新创建的节点作为该空链表的唯一节点;如果链表不是空链表,则需要找到尾节点,然后连接新节点和尾节点,就完成了尾插。
最开始创建的链表是一个空链表 ,phead指向的就是一个空链表,先申请一个新节点newnode时,定义 ptail指向头节点phead,所以ptail为空,开始找尾节点。ptail->next中->操作符相当于指针解引用,对空指针进行解引用,代码就会报错。因此,在代码里要增加判断空链表这一情况:空链表不需要找尾节点,只需要申请一个新的尾节点,phead原本指向的是NULL,现在让phead指向新创建的节点。简而言之:新创立的节点就是phead指向的头节点,就是该空链表唯一的节点。
5、为什么while循环中的判断条件是ptail->next != NULL,而不是ptail != NULL?
while循环是为了找到尾节点,如果循环条件是ptail != NULL,当循环条件结束时,ptail指向的节点不是尾节点,没有找到尾节点,则目的没有达到;如果循环条件是ptail->next != NULL,那么条件结束时,ptail指向的next就是尾节点。
链表不为空的情况图解:
链表为空的情况:
一般情况下,我们只需要让newnode的next指针指向原来的头节点,再让newnode成为新的头节点,当链表为空时,插入的newnode就是新的头节点,所以不需要分开讨论。
- //头插
- void SLTPushFront(SLTNode** pphead, SLTDataType x)
- {
- assert(pphead);//判断二级指针接收的内容是否为空
- //创建一个新节点,插入头部,此时newnode变成新的头节点
- SLTNode* newnode = SLTBuyNode(x);//创建一个新节点
- newnode->next = *pphead;//让该节点指向原来的头节点,连接两个节点
- *pphead = newnode;//让newnode成为新的头,*pphead指向newnode,因为*pphead指向的都是头节点
- }
注:newnode->next= *pphead和*pphead = newnode不能调换顺序,否则原头节点的数据会丢失!
三种情况:
1、pos在中间任意位置的情况:
2、pos在前面(尾节点) 的情况:
3、pos在尾节点的情况:
一般情况下,要找到pos的前一个结点prev,让prev的next指向新结点,而新节点的next指向pos,因为该函数要实现在指定位置之前插入,所以pos传空则没有意义,所以pos不能为空,因为pos不能为空,所以链表也不可能为空,因为要找pos的前一个结点,如果pos恰好就是头结点,那么就相当于是头插了,直接调用之前封装的头插函数。
整体代码如下:
- //在指定位置pos之前插⼊数据
- void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)//**pphead:代表第一个节点。pos可能是头节点,所以传入二级指针
- {
- //1、为什么传入二级指针?
- assert(pphead && *pphead);//*pphead链表不能为空:如果链表为空,pos也为空,就不能在pos之前插入数据。有pos节点那就说明链表不是空链表
- assert(pos);
- SLTNode* newnode = SLTBuyNode(x);
-
- //若pos==*pphead,说明是头插
- if (pos == *pphead)
- {
- SLTPushFront(pphead, x);//调用头插
- }
- else
- {
- SLTNode* prev = *pphead;//从第一个节点开始找pos前一个节点
- while (prev->next != pos)
- {
- prev = prev->next;
-
- }//找到了这个节点pos
- newnode->next = pos;
- prev->next = newnode;
- }
- }
有两种情况:
1、
2、
这种错误方法的解决办法:先存储一下尾节点,然后……
代码如下:
- //在指定位置之后插⼊数据
- void SLTInsertAfter(SLTNode* pos, SLTDataType x)
- {
- //为什么没有头节点参数?如果是最后一个节点,他是前一个节点prev->next=pos,用不到头节点参数
- assert(pos);
- SLTNode* newnode = SLTBuyNode(x);
- newnode->next = pos->next;
- pos->next = newnode;
- }
在一般情况下,我们需要找到尾节点的前一个结点prev,让他的next指针指向NULL,然后再释放尾结点并置空,因为我们需要找尾结点的前一个结点,如果该链表恰好只有一个结点时,是没有前结点的,所以单独讨论,该情况下直接将唯一的结点释放掉即可。当链表为空的时候,删除操作是没有意义的,所以要直接使用断言制止这种情况!
画图示范:
- //尾删
- void SLTPopBack(SLTNode** pphead)
- {
- assert(*pphead && *pphead);
- //链表不能为空,*pphead就不能为空
- //找尾节点:
- //尾节点释放掉后,需要把他前面的节点置为空。否则,该节点就会变成一个野指针
-
- //链表只有一个节点
- if ((*pphead)->next == NULL)//->的优先级高于*,所以要在*pphead上加括号
- {
- free(*pphead);
- *pphead = NULL;
- }
- else
- {
- //链表有多个节点
- SLTNode* prev = *pphead;//用来记录尾结点的前一个结点
- SLTNode* ptail = *pphead;//用来遍历链表,最后释放尾结点
- while (ptail->next)
- {
- prev = ptail;
- ptail = ptail->next;
- }//此时prev恰好是尾结点的前一个结点
- //此时ptail恰好是尾结点,将其释放并置空
- free(ptail);
- ptail = NULL;
- prev->next = NULL;
- }
- }
1、 如果*pphead为空,说明链表为空,但是链表不能为空;pphead也不能为空
ptail指向的next要置为空,避免ptail成为空指针。
2、尾节点删除(释放)后,为什么还要将前一个节点置为空?
前一个节点指向的空间就是最后一个节点,只有一个节点被释放掉后,空间还存在,但是已经没有使用权限了,而前一个节点prev的next存储的地址仍然是指向那个节点的,prev的next成为了一个野指针。
3、
一般情况下,令*pphead指向第二个结点,然后释放掉第一个结点即可,链表只有一个结点的情况,也是释放掉第一个结点,所以不需要分开讨论,链表为空时头删没有意义!!所以必须通过断言来制止!
画图分析:
代码如下:
- //头删
- void SLTPopFront(SLTNode** pphead)
- {
- //链表有多个节点:
- assert(pphead && *pphead);
- //直接释放掉头节点?:释放---第二个节点作为新的头节点,但是:怎么找到这个节点?
- //先存储下一个结点的next指针
- SLTNode* next = (*pphead)->next;//存储next值
- free(*pphead);//直接释放头节点
- *pphead = next;
- //判断这个方法在只有一个节点时能否适用
- }
1. 链表有多个节点
2.链表只有一个节点
3.链表没有节点的情况:删除没有意义。
一般情况下,要找到pos的前一个结点prev,让prev的next指向新结点,而新节点的next指向pos,因为该函数要实现在指定位置之前插入,所以pos传空则没有意义,所以pos不能为空,因为pos不能为空,所以链表也不可能为空,因为要找pos的前一个结点,如果pos恰好就是头结点,那么就相当于是头插了,直接调用之前封装的头插函数。
代码如下:
- //删除pos之前节点
- void SLTErase(SLTNode** pphead, SLTNode* pos)
- {
- assert(*pphead && pphead);
- assert(pos);
- //删除pos节点:
- //pos是头节点/不是头节点
- if (pos == *pphead)
- {
- //头节点
- //SLTNode* next = (*pphead)->next;
- //free(*pphead);
- //*pphead = next;
- //也可以直接调用头删
- SLTPopFront(pphead);//传入二级指针
- }
- else
- {
- SLTNode* prev = *pphead;
- while (prev->next != pos)
- {
- prev = prev->next;
- }
- prev->next = pos->next;
- free(pos);
- pos = NULL;
- }
- }
两种情况:1.
2.
一般情况下,令pos->next=pos->next->next因为是删除指定位置之后的结点,所以必须保证pos的后一个结点存在,要使用断言。
完整代码如下:
- //删除pos之后的节点
- void SLTEraseAfter(SLTNode* pos)
- {
- assert(pos && pos->next);
- SLTNode* del = pos->next;
- //pos del del->next
- pos->next = del->next;
- free(del);
- del = NULL;
- }
注:newnode->next = pos->next和pos->next = newnode不能调换顺序!
如果交换了,
链表和顺序表不一样,顺序表销毁可以直接销毁整个连续空间,而链表是由一个一个的独立的节点组成的,销毁链表就需要销毁一个个的节点。
代码如下:
- //销毁链表
-
- void SLTDesTory(SLTNode** pphead)
- {
- assert(pphead);//保证传入的参数不是NULL
- assert(*pphead);//保证链表不为空
- SLTNode* pcur = *pphead;//用来删除
- SLTNode* next = NULL;//用来遍历
- while (pcur)
- {
- next = pcur->next;
- free(pcur);
- pcur = next;
- }
- *pphead = NULL;//告诉编译器此时*pphead不能用了
- //相当于毁了第一把钥匙,那后面的即使不置空也不会被使用到!
- }
为什么最后只把*pphead置空,而不把他后面的其他结点置空?
我们平时在动态内存释放的时候,其实空间已经返还给操作系统了,即使里面存在数据,也不影响别人的使用,因为直接覆盖就行了,所以我们之所以要置NULL,是为了防止我们写了很多代码后,忘记了其已经被释放,再去使用的话其实就是相当于使用了野指针,此时直接就会导致程序崩溃,所以置NULL,是为了让编译器提醒我们,这块空间不能被使用,在编译的时候,就可以即使地发现自己的问题。所以在这里,*pphead是找到后续链表其他结点的关键,只要*pphead被置空了,就相当于第一把钥匙丢了,那么后续的结点是不可能会被我们误用的
在指定位置之前插入、指定位置之后插入、删除指定位置结点、删除指定位置之后的结点,都涉及到指定位置,所以我们封装一个查找函数,根据我们需要查找的数据,返回该数据所在的结点。
代码如下:
- //查找:链表的遍历
- SLTNode* SLTFind(SLTNode* phead, SLTDataType x)//phead指向链表
- {
- SLTNode* pcur = phead;//记录数据,可以不用改变链表的本来的值
- while (pcur)//pcur != NULL,链表不为空,进入循环
- {
- if (pcur->data == x)
- {
- return pcur;
- }
- pcur = pcur->next;
- }
- //没有进入循环,说明链表为空
- return NULL;
- }
SList.h
- #pragma once
-
- //相关可能用到的头文件
- #include <stdio.h>
- #include <stdlib.h>
- #include <assert.h>
-
- //定义节点的结构
- //data+指向下一个节点的指针
-
- typedef int SLTDataType;//类型用SLDataType来表示
-
- typedef struct SListNode
- {
- SLTDataType data;
- struct SListNode* next;
- }SLTNode;//重命名一个简单的名字
-
- void SLTPrint(SLTNode* phead);//从链表的头phead开始打印
-
- //链表的接口定义,
- //尾插
- void SLTPushBack(SLTNode** pphead, SLTDataType x);//pphead头节点
- //头插
- void SLTPushFront(SLTNode** pphead, SLTDataType x);
-
- //尾删
- void SLTPopBack(SLTNode** pphead);
- //头删
- void SLTPopFront(SLTNode** pphead);
-
- //查找
- SLTNode* SLTFind(SLTNode* phead, SLTDataType x);
-
- //在指定位置之前插⼊数据
- void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
-
- //在指定位置之后插⼊数据
- void SLTInsertAfter(SLTNode* pos, SLTDataType x);
-
- //删除pos节点
- void SLTErase(SLTNode** pphead, SLTNode* pos);
-
- //删除pos之后的节点
- void SLTEraseAfter(SLTNode* pos);
-
- //销毁链表
- void SListDesTroy(SLTNode** pphead);
SList.c
- #define _CRT_SECURE_NO_WARNINGS 1
- #include "SList.h"
-
-
-
- //链表的打印:
- void SLTPrint(SLTNode* phead)//链表的头节点
- {
- SLTNode* pcur = phead;
- while (pcur)//等价于pcur != NULL
- {
- printf("%d->", pcur->data);
- pcur = pcur->next;//两个节点通过next打印输出5
- }
- printf("NULL\n");//此时pcur为NULL,不需要置空了
- }
-
- //当插入节点时,都在重复判断空间是否充足,然后再malloc申请空间,
- // 因此此功能单独提取出来
- SLTNode* SLTBuyNode(SLTDataType x)//SLTNode*为返回值
- {
- SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
- if (newnode == NULL)
- {
- //空间申请失败!
- perror("malloc fail!");
- exit(1);//异常退出(非0退出)
- }
- newnode->data = x;
- newnode->next = NULL;//
- return newnode;
- }
- //尾插
- //要先抓到尾节点,再将尾节点和新节点链接起来
- void SLTPushBack(SLTNode** pphead, SLTDataType x)
- {
- assert(pphead);
- //*pphead可以为空
- //*pphead是指向第一个节点的指针
- SLTNode* newnode = SLTBuyNode(x);//建立一个新节点
- if (*pphead == NULL)//判断第一个节点是否为空
- {
- //空链表:申请一个新的节点,让phead指向这个新的节点,此时,链表就变成只有一个节点的链表
- *pphead = newnode;//
- }
- else
- {
- //非空链表:找尾节点,从头开始找
- SLTNode* ptail = *pphead;//定义ptail先指向头节点,然后从头往后(循环)开始找尾巴
- while (ptail->next)//->表示指针的解引用
- {
- ptail = ptail->next;//指向下一个节点,继续往下走
- }//此时ptail指向的就是尾节点
-
- //连接尾节点之前需要一个新节点,在进行新节点的创建
- ptail->next = newnode;
- }
- } //有诈:最开始是有一个空链表,phead指向空节点,ptail为空时,->:表示指针的解引用,就不可以对空指针进行解引用,
-
-
- //调试发现:行参变了,但是实参没变,说明是传值调用
-
-
- //头插
- void SLTPushFront(SLTNode** pphead, SLTDataType x)
- {
- assert(pphead);//判断二级指针接收的内容是否为空
- //创建一个新节点,插入头部,此时newnode变成新的头节点
- SLTNode* newnode = SLTBuyNode(x);
- newnode->next = *pphead;//让该节点指向原来的头节点,连接两个节点
- *pphead = newnode;//让newnode成为新的头,*pphead指向newnode,因为*pphead指向的都是头节点
-
- }
-
- //尾删
- void SLTPopBack(SLTNode** pphead)
- {
- assert(*pphead && *pphead);
- //链表不能为空,*pphead就不能为空
- //画图
- //找尾节点:
- //尾节点释放掉后,需要把他前面的节点置为空。否则,该节点就会变成一个野指针
-
- //链表只有一个节点
- if ((*pphead)->next == NULL)//->的优先级高于*,因为要先解引用,所以加一个括号
- {
- free(*pphead);
- *pphead = NULL;
- }
- else
- {
- //链表有多个节点
- SLTNode* prev = *pphead;
- SLTNode* ptail = *pphead;
- while (ptail->next)
- {
- prev = ptail;
- ptail = ptail->next;
- }
- free(ptail);
- ptail = NULL;
- prev->next = NULL;
- }
- }
-
- //头删
- void SLTPopFront(SLTNode** pphead)
- {
- //链表有多个节点:
- assert(pphead && *pphead);
- //直接释放掉头节点?:释放---第二个节点作为新的头节点,但是:怎么找到这个节点?
- //先存储下一个结点的next指针
- SLTNode* next = (*pphead)->next;//存储next值
- free(*pphead);//直接释放头节点
- *pphead = next;
- //判断这个方法在只有一个节点时能否适用
- }
-
- //查找:链表的遍历
- SLTNode* SLTFind(SLTNode* phead, SLTDataType x)//phead指向链表
- {
- SLTNode* pcur = phead;//记录数据,可以不用改变链表的本来的值
- while (pcur)//pcur != NULL,链表不为空,进入循环
- {
- if (pcur->data == x)
- {
- return pcur;
- }
- pcur = pcur->next;
- }
- //没有进入循环,说明链表为空
- return NULL;
- }
-
- //在指定位置pos之前插⼊数据
- void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)//**pphead:代表第一个节点。pos可能是头节点,所以传入二级指针
- {
- //1、为什么传入二级指针?
- assert(pphead && *pphead);//*pphead链表不能为空:如果链表为空,pos也为空,就不能在pos之前插入数据。有pos节点那就说明链表不是空链表
- assert(pos);
- SLTNode* newnode = SLTBuyNode(x);
-
- //若pos==*pphead,说明是头插
- if (pos == *pphead)
- {
- SLTPushFront(pphead, x);//调用头插
- }
- else
- {
- SLTNode* prev = *pphead;//从第一个节点开始找pos前一个节点
- while (prev->next != pos)
- {
- prev = prev->next;
-
- }//找到了这个节点pos
- newnode->next = pos;
- prev->next = newnode;
- }
- }
-
-
- //在指定位置之后插⼊数据
- void SLTInsertAfter(SLTNode* pos, SLTDataType x)
- {
- //为什么没有头节点参数?如果是最后一个节点,他是前一个节点prev->next=pos,用不到头节点参数
- assert(pos);
- SLTNode* newnode = SLTBuyNode(x);
- newnode->next = pos->next;
- pos->next = newnode;
- }
-
-
- //删除pos之前节点
- void SLTErase(SLTNode** pphead, SLTNode* pos)
- {
- assert(*pphead && pphead);
- assert(pos);
- //删除pos节点:
- //pos是头节点
- if (pos == *pphead)
- {
- //头节点
- //SLTNode* next = (*pphead)->next;
- //free(*pphead);
- //*pphead = next;
- //也可以直接调用头删
- SLTPopFront(pphead);//传入二级指针
- }
- else
- {
- SLTNode* prev = *pphead;
- while (prev->next != pos)
- {
- prev = prev->next;
- }
- prev->next = pos->next;
- free(pos);
- pos = NULL;
- }
- }
-
- //删除pos之后的节点
- void SLTEraseAfter(SLTNode* pos)
- {
- assert(pos && pos->next);
- SLTNode* del = pos->next;
- //pos del del->next
- pos->next = del->next;
- free(del);
- del = NULL;
- }
-
-
- //销毁链表
- void SListDesTroy(SLTNode** pphead)
- {
- //销毁一个一个的节点
- assert(pphead && *pphead);
- SLTNode* pcur = *pphead;
- while (pcur)
- {
- SLTNode* next = pcur->next;
- free(pcur);
- pcur = next;
- }
- *pphead = NULL;
- }
写到这儿不容易,看到这儿也不容易,所以,要不点个小心心,三连支持一下啊?
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。