文章目录
之前我们实现了顺序表,顺序表是在逻辑上连续物理上也连续,而链表是一种在逻辑上连续但是在物理上并不连续的数据结构。
一、链表是什么?
链表是在逻辑上连续的数据结构,具体如图所示,在使用链表时是使用指针用来指向其下一个地址的位置,而地址并不是连续的,只是我们在逻辑想着为连续的,因此并不用向顺序表一样开辟多个空间,减少了相应的空间浪费。
二、使用步骤
1.创建链表结构体
链表也是有着多组数据,因此我们仍然使用结构体用来存放数据,我们说到了链表是指向下一个的指针,所以我们在结构体中存放下一个结构体的结构体指针。(注:因为代码运行从上而下,所以结构体内的typedef在其本身中无法生效,所以指针老实打完全部)
代码如下(示例):
typedef int typedata;
typedef struct Linklist {
struct Linklist* next;
typedata data;
}Slist;
2.链表的头插
既然说到插入了那一定要开辟空间,因为插入都要开辟空间所以我们直接将其封装为一个函数。链表的头插比较简单,直接将创建好的下一个直接指向链表,如下图一样,直接将开辟的空间的下一个指向原来链表的头指针,就实现了头插。
相信也有人发现了,我们传过来的是二级指针,那为什么传的为二级指针而不是一级指针呢,我们要找到下一个空间的目标是要靠指针找的,所以我们创建数据一定是一个结构体指针类型的数据,如果用一级指针我们无法对其进行改变,我们要做到对指针进行改变,所以我们传二级指针,使其可以改变一级指针。
代码如下(示例):
Slist* capicity(typedata data)
{
Slist* newnode = (Slist*)malloc(sizeof(Slist));
if (newnode == NULL)
{
perror("malloc fail");
return NULL;
}
newnode->data = data;
newnode->next = NULL;
return newnode;
}
void Pushhead(Slist** list, typedata data)
{
assert(list);
Slist* newnode = capicity(data);
newnode->next = *list;
*list = newnode;
}
3.链表的头删
既然说到删除,那一定要检查是否还有数据可以被删除,头删我们需要记住头节点的下一个,将原来的头节点free释放,将原来头节点的下一个赋给头节点,要记得保存一下头节点的下一个再进行释放,因为如果先进行释放的话会导致找不到头节点的下一个。
代码如下(示例):
void Pophead(Slist** list)
{
assert(list);
Slist* newnode = *list;
*list = newnode->next;
free(newnode);
newnode = NULL;
}
4.链表的尾插
链表的尾插我们需要分情况查看,
如果头节点为空,说明一个数据也没有,直接将新开辟的空间付给头节点,如果头节点不为空,就需要通过循环找到链表的尾节点,链表的尾节点的下一个为NULL可以根据这个特性通过循环遍历找到,找到后将尾节点的next赋值为新开辟的空间。
代码如下(示例):
void Pushtail(Slist** list, typedata data)
{
Slist* newnode = capicity(data);
Slist* phead = *list;
if (phead == NULL)
{
*list = newnode;
}
else
{
while (phead->next)
{
phead = phead->next;
}
phead->next = newnode;
}
}
4.链表的尾删
同样的删除要检查是否有数据可删,我们如果要删除尾节点的话,我们也要分情况讨论,如果头节点下一个就为空的话那么说明链表只有一个节点,直接将其释放置空,如果不是的话我们就需要找到尾节点的之前的一个节点,因为我们是单向不循环链表,是无法找到自己的前一个的,所以需要在找到尾节点的前一个的时候就循环进行停止。
代码如下(示例):
void Poptail(Slist** list)
{
assert(list);
Slist* phead = *list;
if (phead->next == NULL)
{
free(phead);
phead = NULL;
}
else
{
while (phead->next->next)
{
phead = phead->next;
}
Slist* prev = phead->next;
free(prev);
prev = NULL;
}
}
5.链表的插入
当实现链表的插入时,我们需要增加要插入目标的地址,以及判断传过来的地址,如果插入地址和头节点地址相同,那么直接运用头插的方式将新开辟空间地址的next指向头节点再将其赋值回去,如果为其他情况,通过循环找到插入目标地址的前一个地址,通过这个地址插入数据。
代码如下(示例):
void SLinsert(Slist** list, Slist* pos, typedata data)
{
if (pos == *list)
{
Slist* newnode = capicity(data);
newnode->next = *list;
*list = newnode;
}
else
{
Slist* prev = *list;
while (prev->next != pos)
{
prev = prev->next;
}
Slist* newnode = capicity(data);
prev->next = newnode;
newnode->next = pos;
}
}
6.链表的删除
先判断是否有数据可删,如果只有一位数据,直接将其free置空,如不不是只有一位数据,通过循环遍历找到指向目标地址的前一个指针地址,将其指向目标地址指向的下一个地址,将目标地址free掉置空。
代码如下(示例):
void SLerase(Slist** list, Slist* pos)
{
assert(list);
if (pos == *list)
{
free(pos);
pos = NULL;
}
else
{
Slist* prev = *list;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = prev->next->next;
free(pos);
pos = NULL;
}
}
7.链表的打印
链表的打印通过循环遍历的方式,如果节点不为空,就将其赋值为它的下一个节点的地址。
代码如下(示例):
void Pinttail(Slist* list)
{
Slist* cur = list;
while (cur)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
三、执行效果
#include"link.h"
int main()
{
Slist* p = NULL;
Pushhead(&p,10);
Pushhead(&p,10);
Pushhead(&p,1);
Pushhead(&p,2);
Pophead(&p);
Pushtail(&p, 4);
Pinttail(p);
return 0;
}
总结
很明显,我们可以看出单链表是不适合尾插尾删的,并且链表难在其逻辑上和物理上是不同的,相比顺序表单链表比顺序表适合头插头删,所以在合适的场景我们可以选择相应合适的数据结构,希望这篇文章对你有所帮助。