数据结构之顺序表(C语言)

数据结构之顺序表(C语言)

1 线性表

线性表是n个具有相同特性的数据元素的有限序列,是一种在实际中广泛应用的数据结构,常见的线性表有:顺序表、链表、栈、队列、字符串等。

线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。

2 顺序表的分类

2.1 顺序表与数组的区别

顺序表是线性表的一种,顺序表的特性是:逻辑结构连续,物理结构也是连续的。顺序表的最底层结构是数组,但顺序表在数组的基础上实现了对数组的封装及增删查改等接口。我们可以把数组看成路边小摊,而顺序表就是米其林餐厅,二者虽然结构类似,但发挥的功能却不一样,档次也就自然有所差别。

2.2 静态顺序表

静态顺序表也就表示顺序表存储空间是提前给定的,无法改变。即使用定长数组来进行数据的存储。如下:

struct SeqList//用结构体来完成顺序表的操作 { int arr[100];//定长数组 int size;//顺序表中有效的数据个数 }; 

如果使用静态顺序表就要面临两个严峻的问题:

一、数组空间开辟小了,空间不够用

二、数组空间开辟大了,空间浪费

基于这种情况我们选择使用动态内存开辟,即使用动态顺序表。

2.3 动态顺序表

动态顺序表需要使用到动态内存开辟,所以在结构体内我们对数组要使用指针进行定义,同时需要增加一个申请的空间大小的变量capacity。

Struct SeqList//定义结构体 { int* arr;//定义数组 int size;//顺序表中有效的数据个数 int capacity;//空间大小 }; 

当代码量非常庞大,我们若想将部分 int 类型替换为 char 类型时,使用上面的定义方式就会引来麻烦。所以在这里我们将 涉及到顺序表的 int 类型重命名一下,如此一来就方便了我们去对源程序进行查找替换等操作。(同时我们也可以对结构体类型进行重命名)

typedef int SLDataType typedef struct SeqList//定义结构体 { SLDataType* arr;//定义数组 SLDataType size;//顺序表中有效的数据个数 SLDataType capacity;//空间大小 }SL; 

3 顺序表的初始化、打印与销毁

在结构体中,为了使每部分功能都独立存在,我们只是对顺序表中的内容进行了定义,而并未进行初始化。故此时我们定义一个初始化函数来完成这部分功能。

void SLInit(SL* ps)//将结构体变量作为参数传到形参 { ps -> arr = Null;//初始化置为空 ps -> size = ps -> capacity = 0;//初始化置为0 }

在我们初始化并插入数据后,我们可以自定义一个打印函数对该顺序表内数据进行打印。

在一系列操作完毕后需要销毁顺序表来释放对应的内存空间,故我们再来定义一个销毁函数来完成这部分功能。

