当前位置: 首页 > news >正文

【数据结构与算法】第5篇:线性表(一):顺序表(ArrayList)的实现与应用

一、什么是顺序表

顺序表是最简单的一种线性结构。用一段地址连续的存储单元依次存储数据元素。

你可以把它理解为一个可以自动扩容的数组。C语言的原生数组长度是固定的,不够用的时候只能重新申请更大的数组,把数据搬过去。顺序表封装了这个过程,让使用者不用操心容量问题。

顺序表的特点

  • 逻辑上相邻的元素,物理位置上也相邻

  • 可以通过下标直接访问,时间复杂度O(1)

  • 插入和删除操作需要移动元素,时间复杂度O(n)


二、顺序表的结构定义

我们需要用一个结构体来管理顺序表:

c

typedef struct { int *data; // 指向动态数组的指针 int size; // 当前元素个数 int capacity; // 总容量 } SeqList;
  • data:指向一块连续内存的指针,真正存数据的地方

  • size:当前有多少个元素

  • capacity:当前最多能存多少个元素(不一定是内存的实际字节数)


三、基本操作实现

3.1 初始化

c

void initSeqList(SeqList *list, int initCapacity) { list->data = (int*)malloc(initCapacity * sizeof(int)); if (list->data == NULL) { printf("初始化失败\n"); exit(1); } list->size = 0; list->capacity = initCapacity; }

初始化时先申请一块内存,size设为0,capacity就是申请的大小。

3.2 销毁

c

void destroySeqList(SeqList *list) { if (list->data != NULL) { free(list->data); list->data = NULL; } list->size = 0; list->capacity = 0; }

用完一定要释放内存,避免泄漏。

3.3 扩容

扩容是顺序表的核心。当size等于capacity时,再插入新元素就需要扩容。

c

void expand(SeqList *list) { int newCapacity = list->capacity * 2; // 翻倍扩容 int *newData = (int*)realloc(list->data, newCapacity * sizeof(int)); if (newData == NULL) { printf("扩容失败\n"); return; } list->data = newData; list->capacity = newCapacity; printf("扩容到 %d\n", newCapacity); }

扩容策略:这里用的是翻倍扩容。也可以每次增加固定大小(比如+10)。翻倍扩容的优点是,随着容量变大,扩容次数越来越少,平均时间复杂度更低。

3.4 插入

在指定位置插入元素,这是顺序表最复杂的操作。

c

int insert(SeqList *list, int pos, int value) { // 检查位置是否合法(可以插在末尾,所以pos可以从0到size) if (pos < 0 || pos > list->size) { printf("插入位置不合法\n"); return -1; } // 满了就扩容 if (list->size == list->capacity) { expand(list); } // 移动元素:从最后一个开始往后移,给新元素腾位置 for (int i = list->size; i > pos; i--) { list->data[i] = list->data[i - 1]; } // 插入新元素 list->data[pos] = value; list->size++; return 0; }

关键点:移动元素必须从后往前移。如果从前往后移,前面的元素会把后面的覆盖掉。

画个图理解一下,在位置2插入一个元素:

text

插入前:[10, 20, 30, 40] size=4 插入位置2,值25 第一步:从最后一个开始往后移 [10, 20, 30, 40, 40] i从4移到3 [10, 20, 30, 30, 40] i移到2时停止 第二步:插入 [10, 20, 25, 30, 40] size变成5

3.5 删除

删除指定位置的元素,同样需要移动数据。

c

int delete(SeqList *list, int pos) { // 检查位置是否合法 if (pos < 0 || pos >= list->size) { printf("删除位置不合法\n"); return -1; } // 保存被删除的值(如果需要的话) int value = list->data[pos]; // 移动元素:从pos+1开始往前移 for (int i = pos; i < list->size - 1; i++) { list->data[i] = list->data[i + 1]; } list->size--; return value; }

删除比插入简单,移动方向是从前往后

3.6 查找

按值查找,返回第一个匹配的位置。

c

int find(SeqList *list, int value) { for (int i = 0; i < list->size; i++) { if (list->data[i] == value) { return i; } } return -1; }

3.7 打印

c

void print(SeqList *list) { printf("size=%d, capacity=%d, [", list->size, list->capacity); for (int i = 0; i < list->size; i++) { printf("%d", list->data[i]); if (i < list->size - 1) printf(", "); } printf("]\n"); }

四、完整代码演示

c

#include <stdio.h> #include <stdlib.h> typedef struct { int *data; int size; int capacity; } SeqList; void initSeqList(SeqList *list, int initCapacity) { list->data = (int*)malloc(initCapacity * sizeof(int)); if (list->data == NULL) { printf("初始化失败\n"); exit(1); } list->size = 0; list->capacity = initCapacity; } void destroySeqList(SeqList *list) { if (list->data != NULL) { free(list->data); list->data = NULL; } list->size = 0; list->capacity = 0; } void expand(SeqList *list) { int newCapacity = list->capacity * 2; int *newData = (int*)realloc(list->data, newCapacity * sizeof(int)); if (newData == NULL) { printf("扩容失败\n"); return; } list->data = newData; list->capacity = newCapacity; printf("扩容到 %d\n", newCapacity); } int insert(SeqList *list, int pos, int value) { if (pos < 0 || pos > list->size) { printf("插入位置不合法\n"); return -1; } if (list->size == list->capacity) { expand(list); } for (int i = list->size; i > pos; i--) { list->data[i] = list->data[i - 1]; } list->data[pos] = value; list->size++; return 0; } int delete(SeqList *list, int pos) { if (pos < 0 || pos >= list->size) { printf("删除位置不合法\n"); return -1; } int value = list->data[pos]; for (int i = pos; i < list->size - 1; i++) { list->data[i] = list->data[i + 1]; } list->size--; return value; } int find(SeqList *list, int value) { for (int i = 0; i < list->size; i++) { if (list->data[i] == value) { return i; } } return -1; } void print(SeqList *list) { printf("size=%d, capacity=%d, [", list->size, list->capacity); for (int i = 0; i < list->size; i++) { printf("%d", list->data[i]); if (i < list->size - 1) printf(", "); } printf("]\n"); } int main() { SeqList list; initSeqList(&list, 3); // 插入几个元素,观察扩容 insert(&list, 0, 10); insert(&list, 1, 20); insert(&list, 2, 30); print(&list); // 再插一个,触发扩容 insert(&list, 3, 40); print(&list); // 中间插入 insert(&list, 2, 25); print(&list); // 删除 int val = delete(&list, 2); printf("删除的值: %d\n", val); print(&list); // 查找 int pos = find(&list, 30); printf("30的位置: %d\n", pos); destroySeqList(&list); return 0; }

运行结果:

text

扩容到 6 size=3, capacity=3, [10, 20, 30] size=4, capacity=6, [10, 20, 30, 40] size=5, capacity=6, [10, 20, 25, 30, 40] 删除的值: 25 size=4, capacity=6, [10, 20, 30, 40] 30的位置: 2

五、复杂度分析

操作时间复杂度说明
按索引访问O(1)直接通过下标计算地址
插入O(n)平均移动n/2个元素
删除O(n)平均移动n/2个元素
查找(按值)O(n)最坏情况遍历全部
扩容均摊O(1)翻倍扩容,平均每次插入的扩容成本很低

关于扩容的均摊分析:假设初始容量为1,翻倍扩容到n的过程中,总共移动的次数约为2n,平均到n次插入,每次插入的扩容成本是O(1)。


六、顺序表的优缺点

优点

  1. 支持随机访问,按下标取元素是O(1)

  2. 空间连续,CPU缓存友好

  3. 尾插尾删效率高(不需要移动元素)

缺点

  1. 中间插入和删除需要移动大量元素,效率低

  2. 扩容时需要重新申请内存并拷贝数据,有开销

  3. 可能浪费空间(capacity > size的部分)

适用场景

  • 需要频繁随机访问

  • 主要在尾部操作

  • 元素个数大致可预估


七、小结

这一篇我们实现了顺序表,要点总结:

要点说明
结构data指针 + size + capacity
扩容realloc实现,翻倍扩容策略
插入从后往前移动元素
删除从前往后移动元素
复杂度随机访问O(1),插入删除O(n)

下一篇我们会讲单链表,它解决了顺序表插入删除慢的问题,但失去了随机访问的能力。没有完美的数据结构,只有合适的选择。


八、思考题

  1. 如果每次扩容只增加10个位置,而不是翻倍,会有什么问题?

  2. 插入操作中,如果插入位置是末尾,还需要移动元素吗?

  3. 写一个函数,删除顺序表中所有等于某个值的元素(要求时间复杂度O(n))。

  4. 为什么顺序表不适合在头部频繁插入?

欢迎在评论区讨论你的答案。

http://www.cnnetsun.cn/news/1480304.html

相关文章:

  • TensorRT性能调优实战指南:从问题诊断到优化落地
  • 贝叶斯网络:从概率依赖到实际建模的简明指南
  • Dify插件开发避坑指南:手把手解决Provider接入的5大高频错误
  • 从Word2Vec到BERT:一文搞懂NLP词嵌入技术的进化史(附实战代码)
  • 如何用Pony V7轻松打造你的AI角色创作工作流
  • 解决深信服超融合添加iSCSI存储时的ATS不支持警告:完整避坑指南
  • 迁移学习新姿势:为什么SpotTune比传统fine-tuning更聪明?从14个数据集实验结果说起
  • Cadence OrCAD 16.6自带库文件大盘点:从Amplifier到Transistor,新手别再用错库了!
  • 虚幻引擎登录界面常见BUG排查手册:解决UI显示与事件调度器问题
  • 七鱼智能客服小程序嵌入H5实战:提升开发效率的架构设计与避坑指南
  • CC2530开发实战:ZStack协议栈OSAL任务与事件处理全解析(附代码示例)
  • ROS2服务(Service)的隐藏技巧:从同步调用到异步响应的进阶用法
  • PlatformIO 脚本进阶:精准控制C++编译选项与库源文件构建
  • AI应用架构师指南:智能运维系统架构中日志分析的设计与实现
  • Python数据分析实战:用matplotlib绘制对比统计特征图的两种方法(附完整代码)
  • 视频下载高效获取:3个维度重新定义开源工具的使用体验
  • SmallThinker-3B快速上手:Postman调用Ollama API实现批量COT推理测试
  • Rockchip RK3588开发板调试实战:用这10个ADB命令搞定性能与功耗排查
  • 从动量和矩的视角解析优化算法:以AdaGrad与Adam为例
  • 墨语灵犀在软件测试中的应用:自动化测试用例与缺陷报告生成
  • Android 12 AOSP实战:如何把第三方APK预装为系统应用(附常见错误解决方案)
  • 阿里速卖通和奥地利邮政签署MOU,加强欧洲本地履约服务
  • FLUX.小红书极致真实V2实战应用:为小红书笔记自动生成封面+内页配图
  • LLC谐振变换器的双环竞争控制实战
  • 开源抢票工具:3步掌握大麦网自动购票脚本,轻松获取热门展览门票
  • 智能多模态内容分析平台:从数据采集到深度理解的全流程解析
  • 基于四旋翼无人机离散建模与增量PID控制及轨迹跟踪研究,MATLAB代码
  • 新手必看!Vue3中ref和reactive的7个典型使用场景对比(含TS类型标注示例)
  • 嵌入式工程师职业发展路径与技术能力提升指南
  • 嵌入式系统7大关键电路接口技术详解