【 数据结构 】单链表的实现 - 详解(C语言版)
目录
前言:
顺序表的缺陷:
单链表:(Single Linked List)
概念及结构:
单链表的实现:
头文件:SList.h
malloc函数:
free函数:
具体函数的实现:SList.c
单链表的打印:
创建一个新的结点:
单链表尾的插:
单链表的头插:
单链表的尾删:
单链表的头删:
单链表的查找:
在单链表pos位置之前插入数据:
在单链表pos位置之后插入数据:
在单链表pos位置删除数据:
在单链表pos后一个位置删除数据:
单链表的销毁:
总结:
前言:
本文用C语言来描述数据结构中的单链表,下文实现的只是简单的无头非循环链表,包括单链表的增加数据,删除数据,查找指定数据,修改指定数据,也就是简单的增删查改等操作。
顺序表的缺陷:
优点:
- 点是个连续的物理空间,方便下标随机访问。
缺点:
- 插入数据,空间不足要扩容,扩容有性能消耗
- 头部或者中间插入删除数据,需要挪动数据,效率较低
- 可能存在一定的空间占用,浪费空间。
- 不能按需申请和释放空间。
基于顺序表的缺点,于是就设计出了链表结构。
单链表:(Single Linked List)
概念及结构:
1.概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表 中的指针链接次序实现的 。
2.单链表的结构为:
3.无头单向非循环链表:
无头单向非循环链表的结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
备注:
一般我们写一个项目的时候,将所要包含的头文件,函数的声明,结构体等放在一个头文件.h 里面,一般将函数的定义也就是函数实现的过程放在.c的文件里面,一般将函数的测试也就是主函数写在另一个.c的文件里面也就是test.c.这个文件里面。
单链表的实现:
头文件:SList.h
#include<stdio.h>
#include<string.h>
#include<assert.h>
#include<stdlib.h>
#include<errno.h>typedef int SLDataType;//单链表结构的基本定义
//逻辑结构
typedef struct SListNode
{SLDataType data;//val - 数据域struct SListNode* next;//存储下一个结点的地址 - 指针域
}SListNode, SLN;//打印单链表
void SListPrint(SListNode* phead);//单链表的尾插
void SListPushBack(SListNode** pphead, SLDataType x);//单链表的头插
void SListPushFront(SListNode** pphead, SLDataType x);//单链表的尾删
void SListPopBack(SListNode** pphead);//单链表的头删
void SListPopFront(SListNode** pphead);//单链表的查找
SListNode* SListFind(SListNode* phead, SLDataType x);//在单链表pos位置之前插入数据
void SListInsertBefore(SListNode** pphead, SListNode* pos, SLDataType x);//在单链表pos位置之后插入数据
void SListInsertAfter(SListNode* pos, SLDataType x);//在单链表pos位置删除数据
void SListErase(SListNode** pphead, SListNode* pos);//在单链表pos后一个位置删除数据
void SListEraseAfter(SListNode* pos);//单链表的销毁
void SListDestroy(SListNode** pphead);
一个struct SListNode类型的结构体又叫做一个结点(节点),包含数据域和指针域,数据域存放的是一个数据,指针域存放的是下一个结点的地址。
malloc函数:
C语言提供了一个动态内存开辟的函数,函数原型如下:
这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针:
- 如果开辟成功,则返回一个指向开辟好空间的指针。
- 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
- 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定,通常情况下要对malloc返回的指针强转成所需要的指针。
- 如果参数 size为0,malloc的行为是标准是未定义的,取决于编译器。
free函数:
C语言提供了专门用来做动态内存的释放和回收的函数:函数原型如下:
这个函数用来释放动态开辟的内存:
- 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
- 如果参数 ptr 是NULL指针,则函数什么事都不做。
同时malloc和free都声明在 stdlib.h 头文件中,malloc函数是在堆区申请空间的。
//malloc函数的使用:
int main()
{//开辟10个整形的空间//int arr[10];int* p = (int*)malloc(sizeof(int) * 10);if (p == NULL){printf("%s\n", strerror(errno));return 0;//结束代码}//使用int i = 0;for (i = 0; i < 10; i++){*(p + i) = i;}for (i = 0; i < 10; i++){printf("%d ", p[i]);}free(p);//当释放后p就变成野指针了p = NULL;return 0;
}
malloc函数最好要和free函数配合使用,不然申请空间不释放(虽然在程序结束时申请的内存会被回收)但在程序结束前就可能会造成内存泄漏。free掉空间之后要将空间的首地址置空,在后续操作中该指针可能被用到从而造成非法访问。
具体函数的实现:SList.c
单链表需要头指针来存放头结点的首地址,所以在测试.c文件的文件中要创造一个phead是
struct SListNode* 类型的头指针用来存放头结点的地址。
即 struct SListNode* phead = NULL;
当链表为空的时候,头指针就为空指针NULL。
单链表的打印:
//单链表的打印
void SListPrint(SListNode* phead)
{//assert(phead); - 不需要断言 - 如果为空指针的话就是空链表SListNode* cur = phead;while (cur != NULL){printf("%d->", cur->data);cur = cur->next;}printf("NULL\n");}
phead拿到传过来的链表的头指针,然后由头指针遍历整个链表,将每个结点中的数据打印出来。
创建一个新的结点:
//创建一个新的结点
SListNode* BuySListNode(SLDataType x)
{SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));//对malloc函数返回值的判断if (newnode == NULL){//printf("malloc fail\n");printf("%s\n", strerror(errno)); //报出错误exit(-1); //结束程序}else{newnode->data = x;newnode->next = NULL;}return newnode;
}
通过malloc函数在堆区申请一块新的结点,将数据放在结点中,将指针域置空,并返回这块新结点的首地址。
单链表尾的插:
将新创建的结点接到原来链表上去
//单链表的尾插 - 将新创建的结点接到原来链表上去
void SListPushBack(SListNode** pphead, SLDataType x)
{assert(pphead); //pphead - 是头指针的地址//创建一个新的结点SListNode* newnode = BuySListNode(x);if (*pphead == NULL)//空链表的尾插{*pphead = newnode;}else //链接到最后一个结点 遍历(找尾){SListNode* tail = *pphead;//找尾while (tail->next != NULL){tail = tail->next;}tail->next = newnode;//将新的头的地址链表最后一个结点里的指针变量。//新的结点的头指针和原来链表的尾指针都在堆区。}
}
1.这里用到了二级指针的解释:
正常情况下的尾插:
如果尾插函数第一个参数是SListNode* phead,那么phead就是SListNode*类型的一级指针,接收的是链表的头指针,这时tail应被赋值成:SListNode* tail = phead,(tail此时指向的是链表的头结点)当链表不为空的时候,tail通过while循环找到链表的尾,将新的结点链接到链表的尾部。
特殊情况下的尾插:(注意)
如果尾插函数第一个参数是SListNode* phead,当尾插的链表是空链表的时候,这时如果只是单纯的将phead = newnode;这样操作之后,实际上的头结点并没有被改变,因为函数的形参只是实参的一份临时拷贝,尾插函数只是将形参的phead的值改成了newnode并没有将真正的头指针改变,同时函数结束时这个phead局部变量会被销毁。所以这里要用到二级指针来接收头指针的地址,之前C语言的学习中函数章节中提到,函数的传值和传址的区别,想要改变值就要传地址(指针),同样的道理这里要想改变地址,就得传地址的地址,所以形参用到了二级指针(参考上正确述代码)。
2.测试函数中就该这样写:
struct SListNode* head = NULL;
SListPushBack(&head);
3.注意点:
有些初学者会在找尾的时候出现经典的错误:
同样的道理:tail指针是在函数中创建的,是在栈区的局部变量,tail = newnode;的操作只是将这个局部变量的值改成了newnode,并没有改变链表中结点的指针域,当函数结束时tail的内容也会被销毁,同样新结点并没有链接到链表的尾部。
4.同样值得注意的是:
这里是将空链表的情况单独处理的,因为tail是赋值成头指针的,当链表为空的时候,*pphead == NULL;tail也是NULL。当找尾的时候,循环进入条件tail->next;这个操作就可能涉及到了对空指针的引用。就会出现错误,所以就将空链表的情况单独拿出来处理。
5.小结:
只要涉及改变链表头指针的操作,就要传二级指针。
单链表的头插:
//单链表的头插
void SListPushFront(SListNode** pphead, SLDataType x)
{//创建一个新的结点SListNode* newnode = BuySListNode(x);newnode->next = *pphead;*pphead = newnode;
}
思路:
将新的结点的尾链接到原来链表的头,再将头指针指向新结点的头。
注意:
这里要注意链接的顺序,如果先将头指针指向新结点的头,再将新的结点的尾链接到原来链表的头的话,原来链表的头就找不到了,因为原来链表的头是放在头指针里面的,若先将头指针改了就找不到原来的头了,就链接不上了。
单链表的尾删:
void SListPopBack(SListNode** pphead)
{assert(pphead);//1.空链表//2.一个结点//3.多个结点//空链表的情况//暴力检查 - assert(*pphead != NULL);if (*pphead == NULL)//温柔检查{return;}//只有一个结点的情况else if ((*pphead)->next == NULL)//解引用和箭头的优先级一样高这里要带括号{free(*pphead);*pphead = NULL;}//多个结点的情况else{SListNode* tail = *pphead;while (tail->next->next != NULL){tail = tail->next;}free(tail->next);tail->next = NULL;}
}
因为这里涉及改变链表的头指针所以传的是二级指针。
思路:
在链表的尾部删除结点通常的想法就是把要删除的结点free掉,然后将要删除的结点的前一个结点的指针域置成空,但是如果遍历链表找到尾结点的话,就不能再找到尾的前一个结点。这时当务之急就是找到要删除的结点的前一个结点,这时就会想到循环判断条件为tail->next->next;这样向后找到尾的前一个结点地址,并且通过这个结点还能找到要删除的结点。
但这样的找法会在极端情况下出错,例如空链表和只有一个结点的情况,所以便有了以下的分析。
这里分为三种情况考虑:
1.链表为空链表:
当链表为空链表时,就不存在删除数据的情况,因为没有是数据可删,直接结束函数即可。
2.链表只有一个结点:
当链表只有一个结点时,tail->next->next;会出现对空指针NULL引用,所以要单独拿出来处理。
3.链表有多个结点 :
就可以按照通常思路找尾,先将尾结点释放掉,再将指向尾结点的指针即倒数第二个结点的指针域置空(NULL)。
这里要注意置空和释放的顺序,如果先置空的话,就找不到尾结点也就不能free释放掉要删除的结点,这样会造成内存泄露。
单链表的头删:
//单链表的头删
void SListPopFront(SListNode** pphead)
{assert(pphead);//1.空//2.非空if (*pphead == NULL){return;}else{SListNode* next = (*pphead)->next;free(*pphead);*pphead = next;}
}
因为这里涉及改变链表的头指针所以传的是二级指针。
思路:
这里要考虑两种情况,链表为空的时,和链表为非空的时。
1.当链表为空的时:
直接结束函数,因为没有可删除的结点。
2.当链表不为空时:
将链表的头指针释放,再将头指针指向第二个结点。
单链表的查找:
//单链表的查找
SListNode* SListFind(SListNode* phead, SLDataType x)
{SListNode* cur = phead;while (cur != NULL){if (cur->data == x){return cur;}cur = cur->next;}return NULL;
}
思路:
定义一个cur指针从头到尾依次整个链表,只要找到符合条件的结点,就返回该结点的地址。
在单链表pos位置之前插入数据:
//在单链表pos位置之前插入数据
void SListInsertBefore(SListNode** pphead, SListNode* pos, SLDataType x)
{assert(pphead);assert(pos);//空链表排除//1.pos是第一个结点//2.pos不是第一个结点if (pos == *pphead){SListPushFront(pphead, x);}else{SListNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}SListNode* newnode = BuySListNode(x);prev->next = newnode;newnode->next = pos;}
}
这个函数需要配合SListFind函数使用来找到pos位置。
assert断言,将pos和pphead为空指针的情况排除。
因为这里涉及改变链表的头指针所以传的是二级指针。
思路:
分两种情况:当pos前为空的时,和pos前不为空时。
1.当pos前为空时:
就相当于头插,直接调用头插函数。
2.当pos前不为空时:
创建一个指针遍历链表找到pos位置前一个结点,再通过创建结点函数创建一个新的结点再将其链接到单链表中。
在单链表pos位置之后插入数据:
//在单链表pos位置之后插入数据方法一:(无关顺序)
//void SListInsertAfter(SListNode* pos, SLDataType x)
//{
// assert(pos);
// SListNode* next = pos->next;
// SListNode* newnode = BuySListNode(x);
// pos->next = newnode;
// newnode->next = next;
//
//}//方法二:(注意顺序)
void SListInsertAfter(SListNode* pos, SLDataType x)
{assert(pos);SListNode* newnode = BuySListNode(x);newnode->next = pos->next;pos->next = newnode;
}
这个函数需要配合SListFind函数使用来找到pos位置。
因为这里涉及改变链表的头指针所以传的是二级指针。
思路:
这时已经拿到了pos位置只需要将申请的新结点链接在指定位置便可,同样要注意链接的顺序问题,如果操作不当会造成原链表pos位置后的结点找不到的问题。
这里提供了两种方法:
第一种:创建临时变量来存放原链表pos位置下一个结点地址,这样就不会丢失了。
第二种:就是直接链接但是要注意链接顺序。
在单链表pos位置删除数据:
//在单链表pos位置删除数据
void SListErase(SListNode** pphead, SListNode* pos)
{assert(pphead);assert(pos);//当传头指针时if (*pphead == pos){SListPopFront(pphead);}else{SListNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}prev->next = pos->next;free(pos);pos = NULL;}}
这个函数需要配合SListFind函数使用来找到pos位置。
思路:
分为两种情况:一种是只有一个结点和多个结点的情况。
1当链表只有一个结点时:
也就相当于头删,直接调用头删函数。
2.当链表有多个结点时:
这里创建了一个prev指针来遍历链表,找到pos位置之前的结点,将pos的下一个位置链接到prev的尾部,再将pos位置free释放掉。
注意:
这里还是要注意释放和链接的顺序的问题,如果先free(pos)释放pos位置的话,pos->next就找 不到了。
pos = NULL;这一步置空是个好习惯。
在单链表pos后一个位置删除数据:
//在单链表pos后一个位置删除数据(不可能是头删)
void SListEraseAfter(SListNode* pos)
{assert(pos);SListNode* next = pos->next;if (next != NULL){pos->next = pos->next->next;free(next);next = NULL;}
}
这个函数需要配合SListFind函数使用来找到pos位置。
assert断言,将pos为空指针的情况排除。
思路:
分三种情况 :一种是pos下一个有结点,一种pos是头,一种是pos下一个是空。
1.pos下一个有结点:
直接创建一个指针next,用来存放pos->next,设置这个next临时变量的作用是防止free释放要删除结点的时候找不到要删除的结点,因为要将pos下下一个结点接到pos->next的位置,但是要删除的结点头是pos->next,如果先链接的话,pos->next就会被改了,被删除的结点就找不到了,就不能free释放掉该结点,有可能会造成内存泄露。
2.pos是头:
这里不可能是头删,因为这里 pos最靠进表头只能传头指针,头结点后一个也不可能是头删。
3.pos下一个是空:
那就没的删只能结束函数。
单链表的销毁:
//单链表的销毁
void SListDestroy(SListNode** pphead)
{assert(pphead);//一个一个结点释放SListNode* cur = *pphead;while (cur){SListNode* next = cur->next;free(cur);cur = next;}*pphead = NULL;}
思路:
用一个指针(cur)遍历整个链表,同时还需要一个指针(next)用来存放当前指针的下一个结点地址,循环free释放当前cur指向的结点。再继续迭代,cur再指向next的位置,再次进循环,直到链表遍历结束,这样就将整个链表申请的节点空间全部释放了。
总结:
数据结构这方面,考虑问题一定要全面,不能将通常情况当做所有的情况,要考虑到极端的个别情况,并将各个细小的细节处理妥当,当程序出现问题时,应当多思考,多调试,用多组数据进行测试,发散思维,有利于能力的提升!
【 数据结构 】单链表的实现 - 详解(C语言版)相关推荐
- 数据结构殷人昆电子版百度云资源_数据结构精讲与习题详解(C语言版第2版清华大学计算机系列教材)...
导语 内容提要 殷人昆编著的<数据结构精讲与习题详解(C语言版第2版清华大学计算机系列教材)>是清华大学出版社出版的<数据结构(C语言版)>(第2版)的配套教材,对" ...
- 链表c++语言 解析,C++ 单链表的基本操作(详解)
链表一直是面试的高频题,今天先总结一下单链表的使用,下节再总结双向链表的.本文主要有单链表的创建.插入.删除节点等. 1.概念 单链表是一种链式存取的数据结构,用一组地址任意的存储单元存放线性表中的数 ...
- 数据结构单链表的基础操作(C语言)
效果如图: 代码及详情如下:(文末总结) 目录 //主函数 //菜单 //创建链表 //插入结点 //删除结点 //查找结点 //链表长度 //打印链表 //清空链表 //逆置链表 //删除偶数元素结 ...
- 单链表的基本操作代码实现(C语言版)
目录 前言: 单链表的基本操作 准备工作(头文件.各种宏定义以及结构体定义) 一.较简单操作 1.单链表的初始化 2.判断单链表是否为空表 3.单链表的销毁 4.单链表的清空 5.求单链表的表长 二. ...
- 数据结构之链表(LinkedList详解)
文章目录 一.什么是LinkedList? 二.LinkedList的使用 三.LinkedList自实现 四.链表实现逆序打印的两种方式(递归和非递归) 五.ArrayList和LinkedList ...
- 【八大排序详解~C语言版】直接插入排序-希尔排序- 直接选择排序-堆排序-冒泡排序-快速排序-归并排序-计数排序
八大排序 1.直接插入排序 2.希尔排序 3.直接选择排序 直接选择排序改进 4.堆排序 1.建堆 2.利用堆删除思想来进行排序 5.冒泡排序 6.快速排序 递归实现 非递归实现 7.归并排序 递归实 ...
- php链表和联表的区别,PHP_浅谈PHP链表数据结构(单链表),链表:是一个有序的列表,但 - phpStudy...
浅谈PHP链表数据结构(单链表) 链表:是一个有序的列表,但是它在内存中是分散存储的,使用链表可以解决类似约瑟夫问题,排序问题,搜索问题,广义表 单向链表,双向链表,环形链表 PHP的底层是C,当一个 ...
- layui 横向表单_对layui中表单元素的使用详解
首先不管是单选框还是复选框或者是下拉框,都要在你写的标签外面套一层div或者是form标签,如: 这样写好了以后,你如果是写在静态页面,这样式可以看见效果,如果写在js里,这样写了还有一步得写,那就是 ...
- C++:创建链表的过程详解
创建链表的过程详解 本人是一名刚开始学习算法的小白,今天遇到了一些关于链表的创建问题,查了一些资料,我把它们整理了一下,希望大家多多指教. 整体的代码: #include<iostream> ...
最新文章
- Pytorch 中的 5 个非常有用的张量操作
- Spring Security太复杂?试试这个轻量、强大、优雅的权限认证框架!
- PCB布线技术 很好很强大
- 自然语言处理之长短时记忆网络(六)
- c语言二维数组赋值前面是行还是列,动态二维数组分配有问题啊 为什么行和列相同才能给数组赋值...
- cursor用法java,Cursor的基本使用方法
- @Transient注解作用
- C#Panel 控件的使用
- 第三十六课:告别演出
- 一发就会被秒赞的句子
- 参数校验@Valid
- 每年的风能部署必须增长四倍,才能到2050年实现净零排放
- session里保存什么信息
- 电脑无法识别扫码枪怎么办?看4点解决方法就知道
- 用 Python 进行金融数据可视化
- vb6.0 生成exe被简称是木马_病毒分析|银行木马样本事件分析
- 新能源产业链全景图(建议收藏)
- MGC token GTR社区宇哥教你如何快速升级V5
- Android 模拟器一键获取root权限 一键安装Google play 服务
- Essential Linux Device Drivers》中文版第2章