hello 大家好,欢迎来到链表的学习,而我们目前要学的是较为简单的单链表,也叫做不带头单向不循环链表,而链表的分类实际上有8种:
链表的概念及结构
链表是⼀种物理存储结构上⾮连续、⾮顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。
链表的结构跟⽕⻋⻋厢相似,淡季时⻋次的⻋厢会相应减少,旺季时⻋次的⻋厢会额外增加⼏节。只需要将⽕⻋⾥的某节⻋厢去掉/加上,不会影响其他⻋厢,每节⻋厢都是独⽴存在的。
⻋厢是独⽴存在的,且每节⻋厢都有⻋⻔。想象⼀下这样的场景,假设每节⻋厢的⻋⻔都是锁上的状态,需要不同的钥匙才能解锁,每次只能携带⼀把钥匙的情况下如何从⻋头⾛到⻋尾?
最简单的做法:每节⻋厢⾥都放⼀把下⼀节⻋厢的钥匙。
与顺序表不同的是,链表⾥的每节"⻋厢"都是独⽴申请下来的空间,我们称之为“结点/节点”
节点的组成主要有两个部分:当前节点要保存的数据和保存下⼀个节点的地址(指针变量)。
图中指针变量 plist保存的是第⼀个节点的地址,我们称plist此时“指向”第⼀个节点,如果我们希望plist“指向”第⼆个节点时,只需要修改plist保存的内容为0x0012FFA0。
为什么还需要指针变量来保存下⼀个节点的位置?
链表中每个节点都是独⽴申请的(即需要插⼊数据时才去申请⼀块节点的空间),我们需要通过指针变量来保存下⼀个节点位置才能从当前节点找到下⼀个节点。
由此,我们可以得出链表每个节点的结构体代码:
struct SListNode{
LNDatatype data;//储存的数据
struct SListNode* next;//指针变量用作保存下一个节点的地址
}
当我们想要保存⼀个整型数据时,实际是向操作系统申请了⼀块内存,这个内存不仅要保存整型数据,也需要保存下⼀个节点的地址(当下⼀个节点为空时保存的地址为空)。
当我们想要从第⼀个节点⾛到最后⼀个节点时,只需要在前⼀个节点拿上下⼀个节点的地址(下⼀个节点的钥匙)就可以了。
单链表的实现
打印单链表
代码如下:
void SLPrint(SLNode* phead) {
assert(phead);//防止头节点为空
SLNode* pcur = phead;//用来遍历链表的变量
while (pcur != NULL)
{
printf("%d->", pcur->data);
pcur = pcur->next;//链表指针向后移动
}
printf("NULL\n");//最后一个元素是NULL
}
流程图:
最后打印出的结果就是:1->2->3->NULL
创建新节点
SLNode* SLBuyNode(SLDataType x) {
SLNode* node = (SLNode*)malloc(sizeof(SLNode));//为新节点node动态分配一块内存
if (node == NULL)//如果动态内存分配失败
{
perror("malloc");
return;
}
node->data = x;//新节点的值赋上
node->next = NULL;//node的next置为空,防止隐患
return node;//返回新节点
}
如果不是很懂的话,可以观看上几篇之中的动态内存分配先了解了解~
尾插
void SLPushBack(SLNode** pphead, SLDataType x) {
assert(pphead);//没有assert(*pphead)的原因是因为头节点为空也无所谓
SLNode* node = SLBuyNode(x);//新节点创建
if (*pphead == NULL)//如果链表为空,则新节点就是头节点
{
*pphead = node;
return;
}
SLNode* pcur = *pphead;
while (pcur->next!=NULL)//遍历链表,直到pcur指向尾节点
{
pcur = pcur->next;
}
pcur->next = node;//pcur的next指向尾插的node,node创建前就已经指向NULL了
}
流程图:
从这张图可以看出,pcur在到达3这个尾节点时,首先让node指向pcur的next,再让pcur的next指向node
而这个顺序是不可以改变的,如果改变了之后,node就找不到pcur原本的next了,虽然也可以一开始用另一个变量存储pcur的next指向的地址,但是肯定是没有顺序写对时那么方便的
另外,大家会发现我们传入的变量变成了(SLNode * * pphead),而之前我们传入的是*phead,这里其实是因为我们如果使用的是后者的话,在用的时候就会变成这样子:
int main()
{
SLNode* plist=(SLNode*)malloc(sizeof(SLNode));
SLPushBack(plist,1);
}
我们可以发现,plist和phead是同一个类型,都是SLNode * ,那么,这和传值调用有什么区别呢?我们都知道,形参的改变无法影响实参,那么这个尾插实际上就是没用的,而只用使用前者时,传入plist的地址,也就是传址调用,才是真正的在plist之后插入了一个值为1的链表尾节点
所以解释完之后,也希望大家可以在观察后续函数时,不再对传入变量的类型有疑问
头插
void SLPushFront(SLNode** pphead, SLDataType x) {
assert(pphead);
SLNode* node = SLBuyNode(x);//创建新节点
node->next = *pphead;//将node的next地址指向原来头节点的位置
*pphead = node;//让node成为新的头节点
}
流程图:
尾删
void SLPopBack(SLNode** pphead) {
assert(pphead);//用于检查指向头节点指针的 pphead 是否为非空
assert(*pphead);//用于检查头节点是否存在,即指向的节点是否为非空
//简而言之就是防止链表为空
//这里把*pphead用括号括起来的原因是因为->的优先级高于*,不括起来解引用就会故障
if ((*pphead)->next == NULL)//只有一个节点的情况
{
free(*pphead);
*pphead = NULL;
}
else
{
SLNode* ptail = *pphead;//找到尾节点
SLNode* prev = NULL;//找到尾节点的前一个节点
while (ptail->next)
{
prev = ptail;
ptail = ptail->next;//这样子ptail一直在prev前面一个节点
}
prev->next = ptail->next;//让prev的next指向ptail的next
free(ptail);//之后就可以尾删了
ptail = NULL;
}
}
示意图
头删
void SLPopFront(SLNode** pphead) {
assert(pphead);
assert(*pphead);
SLNode* del = *pphead;//保存头节点到del
*pphead = (*pphead)->next;//让原本的下一个节点变为头节点
free(del);//头删
del = NULL;
}
示意图
找到指定节点
SLNode* SLFind(SLNode** pphead, SLDataType x) {
assert(pphead);
SLNode* pcur = *pphead;
while (pcur)//遍历链表
{
if (pcur->data == x)
return pcur;//如果找到就返回节点地址
pcur = pcur->next;//没找到,pcur继续遍历
}
return NULL;//没找到,返回空
}
指定位置前插入
void SLInsert(SLNode** pphead, SLNode* pos, SLDataType x) {
assert(pphead);
assert(*pphead);
assert(pos);//不允许传空链表
SLNode* pcur = *pphead;
SLNode* node = SLBuyNode(x);//创建新节点
if (pos == *pphead)//就是头插
{
node->next = *pphead;
*pphead = node;
return;
}
SLNode* prev = *pphead;
while (prev->next != pos)//让prev的next为pos
{
prev = prev->next;
}
node->next = prev->next;//其实就是把node与pos相连
prev->next = node;//再把prev的next指针从与pos相连改为与node相连
}
示意图:
这里也要注意不可以改变顺序!
指定位置后插入
void SLInsertAfter(SLNode* pos, SLDataType x) {
assert(pos);//不可以传空
SLNode* node = SLBuyNode(x);//创建新节点
node->next = pos->next;//先让node与pos的next连接
pos->next = node;//再让pos与node连接
}
示意图:
指定位置删除
void SLErase(SLNode** pphead, SLNode* pos) {
assert(pphead);
assert(pos);
assert(*pphead);
SLNode* prev = *pphead;
if (pos == *pphead)//如果头删
{
*pphead = (*pphead)->next;
free(pos);
pos = NULL;
return;
}
while (prev->next != pos)//让prev走到next指向pos
{
prev = prev->next;
}
prev->next = pos->next;//让prev的next指向pos的next
free(pos);//删除pos
pos = NULL;
}
示意图:
指定位置后删除
void SLEraseAfter(SLNode* pos) {
assert(pos);
assert(pos->next);//pos后也不能是NULL,不然删除操作就出错了
SLNode* del = pos->next;
pos->next = del->next;//pos的next指向del的next
free(del);//删除del
del = NULL;
}
示意图:
销毁链表
void SLDestroy(SLNode** pphead) {
assert(pphead);
SLNode* pcur = *pphead;
while (pcur)
{
SLNode* next = pcur->next;//先保存下一个节点
free(pcur);//再释放此节点
pcur = NULL;
pcur = next;//让pcur继续遍历
}
*pphead = NULL;
//在删除链表时,我们通过遍历并释放每个节点来释放整个链表的内存。
//一旦链表的所有节点都被释放,原始的头节点 *pphead 将会成为悬挂指针,指向一个已经释放的内存地址。
//将 *pphead 置为 NULL 可以防止悬挂指针,并用于指示链表已被销毁
}
以上就是单链表的所有功能函数,希望大家通过注释与图表,对于单链表的了解可以更深,最后可以自己写出单链表的所有功能并进行运用!如果大家发现文章有什么问题的话,也希望大家可以与我探讨!