…
单链表
是一种链式存取的数据结构,用一组地址任意的存储单元存放线性表中的数据元素。链表
中的数据是以结点来表示的,每个结点的构成:元素(数据元素的映象) + 指针(指示后继元素存储位置)
,元素就是存储数据的存储单元,指针就是连接每个结点的地址数据
这是百度百科对于 单链表
的解释,通俗来说,单链表
就是一种数据结构,它包含了一个 数据域 data
和一个 指针域 next
,最大的特点就是 链式存储 。链表有很多种,其中 单链表
是最基本、最简单的一种结构,很多OJ题都会利用 单链表
出题,后面的部分高阶数据结构也会用到 单链表
,因此学好 单链表
很重要。除了 单链表
外,还有 双向带头循环链表
(后面会介绍)等链表类型,先来进入 单链表
的学习吧!
单链表
创建时是一个结构体类型的指针,一开始指向空,只有在经过插入数据后才会有自己的指向 ,因此我们可以根据这一特点,遍历
整个 单链表
,并输出其中的 数据域 data
。
void SLTPrint(const SLT** pphead) //单链表的打印
{
assert(pphead); //不需要断言 *pphead ,因为存在空链表打印的情况,是合法的
printf("\n\n当前链表为: ");
const SLT* cur = *pphead;
while (cur)
{
printf("%d->", cur->data);
cur = cur->next; //cur要向后移动
}
printf("NULL\n\n\n"); //链表最后为空
}
销毁 单链表
也需要将其 遍历
一遍,因为链表中的元素不是连续存放的,只能逐个节点销毁 ,销毁思想为:利用 *pphead
遍历整个 单链表
,保存头节点 *pphead
的下一个节点信息至 cur
,释放原头节点,更新头节点信息(把 cur
的值赋给头节点 *pphead
)如此重复,直到释放完所有节点即可。
//不带哨兵位的单链表不需要初始化
void SLTDestroy(SLT** pphead) //单链表的销毁
{
assert(pphead); //一二级指针都不能为空
//空表不走销毁程序,但不能报错
if (*pphead)
{
while (*pphead)
{
SLT* cur = (*pphead)->next; //记录头的下一个位置
free(*pphead);
*pphead = cur; //向后移动,不断销毁
}
}
}
单链表
是由一个一个独立存在的节点组成的结构,当涉及插入操作时,向内存申请节点,涉及删除操作时,就要把对应的节点销毁
static SLT* buyNode(const SLTDataType x) //买节点
{
SLT* newnode = (SLT*)malloc(sizeof(SLT));
assert(newnode); //防止申请失败的情况
newnode->data = x;
newnode->next = NULL;
return newnode; //返回买好的节点
}
单链表
尾插是比较费劲的,因为得先通过头节点指针向后 遍历
找到尾节点,然后将尾节点与新节点之间建立链接关系,其中还得分情况尾插
关于
单链表
中函数用二级指针的问题:
插入或删除时,如果是第一次操作,需要对头节点本身造成改变,且头节点是一个一级指针
,因此需要通过二级指针
的方式来在函数中改变头节点的值。至于后续的操作,都只是改变了结构体中的next
值,因此使用一级指针
就够了,但是为了函数设计时的普适性,单链表
中的函数参数都设计成了二级指针
的形式。
void SLTPushBack(SLT** pphead, const SLTDataType x) //尾插
{
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
就是新的尾节点,因为原理都差不多,这里就不用动图展示了,值得注意的是尾删也分两种情况
void SLTPopBack(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
,三行代码就解决了。
void SLTPushFront(SLT** pphead, const SLTDataType x) //头插
{
assert(pphead);
SLT* newhead = buyNode(x);
newhead->next = *pphead; //直接链接
*pphead = newhead; //更新头节点信息
//代码简洁之道
//SLTInsert(pphead, *pphead, x); //可以复用任意位置前插
}
头删也是比较简单的,先用 cur
指向头节点,先保存 头节点 cur
的下一个节点信息至 节点 newhead
,释放 原头节点 cur
,更新 newhead
为链表的新头,即 *pphead = newnode
当然涉及删除的操作,都需要进行表空检查,如果链表为空,是不能执行删除的。
void SLTPopFront(SLT** pphead) //头删
{
assert(pphead);
assert(*pphead); //头删同样存在空不能删的情况
//先找到头的下一个节点,然后赋值新头
SLT* cur = *pphead; //指向头节点
SLT* newhead = cur->next; //保存头节点的下一个节点信息
free(cur);
cur = NULL;
*pphead = newhead; //赋值新头
//SLTErase(pphead, *pphead); //可以复用任意位置删除
}
查找函数很简单,遍历+比较
就行了,找到目标元素值后,返回相关节点信息,其实查找这个函数主要是为了配合后面任意插入删除函数的 ,所以比较简单。
SLT* SLTFind(const SLT** pphead, const SLTDataType x) //查找值为x的节点(第一次出现)
{
assert(pphead);
SLT* cur = (SLT*)*pphead; //指向头节点
while (cur)
{
if (cur->data == x)
return cur; //找到了,返回相关节点信息
cur = cur->next;
}
return NULL; //没有找到的情况
}
修改函数是在查找函数基础上进行的:当我们输入元素值交给查找函数,找到目标节点后返回其节点信息,然后直接通过返回的节点改变 data
值就行了,在调用查找函数的前提下,一行代码就解决了。
void SLTModify(SLT* node, const SLTDataType val) //修改 node 节点处的值
{
//注意:在调用函数时,可以通过链式访问,将查找函数的返回值作为形参一传入就行了
assert(node); //断言,如果节点node是空指针,是不能做修改的
node->data = val;
}
简单版是在任意位置后插入元素,删除任意位置后的节点,根据 单链表
的特性,对后面节点进行操作是比较简单,无非就是改变链接关系。但是这种对后操作存在缺陷:不适合实现头插、头删
插入(后插)主要分两步
获取信息:有三个关键信息:被插入节点
cur
、待插入节点newnode
和cur
的下一个节点tail
改变链接关系:很简单,
cur->next = newnode
,后插嘛,先把待插入节点链接到cur
后面,然后再把newnode
和tail
链接起来就行了
这里的 nodeAfter
是需要通过查找函数找出来的,它是一个 一级指针
//这两个有缺陷,不能对头节点进行操作,但实现起来比较简单
void SLTInsertAfter(SLT* nodeAfter, const SLTDataType x) //任意插,后插法
{
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
节点,更改链接关系,当然断言是少不了
void SLTEraseAfter(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; //更改链接关系
}
困难版就比较麻烦了,因为它要实现的是任意位置前插元素、删除任意位置的节点,单链表
的最大缺点是不能回退,这就意味着即使我们得到了待删除节点,也是很难求出它的上一个节点的 ,对于这种尴尬情况,只能老老实实从头节点处开始向后 遍历
寻找,直到找到待删除节点的上一个节点。
其实这个也没有多困难,无非就是比上一种多个参数,然后多了一步遍历操作而已,内核思想任然不变
//这两个实现起来比较麻烦,但是很全能
void SLTInsert(SLT** pphead, SLT* node, const SLTDataType x) //任意插,前插法
{
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
的下一个节点,其余步骤跟插入基本一致,之后也是一样的更改链接关系,一样需要判断是否为空表,如果为空表需要更新头节点信息。
其实现在看来,困难版的插入删除,就像是尾部插入删除的升级版,有些麻烦,但很可靠,困难版的任意位置插入删除可以完全替代头尾插入删除,也就是说写这一对函数就够了。
void SLTErase(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
#include
#include
#include
typedef int SLTDataType; //单链表的数据类型
typedef struct SListNode
{
SLTDataType data; //数据域
struct SListNode* next; //指针域
}SLT; //重命名为 SLT
void SLTDestroy(SLT** pphead); //单链表的销毁
void SLTPrint(const SLT** pphead); //单链表的打印
void SLTPushBack(SLT** pphead, const SLTDataType x); //尾插
void SLTPopBack(SLT** pphead); //尾删
void SLTPushFront(SLT** pphead, const SLTDataType x); //头插
void SLTPopFront(SLT** pphead); //头删
//这两个有缺陷,不能对头节点进行操作,但实现起来比较简单
void SLTInsertAfter(SLT* nodeAfter, const SLTDataType x); //任意插,后插法
void SLTEraseAfter(SLT* nodeAfter); //任意删,删除后面节点
//这两个实现起来比较麻烦,但是很全能
void SLTInsert(SLT** pphead, SLT* node, const SLTDataType x); //任意插,前插法
void SLTErase(SLT** pphead, SLT* node); //任意删,删除当前节点
SLT* SLTFind(const SLT** pphead, const SLTDataType x); //查找值为x的节点(第一次出现)
void SLTModify(SLT* node, const SLTDataType val); //修改 node 节点处的值
#define _CRT_SECURE_NO_WARNINGS 1
#include"SList.h"
//不带哨兵位的单链表不需要初始化
void SLTDestroy(SLT** pphead) //单链表的销毁
{
assert(pphead); //一二级指针都不能为空
//空表不走销毁程序,但不能报错
if (*pphead)
{
while (*pphead)
{
SLT* cur = (*pphead)->next; //记录头的下一个位置
free(*pphead);
*pphead = cur; //向后移动,不断销毁
}
}
}
void SLTPrint(const SLT** pphead) //单链表的打印
{
assert(pphead); //不需要断言 *pphead ,因为存在空链表打印的情况,是合法的
printf("\n\n当前链表为: ");
const SLT* cur = *pphead;
while (cur)
{
printf("%d->", cur->data);
cur = cur->next; //cur要向后移动
}
printf("NULL\n\n\n"); //链表最后为空
}
static SLT* buyNode(const SLTDataType x) //买节点
{
SLT* newnode = (SLT*)malloc(sizeof(SLT));
assert(newnode); //防止申请失败的情况
newnode->data = x;
newnode->next = NULL;
return newnode; //返回买好的节点
}
void SLTPushBack(SLT** pphead, const SLTDataType x) //尾插
{
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); //可以复用任意位置前插
}
void SLTPopBack(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);*/ //可以复用任意位置删除
}
void SLTPushFront(SLT** pphead, const SLTDataType x) //头插
{
assert(pphead);
SLT* newhead = buyNode(x);
newhead->next = *pphead; //直接链接
*pphead = newhead; //更新头节点信息
//代码简洁之道
//SLTInsert(pphead, *pphead, x); //可以复用任意位置前插
}
void SLTPopFront(SLT** pphead) //头删
{
assert(pphead);
assert(*pphead); //头删同样存在空不能删的情况
//先找到头的下一个节点,然后赋值新头
SLT* cur = *pphead; //指向头节点
SLT* newhead = cur->next; //保存头节点的下一个节点信息
free(cur);
cur = NULL;
*pphead = newhead; //赋值新头
//SLTErase(pphead, *pphead); //可以复用任意位置删除
}
//这两个有缺陷,不能对头节点进行操作,但实现起来比较简单
void SLTInsertAfter(SLT* nodeAfter, const SLTDataType x) //任意插,后插法
{
assert(nodeAfter);
SLT* cur = nodeAfter;
SLT* newnode = buyNode(x);
SLT* tail = cur->next; //先保存被插入节点的下一个节点信息
//更改链接关系,后插完成
cur->next = newnode;
newnode->next = tail;
}
void SLTEraseAfter(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; //更改链接关系
}
//这两个实现起来比较麻烦,但是很全能
void SLTInsert(SLT** pphead, SLT* node, const SLTDataType x) //任意插,前插法
{
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; //空表需要更新头节点信息
}
}
void SLTErase(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(const SLT** pphead, const SLTDataType x) //查找值为x的节点(第一次出现)
{
assert(pphead);
SLT* cur = (SLT*)*pphead; //指向头节点
while (cur)
{
if (cur->data == x)
return cur; //找到了,返回相关节点信息
cur = cur->next;
}
return NULL; //没有找到的情况
}
void SLTModify(SLT* node, const SLTDataType val) //修改 node 节点处的值
{
//注意:在调用函数时,可以通过链式访问,将查找函数的返回值作为形参一传入就行了
assert(node); //断言,如果节点node是空指针,是不能做修改的
node->data = val;
}
#define _CRT_SECURE_NO_WARNINGS 1
#include"SList.h"
void menu()
{
printf("0.退出 1.打印\n");
printf("2.尾插 3.尾删\n");
printf("4.头插 5.头删\n");
printf("6.任意插(后插) 7.任意删(后删)\n");
printf("8.任意插(前插) 9.任意删(当前)\n");
printf("10.查找 11.修改\n");
}
int main()
{
SLT* s = NULL;
int input = 1;
while (input)
{
menu();
printf("请选择:>");
scanf("%d", &input);
system("cls"); //清屏函数,让显示效果更好
int val = 0; //待插入值
int pos = 0; //待查找节点值
switch (input)
{
case 0:
printf("成功退出\n");
break;
case 1:
SLTPrint(&s);
break;
case 2:
printf("请输入一个数:>");
scanf("%d", &val);
SLTPushBack(&s, val);
break;
case 3:
SLTPopBack(&s);
break;
case 4:
printf("请输入一个数:>");
scanf("%d", &val);
SLTPushFront(&s, val);
break;
case 5:
SLTPopFront(&s);
break;
case 6:
printf("请输入被插入的节点值:>");
scanf("%d", &pos);
printf("请输入一个数:>");
scanf("%d", &val);
SLTInsertAfter(SLTFind(&s, pos), val);
break;
case 7:
printf("请输入被删除的节点值:>");
scanf("%d", &pos);
SLTEraseAfter(SLTFind(&s, pos));
break;
case 8:
printf("请输入被插入的节点值:>");
scanf("%d", &pos);
printf("请输入一个数:>");
scanf("%d", &val);
SLTInsert(&s, SLTFind(&s, pos), val);
break;
case 9:
printf("请输入被删除的节点值:>");
scanf("%d", &pos);
SLTErase(&s, SLTFind(&s, pos));
break;
case 10:
printf("请输入被查找的节点值:>");
scanf("%d", &pos);
SLT* tmp = SLTFind(&s, pos);
printf("当前节点的地址为:%p\n", tmp);
break;
case 11:
printf("请输入被修改的节点值:>");
scanf("%d", &pos);
printf("请输入一个数:>");
scanf("%d", &val);
SLTModify(SLTFind(&s, pos), val);
break;
default :
printf("选择错误,重新选择!\n");
break;
}
}
SLTDestroy(&s); //每次程序运行结束,都会执行销毁函数
return 0;
}
下面是几道关于 单链表
的OJ试题,可以试着解决一下,加强对链表的认识
以上就是关于 单链表
的全部内容了,单链表
中用到了 二级指针
这个东西,如果使用带哨兵位的 单链表
就可以不用 二级指针
,但是这种结构用的比较少,单纯的学好 单链表
,能快速提高我们对链表的认识,毕竟链表这个工具后续还会用到。从文中可以看出,单链表
相对于 顺序表
,不用考虑空间问题,且头插头删效率很高,可惜 单链表
不支持下标的随机访问。总之,顺序表
和 单链表
各有各的用途,二者相辅相成,都是很不错的数据结构。
如果你觉得本文写的还不错的话,期待留下一个小小的赞,你的支持是我分享的最大动力!
如果本文有不足或错误的地方,随时欢迎指出,我会在第一时间改正
…
相关文章推荐
数据结构 | 顺序表
数据结构 | 时间复杂度与空间复杂度
C语言进阶——动态内存管理