void Destroy(SL* ps) { if(ps -> arr)//等价于 (ps->arr != NULL) { free(ps->arr); } ps -> arr = NULL;//将释放的空间置为空,避免野指针造成错误访问 ps -> size = ps -> capacity = 0; }

在上面的销毁操作中,最值得注意的就是在释放了顺序表内的空间后,原有空间部分的首地址指针要置为空,避免生成野指针造成错误访问。

通过上面两个自定义函数我们可以观察到,我们在传参部分使用的是传址调用而不是传值调用。若使用传值调用,函数内操作的部分是原数据的一份临时拷贝,出栈帧即会销毁。实参部分不会得到真实改变,所以在此处传参我们要使用指针来接收要操作部分的地址,实现传址调用来改变实参部分。

4 顺序表的尾插法与头插法

4.1 空间检查函数

在操作数据前我们应首先考虑申请空间的大小,即一次申请多大的空间,使用哪一个内存函数。我们知道C语言中内存函数有 malloc 、calloc 、realloc。

  • malloc 只负责分配指定字节数的内存块,不对其进行初始化。
  • calloc 在分配内存块的同时,将其初始化为零。
  • realloc 用于重新分配内存块的大小,并尝试保留原始内存块中的数据。

由于我们是需要对数组进行扩容,所以我们选择 realloc。原因如下:

  • 避免频繁地分配和释放内存: 如果每次增加大小就用 malloc 分配新的内存,然后丢弃旧的,会频繁引起内存碎片,并可能导致大量小块内存无法再利用,效率低下。realloc 可以合并相邻的小内存块,减少碎片。
  • 连续存储realloc 能够保持原有数据的连续性,这对于性能优化特别重要,特别是对于需要快速访问元素的数据结构(如链表和数组)。
  • 节省时间realloc 在现有内存基础上进行扩展,相比于多次 malloc 和 free 操作,它减少了系统调用次数,提高了程序响应速度。

确定使用的内存函数后,每次扩容应扩大多少?扩容通常来说是成倍数的增加,一般来说是2倍或3倍(由数学推理得出的最佳值)

void SLcheckcapacity(SL* ps) { if (ps->capacity == ps->size) { int newcapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;//三目表达式 SLDataType* tmp = (SLDataType*)realloc(ps->arr, newcapacity * sizeof(SLDataType));//我们之所以用newcapacity和tmp来过渡是为了保证即使扩容失败原有数据也不会丢失 if (tmp == NULL)//之所以在此判断是为了确定扩容成功 { perror("扩容失败"); exit(1); } ps->capacity = newcapacity;//扩容成功,将过渡变量的值赋值给arr和capacity ps->arr = tmp; } }

4.2 尾插函数

void SLPushBack(SL* ps, SLDataType x) { //判断ps是否为空 //温柔的解决方式 /*if (ps == NULL) { return; }*/ //暴力判断ps是否为空 assert(ps);//等价于assert(ps != NULL) //对空间大小判断 SLcheckcapacity(ps); ps->arr[ps->size] = x; ++ps->size; //更加巧妙地尾插代码 //ps->arr[ps->size++] = x; 使用后置++使得数据插入后size才会发生++操作 }

对数组进行尾插操作就找到数组已有数据后面一位再插入数据,由此我们需要将结构体的地址和要插入的数据传递给尾插函数。

4.3 头插函数

void SLPushfront(SL* ps, SLDataType x) { assert(ps); SLcheckcapacity(ps); //顺序表中数据整体往后移一位,将首元素位空出来 for (int i = ps->size; i > 0; i--) { ps->arr[i] = ps->arr[i - 1];//从最后一个数据往后移 } //循环结束后数据移动完毕 ps->arr[0] = x;//将要插的元素插到首元素位 ps->size++;//顺序表内元素个数加一(下标) }

头插操作中最关键的一步就是对已有数据的挪动,一定是从最后往后移,否则会造成原有数据丢失。

5 顺序表的尾删和头删

5.1 尾删函数

void SLPopBack(SL* ps, SLDataType x) { assert(ps); assert(ps->size); //上面判断顺序表不为空 --ps->size; //我们将顺序表有效数值个数减一后,最后一位元素就无法访问了,即被我们从顺序表中删除 }

尾删的关键在于把顺序表中数组的最后一位下标减一,如此最后一位元素就无法访问了,即被我们删除。

5.2 头删函数

void SLPopfront(SL* ps, SLDataType x) { assert(ps); SLcheckcapacity(ps); //顺序表判空并同时检查空间 //数据整体往前挪动一位 for (int i = 0; i < ps->size - 1; i++) { ps->arr[i] = ps->arr[i + 1]; } --ps->size;//顺序表中有效数据个数减一 }

头删操作和头插操作逻辑类似,关键都在于对于数据的挪动,只是头删时我们需要把数据整体往前挪动一位。

6 指定位置的插入与删除

6.1 指定位置插入数据

如上图所示,顺序表中原有的有效数据个数对应到顺序表元素下标时,其会在最后一个元素后面一位(即留有一处可操作的为空的空间),我们想要在指定位置(此处指定位置我们定义为pos)插入元素时,便需要将pos之后的元素全部后移一位,而 arr[size] 这一块空间就为我们移动数据做了准备。

同时,我们还要考虑一个问题就是指定的位置pos是否在顺序表有效的空间之内,pos能否等于0 , -1 , 10000呢?要解决上面问题j就需要我们对pos的大小进行判断。第一,pos最小是多少?显而易见,pos最小时为0,也就是头插。第二,pos最大是多少?由上类推可知pos最大是pos == size的时候,也就是尾插。

void SLInsert(SL* ps, int pos, SLDataType x) { //判断传过来的结构体地址不为空 assert(ps); //判断指定的位置pos是否在现有情况之下 assert(pos >= 0 && pos <= ps->size); //判断空间大小够不够 SLcheckcapacity(ps); //将pos及其之后的数据统一往后移动一位 for (int i = ps->size; i > pos; i--) //既然是后移,循环的初始化部分就是顺序表中可操作空间的最后,即i == size //循环判断部分可以使用极限情况推导得出,见下面注释 { ps->arr[i] = ps->arr[i - 1];//最后一次循环情况 arr[pos+1] = arr[pos] } ps->arr[pos] = x; ++ps->size;//顺序表中有效数据个数+1 }

6.2 指定位置删除数据

如上图所示,在指定位置删除数据需要将pos后面的数据整体向前移动一位,从而让pos+1处的数据将pos处的数据覆盖以实现删除效果。

void SLErase(SL* ps, int pos) { //判断传来的结构体地址不为空 assert(ps); //判断指定的位置pos是否合理 assert(pos >= 0 && pos <= ps->size); for (int i = pos; i < ps->size - 1; i++) //因为是要在指定位置删除数据,故 i == pos //循环判断部分可以使用极限情况推导得出,见下面注释 { ps->arr[i] = ps->arr[i + 1];//最后一次循环时情况 arr[size-2] = arr[size-1] } --ps->size;//删除数据后,顺序表中有效数据个数 -1 }

7 顺序表数据查找

void SLFind(SL* ps, SLDataType x) { assert(ps); for (int i = 0; i < ps->size; i++) { if (ps->arr[i] == x) { printf("找到啦"); return i; } else { printf("没有找到"); return -1; } } }

8 顺序表全程序及演示结果

8.1 SeqList.h 文件

#pragma once #define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> #include<stdlib.h> #include<assert.h> typedef int SLDataType; typedef struct SeqList { SLDataType* arr; int size; int capacity; }SL; void SLInit(SL* ps); void SLDestroy(SL* ps); void SLPrint(SL s); void SLcheckcapacity(SL* ps); void SLPushBack(SL* ps, SLDataType x); void SLPushfront(SL* ps, SLDataType x); void SLPopBack(SL* ps); void SLPopfront(SL* ps); void SLInsert(SL* ps, int pos, SLDataType x); void SLErase(SL* ps, int pos); void SLFind(SL* ps, SLDataType x);

8.2 SeqList.c 文件

#include"SeqList.h" void SLInit(SL* ps) { ps->arr = NULL; ps->size = 0; ps->capacity = 0; } void SLDestroy(SL* ps) { free(ps->arr); ps->arr = NULL; ps->size = ps->capacity = 0; } void SLPrint(SL s) { int i = 0; for (i = 0; i < s.size; i++) { printf("%d", s.arr[i]); } printf("\n"); } void SLcheckcapacity(SL* ps) { if (ps->capacity == ps->size) { int newcapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity; SLDataType* tmp = (SLDataType*)realloc(ps->arr, newcapacity * sizeof(SLDataType));//我们之所以用newcapacity和tmp来过渡是为了保证即使扩容失败原有数据也不会丢失 if (tmp == NULL)//之所以在此判断是为了确定扩容成功 { perror("扩容失败"); exit(1); } ps->capacity = newcapacity;//扩容成功,将过渡变量的值赋值给arr和capacity ps->arr = tmp; } } void SLPushBack(SL* ps, SLDataType x) { //判断ps是否为空 //温柔的解决方式 /*if (ps == NULL) { return; }*/ //暴力判断ps是否为空 assert(ps);//等价于assert(ps != NULL) //对空间大小判断 SLcheckcapacity(ps); ps->arr[ps->size] = x; ++ps->size; //更加巧妙地尾插插入 //ps->arr[ps->size++] = x; 使用后置++使得数据插入后size才会发生++操作 } void SLPushfront(SL* ps, SLDataType x) { assert(ps); SLcheckcapacity(ps); //顺序表中数据整体往后移一位,将首元素位空出来 for (int i = ps->size; i > 0; i--) { ps->arr[i] = ps->arr[i - 1];//从最后一个数据往后移 } //循环结束后数据移动完毕 ps->arr[0] = x;//将要插的元素插到首元素位 ps->size++;//顺序表内有效数值个数加一(下标) } void SLPopBack(SL* ps) { assert(ps); assert(ps->size); //上面判断顺序表不为空 --ps->size; //我们将顺序表有效数值个数减一后,最后一位元素就无法访问了,即被我们从顺序表中删除 } void SLPopfront(SL* ps) { assert(ps); SLcheckcapacity(ps); //顺序表判空并同时检查空间 //数据整体往前挪动一位 for (int i = 0; i < ps->size - 1; i++) { ps->arr[i] = ps->arr[i + 1]; } --ps->size;//顺序表中有效数据个数减一 } //在指定位置pos插入数据 void SLInsert(SL* ps, int pos, SLDataType x) { //判断传过来的结构体地址不为空 assert(ps); //判断指定的位置pos是否合理 assert(pos >= 0 && pos <= ps->size); //判断空间大小够不够 SLcheckcapacity(ps); //将pos及其之后的数据统一往后移动一位 for (int i = ps->size; i > pos; i--) //既然是后移,循环的初始化部分就是顺序表中可操作空间的最后,即i == size //循环判断部分可以使用极限情况推导得出,见下面注释 { ps->arr[i] = ps->arr[i - 1];//最后一次循环情况 arr[pos+1] = arr[pos] } ps->arr[pos] = x; ++ps->size;//顺序表中有效数据个数+1 } //在指定位置删除数据 void SLErase(SL* ps, int pos) { //判断传来的结构体地址不为空 assert(ps); //判断指定的位置pos是否合理 assert(pos >= 0 && pos <= ps->size); for (int i = pos; i < ps->size - 1; i++) //因为是要在指定位置删除数据,故 i == pos //循环判断部分可以使用极限情况推导得出,见下面注释 { ps->arr[i] = ps->arr[i + 1];//最后一次循环时情况 arr[size-2] = arr[size-1] } --ps->size;//删除数据后,顺序表中有效数据个数 -1 } //查找 void SLFind(SL* ps, SLDataType x) { assert(ps); for (int i = 0; i < ps->size; i++) { if (ps->arr[i] == x) { printf("找到啦"); return i; } else { printf("没有找到"); return -1; } } }

8.3 SeqList_test.c 测试文件

#include"SeqList.h" void SLtest_1() { SL sl; SLInit(&sl); //增删查改操作 //测试尾插 SLPushBack(&sl, 1); SLPushBack(&sl, 2); SLPushBack(&sl, 3); SLPushBack(&sl, 4); SLPrint(sl);//1 2 3 4 //SLPushFront(&sl, 5); //SLPushFront(&sl, 6); //测试尾删 SLPopBack(&sl); SLPrint(sl);//1 2 3 SLPopBack(&sl); SLPrint(sl);//1 2 SLPopBack(&sl); SLPrint(sl);//1 //SLPopBack(&sl); //SLPrint(sl);//空 //........... SLDestroy(&sl); } int main() { SLtest_1(); return 0; }

运行结果:

写作不易,不知各位老板能否给个一键三连或是一个免费的赞呢(*^▽^*)(*^▽^*),这将是对我最大的肯定与支持!!!谢谢!!!(*^▽^*)(*^▽^*)

Read more

为什么90%的物联网项目都用Java+MQTT?真相终于揭晓

第一章:为什么90%的物联网项目都用Java+MQTT?真相终于揭晓 在物联网(IoT)领域,设备间通信的稳定性、低延迟与跨平台兼容性是决定项目成败的关键。Java 与 MQTT 的组合之所以成为行业主流,正是因为它完美地解决了这些核心挑战。 跨平台能力让Java无处不在 Java 的“一次编写,到处运行”特性使其能在从嵌入式设备到云端服务器的各种环境中无缝部署。物联网系统通常包含多种硬件架构和操作系统,而 JVM 的广泛支持确保了业务逻辑的一致性。 轻量级通信协议MQTT为何胜出 MQTT(Message Queuing Telemetry Transport)是一种基于发布/订阅模式的轻量级消息传输协议,专为低带宽、不稳定网络设计。它具备以下优势: * 极低的网络开销,最小报文仅需 2 字节 * 支持三种服务质量等级(QoS 0, 1, 2),适应不同可靠性需求 * 内置心跳机制,实现设备在线状态管理 Java集成MQTT的实战示例 使用

By Ne0inhk

面试官:公司项目中Java的多线程一般用在哪些场景?

1. 引言:为什么需要多线程? 现代计算机普遍采用多核CPU,单线程程序无法充分利用硬件资源。在服务端领域,高并发、低延迟是核心诉求,多线程成为必然选择。Java从语言层面提供了强大的多线程支持(java.lang.Thread),并在java.util.concurrent包中提供了丰富的并发工具。但多线程也带来了线程安全、死锁、上下文切换开销等问题,因此理解其适用场景至关重要。 2. 异步处理:提升系统响应速度 2.1 场景描述 在Web应用中,某些操作耗时较长且不需要立即返回结果给客户端,例如用户注册成功后发送欢迎邮件、短信通知,或者记录操作日志。如果在请求线程中同步执行这些操作,会延长接口响应时间,降低用户体验。 2.2 实现方案 将耗时操作提交到独立的线程池中异步执行,主线程立即返回成功响应。 java @RestController public class UserController { @Autowired private ThreadPoolTaskExecutor taskExecutor; @PostMapping("

By Ne0inhk
【前端基础】HTML + CSS + JavaScript 快速入门(三):JS 与 jQuery 实战

【前端基础】HTML + CSS + JavaScript 快速入门(三):JS 与 jQuery 实战

【前端基础】HTML + CSS + JavaScript 快速入门(三):JS 与 jQuery 实战 我的主页:寻星探路个人专栏:《JAVA(SE)----如此简单!!! 》《从青铜到王者,就差这讲数据结构!!!》 《数据库那些事!!!》《JavaEE 初阶启程记:跟我走不踩坑》 《JavaEE 进阶:从架构到落地实战 》《测试开发漫谈》 《测开视角・力扣算法通关》《从 0 到 1 刷力扣:算法 + 代码双提升》 《Python 全栈测试开发之路》没有人天生就会编程,但我生来倔强!!! 寻星探路的个人简介: 【前端基础】HTML + CSS + JavaScript 快速入门(三):JS 与 jQuery 实战 摘要:

By Ne0inhk
【油猴脚本】00020 案例 Tampermonkey油猴脚本, 仅用于学习,不要乱搞。添加不可移动到窗口外的功能。Jquery爬虫,JavaScript爬虫HTML+Css+JavaScript编写

【油猴脚本】00020 案例 Tampermonkey油猴脚本, 仅用于学习,不要乱搞。添加不可移动到窗口外的功能。Jquery爬虫,JavaScript爬虫HTML+Css+JavaScript编写

前言:哈喽,大家好,今天给大家分享一篇文章!并提供具体代码帮助大家深入理解,彻底掌握!创作不易,如果能帮助到大家或者给大家一些灵感和启发,欢迎收藏+关注哦 💕 目录 * 【油猴脚本】00020 案例 Tampermonkey油猴脚本, 仅用于学习,不要乱搞。添加不可移动到窗口外的功能。把easyui库更换为JqueryUi库,JqueryUi学习性更好。jqueryui的Draggable组件的bug发现以及解决,Jquery爬虫,JavaScript爬虫,HTML+Css+JavaScript编写 * 使用的库: * Bootstrap的Css库CDN: * Jquery库CDN: * sheetjs库CDN: * Jquery的库JqueryUi CDN: * 📚一、效果 * 📚二、核心解析 * 💡1.jqueryui库的引入: * 💡2.jqueryui的Draggable组件的bug发现以及解决: * 💡3.添加 - 不可移动到窗口外的功能: * 📚三、完整源代码,可以直接复制使用 * ✍️J

By Ne0inhk