🌈个人主页:聆风吟
🔥系列专栏:数据结构手札
🔖少年有梦不应止于心动,更要付诸行动。
文章目录
- 📚专栏订阅推荐
- 📋前言 - 顺序表文章合集
- 一. ⛳️顺序表:重点回顾
- 1.1 🔔顺序表的定义
- 1.2 🔔顺序表的分类
- 1.2.1 👻静态顺序表
- 1.2.2 👻动态顺序表
- 二. ⛳️顺序表的基本操作实现
- 2.1 🔔动态顺序表结构体构建
- 2.2 🔔初始化顺序表
- 2.3 🔔销毁顺序表
- 2.4 🔔打印顺序表
- 三. ⛳️顺序表的源代码
- 3.1 🔔SeqList.h 顺序表的函数声明
- 3.2 🔔SeqList.c 顺序表的函数定义
- 3.3 🔔test.c 顺序表功能测试
- 📝全文总结
📚专栏订阅推荐
| 专栏名称 | 专栏简介 |
|---|---|
| 数据结构手札 | 本专栏主要是我的数据结构入门学习手札,记录个人从基础到进阶的学习总结。 |
| 数据结构手札・刷题篇 | 本专栏是《数据结构手札》配套习题讲解,通过练习相关题目加深对算法理解。 |
📋前言 - 顺序表文章合集
-【顺序表实战指南(一):线性表定义 | 顺序表定义】
-【顺序表实战指南(二):结构体构建 | 初始化 | 打印 | 销毁】
后续文章会陆续补充,尽情期待…
一. ⛳️顺序表:重点回顾
1.1 🔔顺序表的定义
顺序表(Sequential List):用一段物理地址连续的存储单元依次存储数据元素的线性结构。一般情况下采用数组存储。在数组上完成数据的增删查改。
📝顺序表和数组的区别?
顺序表的底层结构是数组,对数组的封装,实现了常用的增删查改等接口。
通俗点讲:数组是 “兵”,顺序表是 “将”。数组提供了最基础的存储能力(士兵),而顺序表则用这些“士兵”组织起了一套完整的作战逻辑和指挥体系(将军),知道如何调度(增删)、如何侦查(查找)、如何整编(修改)。
1.2 🔔顺序表的分类
顺序表一般可以分为:静态顺序表和动态顺序表。
1.2.1 👻静态顺序表
静态顺序表:指存储空间是固定的并且在程序运行前就已经确定大小的顺序表。它通常使用数组来实现,即通过定义一个固定长度的数组来存储数据元素。
📝知识点补充:
- 适用场景:数据量固定且已知,且不希望有运行时内存管理开销的场景。
静态顺序表的结构定义:
//静态顺序表结构定义#defineMAXSIZE7//存储单元初始分配量typedefintSLDataType;//类型重命名,便于统一修改元素类型typedefstructSeqList{SLDataType data[MAXSIZE];//定长数组intsize;//当前有效数据的个数}SeqList;我们可以发现描述静态顺序表需要三个属性:
- 存储空间的起始位置:数组
data,他的存储位置就是存储空间的存储位置; - 线性表的最大存储容量:数组长
MAXSIZE; - 线性表的当前位置:
size。
1.2.2 👻动态顺序表
动态顺序表:通过动态分配内存空间,实现随着数据量的增加而不断扩容的效果。它的结构类似于一个数组,数据元素的存储是连续的,支持随机访问和顺序访问。
📝知识点补充:
- 为什么要有动态顺序表?静态顺序表的大小固定,当数据量超过预设大小时无法处理,动态顺序表解决了这个问题。
- 扩容策略:动态顺序表通常采用倍增策略(如 capacity *= 2),这是时间与空间的权衡。
- 适用场景:数据量不确定或可能增长,需要灵活存储管理的通用场景。
动态顺序表的结构定义:
//动态顺序表结构定义typedefintSLDataType;//类型重命名,便于统一修改元素类型typedefstructSeqList{SLDataType*a;//指向动态开辟的数组intsize;//当前有效数据的个数intcapacity;//当前分配的总容量}SL;我们可以发现描述动态顺序表也需要三个属性:
- 存储空间的起始位置:指针
a,他里面存储的地址就是存储空间的地址; - 线性表当前最大存储容量:
capacity,可以通过动态分配的方式进行扩容; - 线性表的当前位置:
size。
二. ⛳️顺序表的基本操作实现
通过上期学习,我们已经了解了动态顺序表可以使程序更加高效和灵活,可以根据实际数据量动态地调整表的大小,避免在创建静态顺序表时浪费内存空间或者当数据量超出静态顺序表容量时造成数据丢失或程序崩溃等问题。因此,本文将采用动态顺序表结合图文去实现顺序表的一些基本操作。
2.1 🔔动态顺序表结构体构建
//动态顺序表#defineSLCAPACITY4typedefintSLDataType;typedefstructSeqList{SLDataType*a;//指向动态开辟的数组intsize;//有效数据的个数intcapacity;//记录容量的空间大小}SL;代码深剖:
- 结构体中
a指向的数组类型是我们重定义的SLDataType,这样当我们想创建其它类型的顺序表时只需要对typedef后面的数据类型int进行需改即可; size是用来计数的,统计当前顺序表一共有多少个有效元素;capacity是用来表示当前顺序表的容量,当size==capacity时说明当前顺序表已经“装满了”,需要扩容;- 定义标识符
SLCAPACITY作为宏常量,用于指定顺序表初始化时的初始容量。便于将初始容量的值 “集中管理”,后续如果需要调整顺序表的初始容量(比如想把默认 4 个空间改成 8 个),无需在初始化函数的代码里逐个修改数值,只需修改INIT_CAPACITY的定义即可,既减少了代码出错的概率,也让程序的可维护性大大提升。
2.2 🔔初始化顺序表
//初始化顺序表voidSLInit(SL*ps){assert(ps);//使用malloc开辟空间ps->a=(SLDataType*)malloc(sizeof(SLDataType)*SLCAPACITY);//判断空间是否开辟成功if(NULL==ps->a){//打印错误原因(比如 “malloc failed: Out of memory”),方便定位问题;perror("malloc failed");//终止程序并返回非 0 状态码(约定俗成表示程序异常退出),避免后续无效操作。exit(-1);}//初始化顺序表的有效元素个数为 0。ps->size=0;//记录顺序表当前的最大容量。ps->capacity=SLCAPACITY;}代码深剖:
assert(ps);主要用于校验传入的顺序表指针是否有效(非空)。assert是断言宏,若ps为NULL(空指针),程序会直接终止并提示断言失败。因为后续所有操作都基于ps指向的顺序表结构体,如果传入空指针,操作ps->a、ps->size等会导致程序崩溃,这是防御性编程的核心手段,提前规避空指针访问的致命错误。(后文在出现就不多做叙述了)。- 使用
malloc开辟空间,一定要进行判断是否开辟成功。因为malloc申请内存失败时(比如系统剩余内存不足)会返回NULL,若直接使用NULL指针操作数组,会触发 “段错误” 导致程序崩溃;
时间复杂度:
该程序没有循环,根据大O阶的推导方法很容易得出:初始化顺序表的时间复杂度为
O(1)
2.3 🔔销毁顺序表
//销毁顺序表voidSLDestroy(SL*ps){assert(ps);//释放顺序表底层数组占用的动态内存。free(ps->a);//将指针置空,避免 “野指针” 问题。ps->a=NULL;//重置顺序表的状态变量,让其回归 “初始无效状态”。ps->size=ps->capacity=0;}代码深剖:
为什么必须调用销毁函数?因为我们实现的是动态顺序表,其底层数组a指向的内存是通过malloc向操作系统 “临时申请” 的 —— 系统不会自动回收这块内存,如果使用后忘记释放,会造成内存泄漏:轻则持续占用系统资源,导致程序运行越久越卡顿;重则内存被耗尽,引发程序崩溃甚至系统异常。
时间复杂度:
该程序没有循环,根据大O阶的推导方法很容易得出:销毁顺序表的时间复杂度为
O(1)
2.4 🔔打印顺序表
//打印顺序表voidSLPrint(SL*ps){assert(ps);for(inti=0;i<ps->size;i++){printf("%d ",ps->a[i]);}printf("\n");}代码深剖:
打印顺序表就是进行简单的遍历循环,此处不多做叙述。
时间复杂度:
该程序有单层循环,根据大O阶的推导方法很容易得出:打印顺序表的时间复杂度为
O(n)
三. ⛳️顺序表的源代码
3.1 🔔SeqList.h 顺序表的函数声明
#include<stdio.h>#include<stdlib.h>#include<assert.h>//动态顺序表#defineSLCAPACITY4typedefintSLDataType;typedefstructSeqList{SLDataType*a;//指向动态开辟的数组intsize;//有效数据的个数intcapacity;//记录容量的空间大小}SL;//初始化voidSLInit(SL*ps);//销毁顺序表voidSLDestroy(SL*ps);//打印顺序表voidSLPrint(SL*ps);3.2 🔔SeqList.c 顺序表的函数定义
#include"SeqList.h"//初始化顺序表voidSLInit(SL*ps){assert(ps);//使用malloc开辟空间ps->a=(SLDataType*)malloc(sizeof(SLDataType)*SLCAPACITY);//判断空间是否开辟成功if(NULL==ps->a){//打印错误原因(比如 “malloc failed: Out of memory”),方便定位问题;perror("malloc failed");//终止程序并返回非 0 状态码(约定俗成表示程序异常退出),避免后续无效操作。exit(-1);}//初始化顺序表的有效元素个数为 0。ps->size=0;//记录顺序表当前的最大容量。ps->capacity=SLCAPACITY;}//销毁顺序表voidSLDestroy(SL*ps){assert(ps);//释放顺序表底层数组占用的动态内存。free(ps->a);//将指针置空,避免 “野指针” 问题。ps->a=NULL;//重置顺序表的状态变量,让其回归 “初始无效状态”。ps->size=ps->capacity=0;}//打印顺序表voidSLPrint(SL*ps){assert(ps);for(inti=0;i<ps->size;i++){printf("%d ",ps->a[i]);}printf("\n");}3.3 🔔test.c 顺序表功能测试
intmain(){SL sl;//初始化循序表SLInit(&sl);printf("初始化后的顺序表状态:\n");printf("size = %d, capacity = %d\n",sl.size,sl.capacity);//还没有学习插入操作,在这里手动设置一些数据用于测试打印//不能超过4个,因为我们初始化只申请了四个数据的空间sl.a[0]=10;sl.a[1]=20;sl.a[2]=30;sl.size=3;//有效数据个数SLPrint(&sl);//销毁顺序表SLDestroy(&sl);return0;}📝全文总结
本文重点讲解动态顺序表的结构体构建,并实现了初始化、打印、销毁三大基础操作,明确了每个步骤的核心逻辑与注意事项,为后续学习顺序表的增删查改等进阶操作奠定了坚实基础。
今天的干货分享到这里就结束啦!如果觉得文章还可以的话,希望能给个三连支持一下,聆风吟的主页还有很多有趣的文章,欢迎小伙伴们前去点评,您的支持就是作者前进的最大动力!