在顺序表中我们了解了顺序表的概念与如何创建一个顺序表与它的接口函数完成增删查改的功能。本文将继续介绍线性存储的内容,对单链表的概念进行了相关阐述并且给出了它的接口函数的C语言实现
单链表及其接口函数
顺序表的一些缺点:
在顺序表阶段,我们了解到顺序表在内存中是一块连续存储的空间,元素与元素之间紧挨存放。不用经过太多思考,我们就可以发现顺序表在实际应用中的一些缺点:
- 为了能动态的进行存储,那么就需要创建一个动态链表进行数据存储。
- 为了避免频繁扩容,在使用realloc进行申请空间时,往往直接申请当前空间的二倍空间,这就免不了空间浪费。
- 在插入或者删除数据时,需要挪动整个顺序表,效率极低。
- realloc函数在申请内存空间时,有申请失败的风险。
于是有人就考虑用链表的方式来解决上述相关问题。
什么是单链表?
顾名思义,单链表是一种区别于顺序表的一种不需要在内存中连续存储的数据存储类型。各个数据元素可以在内存中分别存储,数据与数据之间通过next指针的方式,相互连接,在结构上如同“铰链”一样形成一个整体。如图所示,可以方便你更加清晰的认识单链表的结构:
单链表仍然以一个结构体作为基本结构。在Data中存放数据,而结构体的next指针中存放下一个数据的地址。只要通过对当前数据的next指针进行解引用操作就可以找到下一个数据元素。单链表总是以一个指向第一个元素的头指针开始,直到最后一个元素的空指针作为结束标志。在上述图中,我们可以发现:由于指针的寻址特性,所以链表不需要连续存储在内存中。
链表的优点:
按需申请空间,不用了就释放空间(更合理的使用了空间)
头部、中间插入数据,不需要挪动数据
链表的缺点:
每一个数据都要一个指针去存链接后面数据的结点
不支持随机访问(用下标访问第i个)
与顺序表不同的是,
链表是以头指针开始的,而不是如顺序表一样单纯的用一个结构体变量的实体化来创建的。单链表是用指针来进行维护的。链表的起始是用一个为NULL的结构体指针开始的,对于指针进行了赋值操作即完成了初始化,固不再额外使用初始化函数对其进行初始化。
注意:要理解好空指针NULL的相关概念,空指针是不能进行解引用操作的,空指针存放的就是0x00000000,它不能解引用访问任何内容,所以一切对空指针的解引用操作都是错误的。但是存放NULL的指针(也就是空指针)是有地址的,尾插传递的就是这个空指针的地址,对空指针的地址的解引用是有效的,得到的是0x00000000这个NULL值。
单链表的实现
链表的每一个节点需要具备的功能有:
1、存放数据
2、访问下一个节点存放的内容
所以对于一个单链表来说,同样采用结构体的方式对其中内容进行定义:
为了方便更换数据类型将我们将int等类型可以类型重定义为SLTDataType
//.h文件
#pragma once
#include<stido.h>
#include<stdlib.h>
typedef int SLTDataType
typedef struct SListNode
{
SLTDataType data;//存的数据
struct SlistNode* next;//next指针
}SLTNode;
//辅助函数用于新节点创建
SLTNode* BuyListNode(SLTDataType x);
//打印函数
void SListPrint(SLTNode* phead);
//尾插
void SListPushBack(SLTNode** pphead,SLDataType x);
//头插
void SListPushFront(SLTNode** pphead,SLDataType x);
//尾删
void SListPopBack (SLNode** pphead);
//头删
void SListPopFront(SLNode** pphead);
//查找函数
void SListFind(SLTNode* phead,SLTNodeType x);
//指定下标插入
void SListInsert(SLTNode* phead,SLTNode *pos,SLTDataType x);
//指定下标之后的一个位置插入
void SListInsertAfter(SLNode* pos,SLTDatatpye x);
//擦除某个特定位置的元素
void SListErase(SLTNode** pphead, STLNode pos,SLTDataType x);
//擦除某个特定未知的之后的元素
void SListEraseAfter(SLTNode* pos);
//销毁链表
void SlistDestory(SLTNode** pphead);
接口函数的具体实现
//.h对应的.c文件
//辅助函数 用于结点创建
SLTNode* BuyListNode(SLTDataType x)
{
SLTNode* newnode=(SLTNode*)malloc(sizeof(SLTNode));
if(newnode==NULL)
{
printf("空间不够了");
exit(-1);
}
else
{
newnode->data=x;
newnode->next=NULL;
}
return newnode;
}
//打印函数
void SListPrint(SLTNode* phead)
{
SLTNode*cur=phead;
while(cur!=NULL)
{
printf("%d->",cur->data);
cur=cur->next;
}
}
//尾插函数
void SListPushBack(SLTNode** pphead, SLTDataType x)
//切记这个地方要用二级指针来接收 维护链表的phead对其进行传址操作处理
{
SListNode* newnode=BuyListNode(x);
if(*pphead==NULL)//判断是否phead内存储的是空,即需要判断链表是否开始了
{
*pphead=newnode;//让头指针指向 这个newnode新节点所维护的struct空间
}
else//此时说明链表中的有元素,需要寻找尾结点
{
SLTNode* tail=*pphead;
//尾结点的标志是next为空
//找到尾结点
while(tail->next !=NULL)
{
tail=tail->next;
}
tail->next=newnode;
}
}
//头插函数
//头插时不需要对头指针判空,只需要申请空间创建节点并赋予头指针即可
void SListPushFront(SLTNode** pphead,SLTDataType x)
{
SListNode* newnode=BuyListNode(x);
newnode->next=*pphead;
*pphead=newnode;
}
//尾删
void SListPopBack (SLNode** pphead)
{
if(*pphead==NULL)//链表为空的时候直接返回0,这里也可以用assert保证链表不为空
{
return 0;
}
if((*phead)->next ==NULL)//只有一个节点时
{
free(*pphead);
*pphead=NULL;
}
else
{
SLNode* prev=NULL;
SLNode* tail= *pphead;
while(tail->next!=NULL)
{
prev=tail;
tail=tail->next;
}
free(tail);//将最后一个节点的内容free掉
tail=NULL;
//将指向最后节点的tail指针置空,但此时链表中的倒数第二个的next指针并没有被修改为NULL,原因是tail被free掉之后为一个随机值
//所以要对倒数第二个节点的next置NULL完成尾删。
prev->next=NULL;
//通过倒数第三个结构体的next指针访问倒数第二个next指针将其置空,就完成了倒数第二个的next指针置空
}
/*//也可以用这种方法
SLTNode* tail=*pphead;
while(tail->next->next)
{
tail=tail->next;
}
//当调出循环时,tail为倒数第二个节点
//将tail的下一个位置的free掉
free(tail->next);
//tail作为倒数第二个节点需要对其next指针置空
tail->next=NULL;
*/
}
//头删
void SListPopFront(SLNode** pphead)
{
if(*pphead==NULL)
{
return 0;
}
SLNode* head =*pphead->next;
free(*pphead);
*pphead=head;
}
//查找函数
STLNode* SListFind(SLTNode* phead,SLTNodeType x)
{
SLTNode* pf=phead;
/* while(pf->data!=x)
{
pf=pf->next;
}
if(pf->data==x)
{
retrun 1;
}
else
{
return -1;
}*/
while(pf)
{
if(of->data==x)
{
retunn pf;
}
else
{
pf=pf->data;
}
}
return NULL;
}
//指定Pos位置插入(前插)
void SListInsert(SLTNode** pphead, STLNode * pos,SLTDataType x)
{
assert(pphead);
assert(pos);
STLNode* newnode=BuyListNode(x);
//找到pos位置
STLNode* posPrev=*pphead;
while(posPrev->next!=pos)
{
posPrev=posPre->next;
}
posPrev->next=newnode;
newnode->next=pos;
}
//指定位置pos之后插入
void SListInsertAfter(SLTNode* pos, SLTDateType x)
{
assert(pos);
SLTNode* newnode = BuyListNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
//指定位置擦除(从前遍历之后擦除)
void SListErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead);
assert(pos);
if (*pphead == pos)
{
/**pphead = pos->next;
free(pos);*/
SListPopFront(pphead);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);//将pos销毁 在立案表中pos维护的空间置为随机值
//pos = NULL;
}
}
//指定pos位置删除(pos位置后删除)
void SListEraseAfter(SLTNode* pos)
{
assert(pos);
assert(pos->next);
SLTNode* next = pos->next;//创建next结构体指针,将pos的下一个位置的结构体交于它维护,目的是找到pos下一个的下一个,以便让pos直接指向下一个的下一个,然后就可以对pos之后的内容进行删除
pos->next = next->next;//用赋值的方式将next结构体指针所维护的空间中的next指针(pos的下一个的下一个)给pos的下一个,对pos的下一个位置进行销毁
free(next);
//next = NULL;
}
//销毁
void SListDestory(SLTNode** pphead)
{
assert(pphead);
SLTNode* cur = *pphead;
while (cur)
{
SLTNode* next = cur->next;
free(cur);
cur = next;
}
*pphead = NULL;
}
//.c文件 进行test
void Test_list()
{
SLTNode* plist = NULL;
//单链表以一个结构体指针开始从而创建出整个链表,所以呼应了上述中需要传递二级指针来修改一级指针的值。
SListPushBack(&plist, 1);
SListPushBack(&plist, 2);
SListPushBack(&plist, 3);
SListPushBack(&plist, 4);
SListPrint(plist);
SListPushFront(&plist, 1);
SListPushFront(&plist, 2);
SListPushFront(&plist, 3);
SListPushFront(&plist, 4);
SListPrint(plist);
//你可以试一试其他接口函数的调用,注意传递的参数类型哦!
}
int main()
{
Test_list();
}
小结
对于一个单链表来说,不需要在内存中连续存储,实现了存储的便捷化,但是也由于这种特性不能通过下标对其进行随机访问。单链表的实体化过程用一个结构体指针来实现,在实现接口函数时,由于头指针为一个一级指针,若想修改一级指针所维护的空间的值,就需要传递一级指针的地址,用二级指针对其进行接收并通过对二级指针解引用操作来修改头指针维护的结构空间。因为单链表的每一个节点通过malloc函数开辟,所以在最后不要忘记对其进行销毁,防止出现野指针等不可预计错误。