0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

单链表学习的总结(一)

电子设计 来源:电子设计 作者:电子设计 2020-12-24 17:35 次阅读
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

想必大多数人和我一样,刚开始学数据结构中的单链表还是蛮吃力的,特别是后面的双链表操作更是如此。还有就是在实践代码操作时,你又会感到无从下手,没有思路。造成这样的缘由,还是没有完全把链表吃透,今天刚好看书又看到了这里,总结一下,分享给大家,希望对大家有帮助。

一、链表引入的缘由:

在一开始,不知大家用了这么久的数组,你有没有发现数组存在两个明显的缺陷?1)一个是数组中所有元素的类型必须一致;2)第二个是数组的元素个数必须事先制定并且一旦指定之后不能更改。于是乎为了解决数组的缺陷,先辈们发明的一些特殊方法来解决:a、数组的第一个缺陷靠结构体去解决。结构体允许其中的元素的类型不相同,因此解决了数组的第一个缺陷。所以说结构体是因为数组不能解决某些问题所以才发明的;b、我们希望数组的大小能够实时扩展。譬如我刚开始定了一个元素个数是10,后来程序运行时觉得不够因此动态扩展为20.普通的数组显然不行,我们可以对数组进行封装以达到这种目的;我们还可以使用一个新的数据结构来解决,这个新的数据结构就是链表(几乎可以这样理解:链表就是一个元素个数可以实时变大/变小的数组)。

二、什么是链表?

顾名思义,链表就是用锁链连接起来的表。这里的表指的是一个一个的节点(一个节点可以比喻成大楼里面的空房子一样用来存放东西的),节点中有一些内存可以用来存储数据(所以叫表,表就是数据表);这里的锁链指的是链接各个表的方法,C语言中用来连接2个表(其实就是2块内存)的方法就是指针。它的特点是:它是由若干个节点组成的(链表的各个节点结构是完全类似的),节点是由有效数据和指针组成的。有效数据区域用来存储信息完成任务的,指针区域用于指向链表的下一个节点从而构成链表。

三、单链表中的一些细节:

1、单链表的构成:

a、链表是由节点组成的,节点中包含:有效数据和指针。

b、定义的struct node只是一个结构体,本身并没有变量生成,也不占用内存。结构体定义相当于为链表节点定义了一个模板,但是还没有一个节点,将来在实际创建链表时需要一个节点时用这个模板来复制一个即可。例如:

