目录

前言:

顺序表的缺陷:

单链表:(Single Linked List)

概念及结构:

单链表的实现:

头文件:SList.h

malloc函数:

free函数:

具体函数的实现:SList.c

单链表的打印:

创建一个新的结点:

单链表尾的插:

单链表的头插:

单链表的尾删:

单链表的头删:

单链表的查找:

在单链表pos位置之前插入数据:

在单链表pos位置之后插入数据:

在单链表pos位置删除数据:

在单链表pos后一个位置删除数据:

单链表的销毁:

总结:


前言:

本文用C语言来描述数据结构中的单链表,下文实现的只是简单的无头非循环链表,包括单链表的增加数据,删除数据,查找指定数据,修改指定数据,也就是简单的增删查改等操作。


顺序表的缺陷:

优点:

  1. 点是个连续的物理空间,方便下标随机访问。

缺点:

  1. 插入数据,空间不足要扩容,扩容有性能消耗
  2. 头部或者中间插入删除数据,需要挪动数据,效率较低
  3. 可能存在一定的空间占用,浪费空间。
  4. 不能按需申请和释放空间。

基于顺序表的缺点,于是就设计出了链表结构。


单链表:(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语言版)相关推荐

  1. 数据结构殷人昆电子版百度云资源_数据结构精讲与习题详解(C语言版第2版清华大学计算机系列教材)...

    导语 内容提要 殷人昆编著的<数据结构精讲与习题详解(C语言版第2版清华大学计算机系列教材)>是清华大学出版社出版的<数据结构(C语言版)>(第2版)的配套教材,对" ...

  2. 链表c++语言 解析,C++ 单链表的基本操作(详解)

    链表一直是面试的高频题,今天先总结一下单链表的使用,下节再总结双向链表的.本文主要有单链表的创建.插入.删除节点等. 1.概念 单链表是一种链式存取的数据结构,用一组地址任意的存储单元存放线性表中的数 ...

  3. 数据结构单链表的基础操作(C语言)

    效果如图: 代码及详情如下:(文末总结) 目录 //主函数 //菜单 //创建链表 //插入结点 //删除结点 //查找结点 //链表长度 //打印链表 //清空链表 //逆置链表 //删除偶数元素结 ...

  4. 单链表的基本操作代码实现(C语言版)

    目录 前言: 单链表的基本操作 准备工作(头文件.各种宏定义以及结构体定义) 一.较简单操作 1.单链表的初始化 2.判断单链表是否为空表 3.单链表的销毁 4.单链表的清空 5.求单链表的表长 二. ...

  5. 数据结构之链表(LinkedList详解)

    文章目录 一.什么是LinkedList? 二.LinkedList的使用 三.LinkedList自实现 四.链表实现逆序打印的两种方式(递归和非递归) 五.ArrayList和LinkedList ...

  6. 【八大排序详解~C语言版】直接插入排序-希尔排序- 直接选择排序-堆排序-冒泡排序-快速排序-归并排序-计数排序

    八大排序 1.直接插入排序 2.希尔排序 3.直接选择排序 直接选择排序改进 4.堆排序 1.建堆 2.利用堆删除思想来进行排序 5.冒泡排序 6.快速排序 递归实现 非递归实现 7.归并排序 递归实 ...

  7. php链表和联表的区别,PHP_浅谈PHP链表数据结构(单链表),链表:是一个有序的列表,但 - phpStudy...

    浅谈PHP链表数据结构(单链表) 链表:是一个有序的列表,但是它在内存中是分散存储的,使用链表可以解决类似约瑟夫问题,排序问题,搜索问题,广义表 单向链表,双向链表,环形链表 PHP的底层是C,当一个 ...

  8. layui 横向表单_对layui中表单元素的使用详解

    首先不管是单选框还是复选框或者是下拉框,都要在你写的标签外面套一层div或者是form标签,如: 这样写好了以后,你如果是写在静态页面,这样式可以看见效果,如果写在js里,这样写了还有一步得写,那就是 ...

  9. C++:创建链表的过程详解

    创建链表的过程详解 本人是一名刚开始学习算法的小白,今天遇到了一些关于链表的创建问题,查了一些资料,我把它们整理了一下,希望大家多多指教. 整体的代码: #include<iostream> ...

最新文章

  1. Pytorch 中的 5 个非常有用的张量操作
  2. Spring Security太复杂?试试这个轻量、强大、优雅的权限认证框架!
  3. PCB布线技术 很好很强大
  4. 自然语言处理之长短时记忆网络(六)
  5. c语言二维数组赋值前面是行还是列,动态二维数组分配有问题啊 为什么行和列相同才能给数组赋值...
  6. cursor用法java,Cursor的基本使用方法
  7. @Transient注解作用
  8. C#Panel 控件的使用
  9. 第三十六课:告别演出
  10. 一发就会被秒赞的句子
  11. 参数校验@Valid
  12. 每年的风能部署必须增长四倍,才能到2050年实现净零排放
  13. session里保存什么信息
  14. 电脑无法识别扫码枪怎么办?看4点解决方法就知道
  15. 用 Python 进行金融数据可视化
  16. vb6.0 生成exe被简称是木马_病毒分析|银行木马样本事件分析
  17. 新能源产业链全景图(建议收藏)
  18. MGC token GTR社区宇哥教你如何快速升级V5
  19. Android 模拟器一键获取root权限 一键安装Google play 服务
  20. Essential Linux Device Drivers》中文版第2章

热门文章

  1. React——react-router-dom V6 使用
  2. 农历与西历对照、万年历
  3. 新型勒索软件PYSA浅析,又要如何防御??
  4. 《从零开始学PHP》 何俊斌
  5. Python—实现sftp客户端(连接远程服务器)
  6. 华为HCIP云计算考证心得
  7. Switch-零基础完全上手指南(日版)
  8. CK+人脸表情数据库地址
  9. festival - ubuntu下的TTS引擎
  10. 离散数学学习笔记----一阶逻辑等值演算与推理