链表是⼀种物理存储结构上⾮连续、⾮顺序的存储结构,数据元素的逻辑顺序是通过链表中的
指针链接次序实现的。
数据和数据之间地址不一定时连续的,逻辑结构是连续的
以火车为例,根据客流量来确定需要多少空间,而链表则是通过友多少个空间去确定有多少个空间
那么每节车厢就是含有本身的数组和下一个地址组成,就是乘客本身和之后车厢之后的钩子,
而在链表之中两者共同构成结点,而结点构成了链表
而最后的节点就是指向null
不断通过前一个结点去要求上一个节点的位置
链表难在结构的定义,但是其他要比顺序表好实现
slist:single list
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int SLTDataType;
//定义链表结点的结构
typedef struct SListNode
{
SLTDataType data;
struct SListNode* next;
}SLTNode;
那么这样就是链表的结构
接下来就是代码的实现
首先要构造一个链表
void test()
{
//手动构造一个链表
SLTNode* node1 = (SLTNode*)malloc(sizeof(SLTNode));
SLTNode* node2 = (SLTNode*)malloc(sizeof(SLTNode));
SLTNode* node3 = (SLTNode*)malloc(sizeof(SLTNode));
SLTNode* node4 = (SLTNode*)malloc(sizeof(SLTNode));
//申请空间
node1->data = 1;
node2->data = 2;
node3->data = 3;
node4->data = 4;
//这里是保存的数据
node1->next = node2;
node2->next = node3;
node3->next = node4;
node4->next = NULL;
//数据进行链接
}
这样就是已经构造好了
为了方便传参数,可以再定义一下首节点
void test()
{
//手动构造一个链表
SLTNode* node1 = (SLTNode*)malloc(sizeof(SLTNode));
SLTNode* node2 = (SLTNode*)malloc(sizeof(SLTNode));
SLTNode* node3 = (SLTNode*)malloc(sizeof(SLTNode));
SLTNode* node4 = (SLTNode*)malloc(sizeof(SLTNode));
//申请空间
node1->data = 1;
node2->data = 2;
node3->data = 3;
node4->data = 4;
//这里是保存的数据
node1->next = node2;
node2->next = node3;
node3->next = node4;
node4->next = NULL;
//数据进行链接
SLTNode* plist = node1;
SLTPrint(plist);
}
为了方便之后看
首先我们先实现一下打印
void SLTPrint(SLTNode* phead)
{
SLTNode* pcur = phead;
//首先是第一个节点
while (pcur != NULL)
{
printf("%d -> ", pcur->data);
//打印节点数据
pcur = pcur->next;
//将数据的指针更改,就像是火车改成下一个钩子
}
printf("NULL\n");
//最后打印一下末尾,方便解读与调试
}
链表就像是开始给了一把钥匙,第一个的🔑,之后箱子里面存在下一个的钥匙和宝藏,再读取数据时不断的更换钥匙,二这里的数据交换就是交换钥匙
这里为什么使用pucr:为了避免指针的指向的改变,因为同时只能使用一把钥匙,那么之前的钥匙就必须要保留,所以那么丢弃的就是复制的钥匙,最初的钥匙最好保留
毫无疑问的是这样建立链表太麻烦了
所以如下
直接在链表之中插入数据
以下是申请空间去将数据完成一个封装
SLTNode* SLTBuyNode(SLTDataType x)
{
SLTNode* node = (SLTNode*)malloc(sizeof(SLTNode));
if (node == NULL)
{
perror("malloc fail!\n");
exit(1);
}
node->data = x;
node->next = NULL;
return node;
}
首先是需要首个地址,那么还有需要存储数据
链表为空,那么直接申请空的,如果申请失败,那么打印,方便看是哪里出了问题
接下来就是尾插
//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newnode = SLTBuyNode(x);//赋值封装
//返回新的封装的表的新节点
if (*pphead == NULL)
{
//为空申请
*pphead = newnode;
}
else {
//链表非空,找尾结点
//首先需要找见
SLTNode* ptail = *pphead;
//首先等于头节点
while (ptail->next)//等价于ptail->next != NULL
{
ptail = ptail->next;
}
//ptail newnode
ptail->next = newnode;
}
}
这里就是尾插
那么首先我们就是需要发送的是地址,而需要使用的是二级指针
形参改编不会影响实参,不应该传值,应该传地址
那为什么这里如果不是二级指针不是地址,指针的传送也是需要传送地址,需要&符号,接受一级指针的地址需要二级指针,接受其他变量才是一级指针,那么这里就是二级指针
实质就是宝箱里面的内容复制品和宝箱本身的区别,函数传值时,类型时必须一样的,那么指针传指针,如果级数时一样的,那么就是单纯的值,而如果要传送地址的话,那么就是对指针的解引用,第一个*是解引用,第二个是代表类型,之后同理,二级指针对应的话就hi是***,第一个解引用,后面两个表示类型
那么尾插是可以的
接下来就是头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newnode = SLTBuyNode(x);
//newnode *pphead
newnode->next = *pphead;
*pphead = newnode;
}
那么首席还是先断言,防止首地址就是乱七八糟的空地址
头插进来还是熟悉的先封装,那么就是直接将这里的下一个地址改为原本的头地址,然后就是将投地址改为新节点,就是把第一个宝箱开一下,然后将原本的随身的钥匙给放进去新宝箱,将随身钥匙换成开这个宝箱的钥匙
毫无疑问的是这样的话其实原本是空的页可以,
其实这里补充一下,链表更像是先钥匙,才有的宝箱
接下来就简单了,首先是删除
//尾删
void SLTPopBack(SLTNode** pphead)
{
assert(pphead && *pphead);
//只有一个结点的情况
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else {
SLTNode* prev = NULL;
SLTNode* ptail = *pphead;
while (ptail->next)
{
prev = ptail;
ptail = ptail->next;
}
//prev ptail
prev->next = NULL;
free(ptail);
ptail = NULL;
}
}
首先判断不是个空的
接下来是不是只有一个节点
直接空间删了,头节点(唯一一个节点)指向空
接下来就是最正常的情况
首先是找见尾节点,(定义变量的原因是不但要删除最后一个,还要改变前一个的钥匙),先保存,后不断改变,之后删除prev指向的钥匙,去除最后一个宝箱
接下来就是头删
void SLTPopFront(SLTNode** pphead)
{
assert(pphead && *pphead);
SLTNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
首先是先保存一下钥匙,先把第一个宝箱开了,钥匙拿出来,之后宝箱丢了,身上的钥匙换成开原本第二个的
其实到现在可以发现头删和尾删我们可以发现和顺序表是相反的,
//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
SLTNode* pcur = phead;
while (pcur) //pcur != NULL
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}
//未找到
return NULL;
}
接下来是查找 首先是循环遍历,之后返回地址就可以,没有找见之后就是返回null
之后就是在指定位置之前改变,那么就是要实际改变,那么就是二级指针,头节点不可以为空,空那么就是压根没有,和之后的压根没有联系
指定位置是头节点就是头插,因为如果是第一个节点就是找不见前一个节点的位置
那么就是将指定位置之前原本的钥匙给换成要插入宝箱的,在宝箱之中放入指定位置的宝箱的钥匙
先数据封装,之后就是在保存一下指定位置原本之前的里面的钥匙,之后就是将循环到达指定位置,之后就是将新宝箱里面的应该放下一个钥匙的放入下一个宝箱的钥匙,这个宝箱的钥匙放入前一个宝箱
//在指定位置之前插?数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pphead && pos);
//当pos就是头结点时,相当于头插
if (pos == *pphead)
{
SLTPushFront(pphead, x);
}
else {
SLTNode* newnode = SLTBuyNode(x);
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
//prev newnode pos
newnode->next = pos;
prev->next = newnode;
}
}
之后就是指定位置之后插入数据
先是将数据封装,之后就是直接将赋值就是可以
原因:这是单链表,不可以返回,所以就是之前插需要保存一下前一个链表
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
SLTNode* newnode = SLTBuyNode(x);
//pos newnode -> (pos->next)
newnode->next = pos->next;
pos->next = newnode;
}
而尾插,直接将这个宝箱开了,将指定宝箱开了,之后换一下钥匙就可以,注意一下,新宝箱是一个钥匙和里面数据和空钥匙,那么就是我们不需保存,直接将指定位置的宝箱先放到新宝箱,而之后我们再将新的钥匙放进去
接下来是 删除某一个节点,这样的话就是将里面的和之后相关的地址那出来,之后找见指定之前,原本内容放了,将前一个的钥匙换成指定的下一个
如果
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead && pos);
//要删除的节点就是头结点
if (pos == *pphead)
{
SLTPopFront(pphead);
}
else {
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
//prev pos pos->next
prev->next = pos->next;
free(pos);
pos = NULL;
}
}
是头节点的话就是头删,不然的话就是找不见,这玩意单向的
而接下来就是指定位置之后的节点那么我们就可以发现直接用指定位置的存的地址开了下一个地址,找见要删的那个,将自己的地址换成先一个的,也就是先存再删除
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)
{
SLTNode* pcur = *pphead;
while (pcur)
{
SLTNode* next = pcur->next;
free(pcur);
pcur = next;
}
*pphead = NULL;
}
那么链表的操作就此完成了