含义
1.顺序表的回顾
之前的文章已经谈到了顺序表,关于顺序表,有一下的几种特点
1.中间,头部的删除,复杂度为O(N);
2.增容会有申请新的空间,拷贝数据,释放旧的空间,会有不小的消耗;
3:整容一般是呈2的倍数增长,但是也会造成一定的空间浪费,比如,当前的容量是100,满了以后·增加到200,我们继续插入5个数据,后面没有数据插入,就浪费了95个空间
2.链表的含义
那么,是否存在一种数据结构,能够解决上面的问题:
1.中间,头部数据的删除,不需要挪动数据
2.不需要扩容
3.不会造成空间的浪费
没错,就是今天的链表
链表的组成:
链表是由当前节点要存储的数据+下一个节点的地址组成的,就比如下面的这个
你可以把链表看成一节一节的火车车厢,想要加车厢的时候随时加,前面一个车厢掌管后面一个车厢的钥匙
放下一个节点的地址是为了方便找到下一个节点(这里有一点废话了哈),话不多说,上代码
cpp
typedef int SLdatetype;//链表的创建
struct Slist
{
SLdatetype a;
struct Slist* arr;
};
有的时候你觉得结构体的类型太长了,懒得写,以后在使用的时候也不太发表,可以换成下面的
cpp
typedef int SLdatetype;
typdef struct Slist
{
SLdatetype a;
struct Slist* arr;
}SL;
//下面的写法是不行的
typedef int SLdatetype;
typdef struct Slist
{
SLdatetype a;
SL* arr;//这种写法是不行的,因为编译器在执行程序的时候,是自上而下来扫描的,SL这种类型还没有
//创建完毕
}SL;
链表的打印
cpp
#include"Slist.h"
void SLprint(SL* phead)
{
SL* ptemp = phead;//建立临时的变量,防止下次要找头节点的时候找不到了
while (ptemp!= NULL)
{
printf("%d->", ptemp->date);
ptemp = ptemp->next;
}
printf("NULL");
}
测试代码
cpp
#include"Slist.h"
void test01()
{
//在写的时候不建议这样写,这里只是为了测试
//申请空间
SL* node1 = (SL*)malloc(sizeof(SL));
node1->date =1;
SL* node2 = (SL*)malloc(sizeof(SL));
node2->date =2;
SL* node3 = (SL*)malloc(sizeof(SL));
node3->date =3;
//连接
node1->next = node2;
node2->next = node3;
node3->next = NULL;
SLprint(node1);
}
int main()
{
test01();
return 0;
}
输出结果
链表的尾插
cpp
SL* SLbuynode(SLdatetype x)
{
SL* newnode = (SL*)malloc(sizeof(SL));
newnode->date = x;
newnode->next = NULL;
return newnode;
}
void SLpushback(SL** pphead, SLdatetype x)
{
assert(pphead);//判断有效
SL* pnew = SLbuynode(x);
if (*pphead == NULL)
{
*pphead = pnew;
return;
}
SL* Ptail = *pphead;
while (Ptail->next)
{
Ptail = Ptail->next;
}
Ptail->next = pnew;
}
测试代码
cpp
void test01()
{
SL* pptest = NULL;
SLpushback(&pptest,1);
SLprint(pptest);
}
int main()
{
test01();
return 0;
}
代码说明
对上面的代码进行的几点说明
cpp
SLpushback(&pptest,1);
1.因为是对pphead进行的复制,所以上面函数进行的是传址调用,如果是传值调用的话,由于形参是实参的一份临时拷贝,所以在进行复制的时候,等到函数结束的时候形参销毁,该函数又没有返回值,所以对形参的改变并不会引起实参的改变
cpp
assert(pphead);//判断有效
2.断言部分,当你传入的地址是空指针的时候,编译其会报错,所以加上断言,但是我觉得具体原因是:当你传入空指针的时候,对该地址进行解引用的时候是找不到你要插入的位置的,就像别人给了你一张空头支票,你去银行去取钱,结果柜员一看,根本就找不到支票里面的钱在哪个地方一样,这是你的表情一定会和下面的一样
3.对于*pphead是否需要断言,答案是否定的,那里就是需要插入数据,因此是不需要断言的
链表的头插
cpp
void SLpushfront(SL** pphead, SLdatetype x)
{
assert(pphead);
SL*pnew= SLbuynode(x);
pnew->next = *pphead;
*pphead=pnew;
}
链表的尾删
cpp
void SLpopback(SL** pphead)
{
assert(pphead);
assert(*pphead);
//链表只有一个节点的情况
if ((*pphead)->next == NULL)
{
*pphead = NULL;
free(*pphead);
return;
}
SL* tail = *pphead;
SL* pre = NULL;//尾节点的前一个节点,在尾节点删除的时候,要将前面的节点的下一个节点置为空
while (tail->next)
{
pre = tail;//找到前驱节点
tail = tail->next;
}
pre->next = NULL;
free(tail);
tail = NULL;
}
链表的头删
关于链表的头删,只需要把第一个节点释放掉,然后然第二个节点变成第一个节点就行了
cpp
void SLpopfront(SL** pphead)
{
SL* pcur = *pphead;
*pphead= pcur->next;
free(pcur);
pcur = NULL;
}
记住,在删除链表的头节点的时候,要先把头节点赋值给第二个节点,在对头节点进行释放,比如,写成下面的代码就不行
cpp
void SLpopfront(SL**pphead)
{
freep(*pphead);
*pphead=(*pphead)->next;
}
//因为已经释放了头节点,头节点已经变成了空指针
//空指针的下一个节点是不知道的
链表的查找
链表的查找和数组元素的查找是一样的,都是通过遍历的方式查找,找到了就返回当前的地址,没有找到就返回空指针
cpp
SL* SLfind(SL** pphead, SLdatetype x)
{
assert(pphead);
SL* pfind = *pphead;
while (pfind)
{
if (pfind->date == x)
{
return pfind;//找到了,返回当前的指针
}
pfind = pfind->next;
}
return NULL;//没有找到,返回空指针
}
在指定位置之前插入数据
思路:情况一
该链表只有一个节点,直接调用头插函数
情况2:
该链表有多个链表,通过遍历的方式找到前驱节点,改变前驱节点的指向
cpp
void SLpushposfront(SL** pphead, SL* pos, SLdatetype x)
{
assert(pphead);
assert(*pphead);
SL* prev = *pphead;//定义前驱节点
if ((*pphead)->next == NULL)//该链表只有一个节点
{
SLpushfront(pphead, x);
return;
}
SL* pnew = SLbuynode( x);
while (prev->next != pos)
{
prev = prev->next;
}
//该节点为前驱节点
prev->next = pnew;
pnew->next = pos;
}
在指定位置之后插入节点
cpp
void SLpushposback(SL* pos, SLdatetype x)
{
assert(pos);
SL* posback = pos->next;//记录pos后面位置的节点
SL* newnode = SLbuynode(x);
pos->next = newnode;
newnode->next = posback;
}
对上面代码的说明
为啥要在插入节点之前的步骤中先记录pos后面的节点嘞?
原因上面的最后一句代码
cpp
newnode->next = posback;
如果不记录的话,posback就变成了要插入的代码了,比如你写成这样
cpp
pos->next = newnode;
newnode->next = pos->next;//pos->next是newnode
删除pos节点
思路:情况一:多个节点找到先驱节点,改变先驱的指向,让它指向pos节点的下一个节点
情况二:只有一个节点,执行头删或尾部删除
cpp
void SLerasepos(SL**pphead, SL* pos)
{
assert(pphead);
assert(*pphead);
SL* prev = *pphead;//定义前驱节点
if ((*pphead)->next == NULL)//只有一个节点,删除尾节点
{
SLpopfront(pphead);
return;
}
while (prev->next != NULL)
{
prev = prev->next;
}//退出循环,前驱节点被找到
prev->next = pos->next;
free(pos);
pos = NULL;
}
注意:上面最后三行代码的顺序不能交换,不能写成下面的代码
cpp
free(pos);
pos = NULL;
prev->next = pos->next;
原因是pos节点已经被释放,后面哪来的pos->next
删除pos之后的节点
cpp
void SLTEraseAfter(SLTNode* pos) {
assert(pos);
//pos->next不能为空
assert(pos->next);
//pos pos->next pos->next->next
SLTNode* del = pos->next;
pos->next = pos->next->next;
free(del);
del = NULL;
}
摧毁链表
cpp
void SListDesTroy(SLTNode** pphead) {
assert(pphead);
assert(*pphead);
SLTNode* pcur = *pphead;
while (pcur)
{
SLTNode* next = pcur->next;
free(pcur);
pcur = next;
}
*pphead = NULL;
}