目录
前言
哈喽——大家好鸭,欢迎来到小鸥的博客~
新人进步中~~
有各种不足欢迎大家指出:海盗猫鸥-CSDN博客
那么我们正式开始:
在之前的博客中我们介绍了顺序表这个数据结构,今天我们就继续讲解下一个数据结构——单链表吧!
单链表的介绍
链表和顺序表一样也是线性表的一种,链表的逻辑结构是线性的,但他的物理结构不一定是线性的;链表又详细分为了8种,具体的分类方式这里先按下不表,之后我还会再出一篇双向链表的博客,在那里再进行解读。
单链表结构分析:
链表的结构就像一个火车一样,一个接着一个;也和我们生活中的链子相似,一个一个的连在一起,所以称为链表;而链表中的每一个节点都是单独申请的,就像火车车厢,可以按需要随意的添加和删减。
理解节点之前的链接关系:
每一个节点都存放着一份数据,但每一个节点之间,不能无缘无故的链接起来,所以还要有一个办法能用来链接每个节点。
是的,你想得没错,这里我们就要用到指针啦!让每一个节点存储数据的同时,在存放一个指针,让这个指针指向下一个节点的地址,这样我们就可以通过一个节点找到后面的所有节点啦!
图解:
1.每一个节点的next里都存放了下一个节点的地址;
2.第一个节点的地址存放在phead指针中;
3.尾节点的next指向NULL,以此表示结尾。
链表的优点
由于链表的每一个节点都是通过指针来链接起来的,相互可以是独立的,并且每有一个数据,就可以申请一个节点将其储存,再加入到链表中,所以相对于顺序表,链表的优点有:
1.空间利用率相对更高,不会浪费空间;
2.管理更加便捷,在进行删除和添加数据时更加便捷;
单链表的实现
上面我们提到,每个节点要存放数据,以及一个指针,那么可想而知,链表的节点也是一个自定义的结构体
定义节点结构体
typedef int SLTDataType;
//单链表节点
//数据+下一个节点的地址
typedef struct SListNode
{
SLTDataType data;
struct SListNode* next;
}SLTNode;
这里存储的数据类型我们也像顺序表中一样,进行了重命名,方便以后需要时的修改。
初始化和销毁
在创建单链表时,会定义一个指针plist,置为NULL时表示没有节点,后续使他指向链表的第一个节点。
SLTNode* plist = NULL;
后续使用的二级指针pphead即为&plist。
pphead == &plist 表示的是plist的地址;
*pphead == plist 表示的是第一个节点的地址,若没有节点则为初始值NULL;
初始化:
由于单链表是没有实际上的头节点(哨兵位,会在接触双链表时讲解)的,第一个节点就是直接有效的数据节点,所以只需要在定义单链表时,将定义的指向第一个节点的指针置为NULL即可,不需要单独的函数来初始化;
销毁:
//销毁链表
void SLTDesTroy(SLTNode** pphead)
{
assert(pphead && *pphead);
while (*pphead)
{
SLTNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
*pphead = NULL;
}
1.判断指向第一个有效节点的指针是否定义,以及单链表是否存在有效节点;
2.循环释放动态申请空间;
3.在将最后一个节点释放后,将*pphead即plist指针置为空,回到初始状态。
打印链表和查找节点
注意:
打印和查找都是基于当前int数据类型实现的,当数据类型改变时打印和查找的方式就会随之改变;
打印链表:
即为遍历链表将数据打印:
//打印
void SLTPrint(SLTNode* phead)
{
while (phead)
{
printf("%d->", phead->data);
phead = phead->next;
}
printf("NULL\n");
}
1.打印不会修改plist的内容,所以不需要传址调用,参数为一级指针就足够了(二级指针也对);
2.本次单链表的数据类型是int类型,所以%d打印phead->data;
3.从phead指向的第一个有效节点开始循环往后打印;
4.phead = phead->next 表示将指向当前节点的指针phead的指向改为下一个节点,因为phead->next存放的就是下一个节点的地址(链表的遍历)。
查找节点:
即对比待查找数据是否存在在链表中,存在返回其地址;
//查找数据
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
SLTNode* pcur = phead;
while (pcur)
{
if (pcur->data == x)
{
return pcur;//找到后返回当前数据所在的节点的地址
}
pcur = pcur->next;
}
return NULL;
}
1.和打印同理,传参一级指针即可;
2.遍历链表,遇到相同的数据就直接返回当前节点地址;
3.不存在目标数据时,返回空指针。
创建节点
在想要存放数据时,我们就需要创建一个新的节点来存放它,再对这个新节点进行各种操作。
//创建节点
SLTNode* SLTBuyNode(SLTDataType x)
{
//开辟动态空间
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
perror("malloc fail!");
exit(1);//0为正常退出码
}
//开辟成功
newnode->data = x;
newnode->next = NULL;
return newnode;
}
1.定义一个指向新节点的指针,并开辟动态内存;
2.若开辟成功,将要存放的数据放到新节点的data成员中,将next指针置为空;
尾插与头插
尾部插入数据:
//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
//头指针必须存在
assert(pphead);
//创建节点
SLTNode* newnode = SLTBuyNode(x);
//1.链表没有节点时
if (*pphead == NULL)
{
*pphead = newnode;
}
//2.链表有节点时
else
{
SLTNode* ptail = *pphead;
//使用临时节点指针找到最后一个节点
while (ptail->next)
{
ptail = ptail->next;
}
//找到了尾节点
ptail->next = newnode;
}
}
1.创建新节点存放数据;
2.若当前链表没有节点,则直接将链表指针*pphead指向新节点,使其成为第一个有效节点;
若存在有效节点,则定义一个ptail指针,从头开始遍历链表,当ptail->next为NULL时,即表示ptail当前指向的就是尾节点,使其next指针指向newnode即可。
头部插入数据:
头插就是在第一个有效节点之前插入数据。
头插不需要考虑没有节点的情况,因为此时*pphead是NULL,而newnode->next原本也是NULL,*pphead=newnode就直接完成了操作。
//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
//创建新节点
SLTNode* newnode = SLTBuyNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
1.创建新节点存放数据;
2.让新节点的next指向第一个节点,再让原本指向第一个有效节点的指针*pphead(plist)指向新节点即可。
注:
newnode->next = *pphead; *pphead = newnode;
这两步是不能交换位置的,由于原本第一个节点的地址存放在*pphead中,如果先将*pphead指向newnode,原本的地址就被覆盖掉了,就无法将原本第一个节点的地址给newnode的next了。
尾删与头删
尾部删除:
//尾删
void SLTPopBack(SLTNode** pphead)
{
assert(pphead && *pphead);
//只有一个节点时
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
//多个节点时
else
{
SLTNode* ptail = *pphead;
SLTNode* prev = *pphead;
//找到尾节点ptail和倒数第二个节点prev
while (ptail->next)
{
prev = ptail;
ptail = ptail->next;
}
//prev ptail ptail->next
free(ptail);
ptail = NULL;
prev->next = NULL;
}
}
1.删除数据相较于插入数据,需要额外保证链表中必须要存在节点;
2.定义两个指针,一个用来表示尾部节点,一个表示尾节点的前一个节点(prev用于在将尾节点释放后,将新尾节点的next置为NULL);
3.遍历链表找到尾节点和倒数第二个节点,释放尾节点,将ptail指针置为NULL,将prev->next置为NULL;
4.当只有一个节点时,上面的方法就不适用了,所以单独为一种情况,直接释放*pphead指向的节点并置为NULL即可。
头部删除:
和头插一样,头删也不需要像尾插尾删那样考虑额外特殊情况
//头删
void SLTPopFront(SLTNode** pphead)
{
assert(pphead && *pphead);
//->操作符的优先级大于*
SLTNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
1.assert断言和尾删同理;
2.定义一个临时的next结构体指针,存放第二个节点的地址;
3.释放第一个有效节点,将*pphead指向next,使第二个节点成为新的首节点。
指定位置之前插入与指定位置之后插入
注:指定位置,指的是指定的数据所在的节点位置。
指定位置的操作,都有一个pos结构体指针,它指向的就是目标节点;pos指针是使用上文的查找节点函数SLTFind函数来得到的。
指定位置之前插入:
//指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pphead && &pphead);
assert(pos);
//创建包含数据x的新节点
SLTNode* newnode = SLTBuyNode(x);
//链表只有一个节点时
if (pos == *pphead)
{
//就相当于只有头插
SLTPushFront(pphead, x);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
newnode->next = pos;
prev->next = newnode;
}
}
1.指定位置的操作必须存在节点才能进行,没有节点时无法得到pos指针;
2.创建新节点存放数据;
3.定义一个prev指针,遍历链表,使其指向pos目标节点的上一个节点;
4.在prev节点后面插入新节点;
5.若只有一个节点时,上面的方法同样不适用,所以单独讨论;当只有一个节点时,在这个节点前插入数据,就相当于头插,直接调用头插函数即可。
指定位置之后插入:
//指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
//创建储存x的新节点
SLTNode* newnode = SLTBuyNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
1.直接使用pos的地址就能直接插入数据,所以不需要SLTNode** pphead参数;
2.创建节点后,直接将节点插入到pos节点之后即可;
注:后面两步赋值操作和上文头插中同理,同样不能交换先后顺序,不然将导致出错。
删除指定节点与删除指定位置之后的节点
删除指定节点:
原理和尾删有相似
//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead&&* pphead);
assert(pos);
//当pos为第一个节点
if (pos == *pphead)
{
*pphead = pos->next;
free(pos);
pos = NULL;
//头删
//SLTPopFront(pphead);
}
//pos不为第一个节点
else
{
//定义prev用来寻找pos的前一个节点
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
//prev pos pos->next
prev->next = pos->next;
free(pos);
pos = NULL;
}
}
1.先进行断言判空,保证函数正常运行;
2.当目标节点不是第一个节点时,原理就和尾删相似,只不过将找到尾节点和倒数第二个节点的操作,改为了找到pos节点之前的一个节点(pos节点作为参数已经找到了);
3.将pos节点排除到链表之外,即prev->next = pos->next操作(尾删中,是将prev节点的next置为NULL,以此来表示尾节点);
4.释放pos节点,将指针置为NULL。
删除指定节点之后的节点:
和指定位置之后插入一样,不需要SLTNode** pphead参数,直接使用pos指针就可完成操作
//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos)
{
assert(pos);
//定义一个del为pos的下一个节点,即为要删除的节点
SLTNode* del = pos->next;
//将del节点的下一个节点给pos->next
pos->next = del->next;
//释放节点
free(del);
del = NULL;
}
1.定义一个del指针,表示要删除的节点;
2.让pos->next指向del的下一个节点;
3.释放节点并置NULL。
后记
单链表的介绍和实现就到这里结束啦!有讲得不清楚或者不足的地方大家可以在评论区或者私信指出喔~
那么我们下篇再见——
附代码
大家可以在自行实现后做参考
SList.h文件
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int SLTDataType;
//单链表节点
//数据+下一个节点的地址
typedef struct SListNode
{
SLTDataType data;
struct SListNode* next;
}SLTNode;
//打印
void SLTPrint(SLTNode* phead);
//初始化(创建节点)
SLTNode* SLTBuyNode(SLTDataType x);
//查找数据
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);
//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x);
//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x);
//尾删
void SLTPopBack(SLTNode** pphead);
//头删
void SLTPopFront(SLTNode** pphead);
//指定位置之前插入数据
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 SLTDesTroy(SLTNode** pphead);
SList.c文件
#include "SList.h"
//打印
void SLTPrint(SLTNode* phead)
{
while (phead)
{
printf("%d->", phead->data);
phead = phead->next;
}
printf("NULL\n");
}
//初始化(创建节点)
SLTNode* SLTBuyNode(SLTDataType x)
{
//开辟动态空间
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);
//创建节点
SLTNode* newnode = SLTBuyNode(x);
//1.链表没有节点时
if (*pphead == NULL)
{
*pphead = newnode;
}
//2.链表有节点时
else
{
SLTNode* ptail = *pphead;
//使用临时节点指针找到最后一个节点
while (ptail->next)
{
ptail = ptail->next;
}
//找到了尾节点
ptail->next = newnode;
}
}
//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
//创建新节点
SLTNode* newnode = SLTBuyNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
//尾删
void SLTPopBack(SLTNode** pphead)
{
assert(pphead && *pphead);
//只有一个节点时
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
//多个节点时
else
{
SLTNode* ptail = *pphead;
SLTNode* prev = *pphead;
//找到尾节点ptail和倒数第二个节点prev
while (ptail->next)
{
prev = ptail;
ptail = ptail->next;
}
//prev ptail ptail->next
free(ptail);
ptail = NULL;
prev->next = NULL;
}
}
//头删
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)
{
if (pcur->data == x)
{
return pcur;//找到后返回当前数据所在的节点的地址
}
pcur = pcur->next;
}
return NULL;
}
//指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pphead && &pphead);
assert(pos);
//创建包含数据x的新节点
SLTNode* newnode = SLTBuyNode(x);
//链表只有一个节点时
if (pos == *pphead)
{
//就相当于只有头插
SLTPushFront(pphead, x);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
newnode->next = pos;
prev->next = newnode;
}
}
//指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
//创建储存x的新节点
SLTNode* newnode = SLTBuyNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead&&* pphead);
assert(pos);
//当pos为第一个节点
if (pos == *pphead)
{
*pphead = pos->next;
free(pos);
pos = NULL;
//头删
//SLTPopFront(pphead);
}
//pos不为第一个节点
else
{
//定义prev用来寻找pos的前一个节点
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
//prev pos pos->next
prev->next = pos->next;
free(pos);
pos = NULL;
}
}
//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos)
{
assert(pos);
//定义一个del为pos的下一个节点,即为要删除的节点
SLTNode* del = pos->next;
//将del节点的下一个节点给pos->next
pos->next = del->next;
//释放节点
free(del);
del = NULL;
}
//销毁链表
void SLTDesTroy(SLTNode** pphead)
{
assert(pphead && *pphead);
while (*pphead)
{
SLTNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
*pphead = NULL;
}