由上文提及因顺序表有连续的物理存储结构,所以其头插头删效率较低,而本文就来介绍一种头插头删效率较高的数据结构——链表
一、定义:
链表是一种物理上不连续,逻辑上连续的线性数据结构,可以将数据依次存放在一个又一个节点中,并且前后的两个节点都有一定的关联性。下图是一种链表类型(单向不带头不循环链表)的逻辑图
二、分类:
链表有三个可以分类的点,分别是单向还是双向、是否循环,有无头结点
头结点:它是在第一个存储数据的节点之前附设的一个新节点,其指针域指向第一个存储数据的节点。头结点可以不存储任何数据,也可根据需求存储数据(可存可不存)。
由上图可得严格意义上链表分为8类,但从实用性来分析,主要用的是单向不带头不循环链表
和双向带头循环链表。本文主要详细讲单向不带头不循环链表,下节再讲双向带头循环链表。
上面展示了单向不带头不循环链表的逻辑图就不再重复了
下图则为双向带头循环链表的逻辑图(首元结点指的是链表中的存储第一个有效数据的节点)
三、结构:
根据三个特点:单向、不带头、不循环。
实现单向:链表中的每个节点不仅要存储数据还要可以单一地指向下一个节点
而在C语言中只有指针具有指向作用的,所以我们每个节点要需要通过结构体包含一个数据和一个指针,结构就如下所示
typedef int SLTDataType;
struct Seqlist
{
SLTDataType data;
struct Seqlist* next;
};
typedef struct Seqlist SLTNode;
不带头:不另设一个节点指向第一个存储数据的节点。
不循环:尾结点不指向首节点,而是指向空(NULL)。
如下图所示:
四、代码实现(单向不带头不循环):
1.创建节点
两大步骤:
一、申请一块空间可以存储数据
二、存储好数据、将指针置为空(防止出现野指针问题)
SLTNode* SListBuyNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
perror("malloc fail");
return;
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
2.打印
两步走:一、打印节点数据
二、通过每个节点的指针找到下一个节点,遍历每个节点,直到为空结束。
void SLTPrint(SLTNode* phead)
{
SLTNode* pcur = phead;
while (pcur != NULL)
{
printf("%d->", pcur->data);
pcur = pcur->next;
}
printf("NULL");
}
3.尾插
在链表尾部插入一个节点,只需三大步
一、创建新的一个节点、存储好数据,将指针置空
二、通过指针,遍历链表,找到当前链表的尾结点。
三、改变尾结点指针指向,使新创建的节点变成新的尾结点
SLTNode* PushBack(SLTNode** pphead, SLTDataType x)
{
SLTNode* node = (SLTNode*)malloc(sizeof(SLTNode));
node->data = x;
node->next = NULL;
if (*pphead == NULL)
{
*pphead = node;
}
else
{
SLTNode* tail = *pphead;
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = node;
}
}
4.尾删
删除链表尾部节点,需要分情况讨论
若链表除了尾结点还有其余节点则需三步走
一、通过指针,遍历链表,找到当前链表的尾结点以及尾结点前一个节点
二、销毁尾结点,使原尾结点前一个节点成为新的尾结点。
三、将新尾结点的指针置空。
若链表除了尾结点无其余节点,则只需销毁尾节点。
assert(*pphead );
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
SLTNode* tail = *pphead;
SLTNode* plist = *pphead;
while (tail->next)
{
plist = tail;
tail = tail->next;
}
free(tail);
plist->next = NULL;
}
5.头插
在链表头部新插入一个节点,只需
一、创建新的一个节点、存储好数据,将指针置空。
二、新的节点指针指向原链表中的头结点,成为新头结点
SLTNode* PushFront(SLTNode** pphead, SLTDataType x)
{
SLTNode* node = (SLTNode*)malloc(sizeof(SLTNode));
node->data = x;
assert(*pphead != NULL);
node->next = *pphead;
*pphead = node;
}
6.头删
删除链表尾部节点,
分情况处理:
若链表除了头结点还有其余节点则两步走
一、通过指针关系,找到头节点的下一个节点
二、销毁原头结点、使头结点的下一个节点成为新头节点
若链表除了头结点无其余节点,则只需销毁头节点。
SLTNode* PopFront(SLTNode** pphead)
{
assert(pphead);
assert(*pphead);
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
SLTNode* tail=*pphead;
*pphead = (*pphead)->next;
free(tail);
tail = NULL;
}
}
7.查找
通过指针关系遍历这个链表,将链表每个节点的数据与所要找的数据一一判断,若没有则返回空,若有则返回查找到的节点位置
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
SLTNode* pcure = phead;
while (pcure)
{
if (pcure->data == x)
{
return pcure;
}
pcure = pcure->next;
}
return NULL;
}
8.插入
一般和查找搭配使用,在所找到的节点前插入新的节点,需分情况解决
若链表本身无节点则直接头插即可。
若有节点则需两步走
一、通过指针关系遍历整个链表找到所查找到节点的前一个节点。
二、改变指针关系,将所查找到节点的前一个节点的指针指向新节点,新节点则指向所查找到的节点。
void SLTInsert(SLTNode** pphead, SLTNode* pcure, SLTDataType x)
{
SLTNode* node = (SLTNode*)malloc(sizeof(SLTNode));
node->data = x;
if (pcure==*pphead)
{
SLTNode* PushFront(pphead, x);
}
else
{
SLTNode* tail = *pphead;
while (tail->next != pcure)
{
tail = tail->next;
}
node->next = pcure;
tail->next = node;
}
}
9.销毁
三步走: 一、通过指针关系遍历每个节点
二、记录节点的下一个节点位置
三、销毁节点,直到为空
void SLTDestory(SLTNode** pphead)
{
assert(pphead && *pphead);
SLTNode* tail = *pphead;
SLTNode* next = *pphead;
while (tail )
{
next = tail->next;
free(tail);
tail = next;
}
*pphead = NULL;//若不进行这个操作,phead就会变成野指针,很容易造成错误
}
五、总结:
链表逻辑上是连续的,物理存储是不连续的。
优点:头插头删效率较高,而且即用即插,需要一个数据就直接在合适的位置插入一个节点,没有很多空间上的浪费,
缺点:因为物理存储是不连续的,所以访问效率很低,需要从头开始,才能访问到所要的数据,故不支持随机访问。
在本文中,我们需要充分认识到链表的特性,用好其长处,避掉其短处,可以做到在合适的地方用好链表结构实现所需功能,对于我们未来学习更难的数据结构有重要的基础作用。
以上就是我分享的全部内容了,希望对大家有些 帮助,也希望与一样喜欢编程的朋友们共进步
谢谢观看
如果觉得还阔以的话,三连一下,以后会持续更新的,我会加油的
若有错误和不足,欢迎各位读者前来指正
祝大家早安午安晚安