1 概述
1.1 背景介绍
在现代软件开发中,数据结构的选择对程序的性能和可维护性有着至关重要的影响。数组和链表作为两种最基本的数据结构,分别适用于不同的场景。理解它们的特性和优劣,能够帮助开发者在实际项目中做出更合理的技术选型,从而优化系统性能。
链表是一种动态的数据结构,它通过结点之间的指针链接来组织数据。与数组不同,链表的存储空间是动态分配的,不需要预先分配固定大小的内存。单向链表是一种基础的数据结构,由一系列节点组成,每个节点包含两部分:数据域和指针域。其核心特点是节点之间通过单向指针连接,形成线性序列,仅支持从头节点开始顺序访问。
本案例相关实验将在华为云开发者空间云主机进行,开发者空间云主机为开发者提供了高效稳定的云资源,确保用户的数据安全。云主机当前已适配完整的C/C++开发环境,支持Visual Studio Code等多种IDE工具安装调测。
1.2 适用对象
- 个人开发者
- 高校学生
1.3 案例时间
本案例总时长预计90分钟。
1.4 案例流程
说明:
① 开通开发者空间,搭建C/C++开发环境。 ② 打开VS Code,编写代码运行程序。
1.5 资源总览
本案例预计花费总计0元。
资源名称 | 规格 | 单价(元) | 时长(分钟) |
开发者空间-云主机 | 4vCPUs | 8GB ARM Ubuntu Ubuntu 24.04 Server 定制版 | 免费 | 90 |
VS Code | 1.97.2 | 免费 | 90 |
最新案例动态,请查阅 《链表操作秘籍—通讯录管理全接触》。小伙伴快来领取华为开发者空间进行实操体验吧!
2 配置实验环境
参考案例中心 《基于开发者空间,定制C&C++开发环境云主机镜像》“2. 实验环境搭建”、“3. VS Code安装部署”章节完成开发环境、VS code及插件安装。
3 单向链表的基本操作
3.1 单向链表的初始化
- 添加必要的头文件,定义链表结点结构。
- 实现链表初始化函数list_init。 1) 创建头结点,头结点的指针域置空; 2) 创建头指针,头指针指向头结点(将头结点的地址赋值给头指针)。
- 编写main函数,测试初始化函数,验证初始化结果。释放内存。
实验结果:
3.2 单向链表的销毁
- 添加必要的头文件和宏,定义链表结点结构。
- 创建带3个结点的测试链表,用于测试。
- 实现list_destroy函数。 1) 使用临时指针t从链表的第一个数据结点开始遍历,直到遍历到链表的末尾; 2) 每遍历到一个结点首先将头结点的next赋值为当前结点的下一个结点即head-\>next = t-\>next,然后临时指针t指向下一个结点 t = head-\>next; 3) 如果整个链表遍历完毕则删除结点。
- 编写main函数,创建链表,调用销毁函数,测试正常销毁和传入NULL的情况。
实验结果:
3.3 单向链表的插入操作
3.3.1 单向链表头插法
- 添加必要的头文件,定义链表结点结构。
- 实现单向链表头插法函数head_insert。 1)创建一个新的结点p; 2)将新的结点p的next指向头结点的下一个结点(head-\>next); 3)头结点的next指向新的结点p。
- 实现辅助函数print_list,打印单向链表。
- 参考3.2 单向链表的销毁实验步骤:3.实现函数list_destroy,释放内存。
- 编写main函数,测试链表头插法函数,打印链表内容。最后释放内存。
实验结果:
3.3.2 单向链表尾插法
- 参考3.3.1 单向链表头插法小节的实验步骤:1.添加必要的头文件,定义链表结点结构。
- 实现尾插法函数tail_insert。 1) 新建一个新的结点; 2) 将尾指针的next指向新的结点; 3) 将尾指针指向新的结点。
- 参考3.3.1 单向链表头插法小节的实验步骤:3.实现辅助函数print_list,打印单向链表。
- 参考3.2 单向链表的销毁小节的实验步骤:3.实现函数list_destroy,释放内存。
- 编写main函数,测试链表尾插法以及边界测试。
实验结果:
3.4 单向链表的查找操作
3.4.1 查找单向链表的指定元素
- 参考3.3.1 单向链表头插法小节的实验步骤:1.添加必要的头文件,定义链表结点结构。
- 参考3.3.1 单向链表头插法小节的实验步骤:2. 实现单向链表头插法函数head_insert。
- 参考3.3.1 单向链表头插法小节的实验步骤:3.实现辅助函数print_list,打印单向链表。
- 参考3.2 单向链表的销毁小节的实验步骤:3.实现函数list_destroy,释放内存。
- 实现单向链表查找指定元素的函数get_elem。 1) 从链表的第一个数据结点开始遍历 2) 将遍历到的每一个结点上的数据域与需要查找的元素比较 3) 如果相等返回该结点的地址,如果不相等则继续往后遍历,如果遍历到链表末尾依然没有找到则返回NULL。
- 编写main函数,测试查找存在和不存在的元素。
实验结果:
3.4.2 查找单向链表指定位置的元素
- 参考3.3.1 单向链表头插法小节的实验步骤:1.添加必要的头文件,定义链表结点结构。
- 参考3.3.1 单向链表头插法小节的实验步骤:2. 实现单向链表头插法函数head_insert。
- 参考3.3.1 单向链表头插法小节的实验步骤:3.实现辅助函数print_list,打印单向链表。
- 参考3.2 单向链表的销毁小节的实验步骤:3.实现函数list_destroy,释放内存。
- 实现单向链表指定位置元素查找函数get_elem_by_index。 1) 从链表的第一个数据结点开始遍历; 2) 每遍历一个结点遍历次数+1,同时判断是否遍历到了链表的末尾; 3) 如果遍历到了链表末尾返回NULL; 4) 或者遍历到了指定位置返回结点指针。
- 编写main函数,测试查找越界和非越界的元素。
实验结果:
3.5 单向链表的删除操作
3.5.1 单向链表删除指定位置的元素
- 参考3.3.1 单向链表头插法小节的实验步骤:1.添加必要的头文件,定义链表结点结构。
- 参考3.3.1 单向链表头插法小节的实验步骤:2. 实现单向链表头插法函数head_insert。
- 参考3.3.1 单向链表头插法小节的实验步骤:3.实现辅助函数print_list,打印单向链表。
- 参考3.2 单向链表的销毁小节的实验步骤:3.实现函数list_destroy,释放内存。
- 实现单向链表指定位置元素删除函数delete_by_index。 1 ) 使用临时指针current从链表的第一个数据结点开始遍历; 2 ) 使用临时指针prev保存遍历到的结点的前驱结点; 3 ) 每遍历一个结点遍历次数+1,指针prev与随之往后移动,同时判断是否遍历到了链表的末尾; 4 ) 如果遍历到了链表的末尾则返回FALSE; 5 ) 如果遍历到的位置恰好是最后一个结点(尾结点),则将尾指针指向该结点的前一个结点,尾指针的next赋值为NULL,并且删除最后一个结点; 6 ) 如果遍历到的结点是中间结点,则将前驱结点指向遍历到的结点的下一个结点(指针 p-\>next = t-\>next)。
- 编写main函数,测试删除中间结点和尾结点。
实验结果:
3.5.2 单向链表删除指定的元素
- 参考3.3.1 单向链表头插法小节的实验步骤:1.添加必要的头文件,定义链表结点结构。
- 参考3.3.1 单向链表头插法小节的实验步骤:2. 实现单向链表头插法函数head_insert。
- 参考3.3.1 单向链表头插法小节的实验步骤:3.实现辅助函数print_list,打印单向链表。
- 参考3.2 单向链表的销毁小节的实验步骤:3.实现函数list_destroy,释放内存。
- 实现单向链表指定元素删除函数delete_by_elem_value。
- 编写main函数,测试删除中间结点、尾结点和不存在的结点。
实验结果:
4 综合案例:通讯录管理系统
4.1 需求分析及代码实现
4.1.1 功能需求
通讯录管理系统功能设计如下:
- 添加联系人:用户可以输入联系人的姓名、电话号码和地址,将其添加到通讯录中。
- 查找联系人:用户可以通过姓名查找联系人的电话号码和地址。
- 删除联系人:用户可以通过姓名删除联系人。
- 显示通讯录:用户可以查看所有联系人的信息。
- 退出系统:用户可以退出程序。
4.1.2 编写代码
- 添加必要的头文件和宏,定义结构体。
将原先的单向链表结点结构,修改成Contact结构体,用于存储联系人信息(姓名、电话、地址),修改链表结点数据域为Contact类型
- 添加初始化链表函数。
- 添加销毁链表函数。
- 使用尾插法添加联系人。
- 按姓名查找联系人。
- 删除联系人。
- 显示所有联系人。
- 获取用户输入。
- 实现菜单界面和main函数。
4.1.3 功能演示
- 添加联系人:输入联系人姓名、电话号码和地址,将其添加到链表中。
- 添加联系人 "张三",电话号码 " 13611112222",地址“北京”。
- 添加联系人 "李四",电话号码 " 13899990000",地址“南京”。
- 查找联系人:输入联系人姓名,查找并显示其电话号码、地址。例如:查找 "张三",显示电话号码 " 13611112222",地址“北京”。
- **显示通讯录:**显示所有联系人的姓名和电话号码。
- **删除联系人:**输入联系人姓名,从链表中删除该联系人。例如:删除 "张三",通讯录中不再显示该联系人。
- **退出系统:**销毁链表并退出程序。
4.2 总结
4.2.1 单向链表的优缺点
优点:
- 动态数据管理:链表适用于需要频繁插入和删除数据的场景,如通讯录管理。
- 内存高效利用:链表可以动态分配内存,避免内存浪费,适合资源受限的系统。
- 扩展性强:可以轻松扩展功能,如添加联系人分组、排序等功能。
缺点:
- 随机访问效率低(必须遍历):必须从头节点开始逐个遍历(O(n)),无法像数组一样通过索引直接访问(O(1))。
- 额外空间开销:每个节点需存储指向下一个节点的指针(next),若数据本身较小(如存储一个整数),指针可能占用较大比例的内存。
- 缓存不友好:节点在内存中非连续存储,无法利用CPU缓存的局部性原理,访问效率低于数组。
- 操作复杂度与边界问题:中间或尾部操作需遍历前驱节点,代码实现需注意边界条件(如空链表、单节点链表)。指针操作易出错,例如未正确处理指针可能导致链表断裂或内存泄漏。
4.2.2 案例总结
我们通过本案例的前半部分,系统的学习了单向链表的初始化、销毁、插入、查找和删除等操作逻辑和代码实现。最后通过实现一个电话通讯录管理系统,展示了链表在实际应用中的灵活性和实用性。链表的基本操作被广泛应用于各种场景,如内存管理、文件系统、网络编程等。通过此案例,可以更好地理解链表的工作原理及其在实际开发中的应用价值。