1 struct node{2 int data;//有效数据34struct node *pNext;//指向下一个节点的指针56 };//构建一个链表的节点。

2、堆内存的申请和使用:

a、先了解一下什么是堆:堆(heap)是种内存管理方式,它的特点是:就是自由管理(随时申请,灵活,大小块随意)。堆内存是操作系统规划给堆管理器(操作系统中的的一段代码,属于操作系统的内存管理单元),来管理的,然后向使用者(用户进程)提供api(malloc和free)来使用堆内存。

b、为什么要使用堆呢?

需要内存容量比较大的时候,需要反复使用及释放时,需要反复使用及释放很多数据结构(譬如链表)的实现都要使用堆内存;它的特点:容量不限(常规使用的需求容量都能满足),申请及释放都需要手工进行,手工进行的含义就是需要程序员写代码明确进行申请malloc及释放free。如果程序员申请内存并使用没有释放,这段内存就丢失了(在堆管理器的记录中,这段内存仍然属于你这个进程,但是进程自己又以为这段内存已经不用了,再用的时候又会申请新的内存块,这就叫吃内存),称为内存泄漏。

c、基本概念:

作用域:起作用的区域,也就是可以工作的范围。

代码块:所谓代码块,就是用{}括起来的一段代码。

数据段:数据段存的是数,像全局变量就是存在数据段的。

代码段:存的是程序代码,一般是只读的。

栈(stack):先进后出。C语言中局部变量就分配在栈中。

这里随便也讲一下什么是栈:

栈是一种数据结构,c语言中使用栈来保存局部变量。栈是被发明出来管理内存的;它的特点:是先进后出;而先进先出,它是队列的特点;栈的特点是入口即出口,另外一个口是堵死的。所以先进去的必须后出来队列的特点是入口和出口都有,必须从入口进去,从出口出来,所以先进去的必须先出来,否则就堵住后面的。在c 语言中的局部变量是用栈来实现的。我们在c中定义一个局部变量时(int a ),编译器会在栈中分配一段空间(4字节)给这个局部变量用(分配时栈顶指针会移动给出空间,给局部变量a用的意思就是,将这4字节的栈内存地址和我们定义的局部变量名a 给关联起来),对应栈的操作时入栈。

注意:这里栈指针的移动和内存分配是自动的(栈自己完成,不用我们写代码去操作);然后等我们函数退出的时候,局部变量要灭亡。对应栈的操作时出栈。出栈时也是栈顶指针移动将栈空间中与a关联的那4个字节空间释放。这个动作也是自动的,也不用人去写代码去控制。栈的优点:栈管理内存,好处是方便,分配和最后回收都不用程序员操心,c语言自动完成。分析一个细节:c语言中,定义局部变量时如果未初始化,则值时随机的为什么?定义局部变量,其实就是在栈中通过移动栈指针来给程序提供一个内存空间和这个局部变量名绑定,因为这段内存空间在栈上,而栈内存是反复使用的(脏的,上次用完没有清零的),所以说使用栈来实现的局部变量定义时如果不显示初始化,值就是脏的。如果你显示初始化会怎样?

c语言是通过一个小手段来实现局部变量的初始化的。比如 int a=10;相当于 int a ;

a=10;

栈的缺点:首先,栈是有大小的。所以栈内存大小不好设置,如果太小怕溢出,太大跑浪费内存;所以栈的溢出危害很大,一定避免。所以我们在c语言中定义局部变量时不能定义太多或者太大(譬如不能定义局部变量时int a[10000])

使用递归来解决问题时一定要注意递归收敛.

d、注意:链表的内存要求比较灵活,不能用栈,也不能用data数据段。只能用堆内存。

使用堆内存来创建一个链表节点的步骤:1、申请堆内存,大小为一个节点的大小(检查申请结果是否正确);2、清理申请到的堆内存;3、把申请到的堆内存当作一个新节点;4、填充你哦个新节点的有效数据和指针区域。

实例:

1 #include <stdio.h> 2 #include <strings.h> 3 #include <stdlib.h> 4 int main(void) 5{ 6 //创建一个链表节点 7 struct node *p=(struct node*)malloc(sizeof(struct node)); 8 if(NULL==p) 9 {10 printf("malloc error.n");11 }12 //清理申请到的堆内存13 bzero(p,sizeof(struct node));14 //填充节点15 p->data=1;16 p->pNext =NULL;//将来要指向下一个节点的首地址;实际操作时将下 一 个节点malloc返回的指针赋值给这个17}

四、实例演示:

1、单链表的实现:

1 #include <stdio.h> 2 #include <strings.h> 3 #include <stdlib.h> 4 // 构建一个链表的节点 5 struct node 6 { 7 int data; // 有效数据 8struct node *pNext; // 指向下一个节点的指针 9 };10 int main(void)11 {12// 定义头指针13struct node *pHeader = NULL;14//15// 每创建一个新的节点,把这个新的节点和它前一个节点关联起来16// 创建一个链表节点17struct node *p = (struct node *)malloc(sizeof(struct node));18if (NULL == p)19{20 printf("malloc error.n");21 return -1;22}23// 清理申请到的堆内存24bzero(p, sizeof(struct node));25// 填充节点26p->data = 1;27p->pNext = NULL; // 将来要指向下一个节点的首地址28 // 实际操作时将下一个节点malloc返回的指针赋值给这个2930pHeader = p; // 将本节点和它前面的头指针关联起来 31//33// 每创建一个新的节点,把这个新的节点和它前一个节点关联起来34// 创建一个链表节点35struct node *p1 = (struct node *)malloc(sizeof(struct node));36if (NULL == p1)37{38 printf("malloc error.n");39 return -1;40}41// 清理申请到的堆内存42bzero(p1, sizeof(struct node));43// 填充节点44p1->data = 2;45p1->pNext = NULL; // 将来要指向下一个节点的首地址46 // 实际操作时将下一个节点malloc返回的指针赋值给这个474849p->pNext = p1; // 将本节点和它前面的头指针关联起来 505152//5354//5556// 每创建一个新的节点,把这个新的节点和它前一个节点关联起来5758// 创建一个链表节点5960struct node *p2 = (struct node *)malloc(sizeof(struct node));61if (NULL == p2)62{63 printf("malloc error.n");64 return -1;65}66// 清理申请到的堆内存67bzero(p2, sizeof(struct node));68// 填充节点69p2->data = 3;70p1->pNext = p2; // 将来要指向下一个节点的首地址71 // 实际操作时将下一个节点malloc返回的指针赋值给这个 72//73// 至此创建了一个有1个头指针+3个完整节点的链表。7475// 下面是4.9.3节的代码76// 访问链表中的各个节点的有效数据,这个访问必须注意不能使用p、p1、p2,而只能77// 使用pHeader。7879// 访问链表第1个节点的有效数据80printf("node1 data: %d.n", pHeader->data); 81printf("p->data: %d.n", p->data); // pHeader->data等同于p->data8283// 访问链表第2个节点的有效数据84printf("node2 data: %d.n", pHeader->pNext->data); 85printf("p1->data: %d.n", p1->data); 86// pHeader->pNext->data等同于p1->data8788// 访问链表第3个节点的有效数据89printf("node3 data: %d.n", pHeader->pNext->pNext->data); 90printf("p2->data: %d.n", p2->data); 91// pHeader->pNext->pNext->data等同于p2->data9293return 0;94}

编译结果如下:

1 root@ubuntu-virtual-machine:/mnt/hgfs/day# gcc file2.c2 root@ubuntu-virtual-machine:/mnt/hgfs/day# ./a.out3 node1 data: 1.4 p->data: 1.5 node2 data: 2.6 p1->data: 2.7 node3 data: 3.8 p2->data: 3.

2、在链表末尾添加元素:

思路:由头指针向后遍历,直到走到原来的最后一个节点。原来最后一个节点里面的pNext是NULL,现在我们只要将它改成new就可以了。添加了之后新节点就变成了最后一个。代码实例;

1 #include <stdio.h> 2 #include <strings.h> 3 #include <stdlib.h> 4 // 构建一个链表的节点 5 struct node 6{ 7int data; // 有效数据 8struct node *pNext; // 指向下一个节点的指针 9 };10// 作用:创建一个链表节点11// 返回值:指针,指针指向我们本函数新创建的一个节点的首地址12struct node * create_node(int data)13{14struct node *p = (struct node *)malloc(sizeof(struct node));15if (NULL == p)16{17 printf("malloc error.n");18 return NULL;19}20// 清理申请到的堆内存21bzero(p, sizeof(struct node));22// 填充节点23p->data = data;24p->pNext = NULL; 25return p;26 }27 void insert_tail(struct node *pH, struct node *new)28 {29// 分两步来完成插入30// 第一步,先找到链表中最后一个节点31struct node *p = pH;32while (NULL != p->pNext)33{34 p = p->pNext; 35// 往后走一个节点36}37// 第二步,将新节点插入到最后一个节点尾部38p->pNext = new;39 }40 int main(void)41 {42// 定义头指针43//struct node *pHeader = NULL; 44// 这样直接insert_tail会段错误。45struct node *pHeader = create_node(1);46insert_tail(pHeader, create_node(2));47insert_tail(pHeader, create_node(3));48insert_tail(pHeader, create_node(4));49 /*50pHeader = create_node(1);51 // 将本节点和它前面的头指针关联起来 52pHeader->pNext = create_node(432); 53// 将本节点和它前面的头指针关联起来 5455pHeader->pNext->pNext = create_node(123); 56// 将来要指向下一个节点的首地址5758// 至此创建了一个有1个头指针+3个完整节点的链表。59 */60 // 访问链表中的各个节点的有效数据,这个访问必须注意不能使用p、p1、p2,而只能61 // 使用pHeader。62// 访问链表第1个节点的有效数据63printf("node1 data: %d.n", pHeader->data); 64//printf("p->data: %d.n", p->data); 65 // pHeader->data等同于p->data66// 访问链表第2个节点的有效数据67printf("node2 data: %d.n", pHeader->pNext->data); 68//printf("p1->data: %d.n", p1->data); 69// pHeader->pNext->data等同于p1->data70// 访问链表第3个节点的有效数据71printf("node3 data: %d.n", pHeader->pNext->pNext->data); 72//printf("p2->data: %d.n", p2->data); 73// pHeader->pNext->pNext->data等同于p2->data74return 0;75}

编译结果:

1root@ubuntu-virtual-machine:/mnt/hgfs/day# gcc file3.c2root@ubuntu-virtual-machine:/mnt/hgfs/day# ./a.out3node1 data: 1.4node2 data: 2.5node3 data: 3.

3、在第一个节点插入元素:

在代码演示之前,先名两个概念:头指针和头节点

a、什么是头指针?

头指针并不是节点,而是一个普通指针,只占4字节。头指针的类型是struct node *类型的,所以它才能指向链表的节点。一个典型的链表的实现就是:头指针指向链表的第1个节点,然后第1个节点中的指针指向下一个节点,然后依次类推一直到最后一个节点。这样就构成了一个链。

b、什么是头节点?

其实它和一般的节点差不多,只不过要注意的是:第一,它紧跟在头指针后面。第二,头节点的数据部分是空的(有时候不是空的,而是存储整个链表的节点数),指针部分指向下一个节点,也就是第一个节点。

1 #include <stdio.h> 2 #include <strings.h> 3 #include <stdlib.h> 4 // 构建一个链表的节点 5 struct node 6 { 7int data; // 有效数据 8struct node *pNext; // 指向下一个节点的指针 9 };10 // 作用:创建一个链表节点11 // 返回值:指针,指针指向我们本函数新创建的一个节点的首地址12 struct node * create_node(int data)13 {14struct node *p = (struct node *)malloc(sizeof(struct node));15if (NULL == p)16{17 printf("malloc error.n");18 return NULL;19 }20// 清理申请到的堆内存21bzero(p, sizeof(struct node));22// 填充节点23p->data = data;24p->pNext = NULL; 25return p;26 }27 // 思路:由头指针向后遍历,直到走到原来的最后一个节点。原来最后一个节点里面的pNext是NULL,现在我们只要将它改成new就可以了。添加了之后新节点就变成了最后一个。2829 // 计算添加了新的节点后总共有多少个节点,然后把这个数写进头节点中。3031void insert_tail(struct node *pH, struct node *new)32 {33int cnt = 0;34// 分两步来完成插入35// 第一步,先找到链表中最后一个节点36struct node *p = pH;37while (NULL != p->pNext)38{39 p = p->pNext; 40 // 往后走一个节点41 cnt++;42}43// 第二步,将新节点插入到最后一个节点尾部44p->pNext = new;45pH->data = cnt + 1;46 }47void insert_head(struct node *pH, struct node *new)48{49// 第1步: 新节点的next指向原来的第一个节点50new->pNext = pH->pNext;51// 第2步: 头节点的next指向新节点的地址52pH->pNext = new;53// 第3步: 头节点中的计数要加154pH->data += 1;55 }56int main(void)57{58// 定义头指针59//struct node *pHeader = NULL; 60 // 这样直接insert_tail会段错误。61struct node *pHeader = create_node(0);62insert_head(pHeader, create_node(1));63insert_tail(pHeader, create_node(2));64insert_head(pHeader, create_node(3));65 /*66pHeader = create_node(1);6768// 将本节点和它前面的头指针关联起来 69pHeader->pNext = create_node(432); 70// 将本节点和它前面的头指针关联起来 71pHeader->pNext->pNext = create_node(123); 72// 将来要指向下一个节点的首地址73// 至此创建了一个有1个头指针+3个完整节点的链表。74 */75// 访问链表中的各个节点的有效数据,这个访问必须注意不能使用 p、p1、p2,而只能76// 使用pHeader。77// 访问链表头节点的有效数据78printf("beader node data: %d.n", pHeader->data); 79// 访问链表第1个节点的有效数据80printf("node1 data: %d.n", pHeader->pNext->data); 81// 访问链表第2个节点的有效数据82printf("node2 data: %d.n", pHeader->pNext->pNext->data); 83// 访问链表第3个节点的有效数据84printf("node3 data: %d.n", pHeader->pNext->pNext->pNext->data); 85return 0;86}

编译结果;

1 root@ubuntu-virtual-machine:/mnt/hgfs/day# gcc file4.c2 root@ubuntu-virtual-machine:/mnt/hgfs/day# ./a.out3 beader node data: 3.4 node1 data: 3.5 node2 data: 1.6 node3 data: 2.

五、总结:

通过本次链表的学习,让自己对链表的理解更加深了,接下来双链表的使用会在后面更新,欢迎大家来关注!!

审核编辑:符乾江
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • C语言
    +关注

    关注

    183

    文章

    7642

    浏览量

    144593
  • 可编程逻辑
    +关注

    关注

    7

    文章

    526

    浏览量

    45305
收藏 人收藏
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

    评论

    相关推荐
    热点推荐

    rt_object_get_information获取到的链表为空怎么解决?

    rtt启动过程,在初始化堆的时候,进入rt_object_init,调用rt_object_get_information获取到的链表为空,导致系统起不来。
    发表于 10-11 11:44

    第1章 如何学习单片机

      在错误的道路上日夜兼程,最终也无法成功,方法和思路绝对是最重要的。本章节讲到的学习单片机的方法,都是作者学习单片机的无数经验和教训总结出来的瑰宝。通过作者前面的披荆斩棘,开辟了道路,可以告诉读者
    的头像 发表于 10-10 10:32 233次阅读

    常用PromQL查询案例总结

    在云原生时代,Prometheus已经成为监控领域的事实标准。作为名资深运维工程师,我见过太多团队在PromQL查询上踩坑,也见过太多因为监控不到位导致的生产事故。今天分享10个实战中最常用的PromQL查询案例,每个都是血泪经验的
    的头像 发表于 09-18 14:54 444次阅读

    rtt链表的rt_slist_for_each_entry编译报错怎么解决?

    这个要自己补头文件?
    发表于 09-18 07:16

    分享个嵌入式学习阶段规划

    给大家分享个嵌入式学习阶段规划: ()基础筑牢阶段(约 23 天) 核心目标:打牢 C 语言、数据结构、电路基础C 语言开发:学变量 / 指针 / 结构体等核心语法,用 Dev-C++ 实操
    发表于 09-12 15:11

    AI耳机变身翻译官+会议总结大师?涂鸦AI音频开发方案,让耳机升级到下个level

    在接入AI能力后,耳机这种日常化的产品,能有多大的想象空间?它不仅能帮你轻松听懂全球外语和地方方言,还能将语音转化为文字、翻译成不同语言,甚至自动总结会议要点、生成思维导图,适配办公、学习、跨语言
    的头像 发表于 07-10 18:47 1464次阅读
    AI耳机变身翻译官+会议<b class='flag-5'>总结</b>大师?涂鸦AI音频开发方案,让耳机升级到下<b class='flag-5'>一</b>个level

    没辞职、没报天价班,6个月AI学习的成绩

    结业测评考试,以94分的优异成绩顺利结业。学习记录情况:虽然成绩不代表全部,但这份成绩依然可以给所有初学者带来信心:人工智能学习并没有那么难,6个月,只要用心努力,
    的头像 发表于 07-04 10:37 381次阅读
    没辞职、没报天价班,6个月AI<b class='flag-5'>学习</b>的成绩<b class='flag-5'>单</b>

    相关协议信号总结

    电子发烧友网站提供《相关协议信号总结.xlsx》资料免费下载
    发表于 06-25 15:34 5次下载

    嵌入式AI技术之深度学习:数据样本预处理过程中使用合适的特征变换对深度学习的意义

      作者:苏勇Andrew 使用神经网络实现机器学习,网络的每个层都将对输入的数据做次抽象,多层神经网络构成深度学习的框架,可以深度理解数据中所要表示的规律。从原理上看,使用深度学习
    的头像 发表于 04-02 18:21 1280次阅读

    【AIBOX 应用案例】目深度估计

    了关键作用。深度估计技术可以分为多目深度估计和目深度估计。其中目摄像头具有成本低、设备普及、图像获取方便等优势,使得目深度估计技术备受关注‌。深度学习技术的快
    的头像 发表于 03-19 16:33 874次阅读
    【AIBOX 应用案例】<b class='flag-5'>单</b>目深度估计

    GaN E-HEMTs的PCB布局经验总结

    GaN E-HEMTs的PCB布局经验总结
    的头像 发表于 03-13 15:52 1022次阅读
    GaN E-HEMTs的PCB布局经验<b class='flag-5'>总结</b>

    TOF学习总结

    iToF(间接飞行时间)技术中,波长越短,分辨细节的能力越好,主要原因与光的波动特性和调制信号的特性密切相关。以下是详细解释:1. 光的波动特性:波长与分辨率的关系波长越短,空间分辨率越高:光的波长决定了其能够分辨的最小细节。根据光学理论,分辨率与波长成反比,波长越短,能够分辨的特征尺寸越小。例如,可见光的波长(400-700 nm)比红外光(通常用于iToF,波长约850 nm)更短,因此可见光能够分辨更细微的细节。衍射极限:光的衍
    发表于 02-25 17:49 0次下载

    关于RISC-V芯片的应用学习总结

    RISC-V芯片作为种基于精简指令集计算(RISC)原则的开源指令集架构(ISA)芯片,近年来在多个领域展现出了广泛的应用潜力和显著优势。以下是对RISC-V芯片应用的总结。 RISC-V芯片
    发表于 01-29 08:38

    轴测径仪也可以智能化

    关键字:蓝鹏牌测径仪,智能测径仪,轴智能测径仪,测径仪智能化,测径仪智能测控,外径智能测量,路测径仪,小型测径仪智能化, 轴测径仪作为种精密的测量设备,在多个领域都发挥着重要作
    发表于 12-31 13:55

    请问ADS9110输入可以采用端输入?

    手册第页中写明:单极差分输入,未说明是否可用作端输入. 自制板卡测试中发现,若将AIN-N接入GND,AIN-P接入信号源的端输出,出现了莫名的问题,比如: 1) 采出的波形杂乱无章,
    发表于 12-18 07:00