数据结构 | 单链表

简介: 这是百度百科对于 单链表 的解释,通俗来说,单链表 就是一种数据结构,它包含了一个 数据域 data 和一个 指针域 next ,最大的特点就是 链式存储 。

🌳前言

单链表 是一种链式存取的数据结构,用一组地址任意的存储单元存放线性表中的数据元素。链表 中的数据是以结点来表示的,每个结点的构成:元素(数据元素的映象) + 指针(指示后继元素存储位置) ,元素就是存储数据的存储单元,指针就是连接每个结点的地址数据


这是百度百科对于 单链表 的解释,通俗来说,单链表 就是一种数据结构,它包含了一个 数据域 data 和一个 指针域 next ,最大的特点就是 链式存储 。链表有很多种,其中 单链表 是最基本、最简单的一种结构,很多OJ题都会利用 单链表 出题,后面的部分高阶数据结构也会用到 单链表 ,因此学好 单链表 很重要。除了 单链表 外,还有 双向带头循环链表 (后面会介绍)等链表类型,先来进入 单链表 的学习吧!


facdf8dde0e442b19e0124dc8f629826.png

🌳正文

🌲链表打印与销毁

🪴打印

单链表 创建时是一个结构体类型的指针,一开始指向空,只有在经过插入数据后才会有自己的指向 ,因此我们可以根据这一特点,遍历 整个 单链表 ,并输出其中的 数据域 data

7f1e2e59ee6f4eba8c85f00d00267572.gif

voidSLTPrint(constSLT**pphead)   //单链表的打印{
assert(pphead); //不需要断言 *pphead ,因为存在空链表打印的情况,是合法的printf("\n\n当前链表为: ");
constSLT*cur=*pphead;
while (cur)
    {
printf("%d->", cur->data);
cur=cur->next;    //cur要向后移动    }
printf("NULL\n\n\n");   //链表最后为空}

🪴销毁

销毁 单链表 也需要将其 遍历 一遍,因为链表中的元素不是连续存放的,只能逐个节点销毁 ,销毁思想为:利用 *pphead 遍历整个 单链表 ,保存头节点 *pphead 的下一个节点信息至 cur,释放原头节点,更新头节点信息(把 cur 的值赋给头节点 *pphead )如此重复,直到释放完所有节点即可。

42a0c3395e9d41a9bb62fd2f5a1173a2.gif

//不带哨兵位的单链表不需要初始化voidSLTDestroy(SLT**pphead)   //单链表的销毁{
assert(pphead); //一二级指针都不能为空//空表不走销毁程序,但不能报错if (*pphead)
    {
while (*pphead)
        {
SLT*cur= (*pphead)->next; //记录头的下一个位置free(*pphead);
*pphead=cur;  //向后移动,不断销毁        }
    }
}

🌲尾部插入与删除

🪴节点申请

单链表 是由一个一个独立存在的节点组成的结构,当涉及插入操作时,向内存申请节点,涉及删除操作时,就要把对应的节点销毁

staticSLT*buyNode(constSLTDataTypex)    //买节点{
SLT*newnode= (SLT*)malloc(sizeof(SLT));
assert(newnode);    //防止申请失败的情况newnode->data=x;
newnode->next=NULL;
returnnewnode; //返回买好的节点}

🪴尾插

单链表 尾插是比较费劲的,因为得先通过头节点指针向后 遍历 找到尾节点,然后将尾节点与新节点之间建立链接关系,其中还得分情况尾插


链表为空,直接把新节点赋给头节点

不为空,就需要找到尾节点,建立链接关系

关于 单链表 中函数用二级指针的问题:

插入或删除时,如果是第一次操作,需要对头节点本身造成改变,且头节点是一个 一级指针 ,因此需要通过 二级指针 的方式来在函数中改变头节点的值。至于后续的操作,都只是改变了结构体中的 next 值,因此使用 一级指针 就够了,但是为了函数设计时的普适性,单链表 中的函数参数都设计成了 二级指针 的形式。

单链表尾插.gif

voidSLTPushBack(SLT**pphead, constSLTDataTypex) //尾插{
assert(pphead);
SLT*newnode=buyNode(x);
SLT*tail=*pphead;
//尾插分情况,链表为空,直接赋值,不为空,找到尾,再链接if (tail==NULL)
    {
*pphead=tail=newnode;   //直接赋值    }
else    {
while (tail->next!=NULL)
        {
tail=tail->next;  //找到尾节点        }
tail->next=newnode;   //链接    }
//SLTInsert(pphead, NULL, x);       //可以复用任意位置前插}

🪴尾删

尾删操作与尾插基本一致,同样是需要找到尾节点,不过每次 tail 指针在向后移动前,会先使用一个 prev 指针保存 tail 的信息,当 tail 为尾节点时,释放 tail ,并将 prev->next 置空,此时的 prev 就是新的尾节点,因为原理都差不多,这里就不用动图展示了,值得注意的是尾删也分两种情况


只有一个节点,此时直接释放头节点(尾节点),链表置空

存在多个节点,需要先找到尾节点与尾节点的上一个节点,确定新的尾

并不是所有情况都能删除,比如表空的情况,是不能执行操作的,可以用断言处理

6afc0fb223144debaa2dba2207f36274.png

80dccd48b86c481189a56fcae709143d.png

voidSLTPopBack(SLT**pphead)   //尾删{
assert(pphead);
assert(*pphead);    //如果链表为空,是删不了的SLT*tail=*pphead;
SLT*prev=NULL;
while (tail->next)
    {
prev=tail;    //保存上一个节点信息tail=tail->next;  //找到尾节点    }
free(tail);
//分情况,如果prev是空,说明删除的尾节点同时也是头节点if (prev)
prev->next=tail=NULL;   //把尾节点的上一个节点指向空else*pphead=NULL; //此时直接把prev置空/*SLT* tail = *pphead;while (tail->next){tail = tail->next;}SLTErase(pphead, tail);*///可以复用任意位置删除}

🌲头部插入与删除

🪴头插

对于头部操作来说,单链表 是很轻松的,比如 单链表 头插的本质就是将 新节点 newnode头节点 *pphead 链接,然后更新头节点信息就行了,即 *pphead = newnode ,三行代码就解决了。

9867d97916ea47baa98ed386e1be1133.png

b1b6cf275b624ae4b4f553ba1a6b21ca.png

voidSLTPushFront(SLT**pphead, constSLTDataTypex)    //头插{
assert(pphead);
SLT*newhead=buyNode(x);
newhead->next=*pphead;    //直接链接*pphead=newhead;  //更新头节点信息//代码简洁之道//SLTInsert(pphead, *pphead, x);    //可以复用任意位置前插}

🪴头删

头删也是比较简单的,先用 cur 指向头节点,先保存 头节点 cur 的下一个节点信息至 节点 newhead,释放 原头节点 cur,更新 newhead 为链表的新头,即 *pphead = newnode 当然涉及删除的操作,都需要进行表空检查,如果链表为空,是不能执行删除的。

f460f12297004083939f08c17fb5a1e3.png

e56b85e70e2f41baa139bb36587a73cb.png

voidSLTPopFront(SLT**pphead)  //头删{
assert(pphead);
assert(*pphead);    //头删同样存在空不能删的情况//先找到头的下一个节点,然后赋值新头SLT*cur=*pphead; //指向头节点SLT*newhead=cur->next;   //保存头节点的下一个节点信息free(cur);
cur=NULL;
*pphead=newhead;  //赋值新头//SLTErase(pphead, *pphead);    //可以复用任意位置删除}

🌲节点查找与修改

🪴查找

查找函数很简单,遍历+比较 就行了,找到目标元素值后,返回相关节点信息,其实查找这个函数主要是为了配合后面任意插入删除函数的 ,所以比较简单。

SLT*SLTFind(constSLT**pphead, constSLTDataTypex)   //查找值为x的节点(第一次出现){
assert(pphead);
SLT*cur= (SLT*)*pphead;   //指向头节点while (cur)
    {
if (cur->data==x)
returncur; //找到了,返回相关节点信息cur=cur->next;
    }
returnNULL;    //没有找到的情况}

🪴修改

修改函数是在查找函数基础上进行的:当我们输入元素值交给查找函数,找到目标节点后返回其节点信息,然后直接通过返回的节点改变 data 值就行了,在调用查找函数的前提下,一行代码就解决了。

voidSLTModify(SLT*node, constSLTDataTypeval)    //修改 node 节点处的值{
//注意:在调用函数时,可以通过链式访问,将查找函数的返回值作为形参一传入就行了assert(node);   //断言,如果节点node是空指针,是不能做修改的node->data=val;
}

🌲任意位置插入与删除

🪴简单版

简单版是在任意位置后插入元素,删除任意位置后的节点,根据 单链表 的特性,对后面节点进行操作是比较简单,无非就是改变链接关系。但是这种对后操作存在缺陷:不适合实现头插、头删


🌱插入

插入(后插)主要分两步


获取信息

改变链接关系

获取信息:有三个关键信息:被插入节点 cur、待插入节点 newnode 和 cur 的下一个节点 tail


改变链接关系:很简单,cur->next = newnode,后插嘛,先把待插入节点链接到 cur 后面,然后再把 newnode 和 tail 链接起来就行了


这里的 nodeAfter 是需要通过查找函数找出来的,它是一个 一级指针

//这两个有缺陷,不能对头节点进行操作,但实现起来比较简单voidSLTInsertAfter(SLT*nodeAfter, constSLTDataTypex)    //任意插,后插法{
assert(nodeAfter);
SLT*cur=nodeAfter;
SLT*newnode=buyNode(x);
SLT*tail=cur->next;  //先保存被插入节点的下一个节点信息//更改链接关系,后插完成cur->next=newnode;
newnode->next=tail;
}

🌱删除

删除思路,和头删差不多

  • 找到待插入节点 cur
  • 找到 cur 的下一个节点 tail
  • 找到 tail 的下一个节点 newtail

接下来就很简单了,释放 tail 节点,更改链接关系,当然断言是少不了

9fabbbd9dcf1480ca5fc5b858978682e.png

1dfce5ead6ba4bf9a578ddf343bcf342.png

voidSLTEraseAfter(SLT*nodeAfter)  //任意删,删除后面节点{
assert(nodeAfter);
assert(nodeAfter->next);    //如果目标节点的下一个为空,是后删不了的SLT*cur=nodeAfter;
SLT*tail=cur->next;  //待删除的节点SLT*newtail=tail->next;  //新尾free(tail);
tail=NULL;
cur->next=newtail;    //更改链接关系}

🪴困难版

困难版就比较麻烦了,因为它要实现的是任意位置前插元素、删除任意位置的节点,单链表 的最大缺点是不能回退,这就意味着即使我们得到了待删除节点,也是很难求出它的上一个节点的 ,对于这种尴尬情况,只能老老实实从头节点处开始向后 遍历 寻找,直到找到待删除节点的上一个节点。

🌱插入

其实这个也没有多困难,无非就是比上一种多个参数,然后多了一步遍历操作而已,内核思想任然不变


获取信息

更改链接关系

cd4d4d438ae8496399dcda28e177db9a.gif

//这两个实现起来比较麻烦,但是很全能voidSLTInsert(SLT**pphead, SLT*node, constSLTDataTypex)    //任意插,前插法{
assert(pphead);
SLT*newnode=buyNode(x);
SLT*cur=*pphead;
SLT*prev=NULL;
while (cur!=node)
    {
prev=cur; //要找到目标节点的上一个节点cur=cur->next;    
    }
//判断一下,是否为空表插入//走的是尾插的那一套思想if (prev)
    {
prev->next=newnode;
newnode->next=node;
    }
else    {
newnode->next=node;
*pphead=newnode;  //空表需要更新头节点信息    }
}

🌱删除

删除是一样的逻辑,不过多了一个 tail 而已,指向的位置是 node 的下一个节点,其余步骤跟插入基本一致,之后也是一样的更改链接关系,一样需要判断是否为空表,如果为空表需要更新头节点信息。


其实现在看来,困难版的插入删除,就像是尾部插入删除的升级版,有些麻烦,但很可靠,困难版的任意位置插入删除可以完全替代头尾插入删除,也就是说写这一对函数就够了。

voidSLTErase(SLT**pphead, SLT*node)  //任意删,删除当前节点{
assert(pphead);
assert(node);   //不必检查*pphead的合法性,查验node就行了SLT*cur=*pphead; //走的是尾删的那一套思想SLT*prev=NULL;
SLT*tail=node->next;
while (cur!=node)
    {
prev=cur;
cur=cur->next;
    }
free(node);
//跟尾插一样,需要判断一下if (prev)
prev->next=tail;
else*pphead=tail;
}

🌲源码区

代码放一起看,会更好理解一些~

🪴功能声明头文件部分

#pragma once#include<stdio.h>#include<stdlib.h>#include<assert.h>#include<windows.h>typedefintSLTDataType;    //单链表的数据类型typedefstructSListNode{
SLTDataTypedata;   //数据域structSListNode*next; //指针域}SLT;   //重命名为 SLTvoidSLTDestroy(SLT**pphead);  //单链表的销毁voidSLTPrint(constSLT**pphead);  //单链表的打印voidSLTPushBack(SLT**pphead, constSLTDataTypex);    //尾插voidSLTPopBack(SLT**pphead);  //尾删voidSLTPushFront(SLT**pphead, constSLTDataTypex);   //头插voidSLTPopFront(SLT**pphead); //头删//这两个有缺陷,不能对头节点进行操作,但实现起来比较简单voidSLTInsertAfter(SLT*nodeAfter, constSLTDataTypex);   //任意插,后插法voidSLTEraseAfter(SLT*nodeAfter); //任意删,删除后面节点//这两个实现起来比较麻烦,但是很全能voidSLTInsert(SLT**pphead, SLT*node, constSLTDataTypex);   //任意插,前插法voidSLTErase(SLT**pphead, SLT*node); //任意删,删除当前节点SLT*SLTFind(constSLT**pphead, constSLTDataTypex);  //查找值为x的节点(第一次出现)voidSLTModify(SLT*node, constSLTDataTypeval);   //修改 node 节点处的值

🪴功能实现源文件部分

#define _CRT_SECURE_NO_WARNINGS 1   #include"SList.h"//不带哨兵位的单链表不需要初始化voidSLTDestroy(SLT**pphead)   //单链表的销毁{
assert(pphead); //一二级指针都不能为空//空表不走销毁程序,但不能报错if (*pphead)
    {
while (*pphead)
        {
SLT*cur= (*pphead)->next; //记录头的下一个位置free(*pphead);
*pphead=cur;  //向后移动,不断销毁        }
    }
}
voidSLTPrint(constSLT**pphead)   //单链表的打印{
assert(pphead); //不需要断言 *pphead ,因为存在空链表打印的情况,是合法的printf("\n\n当前链表为: ");
constSLT*cur=*pphead;
while (cur)
    {
printf("%d->", cur->data);
cur=cur->next;    //cur要向后移动    }
printf("NULL\n\n\n");   //链表最后为空}
staticSLT*buyNode(constSLTDataTypex)    //买节点{
SLT*newnode= (SLT*)malloc(sizeof(SLT));
assert(newnode);    //防止申请失败的情况newnode->data=x;
newnode->next=NULL;
returnnewnode; //返回买好的节点}
voidSLTPushBack(SLT**pphead, constSLTDataTypex) //尾插{
assert(pphead);
SLT*newnode=buyNode(x);
SLT*tail=*pphead;
//尾插分情况,链表为空,直接赋值,不为空,找到尾,再链接if (tail==NULL)
    {
*pphead=tail=newnode;   //直接赋值    }
else    {
while (tail->next!=NULL)
        {
tail=tail->next;  //找到尾节点        }
tail->next=newnode;   //链接    }
//SLTInsert(pphead, NULL, x);       //可以复用任意位置前插}
voidSLTPopBack(SLT**pphead)   //尾删{
assert(pphead);
assert(*pphead);    //如果链表为空,是删不了的SLT*tail=*pphead;
SLT*prev=NULL;
while (tail->next)
    {
prev=tail;    //保存上一个节点信息tail=tail->next;  //找到尾节点    }
free(tail);
//分情况,如果prev是空,说明删除的尾节点同时也是头节点if (prev)
prev->next=tail=NULL;   //把尾节点的上一个节点指向空else*pphead=NULL; //此时直接把prev置空/*SLT* tail = *pphead;while (tail->next){tail = tail->next;}SLTErase(pphead, tail);*///可以复用任意位置删除}
voidSLTPushFront(SLT**pphead, constSLTDataTypex)    //头插{
assert(pphead);
SLT*newhead=buyNode(x);
newhead->next=*pphead;    //直接链接*pphead=newhead;  //更新头节点信息//代码简洁之道//SLTInsert(pphead, *pphead, x);    //可以复用任意位置前插}
voidSLTPopFront(SLT**pphead)  //头删{
assert(pphead);
assert(*pphead);    //头删同样存在空不能删的情况//先找到头的下一个节点,然后赋值新头SLT*cur=*pphead; //指向头节点SLT*newhead=cur->next;   //保存头节点的下一个节点信息free(cur);
cur=NULL;
*pphead=newhead;  //赋值新头//SLTErase(pphead, *pphead);    //可以复用任意位置删除}
//这两个有缺陷,不能对头节点进行操作,但实现起来比较简单voidSLTInsertAfter(SLT*nodeAfter, constSLTDataTypex)    //任意插,后插法{
assert(nodeAfter);
SLT*cur=nodeAfter;
SLT*newnode=buyNode(x);
SLT*tail=cur->next;  //先保存被插入节点的下一个节点信息//更改链接关系,后插完成cur->next=newnode;
newnode->next=tail;
}
voidSLTEraseAfter(SLT*nodeAfter)  //任意删,删除后面节点{
assert(nodeAfter);
assert(nodeAfter->next);    //如果目标节点的下一个为空,是后删不了的SLT*cur=nodeAfter;
SLT*tail=cur->next;  //待删除的节点SLT*newtail=tail->next;  //新尾free(tail);
tail=NULL;
cur->next=newtail;    //更改链接关系}
//这两个实现起来比较麻烦,但是很全能voidSLTInsert(SLT**pphead, SLT*node, constSLTDataTypex)    //任意插,前插法{
assert(pphead);
SLT*newnode=buyNode(x);
SLT*cur=*pphead;
SLT*prev=NULL;
while (cur!=node)
    {
prev=cur; //要找到目标节点的上一个节点cur=cur->next;    
    }
//判断一下,是否为空表插入//走的是尾插的那一套思想if (prev)
    {
prev->next=newnode;
newnode->next=node;
    }
else    {
newnode->next=node;
*pphead=newnode;  //空表需要更新头节点信息    }
}
voidSLTErase(SLT**pphead, SLT*node)  //任意删,删除当前节点{
assert(pphead);
assert(node);   //不必检查*pphead的合法性,查验node就行了SLT*cur=*pphead; //走的是尾删的那一套思想SLT*prev=NULL;
SLT*tail=node->next;
while (cur!=node)
    {
prev=cur;
cur=cur->next;
    }
free(node);
//跟尾插一样,需要判断一下if (prev)
prev->next=tail;
else*pphead=tail;
}
SLT*SLTFind(constSLT**pphead, constSLTDataTypex)   //查找值为x的节点(第一次出现){
assert(pphead);
SLT*cur= (SLT*)*pphead;   //指向头节点while (cur)
    {
if (cur->data==x)
returncur; //找到了,返回相关节点信息cur=cur->next;
    }
returnNULL;    //没有找到的情况}
voidSLTModify(SLT*node, constSLTDataTypeval)    //修改 node 节点处的值{
//注意:在调用函数时,可以通过链式访问,将查找函数的返回值作为形参一传入就行了assert(node);   //断言,如果节点node是空指针,是不能做修改的node->data=val;
}

🪴主函数调用源文件部分

#define _CRT_SECURE_NO_WARNINGS 1   #include"SList.h"voidmenu()
{
printf("0.退出    1.打印\n");
printf("2.尾插    3.尾删\n");
printf("4.头插    5.头删\n");
printf("6.任意插(后插)   7.任意删(后删)\n");
printf("8.任意插(前插)   9.任意删(当前)\n");
printf("10.查找   11.修改\n");
}
intmain()
{
SLT*s=NULL;
intinput=1;
while (input)
    {
menu();
printf("请选择:>");
scanf("%d", &input);
system("cls");  //清屏函数,让显示效果更好intval=0;    //待插入值intpos=0;    //待查找节点值switch (input)
        {
case0:
printf("成功退出\n");
break;
case1:
SLTPrint(&s);
break;
case2:
printf("请输入一个数:>");
scanf("%d", &val);
SLTPushBack(&s, val);
break;
case3:
SLTPopBack(&s);
break;
case4:
printf("请输入一个数:>");
scanf("%d", &val);
SLTPushFront(&s, val);
break;
case5:
SLTPopFront(&s);
break;
case6:
printf("请输入被插入的节点值:>");
scanf("%d", &pos);
printf("请输入一个数:>");
scanf("%d", &val);
SLTInsertAfter(SLTFind(&s, pos), val);
break;
case7:
printf("请输入被删除的节点值:>");
scanf("%d", &pos);
SLTEraseAfter(SLTFind(&s, pos));
break;
case8:
printf("请输入被插入的节点值:>");
scanf("%d", &pos);
printf("请输入一个数:>");
scanf("%d", &val);
SLTInsert(&s, SLTFind(&s, pos), val);
break;
case9:
printf("请输入被删除的节点值:>");
scanf("%d", &pos);
SLTErase(&s, SLTFind(&s, pos));
break;
case10:
printf("请输入被查找的节点值:>");
scanf("%d", &pos);
SLT*tmp=SLTFind(&s, pos);
printf("当前节点的地址为:%p\n", tmp);
break;
case11:
printf("请输入被修改的节点值:>");
scanf("%d", &pos);
printf("请输入一个数:>");
scanf("%d", &val);
SLTModify(SLTFind(&s, pos), val);
break;
default :
printf("选择错误,重新选择!\n");
break;
        }
    }
SLTDestroy(&s); //每次程序运行结束,都会执行销毁函数return0;
}


🌳总结

以上就是关于 单链表 的全部内容了,单链表 中用到了 二级指针 这个东西,如果使用带哨兵位的 单链表 就可以不用 二级指针 ,但是这种结构用的比较少,单纯的学好 单链表 ,能快速提高我们对链表的认识,毕竟链表这个工具后续还会用到。从文中可以看出,单链表 相对于 顺序表 ,不用考虑空间问题,且头插头删效率很高,可惜 单链表 不支持下标的随机访问。总之,顺序表 和 单链表 各有各的用途,二者相辅相成,都是很不错的数据结构。


如果本文有不足或错误的地方,随时欢迎指出,我会在第一时间改正

c3a2fbd4cece4640b5ba77b478ae727a.jpg

目录
相关文章
|
1月前
【数据结构】单链表(长期维护)(1)
【数据结构】单链表(长期维护)(1)
|
8天前
|
存储 算法 C语言
数据结构基础详解(C语言):单链表_定义_初始化_插入_删除_查找_建立操作_纯c语言代码注释讲解
本文详细介绍了单链表的理论知识,涵盖单链表的定义、优点与缺点,并通过示例代码讲解了单链表的初始化、插入、删除、查找等核心操作。文中还具体分析了按位序插入、指定节点前后插入、按位序删除及按值查找等算法实现,并提供了尾插法和头插法建立单链表的方法,帮助读者深入理解单链表的基本原理与应用技巧。
|
4月前
|
存储
[数据结构]——单链表——超详解
[数据结构]——单链表——超详解
|
1月前
|
存储
【数据结构】单链表-->详细讲解,后赋源码
【数据结构】单链表-->详细讲解,后赋源码
21 4
|
30天前
|
算法 索引
【初阶数据结构篇】单链表算法题进阶
深拷贝应该正好由 n 个全新节点组成,其中每个新节点的值都设为其对应的原节点的值。
|
1月前
【数据结构】单链表(长期维护)(2)
【数据结构】单链表(长期维护)(2)
|
2月前
|
存储 DataX C语言
【数据结构】单链表
数据结构中的单链表
23 0
【数据结构】单链表
|
3月前
|
存储 测试技术
【数据结构】最最基础的链式结构——单链表,还不会你就吃大亏了!
【数据结构】最最基础的链式结构——单链表,还不会你就吃大亏了!
41 5
|
3月前
|
算法 程序员 数据处理
【数据结构与算法】使用单链表实现队列:原理、步骤与应用
【数据结构与算法】使用单链表实现队列:原理、步骤与应用
|
3月前
|
算法 C语言
【数据结构与算法 经典例题】返回单链表的倒数第 k 个节点
【数据结构与算法 经典例题】返回单链表的倒数第 k 个节点